From 315f2736879511e68a383020d56208ca946686cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 17 Oct 2026 10:45:37 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=9E=20Sync=202026-10-17=2010:45:37?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- luci-app-fwlive/Makefile | 2 +- .../luci-static/resources/fwlive/css.js | 2 +- .../luci-static/resources/fwlive/fwlive.css | 3 + .../luci-static/resources/fwlive/table.js | 13 +- .../resources/view/status/fwlive.js | 25 ++ luci-app-fwlive/po/de/luci-app-fwlive.po | 324 +++++++++--------- luci-app-fwlive/po/ru/luci-app-fwlive.po | 324 +++++++++--------- .../po/templates/luci-app-fwlive.pot | 324 +++++++++--------- luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po | 324 +++++++++--------- luci-app-fwlive/root/usr/libexec/rpcd/fwlive | 142 +++++++- luci-app-open-box/Makefile | 4 +- luci-app-passwall/Makefile | 2 +- .../root/usr/share/passwall/app.sh | 6 +- luci-app-passwall2/Makefile | 2 +- .../cbi/passwall2/client/type/1_sing-box.lua | 41 +++ .../cbi/passwall2/client/type/2_xray.lua | 84 ++++- .../cbi/passwall2/server/type/1_sing-box.lua | 27 +- .../cbi/passwall2/server/type/2_xray.lua | 23 +- .../luasrc/passwall2/util_sing-box.lua | 65 +++- .../luasrc/passwall2/util_xray.lua | 50 ++- luci-app-passwall2/po/fa/passwall2.po | 50 ++- luci-app-passwall2/po/ru/passwall2.po | 50 ++- luci-app-passwall2/po/zh-cn/passwall2.po | 50 ++- luci-app-passwall2/po/zh-tw/passwall2.po | 50 ++- luci-app-passwall2/root/etc/init.d/passwall2 | 87 +++-- .../root/usr/share/passwall2/app.sh | 33 +- .../root/usr/share/passwall2/rule_update.lua | 76 ++-- .../root/usr/share/passwall2/subscribe.lua | 110 +++--- .../root/usr/share/passwall2/tasks.sh | 2 +- luci-app-smartsafehub/.gitignore | 2 + luci-app-smartsafehub/CHANGELOG.md | 150 ++++++++ luci-app-smartsafehub/Makefile | 5 +- luci-app-smartsafehub/docs/ROUTER_DEMO.md | 25 ++ .../frontend/demo-dist/demo.css | 3 + .../frontend/demo-dist/demo.js | 4 + .../frontend/package-lock.json | 4 +- luci-app-smartsafehub/frontend/package.json | 3 +- .../frontend/src/api/smartsafehub.ts | 16 +- .../frontend/src/app/App.tsx | 35 +- .../frontend/src/app/format.ts | 42 +++ .../frontend/src/app/routes.ts | 5 + .../frontend/src/components/AppShell.tsx | 3 + .../src/components/FirmwareUpdatesCard.tsx | 49 ++- .../frontend/src/components/GuestWifiCard.tsx | 6 +- .../frontend/src/components/PageTabs.tsx | 56 +++ .../src/components/ProductNavigation.tsx | 22 +- .../src/components/RelativeTimestamp.tsx | 56 +++ .../src/components/SoftwareUpdatesCard.tsx | 22 +- .../frontend/src/components/StatePanels.tsx | 3 - .../frontend/src/components/WifiQrDialog.tsx | 8 +- .../frontend/src/demo/data.ts | 182 ++++++++++ .../frontend/src/demo/main.tsx | 154 +++++++++ .../frontend/src/hooks/useHashRoute.ts | 5 + .../frontend/src/hooks/usePageTabs.ts | 36 ++ .../frontend/src/hooks/useSafeShieldRules.ts | 52 ++- luci-app-smartsafehub/frontend/src/main.tsx | 2 +- .../frontend/src/pages/SafeShieldPage.tsx | 37 +- .../src/pages/SafeShieldRulesPage.tsx | 94 ++++- .../frontend/src/pages/SettingsPage.tsx | 203 ++++------- .../frontend/src/pages/UpdatePage.tsx | 27 +- .../frontend/src/pages/WifiPage.tsx | 14 +- .../frontend/src/types/firmware.ts | 2 + .../frontend/src/types/wifi.ts | 6 + .../frontend/src/utils/firmwareBundling.ts | 29 ++ .../frontend/vite.demo.config.ts | 27 ++ .../lib/upgrade/keep.d/smartsafehub-device | 1 + .../root/usr/libexec/smartsafehub-device | 21 +- .../libexec/smartsafehub-recommended-rules | 154 +++++++++ .../rpcd/acl.d/luci-app-smartsafehub.json | 1 + .../root/usr/share/rpcd/ucode/smartsafehub.uc | 6 + .../ucode/smartsafehub/device-registration.uc | 18 + .../share/rpcd/ucode/smartsafehub/firmware.uc | 9 + .../ucode/luci/template/smartsafehub/login.ut | 6 +- .../root/www/luci-static/smartsafehub/app.css | 2 +- .../root/www/luci-static/smartsafehub/app.js | 5 +- luci-app-smartsafehub/spec/contracts_spec.sh | 21 ++ .../tests/runtime/relative-time.test.mjs | 43 +++ .../tests/runtime/router-demo.test.mjs | 81 +++++ .../tests/runtime/rule-origin.test.mjs | 35 ++ .../tests/test-firmware-bundling.sh | 43 +++ .../tests/test-guest-wifi-contract.sh | 5 +- .../tests/test-network-settings.sh | 4 +- .../tests/test-package-contract.sh | 2 + luci-app-smartsafehub/tests/test-page-tabs.sh | 58 ++++ .../tests/test-recommended-rules.sh | 149 ++++++++ .../tests/test-relative-time-contract.sh | 32 ++ .../tests/test-router-demo-contract.sh | 47 +++ .../tests/test-rules-ui-contract.sh | 37 +- .../tests/test-settings-ui-contract.sh | 28 +- .../tests/test-static-validation.sh | 1 + .../tests/test-update-ui-contract.sh | 5 +- .../tests/test-wifi-qr-contract.sh | 2 + luci-app-trafficctl/Makefile | 4 +- minigate/Makefile | 4 +- minigate/README.md | 50 ++- minigate/luasrc/model/cbi/minigate/proxy.lua | 18 + minigate/releases/v2026.10.17.md | 20 ++ minigate/root/etc/config/minigate | 1 + minigate/root/usr/lib/minigate/geofence.sh | 43 ++- minigate/root/usr/lib/minigate/proxy.sh | 105 +++++- minigate/root/usr/lib/minigate/uninstall.sh | 13 +- minigate/root/usr/lib/minigate/update.sh | 2 +- minigate/scripts/build-ipk.sh | 2 +- minigate/tests/geofence.test.cjs | 81 +++++ minigate/tests/lan-dns.test.cjs | 172 ++++++++++ minigate/tests/uninstall.test.cjs | 6 +- mise/Makefile | 4 +- 107 files changed, 3810 insertions(+), 1215 deletions(-) create mode 100644 luci-app-smartsafehub/docs/ROUTER_DEMO.md create mode 100644 luci-app-smartsafehub/frontend/demo-dist/demo.css create mode 100644 luci-app-smartsafehub/frontend/demo-dist/demo.js create mode 100644 luci-app-smartsafehub/frontend/src/components/PageTabs.tsx create mode 100644 luci-app-smartsafehub/frontend/src/components/RelativeTimestamp.tsx create mode 100644 luci-app-smartsafehub/frontend/src/demo/data.ts create mode 100644 luci-app-smartsafehub/frontend/src/demo/main.tsx create mode 100644 luci-app-smartsafehub/frontend/src/hooks/usePageTabs.ts create mode 100644 luci-app-smartsafehub/frontend/src/utils/firmwareBundling.ts create mode 100644 luci-app-smartsafehub/frontend/vite.demo.config.ts create mode 100755 luci-app-smartsafehub/root/usr/libexec/smartsafehub-recommended-rules create mode 100644 luci-app-smartsafehub/tests/runtime/relative-time.test.mjs create mode 100644 luci-app-smartsafehub/tests/runtime/router-demo.test.mjs create mode 100644 luci-app-smartsafehub/tests/runtime/rule-origin.test.mjs create mode 100755 luci-app-smartsafehub/tests/test-firmware-bundling.sh create mode 100755 luci-app-smartsafehub/tests/test-page-tabs.sh create mode 100755 luci-app-smartsafehub/tests/test-recommended-rules.sh create mode 100644 luci-app-smartsafehub/tests/test-relative-time-contract.sh create mode 100755 luci-app-smartsafehub/tests/test-router-demo-contract.sh create mode 100644 minigate/releases/v2026.10.17.md create mode 100644 minigate/tests/geofence.test.cjs create mode 100644 minigate/tests/lan-dns.test.cjs diff --git a/luci-app-fwlive/Makefile b/luci-app-fwlive/Makefile index 67db66198..eb6f4f428 100644 --- a/luci-app-fwlive/Makefile +++ b/luci-app-fwlive/Makefile @@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter LUCI_PKGARCH:=all PKG_VERSION:=0.1.53 -PKG_RELEASE:=68 +PKG_RELEASE:=69 # Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI). ifdef SOURCE_DATE_EPOCH diff --git a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/css.js b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/css.js index 807bd3c9c..a8bb5dd08 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/css.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/css.js @@ -11,5 +11,5 @@ * GENERATED — do not edit. Edit fwlive.css and run: node scripts/embed-fwlive-css.js */ return baseclass.extend({ - styleText: "\n/* SPDX-License-Identifier: Apache-2.0 */\n.fwlive-map {\n\tmax-width: none;\n\twidth: 100%;\n\t--fwlive-pass-color: var(--success-color-high, var(--success-color, #46a546));\n\t--fwlive-deny-color: var(--error-color-high, var(--error-color, #ca3c3c));\n\t--fwlive-bg-medium: var(--background-color-medium, var(--white-color-low, #f9f9f9));\n\t--fwlive-paused-color: #b45309;\n}\n/* Wide page in Detail only — Simple keeps LuCI’s 940px column. */\n#maincontent.container:has(.fwlive-map[data-view=\"detailed\"]) {\n\tmax-width: none;\n\tpadding-left: 16px;\n\tpadding-right: 16px;\n}\n.fwlive-map[data-row-tint=\"accessible\"] {\n\t--fwlive-pass-color: #0d9488;\n\t--fwlive-deny-color: #c2410c;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row,\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tbackground: rgba(180, 83, 9, 0.08);\n\tbackground: color-mix(in srgb, var(--fwlive-paused-color) 8%, transparent);\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row {\n\tmargin: 0 -8px;\n\tpadding: 6px 8px 0;\n\tborder-radius: 3px 3px 0 0;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tmargin: 0 -8px 10px;\n\tpadding: 8px;\n\tborder-radius: 0 0 3px 3px;\n\tborder-bottom-color: transparent;\n}\n.fwlive-title-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 8px 16px;\n\tmargin: 0 0 4px;\n}\n.fwlive-title-row h2 {\n\tmargin: 0;\n\tflex: 0 1 auto;\n}\n.fwlive-title-status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tflex-wrap: wrap;\n\tmin-width: 0;\n\tmargin-left: auto;\n}\n.fwlive-watch-strip {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: stretch;\n\tjustify-content: flex-start;\n\tgap: 0;\n\tmin-height: 40px;\n\tpadding: 8px 0 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n\tmargin-bottom: 10px;\n}\n.fwlive-watch-group {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\tmin-height: 28px;\n\tpadding: 0 10px;\n\tborder-left: 1px solid var(--border-color-medium);\n\tflex-wrap: wrap;\n}\n.fwlive-watch-group:last-child {\n\tpadding-right: 0;\n}\n.fwlive-watch-group-label {\n\tmargin-right: 2px;\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.72em;\n\tfont-weight: 700;\n\tletter-spacing: 0.08em;\n\ttext-transform: uppercase;\n\twhite-space: nowrap;\n}\n.fwlive-watch-seg {\n\tdisplay: inline-flex;\n\talign-items: stretch;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\toverflow: hidden;\n}\n.fwlive-watch-seg .fwlive-seg-btn {\n\tmargin: 0;\n\tborder: 0;\n\tborder-radius: 0;\n\tborder-right: 1px solid var(--border-color-medium);\n\tbackground: transparent;\n\tpadding: 4px 10px;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n}\n.fwlive-watch-seg .fwlive-seg-btn:last-child {\n\tborder-right: 0;\n}\n.fwlive-watch-seg .fwlive-seg-btn[aria-pressed=\"true\"] {\n\tbackground: rgba(26, 111, 156, 0.12);\n\tbackground: color-mix(in srgb, var(--primary-color-high) 12%, transparent);\n\tcolor: var(--primary-color-high);\n\tfont-weight: 600;\n}\n.fwlive-watch-label { font-weight: 600; }\n.fwlive-watch-paused .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-map.fwlive-watch-paused .fwlive-title-status .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-dot {\n\twidth: 8px;\n\theight: 8px;\n\tborder-radius: 50%;\n\tbackground: var(--text-color-low, var(--border-color-high));\n\tdisplay: inline-block;\n\tflex-shrink: 0;\n}\n.fwlive-dot.fwlive-dot-on {\n\tbackground: var(--fwlive-pass-color);\n\tbox-shadow: 0 0 0 3px color-mix(in srgb, var(--fwlive-pass-color) 22%, transparent);\n}\n.fwlive-btn-ghost {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n}\n.fwlive-btn-quiet {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n\tcolor: var(--text-color-medium);\n\tfont-weight: 500;\n}\n.fwlive-log-merged {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 10px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-medium, var(--white-color-low, var(--background-color-high)));\n\tcolor: var(--text-color-high);\n\tfont-weight: 500;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n\tcursor: pointer;\n}\n.fwlive-log-merged:hover {\n\tbackground: var(--background-color-low, var(--background-color-medium));\n}\n.fwlive-log-merged:disabled {\n\topacity: 0.65;\n\tcursor: default;\n}\n.fwlive-log-on-dot {\n\twidth: 6px;\n\theight: 6px;\n\tborder-radius: 50%;\n\tbackground: var(--fwlive-pass-color);\n\tflex-shrink: 0;\n}\n.fwlive-log-rate {\n\tcolor: var(--text-color-medium);\n\tfont-weight: 400;\n}\n.fwlive-display-bar {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmargin: 0 0 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n}\n.fwlive-display-bar-label {\n\tflex: 0 0 auto;\n\tfont-weight: 600;\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-high);\n\twhite-space: nowrap;\n}\n.fwlive-display-controls {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmin-width: 0;\n\tflex: 1 1 auto;\n}\n.fwlive-display-ctl {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tmargin: 0;\n\tfont-size: 0.92em;\n\twhite-space: nowrap;\n}\n.fwlive-display-ctl input[type=\"checkbox\"] {\n\tmargin: 0;\n\tflex-shrink: 0;\n}\n.fwlive-display-ctl select {\n\tmargin-left: 0;\n}\n.fwlive-display-ctl.fwlive-hidden {\n\tdisplay: none;\n}\n/* Draw our own disclosure arrow — themes that restyle summary drop the native marker. */\n.fwlive-more-filters > summary,\n.fwlive-help > summary,\n.fwlive-rules-details > summary {\n\tcursor: pointer;\n\tlist-style: none;\n}\n.fwlive-more-filters > summary::-webkit-details-marker,\n.fwlive-help > summary::-webkit-details-marker,\n.fwlive-rules-details > summary::-webkit-details-marker { display: none; }\n.fwlive-more-filters > summary::before,\n.fwlive-help > summary::before,\n.fwlive-rules-details > summary::before {\n\tcontent: \"▸ \";\n\tcolor: var(--text-color-medium);\n}\n.fwlive-more-filters[open] > summary::before,\n.fwlive-help[open] > summary::before,\n.fwlive-rules-details[open] > summary::before { content: \"▾ \"; }\n.fwlive-ctl {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 0.92em;\n\tflex-wrap: wrap;\n}\n.fwlive-ctl select { margin-left: 4px; }\n#fwlive-limit { width: auto; min-width: 4.5em; }\n.fwlive-grid { display: grid; grid-template-columns: repeat(4, minmax(140px, 1fr)); gap: 8px; margin-bottom: 12px; }\n.fwlive-status { margin: 0; color: var(--text-color-medium); font-size: 0.9em; }\n.fwlive-status-paused { color: var(--warn-color-high); font-weight: 600; }\n.fwlive-status-error { color: var(--warn-color-high); font-weight: 600; }\n.fwlive-hint-line {\n\tmargin: 0 0 10px;\n\tfont-size: 0.85em;\n\tcolor: var(--text-color-medium);\n}\n.fwlive-empty-title {\n\tfont-weight: 600;\n\tmargin: 0 0 6px;\n\tcolor: var(--text-color-high);\n}\n.fwlive-find-row .fwlive-grid-core { margin-bottom: 0; }\n.fwlive-proto-pair {\n\tdisplay: grid;\n\tgrid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);\n\tgap: 6px;\n\talign-items: stretch;\n\tmin-width: 0;\n}\n.fwlive-proto-pair select,\n.fwlive-proto-pair input {\n\twidth: 100%;\n\tmin-width: 0;\n}\n@media (max-width: 520px) {\n\t.fwlive-proto-pair { grid-template-columns: 1fr; }\n}\n.fwlive-scroll {\n\tmax-height: 78vh;\n\tmax-height: 78dvh;\n\toverflow: auto;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n\twidth: 100%;\n\tscrollbar-gutter: stable;\n}\n#fwlive-table { margin: 0; width: 100%; table-layout: auto; border-collapse: collapse; }\n.fwlive-map[data-view=\"simple\"] #fwlive-table { table-layout: fixed; width: 100%; }\n/* Keep the localized action label and message button inside the first cell. */\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-action { width: 11.5em; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-time { width: 5rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-iface { width: 5.5rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-proto { width: 4rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-rule { width: 12rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-flow { width: 320px; }\n.fwlive-map[data-view=\"detailed\"] .fwlive-scroll #fwlive-table {\n\twidth: max-content;\n\tmin-width: 100%;\n}\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-time { width: 11rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-action { width: 4.5rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-rule { width: 8rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-iface-in,\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-iface-out { width: 3.5rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-dir { width: 3rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-proto { width: 3.25rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-src,\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-dst { width: 9rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-sport,\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-dport { width: 3.75rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-flags { width: 5rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-len { width: 3.25rem; }\n.fwlive-scroll.fwlive-msg-oneline #fwlive-table {\n\twidth: max-content;\n\tmin-width: 100%;\n}\n.fwlive-scroll.fwlive-msg-oneline tbody td { vertical-align: middle; }\n#fwlive-table thead th {\n\tposition: sticky;\n\ttop: 0;\n\tz-index: 2;\n\tbackground: var(--background-color-low);\n\tborder-bottom: 2px solid var(--border-color-high);\n\twhite-space: nowrap;\n\tpadding: 5px 8px;\n\tfont-size: 0.9em;\n\tfont-weight: 600;\n\tvertical-align: top;\n\ttext-align: left;\n}\n#fwlive-table thead th.fwlive-port,\n#fwlive-table thead th.fwlive-len {\n\ttext-align: right;\n}\n.fwlive-map[data-view=\"simple\"] #fwlive-table thead th,\n.fwlive-map[data-view=\"simple\"] #fwlive-table tbody td {\n\tvertical-align: top;\n}\n#fwlive-table thead th.fwlive-action {\n\tfont-weight: 700;\n\ttext-transform: lowercase;\n}\n#fwlive-table tbody td {\n\tpadding: 5px 8px;\n\tborder-bottom: 1px solid var(--border-color-low);\n\tvertical-align: top;\n\tfont-size: 0.92em;\n}\n.fwlive-row-alt td {\n\tbackground: #f9f9f9;\n\tbackground: var(--fwlive-bg-medium);\n}\n.fwlive-time, .fwlive-addr, .fwlive-port, .fwlive-len {\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\twhite-space: nowrap;\n}\n.fwlive-port, .fwlive-len { text-align: right; }\n.fwlive-rule, .fwlive-iface, .fwlive-proto, .fwlive-dir, .fwlive-flags { white-space: nowrap; }\n.fwlive-rule { font-size: 0.88em; }\n.fwlive-rule-link {\n\tcolor: var(--primary-color-high);\n\ttext-decoration: none;\n\tfont-weight: 500;\n}\n.fwlive-rule-link:hover { text-decoration: underline; }\n.fwlive-iface-badge {\n\tdisplay: inline-block;\n\tpadding: 1px 6px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tfont-size: 0.85em;\n\ttext-decoration: none;\n}\n.fwlive-iface-badge:hover {\n\tbackground: var(--background-color-medium);\n\tborder-color: var(--primary-color-medium);\n}\n.fwlive-action {\n\tfont-weight: 700;\n\ttext-transform: lowercase;\n\twhite-space: nowrap;\n}\n.fwlive-deny { color: var(--fwlive-deny-color); }\n.fwlive-pass { color: var(--fwlive-pass-color); }\n#fwlive-table td.fwlive-action.fwlive-pass,\n#fwlive-table td.fwlive-action.fwlive-pass a.fwlive-filter-link {\n\tcolor: var(--fwlive-pass-color);\n}\n#fwlive-table td.fwlive-action.fwlive-deny,\n#fwlive-table td.fwlive-action.fwlive-deny a.fwlive-filter-link {\n\tcolor: var(--fwlive-deny-color);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(70, 165, 70, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(70, 165, 70, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(202, 60, 60, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(202, 60, 60, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(70, 165, 70, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(202, 60, 60, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(70, 165, 70, 0.12);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(70, 165, 70, 0.18);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(202, 60, 60, 0.12);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(202, 60, 60, 0.18);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(70, 165, 70, 0.20);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(202, 60, 60, 0.20);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(13, 148, 136, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(13, 148, 136, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(194, 65, 12, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(194, 65, 12, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(13, 148, 136, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(194, 65, 12, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(13, 148, 136, 0.12);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(13, 148, 136, 0.18);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(194, 65, 12, 0.12);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(194, 65, 12, 0.18);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(13, 148, 136, 0.20);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(194, 65, 12, 0.20);\n}\n.fwlive-unknown { color: var(--text-color-medium); font-weight: 500; }\n.fwlive-message {\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\tfont-size: 0.85em;\n\tcolor: var(--text-color-high);\n}\n.fwlive-scroll.fwlive-msg-wrap .fwlive-message-wrap {\n\tdisplay: block;\n\tmin-width: 16em;\n\tmax-width: 42em;\n\twhite-space: normal;\n\tword-break: break-word;\n\tword-wrap: break-word;\n}\n.fwlive-scroll.fwlive-msg-oneline .fwlive-message {\n\twhite-space: nowrap;\n\tmax-width: none;\n\tmin-width: 32em;\n}\n#fwlive-table th.fwlive-th-message,\n.fwlive-scroll.fwlive-msg-oneline .fwlive-message {\n\twidth: 99%;\n}\n.fwlive-empty {\n\tmargin: 12px 0;\n\tpadding: 10px;\n\tbackground: var(--background-color-medium);\n\tborder: 1px dashed var(--border-color-high);\n}\n.fwlive-empty-muted {\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-medium);\n\tmargin: 0 0 8px;\n}\n.fwlive-consent {\n\tmargin: 0 0 12px;\n\tpadding: 10px 12px;\n\tbackground: var(--background-color-high);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n}\n.fwlive-consent-list {\n\tmargin: 0 0 10px;\n\tpadding-left: 1.25em;\n}\n.fwlive-consent-list li {\n\tmargin: 0 0 4px;\n}\n.fwlive-consent-check {\n\tmargin: 0 0 10px;\n\tfont-size: 0.92em;\n}\n.fwlive-consent-actions {\n\tmargin: 0;\n}\n.fwlive-logging-bar {\n\tdisplay: contents;\n}\n.fwlive-logging-status {\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-medium);\n\tmargin-right: 4px;\n}\n.fwlive-logging-notice {\n\tcolor: var(--fwlive-pass-color);\n}\n.fwlive-logging-notice-fail {\n\tcolor: var(--fwlive-deny-color);\n}\n.fwlive-filter-link {\n\tcolor: inherit;\n\ttext-decoration: underline;\n\ttext-decoration-style: dotted;\n\tcursor: pointer;\n}\n.fwlive-filter-link:hover { color: var(--primary-color-high); }\n.fwlive-chips {\n\tdisplay: none;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 6px;\n\tmargin: 0 0 10px;\n}\n.fwlive-chip-label {\n\tline-height: 1.3;\n}\n.fwlive-chip-not {\n\tfont-weight: 700;\n}\n.fwlive-chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 4px;\n\tpadding: 2px 8px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tfont-size: 0.88em;\n}\n.fwlive-chip-negated {\n\tborder-style: dashed;\n\tbackground: var(--background-color-medium);\n}\n.fwlive-chip-include {\n}\n.fwlive-chip-polarity {\n\tfont-weight: 700;\n\tfont-size: 0.78em;\n\ttext-transform: uppercase;\n\tletter-spacing: 0.03em;\n\tcolor: var(--text-color-medium);\n}\n.fwlive-chip-strike {\n\ttext-decoration: line-through;\n\ttext-decoration-thickness: 1.5px;\n}\n.fwlive-chip-sym {\n\tfont-weight: 700;\n\tline-height: 1;\n\tmin-width: 0.9em;\n\ttext-align: center;\n}\n.fwlive-chip-sym-light {\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.95em;\n}\n#fwlive-row-tint { width: auto; min-width: 9.5em; }\n/* labels (default) */\n.fwlive-chips-labels .fwlive-chip-include {\n\tborder-left: 3px solid var(--primary-color-medium, var(--primary-color-high, var(--border-color-high)));\n}\n.fwlive-chips-labels .fwlive-chip-negated .fwlive-chip-not {\n\tcolor: var(--text-color-high);\n}\n.fwlive-chip-invert-wrap {\n\tposition: relative;\n\tdisplay: inline-flex;\n}\n.fwlive-chip-invert-wrap::before {\n\tcontent: attr(data-tip);\n\tposition: absolute;\n\tbottom: calc(100% + 6px);\n\tleft: 50%;\n\ttransform: translateX(-50%);\n\twhite-space: nowrap;\n\tpadding: 4px 8px;\n\tfont-size: 0.85em;\n\tfont-weight: normal;\n\tcolor: var(--text-color-high);\n\tbackground: var(--background-color-high);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbox-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);\n\topacity: 0;\n\tvisibility: hidden;\n\tpointer-events: none;\n\tz-index: 20;\n\ttransition: opacity 0.15s ease 1.5s, visibility 0s linear 2s;\n}\n.fwlive-chip-invert-wrap:hover::before,\n.fwlive-chip-invert-wrap:focus-within::before {\n\topacity: 1;\n\tvisibility: visible;\n\ttransition: opacity 0.12s ease 0.35s, visibility 0s;\n}\n.fwlive-chip-invert {\n\tcolor: var(--text-color-medium);\n\tbackground: none;\n\tborder: none;\n\tpadding: 0;\n\tfont: inherit;\n\tfont-weight: 700;\n\tline-height: 1;\n\tcursor: pointer;\n}\n.fwlive-chip-invert:hover { color: var(--primary-color-high); }\n.fwlive-chip-remove {\n\tcolor: var(--text-color-medium);\n\ttext-decoration: none;\n\tfont-weight: 700;\n\tline-height: 1;\n}\n.fwlive-chip-remove:hover { color: var(--fwlive-deny-color); }\n.fwlive-chip-clear {\n\tfont-size: 0.88em;\n\tmargin-left: 4px;\n}\n.fwlive-flood {\n\tdisplay: none;\n\tmargin: 0 0 10px;\n\tpadding: 8px 12px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--warn-color-high);\n\tborder-radius: 3px;\n\tcolor: var(--text-color-high);\n\tfont-size: 0.92em;\n}\n.fwlive-adaptive {\n\tdisplay: none;\n\tmargin: 0 0 10px;\n\tpadding: 8px 12px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tcolor: var(--text-color-high);\n\tfont-size: 0.92em;\n}\n.fwlive-summary {\n\tmargin: 0 0 10px;\n\tpadding: 8px 12px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tcolor: var(--text-color-high);\n}\n.fwlive-summary-head {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 10px;\n}\n.fwlive-summary-body {\n\tmargin: 8px 0 0;\n\tfont: inherit;\n\twhite-space: pre-wrap;\n}\n.fwlive-tint-warn {\n\tdisplay: none;\n\tcolor: var(--warn-color-high);\n\tfont-size: 0.78em;\n\tfont-weight: 600;\n\twhite-space: nowrap;\n\tpadding: 2px 8px;\n\tborder: 1px solid var(--warn-color-high);\n\tborder-radius: 999px;\n\tbackground: color-mix(in srgb, var(--warn-color-high) 10%, transparent);\n}\n.fwlive-map[data-view=\"simple\"] #fwlive-msg-group { display: none; }\n@media (max-width: 850px) {\n\t.fwlive-title-status {\n\t\tflex-basis: 100%;\n\t\tmargin-left: 0;\n\t}\n\t.fwlive-watch-group {\n\t\tborder-left: 0;\n\t\tpadding-left: 0;\n\t\tpadding-right: 10px;\n\t}\n\t.fwlive-display-bar {\n\t\talign-items: flex-start;\n\t}\n}\n.fwlive-help-row {\n\tdisplay: flex;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 12px;\n\tmargin: 10px 0 0;\n}\n.fwlive-help {\n\tmargin: 0;\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-medium);\n\tmin-width: 0;\n\tflex: 1;\n}\n.fwlive-help ul { margin: 6px 0 0 1.2em; padding: 0; }\n.fwlive-help li { margin: 4px 0; }\n.fwlive-build {\n\tflex-shrink: 0;\n\tfont-size: 0.78em;\n\tcolor: var(--text-color-low, var(--text-color-medium));\n\tfont-variant-numeric: tabular-nums;\n\tuser-select: all;\n\twhite-space: nowrap;\n}\n.fwlive-backend {\n\tfont-size: 0.65em;\n\tfont-weight: normal;\n\tcolor: var(--text-color-medium);\n\tmargin-left: 8px;\n\tvertical-align: middle;\n}\n.fwlive-backend.fwlive-backend-warn {\n\tcolor: var(--warn-color-high);\n\tfont-weight: 600;\n}\n.fwlive-rules-details {\n\tmargin: 4px 0 8px;\n\tcolor: var(--text-color-high);\n}\n#fwlive-rules-details-body {\n\tmargin: 6px 0;\n\twhite-space: pre-line;\n\toverflow-wrap: anywhere;\n}\n.fwlive-filter-panel { margin-bottom: 12px; }\n.fwlive-map[data-view=\"simple\"] .fwlive-grid-core {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(3, minmax(140px, 1fr));\n\tgap: 8px;\n}\n.fwlive-map[data-view=\"simple\"] .fwlive-more-filters { margin-top: 8px; }\n.fwlive-map[data-view=\"simple\"] .fwlive-grid-extra {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(4, minmax(140px, 1fr));\n\tgap: 8px;\n\tmargin-top: 8px;\n}\n.fwlive-map[data-view=\"detailed\"] .fwlive-more-filters > summary { display: none; }\n.fwlive-map[data-view=\"detailed\"] .fwlive-filter-panel {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(4, minmax(140px, 1fr));\n\tgap: 8px;\n}\n.fwlive-map[data-view=\"detailed\"] .fwlive-grid-core,\n.fwlive-map[data-view=\"detailed\"] .fwlive-grid-extra,\n.fwlive-map[data-view=\"detailed\"] .fwlive-more-filters { display: contents; }\n.fwlive-row-clickable { cursor: pointer; }\n.fwlive-row-expand {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 1.5em;\n\tmin-width: 24px;\n\theight: 24px;\n\tpadding: 0;\n\tmargin-left: 3px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: transparent;\n\tcolor: var(--text-color-medium);\n\tfont: inherit;\n\tline-height: 1;\n\tvertical-align: middle;\n\tcursor: pointer;\n}\n.fwlive-row-expand:hover {\n\tcolor: var(--primary-color-high);\n\tborder-color: var(--primary-color-medium, var(--primary-color-high));\n}\n.fwlive-row-expand:focus-visible {\n\toutline: 2px solid var(--primary-color-high);\n\toutline-offset: 2px;\n}\n.fwlive-row-expanded td { border-bottom-color: transparent; }\n.fwlive-msg-expand td {\n\tbackground: var(--background-color-low);\n\tpadding: 8px 12px 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n}\n.fwlive-msg-expand-label {\n\tfont-size: 0.82em;\n\tcolor: var(--text-color-medium);\n\tmargin-bottom: 4px;\n\tfont-weight: 600;\n}\n.fwlive-msg-expand-body {\n\tmargin: 0;\n\twhite-space: pre-wrap;\n\tword-break: break-word;\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\tfont-size: 0.85em;\n\tcolor: var(--text-color-high);\n}\n.fwlive-flow-arrow {\n\tcolor: var(--text-color-low);\n\tfont-weight: 700;\n}\n.fwlive-flow-cell { white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.9em; }\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-pass,\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-pass a.fwlive-filter-link {\n\tcolor: var(--fwlive-pass-color);\n}\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-deny,\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-deny a.fwlive-filter-link {\n\tcolor: var(--fwlive-deny-color);\n}\n.fwlive-iface-badge {\n\tborder-radius: 10px;\n\tpadding: 2px 8px;\n}\n" + styleText: "\n/* SPDX-License-Identifier: Apache-2.0 */\n.fwlive-map {\n\tmax-width: none;\n\twidth: 100%;\n\t--fwlive-pass-color: var(--success-color-high, var(--success-color, #46a546));\n\t--fwlive-deny-color: var(--error-color-high, var(--error-color, #ca3c3c));\n\t--fwlive-bg-medium: var(--background-color-medium, var(--white-color-low, #f9f9f9));\n\t--fwlive-paused-color: #b45309;\n}\n/* Wide page in Detail only — Simple keeps LuCI’s 940px column. */\n#maincontent.container:has(.fwlive-map[data-view=\"detailed\"]) {\n\tmax-width: none;\n\tpadding-left: 16px;\n\tpadding-right: 16px;\n}\n.fwlive-map[data-row-tint=\"accessible\"] {\n\t--fwlive-pass-color: #0d9488;\n\t--fwlive-deny-color: #c2410c;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row,\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tbackground: rgba(180, 83, 9, 0.08);\n\tbackground: color-mix(in srgb, var(--fwlive-paused-color) 8%, transparent);\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row {\n\tmargin: 0 -8px;\n\tpadding: 6px 8px 0;\n\tborder-radius: 3px 3px 0 0;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tmargin: 0 -8px 10px;\n\tpadding: 8px;\n\tborder-radius: 0 0 3px 3px;\n\tborder-bottom-color: transparent;\n}\n.fwlive-title-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 8px 16px;\n\tmargin: 0 0 4px;\n}\n.fwlive-title-row h2 {\n\tmargin: 0;\n\tflex: 0 1 auto;\n}\n.fwlive-title-status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tflex-wrap: wrap;\n\tmin-width: 0;\n\tmargin-left: auto;\n}\n.fwlive-watch-strip {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: stretch;\n\tjustify-content: flex-start;\n\tgap: 0;\n\tmin-height: 40px;\n\tpadding: 8px 0 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n\tmargin-bottom: 10px;\n}\n.fwlive-watch-group {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\tmin-height: 28px;\n\tpadding: 0 10px;\n\tborder-left: 1px solid var(--border-color-medium);\n\tflex-wrap: wrap;\n}\n.fwlive-watch-group:last-child {\n\tpadding-right: 0;\n}\n.fwlive-watch-group-label {\n\tmargin-right: 2px;\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.72em;\n\tfont-weight: 700;\n\tletter-spacing: 0.08em;\n\ttext-transform: uppercase;\n\twhite-space: nowrap;\n}\n.fwlive-watch-seg {\n\tdisplay: inline-flex;\n\talign-items: stretch;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\toverflow: hidden;\n}\n.fwlive-watch-seg .fwlive-seg-btn {\n\tmargin: 0;\n\tborder: 0;\n\tborder-radius: 0;\n\tborder-right: 1px solid var(--border-color-medium);\n\tbackground: transparent;\n\tpadding: 4px 10px;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n}\n.fwlive-watch-seg .fwlive-seg-btn:last-child {\n\tborder-right: 0;\n}\n.fwlive-watch-seg .fwlive-seg-btn[aria-pressed=\"true\"] {\n\tbackground: rgba(26, 111, 156, 0.12);\n\tbackground: color-mix(in srgb, var(--primary-color-high) 12%, transparent);\n\tcolor: var(--primary-color-high);\n\tfont-weight: 600;\n}\n.fwlive-watch-label { font-weight: 600; }\n.fwlive-watch-paused .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-map.fwlive-watch-paused .fwlive-title-status .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-dot {\n\twidth: 8px;\n\theight: 8px;\n\tborder-radius: 50%;\n\tbackground: var(--text-color-low, var(--border-color-high));\n\tdisplay: inline-block;\n\tflex-shrink: 0;\n}\n.fwlive-dot.fwlive-dot-on {\n\tbackground: var(--fwlive-pass-color);\n\tbox-shadow: 0 0 0 3px color-mix(in srgb, var(--fwlive-pass-color) 22%, transparent);\n}\n.fwlive-btn-ghost {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n}\n.fwlive-btn-quiet {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n\tcolor: var(--text-color-medium);\n\tfont-weight: 500;\n}\n.fwlive-log-merged {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 10px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-medium, var(--white-color-low, var(--background-color-high)));\n\tcolor: var(--text-color-high);\n\tfont-weight: 500;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n\tcursor: pointer;\n}\n.fwlive-log-merged:hover {\n\tbackground: var(--background-color-low, var(--background-color-medium));\n}\n.fwlive-log-merged:disabled {\n\topacity: 0.65;\n\tcursor: default;\n}\n.fwlive-log-on-dot {\n\twidth: 6px;\n\theight: 6px;\n\tborder-radius: 50%;\n\tbackground: var(--fwlive-pass-color);\n\tflex-shrink: 0;\n}\n.fwlive-log-rate {\n\tcolor: var(--text-color-medium);\n\tfont-weight: 400;\n}\n.fwlive-display-bar {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmargin: 0 0 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n}\n.fwlive-display-bar-label {\n\tflex: 0 0 auto;\n\tfont-weight: 600;\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-high);\n\twhite-space: nowrap;\n}\n.fwlive-display-controls {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmin-width: 0;\n\tflex: 1 1 auto;\n}\n.fwlive-display-ctl {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tmargin: 0;\n\tfont-size: 0.92em;\n\twhite-space: nowrap;\n}\n.fwlive-display-ctl input[type=\"checkbox\"] {\n\tmargin: 0;\n\tflex-shrink: 0;\n}\n.fwlive-display-ctl select {\n\tmargin-left: 0;\n}\n.fwlive-display-ctl.fwlive-hidden {\n\tdisplay: none;\n}\n/* Draw our own disclosure arrow — themes that restyle summary drop the native marker. */\n.fwlive-more-filters > summary,\n.fwlive-help > summary,\n.fwlive-rules-details > summary {\n\tcursor: pointer;\n\tlist-style: none;\n}\n.fwlive-more-filters > summary::-webkit-details-marker,\n.fwlive-help > summary::-webkit-details-marker,\n.fwlive-rules-details > summary::-webkit-details-marker { display: none; }\n.fwlive-more-filters > summary::before,\n.fwlive-help > summary::before,\n.fwlive-rules-details > summary::before {\n\tcontent: \"▸ \";\n\tcolor: var(--text-color-medium);\n}\n.fwlive-more-filters[open] > summary::before,\n.fwlive-help[open] > summary::before,\n.fwlive-rules-details[open] > summary::before { content: \"▾ \"; }\n.fwlive-ctl {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 0.92em;\n\tflex-wrap: wrap;\n}\n.fwlive-ctl select { margin-left: 4px; }\n#fwlive-limit { width: auto; min-width: 4.5em; }\n.fwlive-grid { display: grid; grid-template-columns: repeat(4, minmax(140px, 1fr)); gap: 8px; margin-bottom: 12px; }\n.fwlive-status { margin: 0; color: var(--text-color-medium); font-size: 0.9em; }\n.fwlive-status-paused { color: var(--warn-color-high); font-weight: 600; }\n.fwlive-status-error { color: var(--warn-color-high); font-weight: 600; }\n.fwlive-hint-line {\n\tmargin: 0 0 10px;\n\tfont-size: 0.85em;\n\tcolor: var(--text-color-medium);\n}\n.fwlive-empty-title {\n\tfont-weight: 600;\n\tmargin: 0 0 6px;\n\tcolor: var(--text-color-high);\n}\n.fwlive-find-row .fwlive-grid-core { margin-bottom: 0; }\n.fwlive-proto-pair {\n\tdisplay: grid;\n\tgrid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);\n\tgap: 6px;\n\talign-items: stretch;\n\tmin-width: 0;\n}\n.fwlive-proto-pair select,\n.fwlive-proto-pair input {\n\twidth: 100%;\n\tmin-width: 0;\n}\n@media (max-width: 520px) {\n\t.fwlive-proto-pair { grid-template-columns: 1fr; }\n}\n.fwlive-scroll {\n\tmax-height: 78vh;\n\tmax-height: 78dvh;\n\toverflow: auto;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n\twidth: 100%;\n\tscrollbar-gutter: stable;\n}\n#fwlive-table { margin: 0; width: 100%; table-layout: auto; border-collapse: collapse; }\n.fwlive-map[data-view=\"simple\"] #fwlive-table { table-layout: fixed; width: 100%; }\n/* Keep the localized action label and message button inside the first cell. */\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-action { width: 11.5em; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-time { width: 5rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-iface { width: 5.5rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-proto { width: 4rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-rule { width: 12rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-flow { width: 320px; }\n.fwlive-map[data-view=\"detailed\"] .fwlive-scroll #fwlive-table {\n\twidth: max-content;\n\tmin-width: 100%;\n}\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-time { width: 11rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-action { width: 4.5rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-rule { width: 8rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-iface-in,\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-iface-out { width: 3.5rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-dir { width: 3rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-proto { width: 3.25rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-src,\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-dst { width: 9rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-sport,\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-dport { width: 3.75rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-flags { width: 5rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-len { width: 3.25rem; }\n.fwlive-scroll.fwlive-msg-oneline #fwlive-table {\n\twidth: max-content;\n\tmin-width: 100%;\n}\n.fwlive-scroll.fwlive-msg-oneline tbody td { vertical-align: middle; }\n#fwlive-table thead th {\n\tposition: sticky;\n\ttop: 0;\n\tz-index: 2;\n\tbackground: var(--background-color-low);\n\tborder-bottom: 2px solid var(--border-color-high);\n\twhite-space: nowrap;\n\tpadding: 5px 8px;\n\tfont-size: 0.9em;\n\tfont-weight: 600;\n\tvertical-align: top;\n\ttext-align: left;\n}\n#fwlive-table thead th.fwlive-port,\n#fwlive-table thead th.fwlive-len {\n\ttext-align: right;\n}\n.fwlive-map[data-view=\"simple\"] #fwlive-table thead th,\n.fwlive-map[data-view=\"simple\"] #fwlive-table tbody td {\n\tvertical-align: top;\n}\n#fwlive-table thead th.fwlive-action {\n\tfont-weight: 700;\n\ttext-transform: lowercase;\n}\n#fwlive-table tbody td {\n\tpadding: 5px 8px;\n\tborder-bottom: 1px solid var(--border-color-low);\n\tvertical-align: top;\n\tfont-size: 0.92em;\n}\n.fwlive-row-alt td {\n\tbackground: #f9f9f9;\n\tbackground: var(--fwlive-bg-medium);\n}\n.fwlive-time, .fwlive-addr, .fwlive-port, .fwlive-len {\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\twhite-space: nowrap;\n}\n.fwlive-port, .fwlive-len { text-align: right; }\n.fwlive-rule, .fwlive-iface, .fwlive-proto, .fwlive-dir, .fwlive-flags { white-space: nowrap; }\n.fwlive-rule { font-size: 0.88em; }\n.fwlive-rule-link {\n\tcolor: var(--primary-color-high);\n\ttext-decoration: none;\n\tfont-weight: 500;\n}\n.fwlive-rule-link:hover { text-decoration: underline; }\n.fwlive-rule-target {\n\topacity: 0.65;\n}\n.fwlive-iface-badge {\n\tdisplay: inline-block;\n\tpadding: 1px 6px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tfont-size: 0.85em;\n\ttext-decoration: none;\n}\n.fwlive-iface-badge:hover {\n\tbackground: var(--background-color-medium);\n\tborder-color: var(--primary-color-medium);\n}\n.fwlive-action {\n\tfont-weight: 700;\n\ttext-transform: lowercase;\n\twhite-space: nowrap;\n}\n.fwlive-deny { color: var(--fwlive-deny-color); }\n.fwlive-pass { color: var(--fwlive-pass-color); }\n#fwlive-table td.fwlive-action.fwlive-pass,\n#fwlive-table td.fwlive-action.fwlive-pass a.fwlive-filter-link {\n\tcolor: var(--fwlive-pass-color);\n}\n#fwlive-table td.fwlive-action.fwlive-deny,\n#fwlive-table td.fwlive-action.fwlive-deny a.fwlive-filter-link {\n\tcolor: var(--fwlive-deny-color);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(70, 165, 70, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(70, 165, 70, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(202, 60, 60, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(202, 60, 60, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(70, 165, 70, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(202, 60, 60, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(70, 165, 70, 0.12);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(70, 165, 70, 0.18);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(202, 60, 60, 0.12);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(202, 60, 60, 0.18);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(70, 165, 70, 0.20);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(202, 60, 60, 0.20);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(13, 148, 136, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(13, 148, 136, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(194, 65, 12, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(194, 65, 12, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(13, 148, 136, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(194, 65, 12, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(13, 148, 136, 0.12);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(13, 148, 136, 0.18);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(194, 65, 12, 0.12);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(194, 65, 12, 0.18);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(13, 148, 136, 0.20);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(194, 65, 12, 0.20);\n}\n.fwlive-unknown { color: var(--text-color-medium); font-weight: 500; }\n.fwlive-message {\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\tfont-size: 0.85em;\n\tcolor: var(--text-color-high);\n}\n.fwlive-scroll.fwlive-msg-wrap .fwlive-message-wrap {\n\tdisplay: block;\n\tmin-width: 16em;\n\tmax-width: 42em;\n\twhite-space: normal;\n\tword-break: break-word;\n\tword-wrap: break-word;\n}\n.fwlive-scroll.fwlive-msg-oneline .fwlive-message {\n\twhite-space: nowrap;\n\tmax-width: none;\n\tmin-width: 32em;\n}\n#fwlive-table th.fwlive-th-message,\n.fwlive-scroll.fwlive-msg-oneline .fwlive-message {\n\twidth: 99%;\n}\n.fwlive-empty {\n\tmargin: 12px 0;\n\tpadding: 10px;\n\tbackground: var(--background-color-medium);\n\tborder: 1px dashed var(--border-color-high);\n}\n.fwlive-empty-muted {\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-medium);\n\tmargin: 0 0 8px;\n}\n.fwlive-consent {\n\tmargin: 0 0 12px;\n\tpadding: 10px 12px;\n\tbackground: var(--background-color-high);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n}\n.fwlive-consent-list {\n\tmargin: 0 0 10px;\n\tpadding-left: 1.25em;\n}\n.fwlive-consent-list li {\n\tmargin: 0 0 4px;\n}\n.fwlive-consent-check {\n\tmargin: 0 0 10px;\n\tfont-size: 0.92em;\n}\n.fwlive-consent-actions {\n\tmargin: 0;\n}\n.fwlive-logging-bar {\n\tdisplay: contents;\n}\n.fwlive-logging-status {\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-medium);\n\tmargin-right: 4px;\n}\n.fwlive-logging-notice {\n\tcolor: var(--fwlive-pass-color);\n}\n.fwlive-logging-notice-fail {\n\tcolor: var(--fwlive-deny-color);\n}\n.fwlive-filter-link {\n\tcolor: inherit;\n\ttext-decoration: underline;\n\ttext-decoration-style: dotted;\n\tcursor: pointer;\n}\n.fwlive-filter-link:hover { color: var(--primary-color-high); }\n.fwlive-chips {\n\tdisplay: none;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 6px;\n\tmargin: 0 0 10px;\n}\n.fwlive-chip-label {\n\tline-height: 1.3;\n}\n.fwlive-chip-not {\n\tfont-weight: 700;\n}\n.fwlive-chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 4px;\n\tpadding: 2px 8px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tfont-size: 0.88em;\n}\n.fwlive-chip-negated {\n\tborder-style: dashed;\n\tbackground: var(--background-color-medium);\n}\n.fwlive-chip-include {\n}\n.fwlive-chip-polarity {\n\tfont-weight: 700;\n\tfont-size: 0.78em;\n\ttext-transform: uppercase;\n\tletter-spacing: 0.03em;\n\tcolor: var(--text-color-medium);\n}\n.fwlive-chip-strike {\n\ttext-decoration: line-through;\n\ttext-decoration-thickness: 1.5px;\n}\n.fwlive-chip-sym {\n\tfont-weight: 700;\n\tline-height: 1;\n\tmin-width: 0.9em;\n\ttext-align: center;\n}\n.fwlive-chip-sym-light {\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.95em;\n}\n#fwlive-row-tint { width: auto; min-width: 9.5em; }\n/* labels (default) */\n.fwlive-chips-labels .fwlive-chip-include {\n\tborder-left: 3px solid var(--primary-color-medium, var(--primary-color-high, var(--border-color-high)));\n}\n.fwlive-chips-labels .fwlive-chip-negated .fwlive-chip-not {\n\tcolor: var(--text-color-high);\n}\n.fwlive-chip-invert-wrap {\n\tposition: relative;\n\tdisplay: inline-flex;\n}\n.fwlive-chip-invert-wrap::before {\n\tcontent: attr(data-tip);\n\tposition: absolute;\n\tbottom: calc(100% + 6px);\n\tleft: 50%;\n\ttransform: translateX(-50%);\n\twhite-space: nowrap;\n\tpadding: 4px 8px;\n\tfont-size: 0.85em;\n\tfont-weight: normal;\n\tcolor: var(--text-color-high);\n\tbackground: var(--background-color-high);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbox-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);\n\topacity: 0;\n\tvisibility: hidden;\n\tpointer-events: none;\n\tz-index: 20;\n\ttransition: opacity 0.15s ease 1.5s, visibility 0s linear 2s;\n}\n.fwlive-chip-invert-wrap:hover::before,\n.fwlive-chip-invert-wrap:focus-within::before {\n\topacity: 1;\n\tvisibility: visible;\n\ttransition: opacity 0.12s ease 0.35s, visibility 0s;\n}\n.fwlive-chip-invert {\n\tcolor: var(--text-color-medium);\n\tbackground: none;\n\tborder: none;\n\tpadding: 0;\n\tfont: inherit;\n\tfont-weight: 700;\n\tline-height: 1;\n\tcursor: pointer;\n}\n.fwlive-chip-invert:hover { color: var(--primary-color-high); }\n.fwlive-chip-remove {\n\tcolor: var(--text-color-medium);\n\ttext-decoration: none;\n\tfont-weight: 700;\n\tline-height: 1;\n}\n.fwlive-chip-remove:hover { color: var(--fwlive-deny-color); }\n.fwlive-chip-clear {\n\tfont-size: 0.88em;\n\tmargin-left: 4px;\n}\n.fwlive-flood {\n\tdisplay: none;\n\tmargin: 0 0 10px;\n\tpadding: 8px 12px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--warn-color-high);\n\tborder-radius: 3px;\n\tcolor: var(--text-color-high);\n\tfont-size: 0.92em;\n}\n.fwlive-adaptive {\n\tdisplay: none;\n\tmargin: 0 0 10px;\n\tpadding: 8px 12px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tcolor: var(--text-color-high);\n\tfont-size: 0.92em;\n}\n.fwlive-summary {\n\tmargin: 0 0 10px;\n\tpadding: 8px 12px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tcolor: var(--text-color-high);\n}\n.fwlive-summary-head {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 10px;\n}\n.fwlive-summary-body {\n\tmargin: 8px 0 0;\n\tfont: inherit;\n\twhite-space: pre-wrap;\n}\n.fwlive-tint-warn {\n\tdisplay: none;\n\tcolor: var(--warn-color-high);\n\tfont-size: 0.78em;\n\tfont-weight: 600;\n\twhite-space: nowrap;\n\tpadding: 2px 8px;\n\tborder: 1px solid var(--warn-color-high);\n\tborder-radius: 999px;\n\tbackground: color-mix(in srgb, var(--warn-color-high) 10%, transparent);\n}\n.fwlive-map[data-view=\"simple\"] #fwlive-msg-group { display: none; }\n@media (max-width: 850px) {\n\t.fwlive-title-status {\n\t\tflex-basis: 100%;\n\t\tmargin-left: 0;\n\t}\n\t.fwlive-watch-group {\n\t\tborder-left: 0;\n\t\tpadding-left: 0;\n\t\tpadding-right: 10px;\n\t}\n\t.fwlive-display-bar {\n\t\talign-items: flex-start;\n\t}\n}\n.fwlive-help-row {\n\tdisplay: flex;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 12px;\n\tmargin: 10px 0 0;\n}\n.fwlive-help {\n\tmargin: 0;\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-medium);\n\tmin-width: 0;\n\tflex: 1;\n}\n.fwlive-help ul { margin: 6px 0 0 1.2em; padding: 0; }\n.fwlive-help li { margin: 4px 0; }\n.fwlive-build {\n\tflex-shrink: 0;\n\tfont-size: 0.78em;\n\tcolor: var(--text-color-low, var(--text-color-medium));\n\tfont-variant-numeric: tabular-nums;\n\tuser-select: all;\n\twhite-space: nowrap;\n}\n.fwlive-backend {\n\tfont-size: 0.65em;\n\tfont-weight: normal;\n\tcolor: var(--text-color-medium);\n\tmargin-left: 8px;\n\tvertical-align: middle;\n}\n.fwlive-backend.fwlive-backend-warn {\n\tcolor: var(--warn-color-high);\n\tfont-weight: 600;\n}\n.fwlive-rules-details {\n\tmargin: 4px 0 8px;\n\tcolor: var(--text-color-high);\n}\n#fwlive-rules-details-body {\n\tmargin: 6px 0;\n\twhite-space: pre-line;\n\toverflow-wrap: anywhere;\n}\n.fwlive-filter-panel { margin-bottom: 12px; }\n.fwlive-map[data-view=\"simple\"] .fwlive-grid-core {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(3, minmax(140px, 1fr));\n\tgap: 8px;\n}\n.fwlive-map[data-view=\"simple\"] .fwlive-more-filters { margin-top: 8px; }\n.fwlive-map[data-view=\"simple\"] .fwlive-grid-extra {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(4, minmax(140px, 1fr));\n\tgap: 8px;\n\tmargin-top: 8px;\n}\n.fwlive-map[data-view=\"detailed\"] .fwlive-more-filters > summary { display: none; }\n.fwlive-map[data-view=\"detailed\"] .fwlive-filter-panel {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(4, minmax(140px, 1fr));\n\tgap: 8px;\n}\n.fwlive-map[data-view=\"detailed\"] .fwlive-grid-core,\n.fwlive-map[data-view=\"detailed\"] .fwlive-grid-extra,\n.fwlive-map[data-view=\"detailed\"] .fwlive-more-filters { display: contents; }\n.fwlive-row-clickable { cursor: pointer; }\n.fwlive-row-expand {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 1.5em;\n\tmin-width: 24px;\n\theight: 24px;\n\tpadding: 0;\n\tmargin-left: 3px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: transparent;\n\tcolor: var(--text-color-medium);\n\tfont: inherit;\n\tline-height: 1;\n\tvertical-align: middle;\n\tcursor: pointer;\n}\n.fwlive-row-expand:hover {\n\tcolor: var(--primary-color-high);\n\tborder-color: var(--primary-color-medium, var(--primary-color-high));\n}\n.fwlive-row-expand:focus-visible {\n\toutline: 2px solid var(--primary-color-high);\n\toutline-offset: 2px;\n}\n.fwlive-row-expanded td { border-bottom-color: transparent; }\n.fwlive-msg-expand td {\n\tbackground: var(--background-color-low);\n\tpadding: 8px 12px 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n}\n.fwlive-msg-expand-label {\n\tfont-size: 0.82em;\n\tcolor: var(--text-color-medium);\n\tmargin-bottom: 4px;\n\tfont-weight: 600;\n}\n.fwlive-msg-expand-body {\n\tmargin: 0;\n\twhite-space: pre-wrap;\n\tword-break: break-word;\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\tfont-size: 0.85em;\n\tcolor: var(--text-color-high);\n}\n.fwlive-flow-arrow {\n\tcolor: var(--text-color-low);\n\tfont-weight: 700;\n}\n.fwlive-flow-cell { white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.9em; }\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-pass,\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-pass a.fwlive-filter-link {\n\tcolor: var(--fwlive-pass-color);\n}\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-deny,\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-deny a.fwlive-filter-link {\n\tcolor: var(--fwlive-deny-color);\n}\n.fwlive-iface-badge {\n\tborder-radius: 10px;\n\tpadding: 2px 8px;\n}\n" }); diff --git a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/fwlive.css b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/fwlive.css index df8a4ff8e..766f7cf28 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/fwlive.css +++ b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/fwlive.css @@ -361,6 +361,9 @@ font-weight: 500; } .fwlive-rule-link:hover { text-decoration: underline; } +.fwlive-rule-target { + opacity: 0.65; +} .fwlive-iface-badge { display: inline-block; padding: 1px 6px; diff --git a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js index 4987a4205..077a30f16 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js @@ -195,10 +195,16 @@ function buildColumnCell(col, row, state, callbacks) { actionChildren.push(' ', rowExpandButton(row, state, callbacks)); return E('td', { 'class': log.actionRowClass(row.action) }, actionChildren); } - case 'rule': - return E('td', { 'class': columnCellClass(col) }, [ + case 'rule': { + const ruleChildren = [ links.ruleAdminLink(row.rule_hint, row.rule_label, onFilterClick) - ]); + ]; + if (typeof row.rule_target === 'string' && row.rule_target) { + ruleChildren.push(' · '); + ruleChildren.push(E('span', { 'class': 'fwlive-rule-target' }, [row.rule_target])); + } + return E('td', { 'class': columnCellClass(col) }, ruleChildren); + } case 'iface': return E('td', { 'class': columnCellClass(col) }, [ links.ifaceLink(row.interface, onFilterClick) @@ -313,6 +319,7 @@ function rowRenderKey(row, state, columns) { row.action, row.rule_hint, row.rule_label, + row.rule_target, row.interface_in, row.interface_out, row.direction, diff --git a/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js b/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js index 69e00397d..1de99a710 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js @@ -207,6 +207,7 @@ return view.extend({ nextRulesRetryAt: 0, followLive: true, rulesMap: {}, + ruleTargets: {}, firewallBackend: 'nft', viewMode: 'simple', expandedRowId: null, @@ -749,6 +750,10 @@ return view.extend({ const res = await callFwliveRules(); if (!this.isCurrentPollEpoch(epoch)) return; this.rulesMap = (res && res.rules) || {}; + this.ruleTargets = + res && res.targets && typeof res.targets === 'object' && !Array.isArray(res.targets) + ? res.targets + : {}; this.rulesMapTruncated = !!(res && res.truncated === true); this.rulesMapLoaded = true; this.firewallBackend = (res && res.backend) || 'nft'; @@ -1106,8 +1111,23 @@ return view.extend({ return log.formatRuleLabel(hint); }, + resolveRuleTarget(hint) { + if (!hint) return ''; + + const own = Object.prototype.hasOwnProperty; + if (own.call(this.ruleTargets, hint) && typeof this.ruleTargets[hint] === 'string') + return this.ruleTargets[hint]; + + const slug = hint.toLowerCase(); + if (own.call(this.ruleTargets, slug) && typeof this.ruleTargets[slug] === 'string') + return this.ruleTargets[slug]; + + return ''; + }, + enrichEntry(row) { row.rule_label = this.resolveRuleLabel(row.rule_hint); + row.rule_target = this.resolveRuleTarget(row.rule_hint); return row; }, @@ -1116,10 +1136,15 @@ return view.extend({ for (let i = 0; i < this.entries.length; i++) { const row = this.entries[i]; const label = this.resolveRuleLabel(row.rule_hint); + const target = this.resolveRuleTarget(row.rule_hint); if (row.rule_label !== label) { row.rule_label = label; changed = true; } + if (row.rule_target !== target) { + row.rule_target = target; + changed = true; + } } if (changed) this.invalidateFilteredRows(); diff --git a/luci-app-fwlive/po/de/luci-app-fwlive.po b/luci-app-fwlive/po/de/luci-app-fwlive.po index 038ca5609..b809db23f 100644 --- a/luci-app-fwlive/po/de/luci-app-fwlive.po +++ b/luci-app-fwlive/po/de/luci-app-fwlive.po @@ -12,32 +12,32 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1766 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1791 msgid "%d matching · %d/%d stored" msgstr "%d Treffer · %d/%d gespeichert" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1731 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1756 msgid "%dd" msgstr "%dd" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1730 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1755 msgid "%dh" msgstr "%dh" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1729 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1754 msgid "%dm" msgstr "%dm" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1728 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1753 msgid "%ds" msgstr "%ds" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1492 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1517 msgid "%s · requested up to %d raw lines · %d firewall messages returned" msgstr "" "%s · bis zu %d Rohzeilen angefordert · %d Firewall-Meldungen zurückgegeben" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1482 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507 msgid "" "%s · requested up to %d raw lines · server limited fetch to %d · %d firewall " "messages returned" @@ -49,17 +49,17 @@ msgstr "" msgid "%s: %s" msgstr "%s: %s" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1768 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1793 msgid "0 matching · %d/%d stored" msgstr "0 Treffer · %d/%d gespeichert" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:486 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:487 msgid "Accessible (teal/orange)" msgstr "Barrierefrei (teal/orange)" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:440 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2871 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2896 msgid "Action" msgstr "Aktion" @@ -70,12 +70,12 @@ msgstr "" "Aktivieren Sie die Nachrichtenschaltfläche oder klicken Sie auf eine Zeile, " "um die vollständige Meldung ein- oder auszublenden" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:998 msgid "Administrator access is required to disable logging." msgstr "" "Zum Deaktivieren der Protokollierung sind Administratorrechte erforderlich." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:982 msgid "Administrator access is required to enable logging." msgstr "" "Zum Aktivieren der Protokollierung sind Administratorrechte erforderlich." @@ -84,7 +84,7 @@ msgstr "" msgid "Allow/deny rules, LAN logging, or anything else." msgstr "Allow/Deny-Regeln, LAN-Protokollierung oder irgendetwas anderes." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1979 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2004 msgid "Also seen" msgstr "Weitere" @@ -102,16 +102,16 @@ msgstr "" "Eine andere Änderung hat die WAN-Protokollierung nach dem Speichern " "überschrieben; prüfen Sie den aktuellen Zustand." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2874 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2899 msgid "Any action" msgstr "Beliebige Aktion" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1972 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1997 msgid "Any protocol" msgstr "Beliebiges Protokoll" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1473 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1545 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1498 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1570 msgid "Auto" msgstr "Auto" @@ -129,20 +129,20 @@ msgstr "" msgid "Changes:" msgstr "Änderungen:" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:485 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:486 msgid "Classic (green/red)" msgstr "Klassisch (grün/rot)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2579 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2604 msgid "Classic uses green/red; Accessible uses teal/orange" msgstr "Klassisch verwendet grün/rot; Barrierefrei verwendet teal/orange" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:274 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:94 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:274 msgid "Clear all" msgstr "Alle löschen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2947 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2972 msgid "" "Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for " "firewall settings · in Simple view, activate the message button or click a " @@ -153,7 +153,7 @@ msgstr "" "Nachrichtenschaltfläche aktivieren oder auf eine Zeile klicken, um die " "vollständige Meldung ein- oder auszublenden" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2659 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2684 msgid "" "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a " "chip) to exclude." @@ -161,7 +161,7 @@ msgstr "" "Klicken Sie auf eine IP, Aktion oder ein Protokoll, um zu filtern; nutzen " "Sie das Protokoll-Menü (oder ≠ auf einem Chip) zum Ausschließen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1973 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1998 msgid "Common" msgstr "Häufig" @@ -174,7 +174,7 @@ msgstr "" "Versionen, die diese Option unterstützen; Kernel/logd können das " "Gesamtvolumen weiterhin begrenzen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1779 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1804 msgid "Connection lost — retrying…" msgstr "Verbindung verloren — neuer Versuch…" @@ -186,53 +186,53 @@ msgstr "Die Protokollierungssperre konnte nicht erworben werden." msgid "Could not clear the WAN zone log option." msgstr "Die WAN-Zonen-Log-Option konnte nicht gelöscht werden." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:799 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:804 msgid "Could not create a temporary file for rule names." msgstr "Für die Regelnamen konnte keine temporäre Datei erstellt werden." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:988 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 msgid "Could not disable logging." msgstr "Protokollierung konnte nicht deaktiviert werden." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:966 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:971 msgid "Could not enable logging." msgstr "Protokollierung konnte nicht aktiviert werden." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:902 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:907 msgid "Could not load logging status." msgstr "Protokollstatus konnte nicht geladen werden." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:805 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:810 msgid "Could not load rule names." msgstr "Die Regelnamen konnten nicht geladen werden." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:801 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:806 msgid "Could not process firewall rule names." msgstr "Die Namen der Firewall-Regeln konnten nicht verarbeitet werden." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:945 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:978 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:994 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:950 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:983 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:999 msgid "Could not reach the router; check the current logging state." msgstr "" "Der Router konnte nicht erreicht werden; prüfen Sie den aktuellen " "Protokollstatus." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:803 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808 msgid "Could not read the active firewall rules." msgstr "Die aktiven Firewall-Regeln konnten nicht gelesen werden." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:901 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:906 msgid "Could not refresh logging status; showing the last known state." msgstr "" "Protokollstatus konnte nicht aktualisiert werden; zeige den zuletzt " "bekannten Zustand." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:810 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:815 msgid "Could not refresh rule names." msgstr "Die Regelnamen konnten nicht aktualisiert werden." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:813 msgid "Could not refresh rule names; previously loaded names are still shown." msgstr "" "Die Regelnamen konnten nicht aktualisiert werden; die zuvor geladenen Namen " @@ -256,7 +256,7 @@ msgstr "" msgid "Could not write the WAN zone log option." msgstr "Die WAN-Zonen-Log-Option konnte nicht geschrieben werden." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2902 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2927 msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set." msgstr "" "Eigenes Protokoll (! zum Ausschließen). Überschreibt das Menü, wenn gesetzt." @@ -270,7 +270,7 @@ msgstr "Ziel-Port" msgid "Destination" msgstr "Ziel" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2932 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2957 msgid "Destination IP contains (! to exclude)" msgstr "Ziel-IP enthält (! zum Ausschließen)" @@ -278,15 +278,15 @@ msgstr "Ziel-IP enthält (! zum Ausschließen)" msgid "Destination port" msgstr "Ziel-Port" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2937 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2962 msgid "Destination port (! to exclude)" msgstr "Ziel-Port (! zum Ausschließen)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2782 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2807 msgid "Detail" msgstr "Detail" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760 msgid "Details" msgstr "Details" @@ -298,7 +298,7 @@ msgstr "Richtung" msgid "Disabling…" msgstr "Wird deaktiviert…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1522 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1547 msgid "" "Display limited to %d rows on this device; the larger Limit still buffers " "rows." @@ -306,11 +306,11 @@ msgstr "" "Anzeige auf diesem Gerät auf %d Zeilen begrenzt; das größere Limit puffert " "weiterhin Zeilen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2520 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2545 msgid "Display options" msgstr "Anzeigeoptionen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2613 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638 msgid "" "Display options set Limit, Fetch budget, Maximum raw lines, row tint, " "palette, and hostnames." @@ -336,7 +336,7 @@ msgstr "" msgid "Enable logging" msgstr "Protokollierung aktivieren" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2608 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2633 msgid "" "Enable logging turns on WAN zone drop/reject logging only (same as Network → " "Firewall). It does not add rules or log normal LAN browsing." @@ -349,20 +349,20 @@ msgstr "" msgid "Enabling…" msgstr "Wird aktiviert…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1986 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2011 msgid "Exclude" msgstr "Ausschließen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190 msgid "Exclude instead" msgstr "Stattdessen ausschließen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2534 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2559 msgid "Fetch budget" msgstr "Abrufbudget" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2623 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648 msgid "" "Fetch budget controls raw log lines per poll. Auto derives from Limit; " "Manual selects a bounded maximum." @@ -370,10 +370,10 @@ msgstr "" "Das Abrufbudget steuert die Rohzeilen pro Abfrage. Auto leitet es aus dem " "Limit ab; Manuell wählt ein begrenztes Maximum." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:149 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:63 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:84 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2871 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:149 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2896 msgid "Filter by %s" msgstr "Filtern nach %s" @@ -383,12 +383,12 @@ msgstr "" "Logs nach Regel filtern (Hinweis: %s). Strg+Klick öffnet die Firewall-" "Einstellungen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2697 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2722 #: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3 msgid "Firewall Live View" msgstr "Firewall-Live-Ansicht" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1746 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1771 msgid "Firewall log has no current entries; showing rows last received %s ago." msgstr "" "Im Firewall-Protokoll sind derzeit keine Einträge; angezeigt werden Zeilen, " @@ -402,7 +402,7 @@ msgstr "Flags" msgid "Flow" msgstr "Fluss" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2633 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2658 msgid "" "For a responsive table on a weak device, keep Limit at 250 rows or below. " "The weak-device cap affects rendered rows; the buffer can still retain more." @@ -419,7 +419,7 @@ msgstr "Weitergeleitet" msgid "Grant access to firewall live log view" msgstr "Zugriff auf die Live-Ansicht der Firewall-Protokolle gewähren" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2599 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2624 msgid "Help" msgstr "Hilfe" @@ -427,15 +427,15 @@ msgstr "Hilfe" msgid "Hide full message" msgstr "Vollständige Nachricht ausblenden" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1620 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645 msgid "Hide rows" msgstr "Zeilen ausblenden" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1518 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1543 msgid "Hostname lookup paused while the router is under load." msgstr "Hostnamenauflösung pausiert, solange der Router ausgelastet ist." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1977 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2002 msgid "ICMPv6" msgstr "ICMPv6" @@ -443,7 +443,7 @@ msgstr "ICMPv6" msgid "IN" msgstr "EINGANG" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2670 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2695 msgid "" "If Row tint looks missing, the active LuCI theme may omit success/error or " "info/warn CSS variables; fwlive falls back to local colors (air-gapped, no " @@ -461,7 +461,7 @@ msgstr "" "Wenn das WAN ruhig ist, warten Sie auf Probes oder nutzen Sie den optionalen " "Ping-Test in der Hilfe / der Anleitung zum Aktivieren der Protokollierung." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2654 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2679 msgid "" "In Simple view, activate the message button or click a row to show or hide " "the full message." @@ -474,8 +474,8 @@ msgstr "" msgid "Inbound" msgstr "Eingehend" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:92 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190 msgid "Include instead" msgstr "Stattdessen einschließen" @@ -487,7 +487,7 @@ msgstr "" "Installieren Sie kmod-nf-log und kmod-nf-log6 mit dem Befehl für Ihre " "OpenWrt-Version und laden Sie dann die Firewall neu." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1777 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802 msgid "Installation is incomplete. Reinstall luci-app-fwlive." msgstr "Installation unvollständig. Installieren Sie luci-app-fwlive neu." @@ -496,7 +496,7 @@ msgstr "Installation unvollständig. Installieren Sie luci-app-fwlive neu." msgid "Interface" msgstr "Schnittstelle" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2917 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2942 msgid "Interface (prefix ! to exclude)" msgstr "Schnittstelle (! voranstellen zum Ausschließen)" @@ -513,11 +513,11 @@ msgstr "Kernel-Logmodule fehlen" msgid "Len" msgstr "Länge" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2523 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2548 msgid "Limit" msgstr "Limit" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:856 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:861 msgid "Live view may be incomplete — a legacy iptables table is registered" msgstr "" "Die Live-Ansicht kann unvollständig sein — eine veraltete iptables-Tabelle " @@ -527,16 +527,16 @@ msgstr "" msgid "Logging is off on this router" msgstr "Protokollierung ist auf diesem Router deaktiviert" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1473 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1546 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1498 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1571 msgid "Manual" msgstr "Manuell" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2552 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2577 msgid "Manual still uses server protection and poll cadence" msgstr "Manuell nutzt weiterhin Serverschutz und Abfrageintervall" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2628 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2653 msgid "" "Manual still uses server protection and poll cadence; it changes the fetch " "budget, not the polling interval." @@ -548,18 +548,18 @@ msgstr "" msgid "Manual test (System → Terminal):" msgstr "Manueller Test (System → Terminal):" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2546 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2571 msgid "Maximum raw lines" msgstr "Maximale Rohzeilen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:370 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:52 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2801 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2794 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:377 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2819 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2826 msgid "Message" msgstr "Nachricht" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2912 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2937 msgid "More filters" msgstr "Weitere Filter" @@ -608,7 +608,7 @@ msgstr "Nichts ändert sich, bis Sie auf „Protokollierung aktivieren“ klicke msgid "OUT" msgstr "AUSGANG" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2824 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2849 msgid "One line" msgstr "Einzeilig" @@ -638,20 +638,20 @@ msgstr "" msgid "Outbound" msgstr "Ausgehend" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2573 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2598 msgid "Palette" msgstr "Palette" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1840 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2748 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1865 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2773 msgid "Pause" msgstr "Pause" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1839 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1864 msgid "Paused" msgstr "Pausiert" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1505 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1530 msgid "" "Polling slowed: the router is responding slowly, so polls run less often." msgstr "" @@ -663,11 +663,11 @@ msgstr "" msgid "Proto" msgstr "Protokoll" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2893 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2918 msgid "Protocol — common values" msgstr "Protokoll — häufige Werte" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2864 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2889 msgid "Quick search" msgstr "Schnellsuche" @@ -675,21 +675,21 @@ msgstr "Schnellsuche" msgid "Rate limit:" msgstr "Ratenbegrenzung:" +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:229 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:230 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93 msgid "Remove filter" msgstr "Filter entfernen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1840 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1865 msgid "Resume" msgstr "Fortsetzen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2563 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2588 msgid "Row tint" msgstr "Zeileneinfärbung" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2664 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2689 msgid "" "Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/" "red, default) or Accessible (teal/orange). Action text stays colored either " @@ -699,7 +699,7 @@ msgstr "" "Klassisch (grün/rot, Standard) oder Barrierefrei (teal/orange). Aktionstext " "bleibt in beiden Fällen farbig." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2720 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2745 msgid "" "Row tint used a local color fallback because the active LuCI theme did not " "apply pass/deny backgrounds." @@ -711,19 +711,19 @@ msgstr "" msgid "Rule" msgstr "Regel" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760 msgid "Rule name details" msgstr "Details zu Regelnamen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:827 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:832 msgid "Rule-name lookup does not change your firewall rules." msgstr "Das Laden der Regelnamen verändert Ihre Firewall-Regeln nicht." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:812 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:817 msgid "Rule-name lookup failed." msgstr "Das Laden der Regelnamen ist fehlgeschlagen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:819 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:824 msgid "Rule-name lookup reached a safety limit." msgstr "Beim Laden der Regelnamen wurde eine Schutzgrenze erreicht." @@ -735,11 +735,11 @@ msgstr "Quell-Port" msgid "Search" msgstr "Suche" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1502 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1527 msgid "Server adaptive protection is disabled." msgstr "Adaptiver Serverschutz ist deaktiviert." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1510 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1535 msgid "" "Server limited fetch: the router is under load; at most %d log lines per " "poll." @@ -747,14 +747,14 @@ msgstr "" "Abruf vom Server begrenzt: Der Router ist ausgelastet; höchstens %d " "Protokollzeilen pro Abfrage." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1515 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1540 msgid "" "Server limited fetch: this poll returned fewer lines to protect the router." msgstr "" "Abruf vom Server begrenzt: Diese Abfrage lieferte weniger Zeilen, um den " "Router zu schonen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1500 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1525 msgid "Server protection state is unknown." msgstr "Serverschutz-Status unbekannt." @@ -762,28 +762,28 @@ msgstr "Serverschutz-Status unbekannt." msgid "Show full message" msgstr "Vollständige Nachricht anzeigen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2590 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2615 msgid "Show hostnames" msgstr "Hostnamen anzeigen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2561 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2586 msgid "Show pass/deny row background colors" msgstr "Pass/Deny-Zeilenhintergrundfarben anzeigen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1620 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2847 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2872 msgid "Show rows" msgstr "Zeilen anzeigen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1580 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1605 msgid "Showing a compact summary of the top of the shown sample." msgstr "Eine kompakte Zusammenfassung des Anfangs der angezeigten Stichprobe." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2771 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2796 msgid "Simple" msgstr "Einfach" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:830 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:835 msgid "" "Some entries may show names derived from their log prefixes. Searching by a " "friendly rule name may miss those entries." @@ -792,7 +792,7 @@ msgstr "" "angezeigt. Bei der Suche nach einem lesbaren Regelnamen werden diese " "Einträge möglicherweise nicht gefunden." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:850 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:855 msgid "Some rule names may be missing" msgstr "Einige Regelnamen fehlen möglicherweise" @@ -801,7 +801,7 @@ msgstr "Einige Regelnamen fehlen möglicherweise" msgid "Source" msgstr "Quelle" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2922 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2947 msgid "Source IP contains (! to exclude)" msgstr "Quell-IP enthält (! zum Ausschließen)" @@ -809,21 +809,21 @@ msgstr "Quell-IP enthält (! zum Ausschließen)" msgid "Source port" msgstr "Quell-Port" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2927 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2952 msgid "Source port (! to exclude)" msgstr "Quell-Port (! zum Ausschließen)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1582 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1607 msgid "Summary data is unavailable; show rows to inspect the sample." msgstr "" "Zusammenfassungsdaten nicht verfügbar; Zeilen anzeigen, um die Stichprobe zu " "prüfen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2838 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2863 msgid "Summary mode" msgstr "Zusammenfassungsmodus" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2618 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643 msgid "" "Summary mode: when the router responds slowly, a compact summary replaces " "the table. Use Show rows to see the table; it returns automatically after " @@ -833,13 +833,13 @@ msgstr "" "kompakte Zusammenfassung die Tabelle. Mit „Zeilen anzeigen“ sehen Sie die " "Tabelle; sie kehrt nach drei schnellen Abfragen automatisch zurück." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1584 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1609 msgid "Summary was bounded before all categories could be included." msgstr "" "Die Zusammenfassung wurde begrenzt, bevor alle Kategorien aufgenommen werden " "konnten." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2663 msgid "" "Switching tabs pauses polling; returning performs one catch-up poll. " "Hostnames are off by default because lookups add work." @@ -848,7 +848,7 @@ msgstr "" "eine einmalige Aufholabfrage. Hostnamen sind standardmäßig deaktiviert, da " "Auflösungen zusätzliche Arbeit verursachen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1675 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1700 msgid "" "Table refresh throttled: the event rate is high, so the table repaints less " "often to protect the browser. The buffer still updates; refresh resumes " @@ -864,7 +864,7 @@ msgstr "" "Die Firewall wurde nicht neu geladen; gespeicherte und aktive " "Protokollierung können voneinander abweichen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2673 msgid "" "The rate shown for WAN logging is the configured WAN zone log_limit, not " "proof the running fw4 supports it. The fw4 bundled with OpenWrt 23.05.5 " @@ -876,11 +876,11 @@ msgstr "" "mit OpenWrt 23.05.5 gelieferte fw4 ignoriert diese Option. Kernel/logd " "können das Gesamtvolumen weiterhin begrenzen; fwlive setzt kein Limit." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1778 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1803 msgid "The router could not read the firewall log — retrying…" msgstr "Der Router konnte das Firewall-Protokoll nicht lesen — neuer Versuch…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2668 msgid "" "The router log ring may evict older events before fwlive reads them. fwlive " "cannot recover evicted entries or change forwarding behavior." @@ -889,7 +889,7 @@ msgstr "" "fwlive sie liest. fwlive kann verdrängte Einträge nicht wiederherstellen und " "ändert das Weiterleitungsverhalten nicht." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2603 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2628 msgid "" "The table updates automatically when your firewall logs traffic. Use Pause " "if it moves too fast." @@ -897,7 +897,7 @@ msgstr "" "Die Tabelle aktualisiert sich automatisch, wenn Ihre Firewall Verkehr " "protokolliert. Pause nutzen, wenn es zu schnell wird." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2723 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2748 msgid "Theme tint fallback" msgstr "Theme-Farb-Fallback" @@ -916,15 +916,15 @@ msgstr "" msgid "Time" msgstr "Zeit" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1589 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1614 msgid "Top drops" msgstr "Häufigste Drops" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1592 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1617 msgid "Top rules" msgstr "Häufigste Regeln" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1586 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1611 msgid "Top talkers" msgstr "Häufigste Absender" @@ -952,17 +952,17 @@ msgstr "" msgid "Undo:" msgstr "Rückgängig:" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2667 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2692 msgid "Use Detail for all columns (flags, length, raw message)." msgstr "" "Verwenden Sie „Detail“ für alle Spalten (Flags, Länge, rohe Nachricht)." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2759 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2753 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2778 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2784 msgid "View" msgstr "Ansicht" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:989 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:994 msgid "WAN drop/reject logging is off." msgstr "WAN-Drop/Reject-Protokollierung ist aus." @@ -974,7 +974,7 @@ msgstr "" "WAN-Drop/Reject-Protokollierung ist an. Blockierter eingehender WAN-Verkehr " "erscheint hier. Normales LAN-Surfen nicht." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:970 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:975 msgid "" "WAN drop/reject logging is on. Blocked inbound traffic should appear here as " "it happens — not normal LAN browsing." @@ -982,7 +982,7 @@ msgstr "" "WAN-Drop/Reject-Protokollierung ist an. Blockierter eingehender Verkehr " "sollte hier in Echtzeit erscheinen — nicht normales LAN-Surfen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:972 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977 msgid "WAN logging is already enabled." msgstr "WAN-Protokollierung ist bereits aktiviert." @@ -1011,25 +1011,25 @@ msgstr "WAN-Protokollierung nicht verfügbar: keine WAN-Zone" msgid "Waiting for firewall events" msgstr "Warte auf Firewall-Ereignisse" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1839 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2711 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1864 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2736 msgid "Watching" msgstr "Beobachtet" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2813 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2838 msgid "Wrap" msgstr "Umbrechen" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:74 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2876 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2901 msgid "block" msgstr "blockiert" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1323 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1348 msgid "buffer filling" msgstr "Puffer füllt sich" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1327 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1352 msgid "buffer full" msgstr "Puffer voll" @@ -1041,7 +1041,7 @@ msgstr "konfiguriertes log_limit: %s" msgid "contains" msgstr "enthält" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1332 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1357 msgid "display limited to %d rows" msgstr "Anzeige auf %d Zeilen begrenzt" @@ -1050,11 +1050,11 @@ msgid "does not contain" msgstr "enthält nicht" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:75 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2877 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2902 msgid "drop" msgstr "verworfen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1336 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1361 msgid "hostname lookup paused" msgstr "Hostnamenauflösung pausiert" @@ -1062,11 +1062,11 @@ msgstr "Hostnamenauflösung pausiert" msgid "is" msgstr "ist" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1322 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1347 msgid "loading buffer" msgstr "Lade Puffer" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1764 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1789 msgid "loading…" msgstr "lädt…" @@ -1074,8 +1074,8 @@ msgstr "lädt…" msgid "no WAN rate limit configured" msgstr "Kein WAN-Ratenlimit konfiguriert" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1562 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1571 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1587 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1596 msgid "none" msgstr "keine" @@ -1083,98 +1083,98 @@ msgstr "keine" msgid "not" msgstr "nicht" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1994 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2019 msgid "not AH" msgstr "nicht AH" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1993 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2018 msgid "not ESP" msgstr "nicht ESP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1992 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2017 msgid "not GRE" msgstr "nicht GRE" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1989 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2014 msgid "not ICMP" msgstr "nicht ICMP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1990 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2015 msgid "not ICMPv6" msgstr "nicht ICMPv6" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1991 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2016 msgid "not IGMP" msgstr "nicht IGMP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1995 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2020 msgid "not SCTP" msgstr "nicht SCTP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1987 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2012 msgid "not TCP" msgstr "nicht TCP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1988 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2013 msgid "not UDP" msgstr "nicht UDP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2882 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2907 msgid "not block" msgstr "nicht blockieren" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2881 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2906 msgid "not drop" msgstr "nicht verwerfen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2880 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2905 msgid "not pass" msgstr "nicht erlauben" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2883 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2908 msgid "not reject" msgstr "nicht zurückweisen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2884 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2909 msgid "not unknown" msgstr "nicht unbekannt" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2900 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2925 msgid "or type…" msgstr "oder tippen…" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:73 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2875 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2900 msgid "pass" msgstr "erlaubt" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1334 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1359 msgid "polling slowed" msgstr "Abfragen verlangsamt" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:76 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2878 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2903 msgid "reject" msgstr "abgelehnt" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1338 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1363 msgid "scroll frozen — scroll to top to follow live" msgstr "Scrollen eingefroren — nach oben scrollen für Live-Ansicht" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1335 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1360 msgid "server limited fetch" msgstr "Abruf vom Server begrenzt" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1329 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1354 msgid "table refresh throttled" msgstr "Tabellenaktualisierung gedrosselt" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:65 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2879 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2904 msgid "unknown" msgstr "unbekannt" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:792 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:797 msgid "using fw4" msgstr "mit fw4" diff --git a/luci-app-fwlive/po/ru/luci-app-fwlive.po b/luci-app-fwlive/po/ru/luci-app-fwlive.po index c2ee40afe..80902298b 100644 --- a/luci-app-fwlive/po/ru/luci-app-fwlive.po +++ b/luci-app-fwlive/po/ru/luci-app-fwlive.po @@ -13,31 +13,31 @@ msgstr "" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1766 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1791 msgid "%d matching · %d/%d stored" msgstr "%d совпад. · сохранено %d/%d" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1731 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1756 msgid "%dd" msgstr "%dд" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1730 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1755 msgid "%dh" msgstr "%dч" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1729 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1754 msgid "%dm" msgstr "%dм" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1728 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1753 msgid "%ds" msgstr "%dс" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1492 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1517 msgid "%s · requested up to %d raw lines · %d firewall messages returned" msgstr "%s · запрошено до %d сырых строк · возвращено %d сообщений firewall" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1482 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507 msgid "" "%s · requested up to %d raw lines · server limited fetch to %d · %d firewall " "messages returned" @@ -49,17 +49,17 @@ msgstr "" msgid "%s: %s" msgstr "%s: %s" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1768 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1793 msgid "0 matching · %d/%d stored" msgstr "0 совпад. · сохранено %d/%d" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:486 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:487 msgid "Accessible (teal/orange)" msgstr "Доступный (бирюзовый/оранжевый)" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:440 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2871 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2896 msgid "Action" msgstr "Действие" @@ -70,11 +70,11 @@ msgstr "" "Активируйте кнопку сообщения или нажмите на строку, чтобы показать или " "скрыть полное сообщение" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:998 msgid "Administrator access is required to disable logging." msgstr "Для отключения журналирования требуются права администратора." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:982 msgid "Administrator access is required to enable logging." msgstr "Для включения журналирования требуются права администратора." @@ -82,7 +82,7 @@ msgstr "Для включения журналирования требуютс msgid "Allow/deny rules, LAN logging, or anything else." msgstr "Правила allow/deny, журналирование LAN и любые другие настройки." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1979 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2004 msgid "Also seen" msgstr "Также встречаются" @@ -100,16 +100,16 @@ msgstr "" "Другое изменение перезаписало журналирование WAN после сохранения; проверьте " "текущее состояние." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2874 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2899 msgid "Any action" msgstr "Любое действие" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1972 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1997 msgid "Any protocol" msgstr "Любой протокол" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1473 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1545 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1498 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1570 msgid "Auto" msgstr "Авто" @@ -126,20 +126,20 @@ msgstr "" msgid "Changes:" msgstr "Изменения:" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:485 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:486 msgid "Classic (green/red)" msgstr "Классический (зелёный/красный)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2579 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2604 msgid "Classic uses green/red; Accessible uses teal/orange" msgstr "Классический — зелёный/красный; доступный — бирюзовый/оранжевый" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:274 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:94 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:274 msgid "Clear all" msgstr "Очистить всё" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2947 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2972 msgid "" "Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for " "firewall settings · in Simple view, activate the message button or click a " @@ -149,7 +149,7 @@ msgstr "" "правилу для настроек МЭ · в Простом виде активируйте кнопку сообщения или " "нажмите на строку, чтобы показать или скрыть полное сообщение" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2659 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2684 msgid "" "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a " "chip) to exclude." @@ -157,7 +157,7 @@ msgstr "" "Щёлкните по IP, действию или протоколу для фильтрации; для исключения " "используйте меню «Протокол» (или ≠ на чипе)." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1973 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1998 msgid "Common" msgstr "Частые" @@ -170,7 +170,7 @@ msgstr "" "fw4 с поддержкой этой опции; ядро/logd всё ещё могут ограничивать общий " "объём журналов." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1779 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1804 msgid "Connection lost — retrying…" msgstr "Соединение потеряно — повторная попытка…" @@ -182,53 +182,53 @@ msgstr "Не удалось получить блокировку журнали msgid "Could not clear the WAN zone log option." msgstr "Не удалось очистить параметр журнала зоны WAN." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:799 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:804 msgid "Could not create a temporary file for rule names." msgstr "Не удалось создать временный файл для имён правил." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:988 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 msgid "Could not disable logging." msgstr "Не удалось отключить журналирование." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:966 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:971 msgid "Could not enable logging." msgstr "Не удалось включить журналирование." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:902 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:907 msgid "Could not load logging status." msgstr "Не удалось загрузить состояние журналирования." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:805 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:810 msgid "Could not load rule names." msgstr "Не удалось загрузить имена правил." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:801 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:806 msgid "Could not process firewall rule names." msgstr "Не удалось обработать имена правил межсетевого экрана." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:945 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:978 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:994 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:950 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:983 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:999 msgid "Could not reach the router; check the current logging state." msgstr "" "Не удалось связаться с маршрутизатором; проверьте текущее состояние " "журналирования." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:803 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808 msgid "Could not read the active firewall rules." msgstr "Не удалось прочитать действующие правила межсетевого экрана." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:901 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:906 msgid "Could not refresh logging status; showing the last known state." msgstr "" "Не удалось обновить состояние журналирования; показан последний известный " "статус." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:810 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:815 msgid "Could not refresh rule names." msgstr "Не удалось обновить имена правил." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:813 msgid "Could not refresh rule names; previously loaded names are still shown." msgstr "" "Не удалось обновить имена правил; отображаются ранее загруженные имена." @@ -251,7 +251,7 @@ msgstr "" msgid "Could not write the WAN zone log option." msgstr "Не удалось записать параметр журнала зоны WAN." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2902 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2927 msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set." msgstr "Свой протокол (! для исключения). Переопределяет меню, если задан." @@ -264,7 +264,7 @@ msgstr "Порт назн." msgid "Destination" msgstr "Назначение" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2932 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2957 msgid "Destination IP contains (! to exclude)" msgstr "IP назначения содержит (! для исключения)" @@ -272,15 +272,15 @@ msgstr "IP назначения содержит (! для исключения) msgid "Destination port" msgstr "Порт назначения" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2937 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2962 msgid "Destination port (! to exclude)" msgstr "Порт назначения (! для исключения)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2782 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2807 msgid "Detail" msgstr "Подробно" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760 msgid "Details" msgstr "Подробности" @@ -292,7 +292,7 @@ msgstr "Напр." msgid "Disabling…" msgstr "Отключение…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1522 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1547 msgid "" "Display limited to %d rows on this device; the larger Limit still buffers " "rows." @@ -300,11 +300,11 @@ msgstr "" "Отображение ограничено %d строками на этом устройстве; больший лимит по-" "прежнему буферизует строки." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2520 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2545 msgid "Display options" msgstr "Параметры отображения" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2613 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638 msgid "" "Display options set Limit, Fetch budget, Maximum raw lines, row tint, " "palette, and hostnames." @@ -330,7 +330,7 @@ msgstr "" msgid "Enable logging" msgstr "Включить журналирование" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2608 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2633 msgid "" "Enable logging turns on WAN zone drop/reject logging only (same as Network → " "Firewall). It does not add rules or log normal LAN browsing." @@ -343,20 +343,20 @@ msgstr "" msgid "Enabling…" msgstr "Включение…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1986 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2011 msgid "Exclude" msgstr "Исключить" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190 msgid "Exclude instead" msgstr "Исключить вместо этого" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2534 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2559 msgid "Fetch budget" msgstr "Бюджет выборки" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2623 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648 msgid "" "Fetch budget controls raw log lines per poll. Auto derives from Limit; " "Manual selects a bounded maximum." @@ -364,10 +364,10 @@ msgstr "" "Бюджет выборки задаёт число сырых строк журнала за опрос. «Авто» выводится " "из лимита; «Вручную» задаёт ограниченный максимум." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:149 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:63 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:84 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2871 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:149 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2896 msgid "Filter by %s" msgstr "Фильтр по %s" @@ -377,12 +377,12 @@ msgstr "" "Фильтр журналов по правилу (подсказка: %s). Ctrl+клик открывает настройки " "файрвола." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2697 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2722 #: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3 msgid "Firewall Live View" msgstr "Просмотр файрвола в реальном времени" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1746 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1771 msgid "Firewall log has no current entries; showing rows last received %s ago." msgstr "" "В журнале межсетевого экрана сейчас нет записей; показаны строки, полученные " @@ -396,7 +396,7 @@ msgstr "Флаги" msgid "Flow" msgstr "Поток" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2633 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2658 msgid "" "For a responsive table on a weak device, keep Limit at 250 rows or below. " "The weak-device cap affects rendered rows; the buffer can still retain more." @@ -413,7 +413,7 @@ msgstr "Транзитный" msgid "Grant access to firewall live log view" msgstr "Предоставить доступ к живому просмотру журнала межсетевого экрана" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2599 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2624 msgid "Help" msgstr "Справка" @@ -421,15 +421,15 @@ msgstr "Справка" msgid "Hide full message" msgstr "Скрыть полное сообщение" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1620 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645 msgid "Hide rows" msgstr "Скрыть строки" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1518 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1543 msgid "Hostname lookup paused while the router is under load." msgstr "Разрешение имён приостановлено из-за нагрузки на маршрутизатор." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1977 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2002 msgid "ICMPv6" msgstr "ICMPv6" @@ -437,7 +437,7 @@ msgstr "ICMPv6" msgid "IN" msgstr "ВХОД" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2670 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2695 msgid "" "If Row tint looks missing, the active LuCI theme may omit success/error or " "info/warn CSS variables; fwlive falls back to local colors (air-gapped, no " @@ -455,7 +455,7 @@ msgstr "" "Если WAN неактивен, дождитесь проб или используйте необязательную проверку " "ping в Справке / руководстве по включению журналов." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2654 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2679 msgid "" "In Simple view, activate the message button or click a row to show or hide " "the full message." @@ -467,8 +467,8 @@ msgstr "" msgid "Inbound" msgstr "Входящий" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:92 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190 msgid "Include instead" msgstr "Включить вместо этого" @@ -480,7 +480,7 @@ msgstr "" "Установите kmod-nf-log и kmod-nf-log6 командой для вашей версии OpenWrt, " "затем перезагрузите файрвол." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1777 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802 msgid "Installation is incomplete. Reinstall luci-app-fwlive." msgstr "Установка не завершена. Переустановите luci-app-fwlive." @@ -489,7 +489,7 @@ msgstr "Установка не завершена. Переустановите msgid "Interface" msgstr "Интерфейс" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2917 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2942 msgid "Interface (prefix ! to exclude)" msgstr "Интерфейс (! в начале для исключения)" @@ -506,11 +506,11 @@ msgstr "Отсутствуют модули журналирования ядр msgid "Len" msgstr "Длина" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2523 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2548 msgid "Limit" msgstr "Лимит" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:856 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:861 msgid "Live view may be incomplete — a legacy iptables table is registered" msgstr "" "Живой просмотр может быть неполным — зарегистрирована устаревшая таблица " @@ -520,16 +520,16 @@ msgstr "" msgid "Logging is off on this router" msgstr "Журналирование на этом роутере отключено" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1473 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1546 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1498 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1571 msgid "Manual" msgstr "Вручную" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2552 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2577 msgid "Manual still uses server protection and poll cadence" msgstr "«Вручную» по-прежнему использует защиту сервера и частоту опроса" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2628 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2653 msgid "" "Manual still uses server protection and poll cadence; it changes the fetch " "budget, not the polling interval." @@ -541,18 +541,18 @@ msgstr "" msgid "Manual test (System → Terminal):" msgstr "Ручной тест (Система → Терминал):" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2546 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2571 msgid "Maximum raw lines" msgstr "Максимум сырых строк" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:370 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:52 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2801 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2794 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:377 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2819 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2826 msgid "Message" msgstr "Сообщение" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2912 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2937 msgid "More filters" msgstr "Больше фильтров" @@ -599,7 +599,7 @@ msgstr "Ничего не изменится, пока вы не нажмёте msgid "OUT" msgstr "ВЫХОД" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2824 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2849 msgid "One line" msgstr "Одна строка" @@ -629,20 +629,20 @@ msgstr "" msgid "Outbound" msgstr "Исходящий" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2573 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2598 msgid "Palette" msgstr "Палитра" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1840 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2748 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1865 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2773 msgid "Pause" msgstr "Пауза" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1839 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1864 msgid "Paused" msgstr "Пауза" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1505 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1530 msgid "" "Polling slowed: the router is responding slowly, so polls run less often." msgstr "" @@ -654,11 +654,11 @@ msgstr "" msgid "Proto" msgstr "Протокол" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2893 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2918 msgid "Protocol — common values" msgstr "Протокол — частые значения" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2864 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2889 msgid "Quick search" msgstr "Быстрый поиск" @@ -666,21 +666,21 @@ msgstr "Быстрый поиск" msgid "Rate limit:" msgstr "Ограничение частоты:" +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:229 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:230 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93 msgid "Remove filter" msgstr "Удалить фильтр" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1840 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1865 msgid "Resume" msgstr "Продолжить" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2563 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2588 msgid "Row tint" msgstr "Цвет строк" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2664 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2689 msgid "" "Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/" "red, default) or Accessible (teal/orange). Action text stays colored either " @@ -690,7 +690,7 @@ msgstr "" "классический (зелёный/красный, по умолчанию) или доступный (бирюзовый/" "оранжевый). Цвет текста действия сохраняется в любом случае." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2720 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2745 msgid "" "Row tint used a local color fallback because the active LuCI theme did not " "apply pass/deny backgrounds." @@ -702,19 +702,19 @@ msgstr "" msgid "Rule" msgstr "Правило" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760 msgid "Rule name details" msgstr "Подробности об именах правил" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:827 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:832 msgid "Rule-name lookup does not change your firewall rules." msgstr "Получение имён правил не изменяет правила межсетевого экрана." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:812 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:817 msgid "Rule-name lookup failed." msgstr "Не удалось получить имена правил." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:819 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:824 msgid "Rule-name lookup reached a safety limit." msgstr "При загрузке имён правил достигнут защитный лимит." @@ -726,11 +726,11 @@ msgstr "Порт ист." msgid "Search" msgstr "Поиск" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1502 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1527 msgid "Server adaptive protection is disabled." msgstr "Адаптивная защита сервера отключена." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1510 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1535 msgid "" "Server limited fetch: the router is under load; at most %d log lines per " "poll." @@ -738,14 +738,14 @@ msgstr "" "Сервер ограничил выборку: маршрутизатор нагружен; не более %d строк журнала " "за опрос." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1515 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1540 msgid "" "Server limited fetch: this poll returned fewer lines to protect the router." msgstr "" "Сервер ограничил выборку: этот опрос вернул меньше строк для защиты " "маршрутизатора." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1500 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1525 msgid "Server protection state is unknown." msgstr "Состояние защиты сервера неизвестно." @@ -753,28 +753,28 @@ msgstr "Состояние защиты сервера неизвестно." msgid "Show full message" msgstr "Показать полное сообщение" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2590 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2615 msgid "Show hostnames" msgstr "Показывать имена узлов" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2561 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2586 msgid "Show pass/deny row background colors" msgstr "Показывать цвета фона строк pass/deny" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1620 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2847 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2872 msgid "Show rows" msgstr "Показать строки" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1580 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1605 msgid "Showing a compact summary of the top of the shown sample." msgstr "Показана краткая сводка верхней части отображаемой выборки." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2771 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2796 msgid "Simple" msgstr "Простой" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:830 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:835 msgid "" "Some entries may show names derived from their log prefixes. Searching by a " "friendly rule name may miss those entries." @@ -782,7 +782,7 @@ msgstr "" "Некоторые записи могут показывать имена, полученные из префиксов журнала. " "Поиск по понятному имени правила может не найти такие записи." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:850 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:855 msgid "Some rule names may be missing" msgstr "Некоторые имена правил могут отсутствовать" @@ -791,7 +791,7 @@ msgstr "Некоторые имена правил могут отсутство msgid "Source" msgstr "Источник" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2922 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2947 msgid "Source IP contains (! to exclude)" msgstr "IP источника содержит (! для исключения)" @@ -799,19 +799,19 @@ msgstr "IP источника содержит (! для исключения)" msgid "Source port" msgstr "Порт источника" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2927 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2952 msgid "Source port (! to exclude)" msgstr "Порт источника (! для исключения)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1582 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1607 msgid "Summary data is unavailable; show rows to inspect the sample." msgstr "Данные сводки недоступны; покажите строки для проверки выборки." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2838 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2863 msgid "Summary mode" msgstr "Режим сводки" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2618 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643 msgid "" "Summary mode: when the router responds slowly, a compact summary replaces " "the table. Use Show rows to see the table; it returns automatically after " @@ -821,11 +821,11 @@ msgstr "" "показывается краткая сводка. Нажмите «Показать строки», чтобы увидеть " "таблицу; она вернётся автоматически после трёх быстрых опросов." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1584 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1609 msgid "Summary was bounded before all categories could be included." msgstr "Сводка ограничена до включения всех категорий." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2663 msgid "" "Switching tabs pauses polling; returning performs one catch-up poll. " "Hostnames are off by default because lookups add work." @@ -834,7 +834,7 @@ msgstr "" "один догоняющий опрос. Имена хостов по умолчанию отключены, поскольку их " "разрешение создаёт дополнительную нагрузку." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1675 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1700 msgid "" "Table refresh throttled: the event rate is high, so the table repaints less " "often to protect the browser. The buffer still updates; refresh resumes " @@ -850,7 +850,7 @@ msgstr "" "Брандмауэр не перезагрузился; сохранённые и активные настройки " "журналирования могут различаться." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2673 msgid "" "The rate shown for WAN logging is the configured WAN zone log_limit, not " "proof the running fw4 supports it. The fw4 bundled with OpenWrt 23.05.5 " @@ -862,13 +862,13 @@ msgstr "" "OpenWrt 23.05.5 эта опция игнорируется. Ядро/logd всё ещё могут ограничивать " "общий объём журналов; fwlive не задаёт ограничение." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1778 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1803 msgid "The router could not read the firewall log — retrying…" msgstr "" "Маршрутизатор не смог прочитать журнал межсетевого экрана — повторная " "попытка…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2668 msgid "" "The router log ring may evict older events before fwlive reads them. fwlive " "cannot recover evicted entries or change forwarding behavior." @@ -877,7 +877,7 @@ msgstr "" "fwlive их прочитает. fwlive не может восстановить вытесненные записи и не " "изменяет пересылку." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2603 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2628 msgid "" "The table updates automatically when your firewall logs traffic. Use Pause " "if it moves too fast." @@ -885,7 +885,7 @@ msgstr "" "Таблица обновляется автоматически, когда файрвол пишет в журнал. Используйте " "Паузу, если слишком быстро." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2723 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2748 msgid "Theme tint fallback" msgstr "Запасной цвет темы" @@ -904,15 +904,15 @@ msgstr "" msgid "Time" msgstr "Время" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1589 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1614 msgid "Top drops" msgstr "Частые сбросы" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1592 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1617 msgid "Top rules" msgstr "Частые правила" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1586 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1611 msgid "Top talkers" msgstr "Частые источники" @@ -939,17 +939,17 @@ msgstr "" msgid "Undo:" msgstr "Отмена:" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2667 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2692 msgid "Use Detail for all columns (flags, length, raw message)." msgstr "" "Используйте «Подробно» для всех столбцов (флаги, длина, исходное сообщение)." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2759 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2753 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2778 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2784 msgid "View" msgstr "Вид" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:989 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:994 msgid "WAN drop/reject logging is off." msgstr "Журналирование drop/reject для WAN отключено." @@ -961,7 +961,7 @@ msgstr "" "Журналирование drop/reject для WAN включено. Заблокированный входящий WAN-" "трафик будет отображаться здесь. Обычный LAN-трафик — нет." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:970 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:975 msgid "" "WAN drop/reject logging is on. Blocked inbound traffic should appear here as " "it happens — not normal LAN browsing." @@ -970,7 +970,7 @@ msgstr "" "должен появляться здесь по мере поступления — обычный LAN-трафик не " "отображается." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:972 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977 msgid "WAN logging is already enabled." msgstr "WAN-журналирование уже включено." @@ -999,25 +999,25 @@ msgstr "WAN-журналирование недоступно: нет зоны W msgid "Waiting for firewall events" msgstr "Ожидание событий межсетевого экрана" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1839 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2711 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1864 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2736 msgid "Watching" msgstr "Наблюдение" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2813 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2838 msgid "Wrap" msgstr "Перенос" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:74 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2876 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2901 msgid "block" msgstr "заблокирован" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1323 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1348 msgid "buffer filling" msgstr "буфер заполняется" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1327 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1352 msgid "buffer full" msgstr "буфер заполнен" @@ -1029,7 +1029,7 @@ msgstr "настроенный log_limit: %s" msgid "contains" msgstr "содержит" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1332 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1357 msgid "display limited to %d rows" msgstr "отображение ограничено %d строками" @@ -1038,11 +1038,11 @@ msgid "does not contain" msgstr "не содержит" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:75 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2877 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2902 msgid "drop" msgstr "отброшен" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1336 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1361 msgid "hostname lookup paused" msgstr "разрешение имён приостановлено" @@ -1050,11 +1050,11 @@ msgstr "разрешение имён приостановлено" msgid "is" msgstr "есть" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1322 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1347 msgid "loading buffer" msgstr "загрузка буфера" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1764 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1789 msgid "loading…" msgstr "загрузка…" @@ -1062,8 +1062,8 @@ msgstr "загрузка…" msgid "no WAN rate limit configured" msgstr "Ограничение частоты WAN не настроено" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1562 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1571 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1587 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1596 msgid "none" msgstr "нет" @@ -1071,99 +1071,99 @@ msgstr "нет" msgid "not" msgstr "не" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1994 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2019 msgid "not AH" msgstr "не AH" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1993 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2018 msgid "not ESP" msgstr "не ESP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1992 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2017 msgid "not GRE" msgstr "не GRE" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1989 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2014 msgid "not ICMP" msgstr "не ICMP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1990 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2015 msgid "not ICMPv6" msgstr "не ICMPv6" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1991 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2016 msgid "not IGMP" msgstr "не IGMP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1995 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2020 msgid "not SCTP" msgstr "не SCTP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1987 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2012 msgid "not TCP" msgstr "не TCP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1988 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2013 msgid "not UDP" msgstr "не UDP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2882 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2907 msgid "not block" msgstr "не блокировать" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2881 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2906 msgid "not drop" msgstr "не отбрасывать" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2880 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2905 msgid "not pass" msgstr "не пропускать" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2883 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2908 msgid "not reject" msgstr "не отклонять" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2884 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2909 msgid "not unknown" msgstr "не неизвестно" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2900 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2925 msgid "or type…" msgstr "или введите…" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:73 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2875 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2900 msgid "pass" msgstr "разрешён" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1334 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1359 msgid "polling slowed" msgstr "опрос замедлен" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:76 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2878 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2903 msgid "reject" msgstr "отклонён" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1338 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1363 msgid "scroll frozen — scroll to top to follow live" msgstr "" "прокрутка заморожена — прокрутите вверх, чтобы следить в реальном времени" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1335 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1360 msgid "server limited fetch" msgstr "сервер ограничил выборку" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1329 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1354 msgid "table refresh throttled" msgstr "обновление таблицы ограничено" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:65 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2879 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2904 msgid "unknown" msgstr "неизвестно" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:792 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:797 msgid "using fw4" msgstr "используется fw4" diff --git a/luci-app-fwlive/po/templates/luci-app-fwlive.pot b/luci-app-fwlive/po/templates/luci-app-fwlive.pot index dfff579cf..731dc1b05 100644 --- a/luci-app-fwlive/po/templates/luci-app-fwlive.pot +++ b/luci-app-fwlive/po/templates/luci-app-fwlive.pot @@ -1,31 +1,31 @@ msgid "" msgstr "Content-Type: text/plain; charset=UTF-8" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1766 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1791 msgid "%d matching · %d/%d stored" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1731 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1756 msgid "%dd" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1730 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1755 msgid "%dh" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1729 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1754 msgid "%dm" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1728 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1753 msgid "%ds" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1492 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1517 msgid "%s · requested up to %d raw lines · %d firewall messages returned" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1482 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507 msgid "" "%s · requested up to %d raw lines · server limited fetch to %d · %d firewall " "messages returned" @@ -35,17 +35,17 @@ msgstr "" msgid "%s: %s" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1768 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1793 msgid "0 matching · %d/%d stored" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:486 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:487 msgid "Accessible (teal/orange)" msgstr "" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:440 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2871 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2896 msgid "Action" msgstr "" @@ -54,11 +54,11 @@ msgid "" "Activate the message button or click a row to show or hide the full message" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:998 msgid "Administrator access is required to disable logging." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:982 msgid "Administrator access is required to enable logging." msgstr "" @@ -66,7 +66,7 @@ msgstr "" msgid "Allow/deny rules, LAN logging, or anything else." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1979 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2004 msgid "Also seen" msgstr "" @@ -80,16 +80,16 @@ msgid "" "state." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2874 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2899 msgid "Any action" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1972 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1997 msgid "Any protocol" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1473 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1545 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1498 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1570 msgid "Auto" msgstr "" @@ -105,33 +105,33 @@ msgstr "" msgid "Changes:" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:485 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:486 msgid "Classic (green/red)" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2579 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2604 msgid "Classic uses green/red; Accessible uses teal/orange" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:274 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:94 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:274 msgid "Clear all" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2947 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2972 msgid "" "Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for " "firewall settings · in Simple view, activate the message button or click a " "row to show or hide the full message" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2659 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2684 msgid "" "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a " "chip) to exclude." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1973 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1998 msgid "Common" msgstr "" @@ -141,7 +141,7 @@ msgid "" "it; kernel/logd may still constrain total log volume." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1779 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1804 msgid "Connection lost — retrying…" msgstr "" @@ -153,49 +153,49 @@ msgstr "" msgid "Could not clear the WAN zone log option." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:799 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:804 msgid "Could not create a temporary file for rule names." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:988 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 msgid "Could not disable logging." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:966 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:971 msgid "Could not enable logging." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:902 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:907 msgid "Could not load logging status." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:805 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:810 msgid "Could not load rule names." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:801 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:806 msgid "Could not process firewall rule names." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:945 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:978 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:994 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:950 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:983 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:999 msgid "Could not reach the router; check the current logging state." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:803 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808 msgid "Could not read the active firewall rules." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:901 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:906 msgid "Could not refresh logging status; showing the last known state." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:810 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:815 msgid "Could not refresh rule names." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:813 msgid "Could not refresh rule names; previously loaded names are still shown." msgstr "" @@ -215,7 +215,7 @@ msgstr "" msgid "Could not write the WAN zone log option." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2902 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2927 msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set." msgstr "" @@ -228,7 +228,7 @@ msgstr "" msgid "Destination" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2932 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2957 msgid "Destination IP contains (! to exclude)" msgstr "" @@ -236,15 +236,15 @@ msgstr "" msgid "Destination port" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2937 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2962 msgid "Destination port (! to exclude)" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2782 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2807 msgid "Detail" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760 msgid "Details" msgstr "" @@ -256,17 +256,17 @@ msgstr "" msgid "Disabling…" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1522 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1547 msgid "" "Display limited to %d rows on this device; the larger Limit still buffers " "rows." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2520 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2545 msgid "Display options" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2613 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638 msgid "" "Display options set Limit, Fetch budget, Maximum raw lines, row tint, " "palette, and hostnames." @@ -288,7 +288,7 @@ msgstr "" msgid "Enable logging" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2608 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2633 msgid "" "Enable logging turns on WAN zone drop/reject logging only (same as Network → " "Firewall). It does not add rules or log normal LAN browsing." @@ -298,29 +298,29 @@ msgstr "" msgid "Enabling…" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1986 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2011 msgid "Exclude" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190 msgid "Exclude instead" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2534 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2559 msgid "Fetch budget" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2623 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648 msgid "" "Fetch budget controls raw log lines per poll. Auto derives from Limit; " "Manual selects a bounded maximum." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:149 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:63 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:84 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2871 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:149 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2896 msgid "Filter by %s" msgstr "" @@ -328,12 +328,12 @@ msgstr "" msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2697 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2722 #: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3 msgid "Firewall Live View" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1746 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1771 msgid "Firewall log has no current entries; showing rows last received %s ago." msgstr "" @@ -345,7 +345,7 @@ msgstr "" msgid "Flow" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2633 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2658 msgid "" "For a responsive table on a weak device, keep Limit at 250 rows or below. " "The weak-device cap affects rendered rows; the buffer can still retain more." @@ -359,7 +359,7 @@ msgstr "" msgid "Grant access to firewall live log view" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2599 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2624 msgid "Help" msgstr "" @@ -367,15 +367,15 @@ msgstr "" msgid "Hide full message" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1620 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645 msgid "Hide rows" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1518 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1543 msgid "Hostname lookup paused while the router is under load." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1977 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2002 msgid "ICMPv6" msgstr "" @@ -383,7 +383,7 @@ msgstr "" msgid "IN" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2670 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2695 msgid "" "If Row tint looks missing, the active LuCI theme may omit success/error or " "info/warn CSS variables; fwlive falls back to local colors (air-gapped, no " @@ -396,7 +396,7 @@ msgid "" "Help / the enabling-logs guide." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2654 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2679 msgid "" "In Simple view, activate the message button or click a row to show or hide " "the full message." @@ -406,8 +406,8 @@ msgstr "" msgid "Inbound" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:92 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190 msgid "Include instead" msgstr "" @@ -417,7 +417,7 @@ msgid "" "release, then reload the firewall." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1777 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802 msgid "Installation is incomplete. Reinstall luci-app-fwlive." msgstr "" @@ -426,7 +426,7 @@ msgstr "" msgid "Interface" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2917 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2942 msgid "Interface (prefix ! to exclude)" msgstr "" @@ -443,11 +443,11 @@ msgstr "" msgid "Len" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2523 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2548 msgid "Limit" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:856 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:861 msgid "Live view may be incomplete — a legacy iptables table is registered" msgstr "" @@ -455,16 +455,16 @@ msgstr "" msgid "Logging is off on this router" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1473 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1546 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1498 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1571 msgid "Manual" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2552 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2577 msgid "Manual still uses server protection and poll cadence" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2628 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2653 msgid "" "Manual still uses server protection and poll cadence; it changes the fetch " "budget, not the polling interval." @@ -474,18 +474,18 @@ msgstr "" msgid "Manual test (System → Terminal):" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2546 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2571 msgid "Maximum raw lines" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:370 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:52 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2801 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2794 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:377 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2819 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2826 msgid "Message" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2912 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2937 msgid "More filters" msgstr "" @@ -528,7 +528,7 @@ msgstr "" msgid "OUT" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2824 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2849 msgid "One line" msgstr "" @@ -555,20 +555,20 @@ msgstr "" msgid "Outbound" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2573 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2598 msgid "Palette" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1840 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2748 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1865 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2773 msgid "Pause" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1839 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1864 msgid "Paused" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1505 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1530 msgid "" "Polling slowed: the router is responding slowly, so polls run less often." msgstr "" @@ -578,11 +578,11 @@ msgstr "" msgid "Proto" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2893 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2918 msgid "Protocol — common values" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2864 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2889 msgid "Quick search" msgstr "" @@ -590,28 +590,28 @@ msgstr "" msgid "Rate limit:" msgstr "" +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:229 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:230 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93 msgid "Remove filter" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1840 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1865 msgid "Resume" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2563 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2588 msgid "Row tint" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2664 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2689 msgid "" "Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/" "red, default) or Accessible (teal/orange). Action text stays colored either " "way." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2720 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2745 msgid "" "Row tint used a local color fallback because the active LuCI theme did not " "apply pass/deny backgrounds." @@ -621,19 +621,19 @@ msgstr "" msgid "Rule" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760 msgid "Rule name details" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:827 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:832 msgid "Rule-name lookup does not change your firewall rules." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:812 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:817 msgid "Rule-name lookup failed." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:819 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:824 msgid "Rule-name lookup reached a safety limit." msgstr "" @@ -645,22 +645,22 @@ msgstr "" msgid "Search" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1502 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1527 msgid "Server adaptive protection is disabled." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1510 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1535 msgid "" "Server limited fetch: the router is under load; at most %d log lines per " "poll." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1515 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1540 msgid "" "Server limited fetch: this poll returned fewer lines to protect the router." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1500 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1525 msgid "Server protection state is unknown." msgstr "" @@ -668,34 +668,34 @@ msgstr "" msgid "Show full message" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2590 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2615 msgid "Show hostnames" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2561 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2586 msgid "Show pass/deny row background colors" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1620 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2847 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2872 msgid "Show rows" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1580 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1605 msgid "Showing a compact summary of the top of the shown sample." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2771 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2796 msgid "Simple" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:830 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:835 msgid "" "Some entries may show names derived from their log prefixes. Searching by a " "friendly rule name may miss those entries." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:850 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:855 msgid "Some rule names may be missing" msgstr "" @@ -704,7 +704,7 @@ msgstr "" msgid "Source" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2922 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2947 msgid "Source IP contains (! to exclude)" msgstr "" @@ -712,36 +712,36 @@ msgstr "" msgid "Source port" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2927 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2952 msgid "Source port (! to exclude)" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1582 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1607 msgid "Summary data is unavailable; show rows to inspect the sample." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2838 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2863 msgid "Summary mode" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2618 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643 msgid "" "Summary mode: when the router responds slowly, a compact summary replaces " "the table. Use Show rows to see the table; it returns automatically after " "three fast polls." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1584 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1609 msgid "Summary was bounded before all categories could be included." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2663 msgid "" "Switching tabs pauses polling; returning performs one catch-up poll. " "Hostnames are off by default because lookups add work." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1675 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1700 msgid "" "Table refresh throttled: the event rate is high, so the table repaints less " "often to protect the browser. The buffer still updates; refresh resumes " @@ -752,7 +752,7 @@ msgstr "" msgid "The firewall did not reload; saved and live logging may differ." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2673 msgid "" "The rate shown for WAN logging is the configured WAN zone log_limit, not " "proof the running fw4 supports it. The fw4 bundled with OpenWrt 23.05.5 " @@ -760,23 +760,23 @@ msgid "" "fwlive does not impose a limit." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1778 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1803 msgid "The router could not read the firewall log — retrying…" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2668 msgid "" "The router log ring may evict older events before fwlive reads them. fwlive " "cannot recover evicted entries or change forwarding behavior." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2603 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2628 msgid "" "The table updates automatically when your firewall logs traffic. Use Pause " "if it moves too fast." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2723 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2748 msgid "Theme tint fallback" msgstr "" @@ -793,15 +793,15 @@ msgstr "" msgid "Time" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1589 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1614 msgid "Top drops" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1592 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1617 msgid "Top rules" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1586 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1611 msgid "Top talkers" msgstr "" @@ -823,16 +823,16 @@ msgstr "" msgid "Undo:" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2667 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2692 msgid "Use Detail for all columns (flags, length, raw message)." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2759 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2753 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2778 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2784 msgid "View" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:989 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:994 msgid "WAN drop/reject logging is off." msgstr "" @@ -842,13 +842,13 @@ msgid "" "here. Normal LAN browsing will not." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:970 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:975 msgid "" "WAN drop/reject logging is on. Blocked inbound traffic should appear here as " "it happens — not normal LAN browsing." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:972 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977 msgid "WAN logging is already enabled." msgstr "" @@ -877,25 +877,25 @@ msgstr "" msgid "Waiting for firewall events" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1839 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2711 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1864 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2736 msgid "Watching" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2813 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2838 msgid "Wrap" msgstr "" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:74 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2876 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2901 msgid "block" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1323 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1348 msgid "buffer filling" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1327 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1352 msgid "buffer full" msgstr "" @@ -907,7 +907,7 @@ msgstr "" msgid "contains" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1332 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1357 msgid "display limited to %d rows" msgstr "" @@ -916,11 +916,11 @@ msgid "does not contain" msgstr "" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:75 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2877 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2902 msgid "drop" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1336 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1361 msgid "hostname lookup paused" msgstr "" @@ -928,11 +928,11 @@ msgstr "" msgid "is" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1322 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1347 msgid "loading buffer" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1764 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1789 msgid "loading…" msgstr "" @@ -940,8 +940,8 @@ msgstr "" msgid "no WAN rate limit configured" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1562 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1571 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1587 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1596 msgid "none" msgstr "" @@ -949,98 +949,98 @@ msgstr "" msgid "not" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1994 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2019 msgid "not AH" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1993 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2018 msgid "not ESP" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1992 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2017 msgid "not GRE" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1989 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2014 msgid "not ICMP" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1990 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2015 msgid "not ICMPv6" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1991 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2016 msgid "not IGMP" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1995 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2020 msgid "not SCTP" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1987 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2012 msgid "not TCP" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1988 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2013 msgid "not UDP" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2882 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2907 msgid "not block" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2881 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2906 msgid "not drop" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2880 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2905 msgid "not pass" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2883 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2908 msgid "not reject" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2884 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2909 msgid "not unknown" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2900 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2925 msgid "or type…" msgstr "" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:73 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2875 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2900 msgid "pass" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1334 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1359 msgid "polling slowed" msgstr "" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:76 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2878 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2903 msgid "reject" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1338 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1363 msgid "scroll frozen — scroll to top to follow live" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1335 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1360 msgid "server limited fetch" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1329 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1354 msgid "table refresh throttled" msgstr "" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:65 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2879 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2904 msgid "unknown" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:792 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:797 msgid "using fw4" msgstr "" diff --git a/luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po b/luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po index 1570f334d..b2f687f42 100644 --- a/luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po +++ b/luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po @@ -12,31 +12,31 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1766 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1791 msgid "%d matching · %d/%d stored" msgstr "%d 条匹配 · 已存 %d/%d" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1731 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1756 msgid "%dd" msgstr "%d天" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1730 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1755 msgid "%dh" msgstr "%d小时" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1729 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1754 msgid "%dm" msgstr "%d分钟" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1728 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1753 msgid "%ds" msgstr "%d秒" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1492 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1517 msgid "%s · requested up to %d raw lines · %d firewall messages returned" msgstr "%s · 请求最多 %d 行原始日志 · 返回 %d 条防火墙消息" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1482 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507 msgid "" "%s · requested up to %d raw lines · server limited fetch to %d · %d firewall " "messages returned" @@ -47,17 +47,17 @@ msgstr "" msgid "%s: %s" msgstr "%s: %s" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1768 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1793 msgid "0 matching · %d/%d stored" msgstr "0 条匹配 · 已存 %d/%d" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:486 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:487 msgid "Accessible (teal/orange)" msgstr "无障碍(青绿/橙色)" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:440 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2871 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2896 msgid "Action" msgstr "动作" @@ -66,11 +66,11 @@ msgid "" "Activate the message button or click a row to show or hide the full message" msgstr "激活消息按钮或单击行即可显示或隐藏完整消息" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:998 msgid "Administrator access is required to disable logging." msgstr "需要管理员权限才能禁用日志记录。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:982 msgid "Administrator access is required to enable logging." msgstr "需要管理员权限才能启用日志记录。" @@ -78,7 +78,7 @@ msgstr "需要管理员权限才能启用日志记录。" msgid "Allow/deny rules, LAN logging, or anything else." msgstr "允许/拒绝规则、LAN 日志记录或任何其他设置。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1979 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2004 msgid "Also seen" msgstr "其他常见" @@ -92,16 +92,16 @@ msgid "" "state." msgstr "保存后另有更改覆盖了 WAN 日志设置;请检查当前状态。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2874 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2899 msgid "Any action" msgstr "所有动作" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1972 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1997 msgid "Any protocol" msgstr "任意协议" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1473 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1545 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1498 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1570 msgid "Auto" msgstr "自动" @@ -117,20 +117,20 @@ msgstr "无法启用日志记录,直到内核日志模块已安装。" msgid "Changes:" msgstr "变更:" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:485 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:486 msgid "Classic (green/red)" msgstr "经典(绿/红)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2579 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2604 msgid "Classic uses green/red; Accessible uses teal/orange" msgstr "经典使用绿/红;无障碍使用青绿/橙色" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:274 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:94 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:274 msgid "Clear all" msgstr "全部清除" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2947 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2972 msgid "" "Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for " "firewall settings · in Simple view, activate the message button or click a " @@ -139,14 +139,14 @@ msgstr "" "点击单元格筛选 · 芯片上的 ≠ 排除 · Ctrl+点击规则打开防火墙设置 · 在简洁视图" "中,激活消息按钮或单击行即可显示或隐藏完整消息" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2659 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2684 msgid "" "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a " "chip) to exclude." msgstr "" "点击 IP、动作或协议进行筛选;使用协议菜单(或筛选标签上的 ≠)进行排除。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1973 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1998 msgid "Common" msgstr "常用" @@ -158,7 +158,7 @@ msgstr "" "已配置的 WAN 区域 log_limit:%s。只有支持该选项的 fw4 构建才会实施此限制;内" "核/logd 仍可能限制日志总量。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1779 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1804 msgid "Connection lost — retrying…" msgstr "连接已断开 — 正在重试…" @@ -170,49 +170,49 @@ msgstr "无法获取日志记录锁。" msgid "Could not clear the WAN zone log option." msgstr "无法清除 WAN 区域日志选项。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:799 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:804 msgid "Could not create a temporary file for rule names." msgstr "无法为规则名称创建临时文件。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:988 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993 msgid "Could not disable logging." msgstr "无法禁用日志记录。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:966 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:971 msgid "Could not enable logging." msgstr "无法启用日志记录。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:902 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:907 msgid "Could not load logging status." msgstr "无法加载日志状态。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:805 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:810 msgid "Could not load rule names." msgstr "无法加载规则名称。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:801 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:806 msgid "Could not process firewall rule names." msgstr "无法处理防火墙规则名称。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:945 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:978 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:994 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:950 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:983 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:999 msgid "Could not reach the router; check the current logging state." msgstr "无法连接路由器;请检查当前日志状态。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:803 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808 msgid "Could not read the active firewall rules." msgstr "无法读取当前生效的防火墙规则。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:901 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:906 msgid "Could not refresh logging status; showing the last known state." msgstr "无法刷新日志状态;显示上次已知状态。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:810 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:815 msgid "Could not refresh rule names." msgstr "无法刷新规则名称。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:813 msgid "Could not refresh rule names; previously loaded names are still shown." msgstr "无法刷新规则名称;仍显示之前加载的名称。" @@ -232,7 +232,7 @@ msgstr "无法安全跟踪日志记录的更改;日志记录未更改。" msgid "Could not write the WAN zone log option." msgstr "无法写入 WAN 区域日志选项。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2902 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2927 msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set." msgstr "自定义协议(! 排除)。有内容时覆盖菜单。" @@ -245,7 +245,7 @@ msgstr "目的端口" msgid "Destination" msgstr "目的地址" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2932 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2957 msgid "Destination IP contains (! to exclude)" msgstr "目的 IP 包含(! 表示排除)" @@ -253,15 +253,15 @@ msgstr "目的 IP 包含(! 表示排除)" msgid "Destination port" msgstr "目标端口" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2937 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2962 msgid "Destination port (! to exclude)" msgstr "目的端口(! 表示排除)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2782 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2807 msgid "Detail" msgstr "详细" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760 msgid "Details" msgstr "详情" @@ -273,17 +273,17 @@ msgstr "方向" msgid "Disabling…" msgstr "正在禁用…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1522 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1547 msgid "" "Display limited to %d rows on this device; the larger Limit still buffers " "rows." msgstr "显示上限为 %d 行(本设备);较大的行数限制仍会缓冲这些行。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2520 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2545 msgid "Display options" msgstr "显示选项" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2613 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638 msgid "" "Display options set Limit, Fetch budget, Maximum raw lines, row tint, " "palette, and hostnames." @@ -305,7 +305,7 @@ msgstr "启用 WAN 区域的丢弃/拒绝日志记录(同 网络 → 防火墙 msgid "Enable logging" msgstr "启用日志" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2608 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2633 msgid "" "Enable logging turns on WAN zone drop/reject logging only (same as Network → " "Firewall). It does not add rules or log normal LAN browsing." @@ -317,20 +317,20 @@ msgstr "" msgid "Enabling…" msgstr "正在启用…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1986 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2011 msgid "Exclude" msgstr "排除" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190 msgid "Exclude instead" msgstr "改为排除" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2534 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2559 msgid "Fetch budget" msgstr "抓取配额" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2623 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648 msgid "" "Fetch budget controls raw log lines per poll. Auto derives from Limit; " "Manual selects a bounded maximum." @@ -338,10 +338,10 @@ msgstr "" "抓取配额控制每次轮询的原始日志行数。「自动」由行数限制推导;「手动」可选择一" "个有上限的最大值。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:149 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:63 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:84 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2871 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:149 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2896 msgid "Filter by %s" msgstr "按 %s 筛选" @@ -349,12 +349,12 @@ msgstr "按 %s 筛选" msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings." msgstr "按规则筛选日志(提示:%s)。Ctrl+点击可打开防火墙设置。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2697 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2722 #: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3 msgid "Firewall Live View" msgstr "防火墙实时视图" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1746 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1771 msgid "Firewall log has no current entries; showing rows last received %s ago." msgstr "防火墙日志当前没有条目;显示的是 %s 前收到的日志行。" @@ -366,7 +366,7 @@ msgstr "标志" msgid "Flow" msgstr "流向" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2633 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2658 msgid "" "For a responsive table on a weak device, keep Limit at 250 rows or below. " "The weak-device cap affects rendered rows; the buffer can still retain more." @@ -382,7 +382,7 @@ msgstr "已转发" msgid "Grant access to firewall live log view" msgstr "授予防火墙实时日志视图的访问权限" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2599 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2624 msgid "Help" msgstr "帮助" @@ -390,15 +390,15 @@ msgstr "帮助" msgid "Hide full message" msgstr "隐藏完整消息" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1620 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645 msgid "Hide rows" msgstr "隐藏行" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1518 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1543 msgid "Hostname lookup paused while the router is under load." msgstr "主机名解析已暂停:路由器负载较高。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1977 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2002 msgid "ICMPv6" msgstr "ICMPv6" @@ -406,7 +406,7 @@ msgstr "ICMPv6" msgid "IN" msgstr "IN" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2670 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2695 msgid "" "If Row tint looks missing, the active LuCI theme may omit success/error or " "info/warn CSS variables; fwlive falls back to local colors (air-gapped, no " @@ -423,7 +423,7 @@ msgstr "" "如果 WAN 侧较安静,请等待探测报文,或使用「帮助」/启用日志指南中的可选 ping " "检查。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2654 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2679 msgid "" "In Simple view, activate the message button or click a row to show or hide " "the full message." @@ -433,8 +433,8 @@ msgstr "在简洁视图中,激活消息按钮或单击行即可显示或隐藏 msgid "Inbound" msgstr "入站" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:92 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190 msgid "Include instead" msgstr "改为包含" @@ -446,7 +446,7 @@ msgstr "" "请使用适用于您 OpenWrt 版本的命令安装 kmod-nf-log 和 kmod-nf-log6,然后重新加" "载防火墙。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1777 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802 msgid "Installation is incomplete. Reinstall luci-app-fwlive." msgstr "安装不完整。请重新安装 luci-app-fwlive。" @@ -455,7 +455,7 @@ msgstr "安装不完整。请重新安装 luci-app-fwlive。" msgid "Interface" msgstr "接口" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2917 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2942 msgid "Interface (prefix ! to exclude)" msgstr "接口(前缀 ! 表示排除)" @@ -472,11 +472,11 @@ msgstr "缺少内核日志模块" msgid "Len" msgstr "长度" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2523 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2548 msgid "Limit" msgstr "行数限制" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:856 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:861 msgid "Live view may be incomplete — a legacy iptables table is registered" msgstr "实时视图可能不完整 — 已注册旧版 iptables 表" @@ -484,16 +484,16 @@ msgstr "实时视图可能不完整 — 已注册旧版 iptables 表" msgid "Logging is off on this router" msgstr "此路由器的日志记录已关闭" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1473 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1546 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1498 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1571 msgid "Manual" msgstr "手动" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2552 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2577 msgid "Manual still uses server protection and poll cadence" msgstr "手动模式仍受服务器保护和轮询节奏约束" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2628 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2653 msgid "" "Manual still uses server protection and poll cadence; it changes the fetch " "budget, not the polling interval." @@ -504,18 +504,18 @@ msgstr "" msgid "Manual test (System → Terminal):" msgstr "手动测试(系统 → 终端):" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2546 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2571 msgid "Maximum raw lines" msgstr "最大原始行数" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:370 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:52 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2801 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2794 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:377 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2819 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2826 msgid "Message" msgstr "消息" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2912 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2937 msgid "More filters" msgstr "更多筛选" @@ -559,7 +559,7 @@ msgstr "点击「启用日志」之前不会有任何更改。" msgid "OUT" msgstr "出接口" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2824 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2849 msgid "One line" msgstr "单行" @@ -588,20 +588,20 @@ msgstr "" msgid "Outbound" msgstr "出站" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2573 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2598 msgid "Palette" msgstr "配色" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1840 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2748 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1865 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2773 msgid "Pause" msgstr "暂停" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1839 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1864 msgid "Paused" msgstr "已暂停" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1505 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1530 msgid "" "Polling slowed: the router is responding slowly, so polls run less often." msgstr "轮询已放慢:路由器响应较慢,因此降低轮询频率。" @@ -611,11 +611,11 @@ msgstr "轮询已放慢:路由器响应较慢,因此降低轮询频率。" msgid "Proto" msgstr "协议" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2893 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2918 msgid "Protocol — common values" msgstr "协议 — 常用值" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2864 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2889 msgid "Quick search" msgstr "快速搜索" @@ -623,21 +623,21 @@ msgstr "快速搜索" msgid "Rate limit:" msgstr "速率限制:" +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:229 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:230 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93 msgid "Remove filter" msgstr "移除筛选" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1840 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1865 msgid "Resume" msgstr "继续" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2563 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2588 msgid "Row tint" msgstr "行着色" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2664 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2689 msgid "" "Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/" "red, default) or Accessible (teal/orange). Action text stays colored either " @@ -646,7 +646,7 @@ msgstr "" "勾选后行着色显示放行/拒绝行背景。可选经典(绿/红,默认)或无障碍(青绿/橙)。" "操作文本在两种模式下均保持着色。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2720 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2745 msgid "" "Row tint used a local color fallback because the active LuCI theme did not " "apply pass/deny backgrounds." @@ -656,19 +656,19 @@ msgstr "由于当前 LuCI 主题未应用放行/拒绝背景色,行着色使 msgid "Rule" msgstr "规则" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760 msgid "Rule name details" msgstr "规则名称详情" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:827 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:832 msgid "Rule-name lookup does not change your firewall rules." msgstr "规则名称查询不会更改您的防火墙规则。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:812 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:817 msgid "Rule-name lookup failed." msgstr "规则名称查询失败。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:819 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:824 msgid "Rule-name lookup reached a safety limit." msgstr "规则名称查询已达到安全限制。" @@ -680,22 +680,22 @@ msgstr "源端口" msgid "Search" msgstr "搜索" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1502 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1527 msgid "Server adaptive protection is disabled." msgstr "服务器自适应保护已禁用。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1510 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1535 msgid "" "Server limited fetch: the router is under load; at most %d log lines per " "poll." msgstr "服务器已限制抓取:路由器负载较高;每次轮询最多 %d 行日志。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1515 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1540 msgid "" "Server limited fetch: this poll returned fewer lines to protect the router." msgstr "服务器已限制抓取:本次轮询返回的行数较少,以保护路由器。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1500 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1525 msgid "Server protection state is unknown." msgstr "服务器保护状态未知。" @@ -703,28 +703,28 @@ msgstr "服务器保护状态未知。" msgid "Show full message" msgstr "显示完整消息" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2590 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2615 msgid "Show hostnames" msgstr "显示主机名" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2561 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2586 msgid "Show pass/deny row background colors" msgstr "显示放行/拒绝行背景色" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1620 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2847 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2872 msgid "Show rows" msgstr "显示行" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1580 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1605 msgid "Showing a compact summary of the top of the shown sample." msgstr "显示当前样本顶部的简要摘要。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2771 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2796 msgid "Simple" msgstr "简洁" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:830 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:835 msgid "" "Some entries may show names derived from their log prefixes. Searching by a " "friendly rule name may miss those entries." @@ -732,7 +732,7 @@ msgstr "" "部分条目可能显示由日志前缀生成的名称。按易读的规则名称搜索可能会遗漏这些条" "目。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:850 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:855 msgid "Some rule names may be missing" msgstr "部分规则名称可能缺失" @@ -741,7 +741,7 @@ msgstr "部分规则名称可能缺失" msgid "Source" msgstr "源地址" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2922 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2947 msgid "Source IP contains (! to exclude)" msgstr "源 IP 包含(! 表示排除)" @@ -749,19 +749,19 @@ msgstr "源 IP 包含(! 表示排除)" msgid "Source port" msgstr "源端口" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2927 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2952 msgid "Source port (! to exclude)" msgstr "源端口(! 表示排除)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1582 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1607 msgid "Summary data is unavailable; show rows to inspect the sample." msgstr "摘要数据不可用;显示行以检查样本。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2838 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2863 msgid "Summary mode" msgstr "摘要模式" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2618 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643 msgid "" "Summary mode: when the router responds slowly, a compact summary replaces " "the table. Use Show rows to see the table; it returns automatically after " @@ -770,11 +770,11 @@ msgstr "" "摘要模式:路由器响应较慢时,会以简要摘要代替表格。点击「显示行」可查看表格;" "连续三次快速轮询后会自动恢复。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1584 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1609 msgid "Summary was bounded before all categories could be included." msgstr "摘要已受限,无法包含所有类别。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2663 msgid "" "Switching tabs pauses polling; returning performs one catch-up poll. " "Hostnames are off by default because lookups add work." @@ -782,7 +782,7 @@ msgstr "" "切换到其他标签页会暂停轮询;返回时执行一次追赶轮询。主机名默认关闭,因为解析" "会增加工作量。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1675 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1700 msgid "" "Table refresh throttled: the event rate is high, so the table repaints less " "often to protect the browser. The buffer still updates; refresh resumes " @@ -795,7 +795,7 @@ msgstr "" msgid "The firewall did not reload; saved and live logging may differ." msgstr "防火墙未能重新加载;已保存的日志设置与当前生效状态可能不一致。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2673 msgid "" "The rate shown for WAN logging is the configured WAN zone log_limit, not " "proof the running fw4 supports it. The fw4 bundled with OpenWrt 23.05.5 " @@ -806,11 +806,11 @@ msgstr "" "该选项。OpenWrt 23.05.5 附带的 fw4 会忽略此选项。内核/logd 仍可能限制日志总" "量;fwlive 不会施加限制。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1778 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1803 msgid "The router could not read the firewall log — retrying…" msgstr "路由器无法读取防火墙日志 — 正在重试…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2668 msgid "" "The router log ring may evict older events before fwlive reads them. fwlive " "cannot recover evicted entries or change forwarding behavior." @@ -818,13 +818,13 @@ msgstr "" "路由器日志环形缓冲区可能在 fwlive 读取前淘汰较旧事件。fwlive 无法恢复已淘汰的" "条目,也不会改变转发行为。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2603 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2628 msgid "" "The table updates automatically when your firewall logs traffic. Use Pause " "if it moves too fast." msgstr "防火墙记录流量时表格会自动更新。如果滚动太快,请使用「暂停」。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2723 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2748 msgid "Theme tint fallback" msgstr "主题着色回退" @@ -841,15 +841,15 @@ msgstr "此路由器报告了 Live View 尚无法识别的日志阻止项。" msgid "Time" msgstr "时间" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1589 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1614 msgid "Top drops" msgstr "主要丢弃" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1592 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1617 msgid "Top rules" msgstr "主要规则" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1586 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1611 msgid "Top talkers" msgstr "主要通信方" @@ -872,16 +872,16 @@ msgstr "开启 WAN 防火墙区域的日志记录并重新加载防火墙。" msgid "Undo:" msgstr "撤销:" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2667 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2692 msgid "Use Detail for all columns (flags, length, raw message)." msgstr "使用「详细」可查看全部列(标志、长度、原始消息)。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2759 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2753 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2778 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2784 msgid "View" msgstr "视图" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:989 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:994 msgid "WAN drop/reject logging is off." msgstr "WAN 丢弃/拒绝日志记录已关闭。" @@ -893,7 +893,7 @@ msgstr "" "WAN 丢弃/拒绝日志记录已开启。被阻止的 WAN 入站流量会显示在这里;正常的 LAN 浏" "览不会。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:970 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:975 msgid "" "WAN drop/reject logging is on. Blocked inbound traffic should appear here as " "it happens — not normal LAN browsing." @@ -901,7 +901,7 @@ msgstr "" "WAN 丢弃/拒绝日志记录已开启。被阻止的入站流量会实时显示在这里 — 正常的 LAN 浏" "览不会。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:972 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977 msgid "WAN logging is already enabled." msgstr "WAN 日志记录已处于启用状态。" @@ -930,25 +930,25 @@ msgstr "WAN 日志不可用:无 WAN 区域" msgid "Waiting for firewall events" msgstr "正在等待防火墙事件" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1839 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2711 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1864 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2736 msgid "Watching" msgstr "监视中" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2813 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2838 msgid "Wrap" msgstr "自动换行" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:74 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2876 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2901 msgid "block" msgstr "拦截" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1323 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1348 msgid "buffer filling" msgstr "缓冲区填充中" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1327 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1352 msgid "buffer full" msgstr "缓冲区已满" @@ -960,7 +960,7 @@ msgstr "已配置的 log_limit:%s" msgid "contains" msgstr "包含" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1332 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1357 msgid "display limited to %d rows" msgstr "显示上限为 %d 行" @@ -969,11 +969,11 @@ msgid "does not contain" msgstr "不包含" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:75 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2877 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2902 msgid "drop" msgstr "丢弃" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1336 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1361 msgid "hostname lookup paused" msgstr "主机名解析已暂停" @@ -981,11 +981,11 @@ msgstr "主机名解析已暂停" msgid "is" msgstr "是" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1322 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1347 msgid "loading buffer" msgstr "正在加载缓冲区" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1764 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1789 msgid "loading…" msgstr "加载中…" @@ -993,8 +993,8 @@ msgstr "加载中…" msgid "no WAN rate limit configured" msgstr "未配置 WAN 速率限制" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1562 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1571 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1587 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1596 msgid "none" msgstr "无" @@ -1002,98 +1002,98 @@ msgstr "无" msgid "not" msgstr "非" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1994 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2019 msgid "not AH" msgstr "非 AH" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1993 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2018 msgid "not ESP" msgstr "非 ESP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1992 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2017 msgid "not GRE" msgstr "非 GRE" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1989 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2014 msgid "not ICMP" msgstr "非 ICMP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1990 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2015 msgid "not ICMPv6" msgstr "非 ICMPv6" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1991 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2016 msgid "not IGMP" msgstr "非 IGMP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1995 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2020 msgid "not SCTP" msgstr "非 SCTP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1987 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2012 msgid "not TCP" msgstr "非 TCP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1988 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2013 msgid "not UDP" msgstr "非 UDP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2882 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2907 msgid "not block" msgstr "非阻止" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2881 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2906 msgid "not drop" msgstr "非丢弃" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2880 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2905 msgid "not pass" msgstr "非放行" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2883 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2908 msgid "not reject" msgstr "非拒绝" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2884 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2909 msgid "not unknown" msgstr "非未知" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2900 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2925 msgid "or type…" msgstr "或输入…" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:73 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2875 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2900 msgid "pass" msgstr "通过" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1334 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1359 msgid "polling slowed" msgstr "轮询已放慢" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:76 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2878 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2903 msgid "reject" msgstr "拒绝" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1338 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1363 msgid "scroll frozen — scroll to top to follow live" msgstr "滚动已冻结 — 滚动到顶部以恢复跟随" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1335 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1360 msgid "server limited fetch" msgstr "服务器已限制抓取" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1329 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1354 msgid "table refresh throttled" msgstr "表格刷新已限流" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:65 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2879 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2904 msgid "unknown" msgstr "未知" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:792 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:797 msgid "using fw4" msgstr "使用 fw4" diff --git a/luci-app-fwlive/root/usr/libexec/rpcd/fwlive b/luci-app-fwlive/root/usr/libexec/rpcd/fwlive index 09c7b83bc..1c1bc5c23 100755 --- a/luci-app-fwlive/root/usr/libexec/rpcd/fwlive +++ b/luci-app-fwlive/root/usr/libexec/rpcd/fwlive @@ -41,6 +41,11 @@ POLL_LINES_MAX=2000 # Cap rules JSON size so a huge ruleset cannot unbound an rpcd reply. RULES_MAP_MAX_KEYS=512 RULES_MAP_MAX_BYTES=65536 +# Reserve the longest envelope (unknown backend, false, mktemp_failed) +# including both objects, commas and the closing brace. Map contents +# are counted separately after JSON escaping. +RULES_MAP_ENVELOPE='{"backend":"unknown","rules":{},"targets":{},"truncated":false,"error":"mktemp_failed"}' +RULES_MAP_MAX_PAYLOAD_BYTES=$(( RULES_MAP_MAX_BYTES - ${#RULES_MAP_ENVELOPE} )) # First-wins dups never increment _map_keys, so they would otherwise walk # the whole dump. Bound prepared prefix records independently. RULES_MAP_MAX_LINES=512 @@ -54,6 +59,28 @@ json_object_append() { OUT="${OUT}\"$1\":\"$2\"" } +# UCI target tokens use only the space left after all rule labels. +# Keys/values have passed is_uci_style_name, so their JSON form is unchanged. +targets_add() { + esc_key="$1" + esc_val="$2" + [ -n "$esc_key" ] && [ -n "$esc_val" ] || return 0 + case "$TARGETS_OUT" in + *"\"$esc_key\":"*) return 0 ;; + esac + _pair_len=$(( ${#esc_key} + ${#esc_val} + 5 )) + _next_len=$(( ${#OUT} + ${#TARGETS_OUT} + _pair_len )) + [ -n "$TARGETS_OUT" ] && _next_len=$((_next_len + 1)) + if [ "$_next_len" -gt "$RULES_MAP_MAX_PAYLOAD_BYTES" ]; then + _targets_overflow=1 + return 0 + fi + if [ -n "$TARGETS_OUT" ]; then + TARGETS_OUT="${TARGETS_OUT}," + fi + TARGETS_OUT="${TARGETS_OUT}\"$esc_key\":\"$esc_val\"" +} + map_add() { key="$1" val="$2" @@ -87,7 +114,7 @@ map_add_escaped() { _next_len=$(( ${#OUT} + _pair_len )) [ -n "$OUT" ] && _next_len=$((_next_len + 1)) if [ "${_map_keys:-0}" -ge "$RULES_MAP_MAX_KEYS" ] || \ - [ "$_next_len" -gt "$RULES_MAP_MAX_BYTES" ]; then + [ "$_next_len" -gt "$RULES_MAP_MAX_PAYLOAD_BYTES" ]; then _map_overflow=1 return 0 fi @@ -98,6 +125,8 @@ map_add_escaped() { uci_rule_names() { # uci missing / empty firewall config is "no names", not fatal. # Read the captured show output once; never fork one uci get per section. + # Emit nameslugtarget (target may be empty). Buffer per section + # so name-then-target and target-then-name both pair. _firewall_show=$(uci -q show firewall 2>/dev/null || true) printf '%s\n' "$_firewall_show" | awk -v max="$RULES_MAP_MAX_LINES" ' @@ -109,36 +138,96 @@ uci_rule_names() { } return out } - function emit(value, quote) { + function strip_quotes(value, quote) { quote = sprintf("%c", 39) if (substr(value, 1, 1) == quote && substr(value, length(value), 1) == quote) value = substr(value, 2, length(value) - 2) + return value + } + function emit_buffered( out_name, out_target) { + out_name = buf_name + out_target = buf_target + buf_name = "" + buf_target = "" + if (truncated) return + if (out_name == "") return + if (buf_kind == "named" && buf_is_rule != 1) return if (count >= max) { truncated = 1 exit 42 } - print value "\t" slug_uci(value) + print out_name "\t" slug_uci(out_name) "\t" out_target count++ } + function new_section(id, kind, is_rule) { + emit_buffered() + buf_id = id + buf_kind = kind + buf_is_rule = 0 + if (is_rule) buf_is_rule = 1 + } { equal = index($0, "=") if (!equal) next key = substr($0, 1, equal - 1) value = substr($0, equal + 1) + if (key ~ /^firewall\.@rule\[[0-9][0-9]*\]$/) { + if (buf_id != key || buf_kind != "anon") + new_section(key, "anon", 1) + else + buf_is_rule = 1 + next + } if (key ~ /^firewall\.@rule\[[0-9][0-9]*\]\.name$/) { - emit(value) + sid = key + sub(/\.name$/, "", sid) + if (buf_id != sid || buf_kind != "anon") + new_section(sid, "anon", 1) + buf_name = strip_quotes(value) + next + } + if (key ~ /^firewall\.@rule\[[0-9][0-9]*\]\.target$/) { + sid = key + sub(/\.target$/, "", sid) + if (buf_id != sid || buf_kind != "anon") + new_section(sid, "anon", 1) + buf_target = strip_quotes(value) next } if (key ~ /^firewall\.[A-Za-z0-9_-]+$/) { section = key sub(/^firewall\./, "", section) - current_rule = (value == "rule") + if (buf_id == section && buf_kind == "named") { + buf_is_rule = 0 + if (value == "rule") buf_is_rule = 1 + } else { + new_section(section, "named", (value == "rule")) + } + next + } + if (key ~ /^firewall\.[A-Za-z0-9_-]+\.name$/) { + section = key + sub(/^firewall\./, "", section) + sub(/\.name$/, "", section) + if (buf_id != section || buf_kind != "named") + new_section(section, "named", 0) + buf_name = strip_quotes(value) + next + } + if (key ~ /^firewall\.[A-Za-z0-9_-]+\.target$/) { + section = key + sub(/^firewall\./, "", section) + sub(/\.target$/, "", section) + if (buf_id != section || buf_kind != "named") + new_section(section, "named", 0) + buf_target = strip_quotes(value) next } - if (current_rule && key == "firewall." section ".name") - emit(value) } - END { if (truncated) exit 42 } + END { + if (truncated) exit 42 + emit_buffered() + } ' } @@ -170,6 +259,7 @@ map_uci_rule_names() { # are skipped; one shared line cap also bounds duplicate UCI names that # do not trip the output key/byte caps. # Keep the loop in this shell so map_add mutations reach OUT. + # TSV is nameslugtarget; target may be empty. if _uci_names="$(uci_rule_names)"; then _uci_names_truncated='' else @@ -189,6 +279,7 @@ map_uci_rule_names() { [ -n "$line" ] || continue name="${line%%${_tab}*}" slug="${line#*"$_tab"}" + slug="${slug%%${_tab}*}" [ -n "$name" ] || continue is_uci_style_name "$name" || continue map_add_escaped "$name" "$name" @@ -204,6 +295,33 @@ EOF fi } +# Reuse the captured UCI records after nft enrichment. Target truncation must +# neither consume rule-label space nor stop the labeled/unlabeled nft passes. +map_uci_rule_targets() { + _targets_overflow='' + while IFS= read -r line || [ -n "$line" ]; do + [ -n "$_targets_overflow" ] && break + name="${line%%${_tab}*}" + rest="${line#*"$_tab"}" + slug="${rest%%${_tab}*}" + target="${rest#*"$_tab"}" + is_uci_style_name "$name" && is_uci_style_name "$target" || continue + # Do not attach targets to UCI names excluded by the rule-map caps. + case "$OUT" in + *"\"$name\":\"$name\""*) ;; + *) continue ;; + esac + targets_add "$name" "$target" + [ -n "$_targets_overflow" ] && break + if [ -n "$slug" ] && [ "$slug" != "$name" ]; then + targets_add "$slug" "$target" + fi + done <comment with TSV escapes (tab as \t, backslash as \\). Production # prepared mode emits kindkeyaliasvalue: kind is the unquoted @@ -516,7 +634,11 @@ enrich_rules_dump() { } build_rules_map() { + # Shell string lengths must count UTF-8 bytes, not locale characters. + LC_ALL=C + export LC_ALL OUT='' + TARGETS_OUT='' _map_keys=0 _map_lines=0 _map_overflow='' @@ -532,10 +654,12 @@ build_rules_map() { enrich_rules_dump fwlive-nft fi + map_uci_rule_targets + if [ "$backend" = unknown ] && [ -z "$_enrich_error" ]; then _enrich_error='no_backend' fi - _rules_json="{\"backend\":\"$backend\",\"rules\":{$OUT}" + _rules_json="{\"backend\":\"$backend\",\"rules\":{$OUT},\"targets\":{${TARGETS_OUT}}" # Map limits and enrichment failures are independent outcomes. _truncated=false [ -n "$_map_overflow" ] && _truncated=true diff --git a/luci-app-open-box/Makefile b/luci-app-open-box/Makefile index 2fe8e8335..953cd6e72 100644 --- a/luci-app-open-box/Makefile +++ b/luci-app-open-box/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-open-box -PKG_VERSION:=0.1.336 -PKG_RELEASE:=32 +PKG_VERSION:=0.1.337 +PKG_RELEASE:=33 PKG_MAINTAINER:=liandu2024 PKG_LICENSE:=GPL-3.0-or-later diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index 3ee93278e..57b4c30d2 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall PKG_VERSION:=26.10.16 -PKG_RELEASE:=335 +PKG_RELEASE:=336 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/root/usr/share/passwall/app.sh b/luci-app-passwall/root/usr/share/passwall/app.sh index df1cbdc7e..82bbd1cfd 100755 --- a/luci-app-passwall/root/usr/share/passwall/app.sh +++ b/luci-app-passwall/root/usr/share/passwall/app.sh @@ -1712,7 +1712,7 @@ acl_app() { start() { busybox pgrep -f ${TMP_PATH}/bin > /dev/null 2>&1 && { logger -t PSW-RESTART "Upgrade or overload residue is detected, and the subprocess is being called to perform complete cleaning..." - (stop) + (stop "$1") sleep 2 } mkdir -p /tmp/etc /tmp/log $TMP_PATH $TMP_BIN_PATH $TMP_SCRIPT_FUNC_PATH $TMP_ROUTE_PATH $TMP_ACL_PATH $TMP_PATH2 @@ -1765,7 +1765,7 @@ start() { } fi - start_crontab $1 + start_crontab "$1" echolog "运行完成!\n" [ "$ENABLED" = 1 ] && [ "$1" = "boot" ] && { @@ -1802,7 +1802,7 @@ stop() { unset V2RAY_LOCATION_ASSET unset XRAY_LOCATION_ASSET unset SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN - stop_crontab $1 + stop_crontab "$1" rm -rf $GLOBAL_DNSMASQ_CONF rm -rf $GLOBAL_DNSMASQ_CONF_PATH [ "1" = "1" ] && { diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index 7a0620b40..7d9d33cf1 100644 --- a/luci-app-passwall2/Makefile +++ b/luci-app-passwall2/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 PKG_VERSION:=26.10.1 -PKG_RELEASE:=143 +PKG_RELEASE:=144 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua index de4d6fd7f..b902a483a 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua @@ -37,6 +37,7 @@ local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | aw local singbox_version = api.get_app_version("sing-box"):match("[^v]+") local version_ge_1_14_0 = api.compare_versions(singbox_version, ">=", "1.14.0") +local version_ge_1_15_0 = api.compare_versions(singbox_version, ">=", "1.15.0") o = s:option(ListValue, "protocol", translate("Protocol")) o:value("socks", "Socks") @@ -68,6 +69,9 @@ end if version_ge_1_14_0 then o:value("snell", "Snell") end +if version_ge_1_15_0 and singbox_tags:find("with_quic") then + o:value("masque", "MASQUE") +end o:value("_urltest", translate("URLTest")) o:value("_shunt", translate("Shunt")) o:value("_iface", translate("Custom Interface")) @@ -197,6 +201,7 @@ o:depends({ protocol = "http" }) o:depends({ protocol = "socks" }) o:depends({ protocol = "ssh" }) o:depends({ protocol = "naive" }) +o:depends({ protocol = "masque" }) o = s:option(Value, "password", translate("Password")) o.password = true @@ -209,6 +214,34 @@ o:depends({ protocol = "tuic" }) o:depends({ protocol = "anytls" }) o:depends({ protocol = "ssh" }) o:depends({ protocol = "naive" }) +o:depends({ protocol = "masque" }) + +-- [[ MASQUE ]] -- +o = s:option(Value, "masque_path", translate("MASQUE Path")) +o:depends({ protocol = "masque" }) + +o = s:option(TextValue, "masque_headers", translate("Custom HTTP Headers")) +o.rows = 5 +o.wrap = "off" +o:depends({ protocol = "masque" }) +o.cfgvalue = function(self, section) + return (m:get(section, self.config_option) or ""):gsub("\\n", "\n") +end +o.validate = function(self, value) + value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n") + local headers = {} + for line in value:gmatch("[^\n]+") do + if api.trim(line) ~= "" then + local key = line:match("^%s*([^:]+):") + key = key and api.trim(key):lower() + if not key or not key:match("^[!#$%%&'*+%.%^_`|~%w%-]+$") or headers[key] then + return nil, translate("Not true format, please re-enter!") .. " (" .. line .. ")" + end + headers[key] = true + end + end + return value:gsub("\n", "\\n") +end if version_ge_1_14_0 then -- snell @@ -549,6 +582,7 @@ o:depends({ tls = true }) o:depends({ protocol = "hysteria"}) o:depends({ protocol = "tuic" }) o:depends({ protocol = "hysteria2" }) +o:depends({ protocol = "masque" }) o = s:option(Value, "tls_serverName", "SNI " .. translate("Domain")) o:depends({ tls = true }) @@ -556,6 +590,7 @@ o:depends({ protocol = "hysteria"}) o:depends({ protocol = "tuic" }) o:depends({ protocol = "hysteria2" }) o:depends({ protocol = "naive" }) +o:depends({ protocol = "masque" }) o = s:option(Flag, "tls_allowInsecure", translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped.")) o.default = "0" @@ -563,12 +598,14 @@ o:depends({ tls = true }) o:depends({ protocol = "hysteria" }) o:depends({ protocol = "tuic" }) o:depends({ protocol = "hysteria2" }) +o:depends({ protocol = "masque" }) o = s:option(Value, "tls_pinSHA256", translate("TLS Chain Fingerprint (SHA256)")) o:depends({ tls = true }) o:depends({ protocol = "hysteria" }) o:depends({ protocol = "tuic" }) o:depends({ protocol = "hysteria2" }) +o:depends({ protocol = "masque" }) o.description = translate("Once set, connects only when the server’s chain fingerprint matches.") .. string.format("%s", "→ " .. translate("Fetch Manually")) @@ -579,6 +616,7 @@ o:depends({ protocol = "hysteria"}) o:depends({ protocol = "tuic" }) o:depends({ protocol = "hysteria2" }) o:depends({ protocol = "naive" }) +o:depends({ protocol = "masque" }) o = s:option(TextValue, "tls_certificate_pem", " ", translate("Full certificate (chain), PEM format.")) o.default = "" @@ -608,6 +646,7 @@ o:value("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384") o:value("TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256") o:value("TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256") o:depends({ tls = true }) +o:depends({ protocol = "masque" }) o = s:option(Flag, "ech", translate("ECH")) o.default = "0" @@ -616,6 +655,7 @@ o:depends({ protocol = "tuic" }) o:depends({ protocol = "hysteria" }) o:depends({ protocol = "hysteria2", hysteria2_realms = false }) o:depends({ protocol = "naive" }) +o:depends({ protocol = "masque" }) o = s:option(TextValue, "ech_config", translate("ECH Config")) o.default = "" @@ -812,6 +852,7 @@ o:depends({ transport = "http" }) o:depends({ transport = "ws" }) o:depends({ transport = "httpupgrade" }) o:depends({ protocol = "naive" }) +o:depends({ protocol = "masque" }) -- [[ Mux ]]-- o = s:option(Flag, "mux", translate("Mux")) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/2_xray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/2_xray.lua index 9091b6a92..338081b87 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/2_xray.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/2_xray.lua @@ -31,8 +31,6 @@ local header_type_list = { "none", "srtp", "utp", "wechat-video", "dtls", "wireguard", "dns" } -local xray_version = api.get_app_version("xray") - o = s:option(ListValue, "protocol", translate("Protocol")) o:value("vmess", translate("Vmess")) o:value("vless", translate("VLESS")) @@ -41,12 +39,9 @@ o:value("socks", translate("Socks")) o:value("shadowsocks", translate("Shadowsocks")) o:value("trojan", translate("Trojan")) o:value("wireguard", translate("WireGuard")) -if api.compare_versions(xray_version, ">=", "26.1.13") then - o:value("hysteria2", translate("Hysteria2")) -end -if api.compare_versions(xray_version, ">=", "1.8.12") then - o:value("_balancing", translate("Balancing")) -end +o:value("hysteria2", translate("Hysteria2")) +o:value("masque", "MASQUE") +o:value("_balancing", translate("Balancing")) o:value("_shunt", translate("Shunt")) o:value("_iface", translate("Custom Interface")) @@ -260,6 +255,7 @@ o:depends({ protocol = "vless" }) o = s:option(Value, "username", translate("Username")) o:depends({ protocol = "http" }) o:depends({ protocol = "socks" }) +o:depends({ protocol = "masque" }) o = s:option(Value, "password", translate("Password")) o.password = true @@ -267,6 +263,57 @@ o:depends({ protocol = "http" }) o:depends({ protocol = "socks" }) o:depends({ protocol = "shadowsocks" }) o:depends({ protocol = "trojan" }) +o:depends({ protocol = "masque" }) + +-- [[ MASQUE ]] -- +o = s:option(Flag, "masque_warp", "WARP") +o.default = "0" +o:depends({ protocol = "masque" }) + +for _, field in ipairs({ + { "masque_warp_private_key", translate("WARP Device Private Key") }, + { "masque_warp_public_key", translate("WARP Endpoint Public Key") } +}) do + o = s:option(TextValue, field[1], field[2]) + o.rows = 3 + o.wrap = "off" + o:depends({ protocol = "masque", masque_warp = "1" }) + o.cfgvalue = function(self, section) + return (m:get(section, self.config_option) or ""):gsub("\\n", "\n") + end + o.validate = function(self, value) + return api.trim(value):gsub("\r\n?", "\n"):gsub("\n", "\\n") + end +end + +o = s:option(DynamicList, "masque_warp_address", "WARP " .. translate("Local Address")) +o:depends({ protocol = "masque", masque_warp = "1" }) + +o = s:option(Value, "masque_path", translate("MASQUE Path")) +o:depends({ protocol = "masque" }) + +o = s:option(TextValue, "masque_headers", translate("Custom HTTP Headers")) +o.rows = 5 +o.wrap = "off" +o:depends({ protocol = "masque" }) +o.cfgvalue = function(self, section) + return (m:get(section, self.config_option) or ""):gsub("\\n", "\n") +end +o.validate = function(self, value) + value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n") + local headers = {} + for line in value:gmatch("[^\n]+") do + if api.trim(line) ~= "" then + local key = line:match("^%s*([^:]+):") + key = key and api.trim(key):lower() + if not key or not key:match("^[!#$%%&'*+%.%^_`|~%w%-]+$") or headers[key] or key == "host" or key == "capsule-protocol" then + return nil, translate("Not true format, please re-enter!") .. " (" .. line .. ")" + end + headers[key] = true + end + end + return value:gsub("\n", "\\n") +end o = s:option(ListValue, "security", translate("Encrypt Method")) for a, t in ipairs(security_list) do o:value(t) end @@ -305,11 +352,7 @@ o:depends({ protocol = "hysteria2", hysteria2_realms = false }) o = s:option(Flag, "hysteria2_realms", translate("Realms")) o.default = "0" -if api.compare_versions(xray_version, ">", "26.5.9") then - o:depends({ protocol = "hysteria2"}) -else - o:depends({ protocol = "__hide"}) -end +o:depends({ protocol = "hysteria2"}) o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") o:depends({ hysteria2_realms = "1" }) @@ -400,6 +443,7 @@ o:value("http/1.1") o:value("h2,http/1.1") o:value("h3,h2,http/1.1") o:depends({ tls = true, reality = false }) +o:depends({ protocol = "masque" }) -- o = s:option(Value, "minversion", translate("minversion")) -- o.default = "1.3" @@ -409,21 +453,25 @@ o:depends({ tls = true, reality = false }) o = s:option(Value, "tls_serverName", "SNI " .. translate("Domain")) o:depends({ tls = true }) o:depends({ protocol = "hysteria2" }) +o:depends({ protocol = "masque" }) o = s:option(Value, "tls_pinSHA256", translate("TLS Chain Fingerprint (SHA256)")) o:depends({ tls = true, reality = false }) o:depends({ protocol = "hysteria2" }) +o:depends({ protocol = "masque" }) o.description = translate("Once set, connects only when the server’s chain fingerprint matches.") .. string.format("%s", "→ " .. translate("Fetch Manually")) o = s:option(Value, "tls_CertByName", translate("TLS Certificate Name (CertName)"), translate("TLS is used to verify the leaf certificate name.")) o:depends({ tls = true, reality = false }) o:depends({ protocol = "hysteria2" }) +o:depends({ protocol = "masque" }) o = s:option(Flag, "tls_certificate", translate("TLS Certificate (PEM)")) o.default = "0" o:depends({ tls = true, reality = false }) o:depends({ protocol = "hysteria2" }) +o:depends({ protocol = "masque" }) o = s:option(TextValue, "tls_certificate_pem", " ", translate("Full certificate (chain), PEM format.")) o.default = "" @@ -442,6 +490,7 @@ o = s:option(Flag, "ech", translate("ECH")) o.default = "0" o:depends({ tls = true, reality = false }) o:depends({ protocol = "hysteria2", hysteria2_realms = false }) +o:depends({ protocol = "masque" }) o = s:option(TextValue, "ech_config", translate("ECH Config")) o.default = "" @@ -502,6 +551,7 @@ o:value("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384") o:value("TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256") o:value("TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256") o:depends({ tls = true, reality = false }) +o:depends({ protocol = "masque" }) o = s:option(TextValue, "reality_mldsa65Verify", "ML-DSA-65 " .. translate("Public key")) o.default = "" @@ -541,10 +591,8 @@ o = s:option(Value, "wireguard_mtu", translate("MTU")) o.default = "1420" o:depends({ protocol = "wireguard" }) -if api.compare_versions(xray_version, ">=", "1.8.0") then - o = s:option(Value, "wireguard_reserved", translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings.")) - o:depends({ protocol = "wireguard" }) -end +o = s:option(Value, "wireguard_reserved", translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings.")) +o:depends({ protocol = "wireguard" }) o = s:option(Value, "wireguard_keepAlive", translate("Keep Alive")) o.default = "0" @@ -701,6 +749,7 @@ o:depends({ transport = "ws" }) o:depends({ transport = "httpupgrade" }) o:depends({ transport = "xhttp" }) o:depends({ transport = "grpc" }) +o:depends({ protocol = "masque" }) -- [[ Mux.Cool ]]-- o = s:option(Flag, "mux", "Mux", translate("Enable Mux.Cool")) @@ -731,6 +780,7 @@ o:depends({ protocol = "trojan" }) o:depends({ protocol = "shadowsocks" }) o:depends({ protocol = "wireguard" }) o:depends({ protocol = "hysteria2", hysteria2_realms = false }) +o:depends({ protocol = "masque" }) o = s:option(TextValue, "finalmask", " ") o:depends({ use_finalmask = true }) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/1_sing-box.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/1_sing-box.lua index fd6e3fa8c..7d94532a7 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/1_sing-box.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/1_sing-box.lua @@ -31,6 +31,7 @@ local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | aw local local_version = api.get_app_version("sing-box"):match("[^v]+") local version_ge_1_14_0 = api.compare_versions(local_version, ">=", "1.14.0") +local version_ge_1_15_0 = api.compare_versions(local_version, ">=", "1.15.0") local ss_method_list = { "none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305", @@ -246,6 +247,24 @@ o:value("", translate("Disable")) o:value("xtls-rprx-vision") o:depends({ protocol = "vless" , tls = true }) +-- [[ MASQUE ]] -- +if version_ge_1_15_0 and singbox_tags:find("with_quic") then + s.fields["protocol"]:value("masque", "MASQUE") + s.fields["users"]:depends({ protocol = "masque" }) + + o = s:option(Value, "masque_path", translate("MASQUE Path")) + o:depends({ protocol = "masque" }) + + o = s:option(DynamicList, "masque_address", translate("MASQUE Address")) + o.rmempty = false + o:depends({ protocol = "masque" }) + + o = s:option(Value, "masque_mtu", "MTU") + o.datatype = "uinteger" + o.default = "1280" + o:depends({ protocol = "masque" }) +end + o = s:option(Flag, "tls", translate("TLS")) o.default = 0 o.validate = function(self, value, t) @@ -322,6 +341,7 @@ o:depends({ protocol = "naive" }) o:depends({ protocol = "hysteria" }) o:depends({ protocol = "tuic" }) o:depends({ protocol = "hysteria2" }) +o:depends({ protocol = "masque" }) o = s:option(FileUpload, "tls_certificateFile", translate("Path to the certificate file"), translate("as:") .. "/etc/ssl/fullchain.crt") o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. s.section .. ".crt" @@ -331,6 +351,7 @@ o:depends({ protocol = "naive", tls_use_pem = false }) o:depends({ protocol = "hysteria", tls_use_pem = false }) o:depends({ protocol = "tuic", tls_use_pem = false }) o:depends({ protocol = "hysteria2", tls_use_pem = false }) +o:depends({ protocol = "masque", tls_use_pem = false }) o.validate = function(self, value, t) if value and value ~= "" then if not api.fs.access(value) then @@ -350,6 +371,7 @@ o:depends({ protocol = "naive", tls_use_pem = false }) o:depends({ protocol = "hysteria", tls_use_pem = false }) o:depends({ protocol = "tuic", tls_use_pem = false }) o:depends({ protocol = "hysteria2", tls_use_pem = false }) +o:depends({ protocol = "masque", tls_use_pem = false }) o.validate = function(self, value, t) if value and value ~= "" then if not api.fs.access(value) then @@ -361,7 +383,7 @@ o.validate = function(self, value, t) return nil end -o = s:option(TextValue, "tls_certificate", "TLS Certificate (PEM)", translate("Full certificate (chain), PEM format.")) +o = s:option(TextValue, "tls_certificate", translate("TLS Certificate (PEM)"), translate("Full certificate (chain), PEM format.")) o.default = "" o.rows = 5 o.wrap = "off" @@ -374,7 +396,7 @@ o.validate = function(self, value) return value:gsub("\n", "\\n") end -o = s:option(TextValue, "tls_key", "TLS Private Key (PEM)", translate("Private key in PEM format.")) +o = s:option(TextValue, "tls_key", translate("TLS Private Key (PEM)"), translate("Private key in PEM format.")) o.default = "" o.rows = 5 o.wrap = "off" @@ -394,6 +416,7 @@ o:depends({ protocol = "naive" }) o:depends({ protocol = "hysteria" }) o:depends({ protocol = "tuic" }) o:depends({ protocol = "hysteria2", hysteria2_realms = false }) +o:depends({ protocol = "masque" }) o = s:option(TextValue, "ech_key", translate("ECH Key")) o.default = "" diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/2_xray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/2_xray.lua index c6ba6c766..35fca626a 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/2_xray.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/2_xray.lua @@ -65,6 +65,7 @@ o:value("shadowsocks", "Shadowsocks") o:value("trojan", "Trojan") o:value("hysteria2", "Hysteria2") o:value("wireguard", "WireGuard") +o:value("masque", "MASQUE") o:value("tunnel", "Tunnel") o:depends({ custom = false }) @@ -85,6 +86,7 @@ o:depends({ protocol = "vless" }) o:depends({ protocol = "trojan" }) o:depends({ protocol = "hysteria2" }) o:depends({ protocol = "wireguard" }) +o:depends({ protocol = "masque" }) o = s:option(ListValue, "d_protocol", translate("Destination protocol")) o:value("tcp", "TCP") @@ -187,6 +189,19 @@ o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false }) o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps")) o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false }) +-- [[ MASQUE ]] -- +o = s:option(Value, "masque_path", translate("MASQUE Path")) +o:depends({ protocol = "masque" }) + +o = s:option(DynamicList, "masque_address", translate("MASQUE Address")) +o.rmempty = false +o:depends({ protocol = "masque" }) + +o = s:option(Value, "masque_mtu", "MTU") +o.datatype = "uinteger" +o.default = "1280" +o:depends({ protocol = "masque" }) + ---- [[ TLS ]] o = s:option(Flag, "tls", "TLS") o.default = 0 @@ -274,12 +289,14 @@ end o = s:option(Flag, "tls_use_pem", translate("Use PEM"), translate("Use certificate and private key PEM content.")) o:depends({ tls = true, reality = false }) o:depends({ protocol = "hysteria2" }) +o:depends({ protocol = "masque" }) o = s:option(FileUpload, "tls_certificateFile", translate("Path to the certificate file"), translate("as:") .. "/etc/ssl/fullchain.crt") o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. s.section .. ".crt" if o and o:formvalue(s.section) then o.default = o:formvalue(s.section) end o:depends({ tls = true, reality = false, tls_use_pem = false }) o:depends({ protocol = "hysteria2", tls_use_pem = false }) +o:depends({ protocol = "masque", tls_use_pem = false }) o.validate = function(self, value, t) if value and value ~= "" then if not api.fs.access(value) then @@ -296,6 +313,7 @@ o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. s.section . if o and o:formvalue(s.section) then o.default = o:formvalue(s.section) end o:depends({ tls = true, reality = false, tls_use_pem = false }) o:depends({ protocol = "hysteria2", tls_use_pem = false }) +o:depends({ protocol = "masque", tls_use_pem = false }) o.validate = function(self, value, t) if value and value ~= "" then if not api.fs.access(value) then @@ -307,7 +325,7 @@ o.validate = function(self, value, t) return nil end -o = s:option(TextValue, "tls_certificate", "TLS Certificate (PEM)", translate("Full certificate (chain), PEM format.")) +o = s:option(TextValue, "tls_certificate", translate("TLS Certificate (PEM)"), translate("Full certificate (chain), PEM format.")) o.default = "" o.rows = 5 o.wrap = "off" @@ -320,7 +338,7 @@ o.validate = function(self, value) return value:gsub("\n", "\\n") end -o = s:option(TextValue, "tls_key", "TLS Private Key (PEM)", translate("Private key in PEM format.")) +o = s:option(TextValue, "tls_key", translate("TLS Private Key (PEM)"), translate("Private key in PEM format.")) o.default = "" o.rows = 5 o.wrap = "off" @@ -336,6 +354,7 @@ end o = s:option(Flag, "ech", translate("ECH")) o.default = "0" o:depends({ tls = true, reality = false }) +o:depends({ protocol = "masque" }) o = s:option(TextValue, "ech_key", translate("ECH Key")) o.default = "" diff --git a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua index 69fbe6b68..651f69194 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua @@ -114,8 +114,8 @@ function gen_outbound(flag, node, tag, proxy_table) local proxy_tag, fragment, record_fragment if proxy_table ~= nil and type(proxy_table) == "table" then proxy_tag = proxy_table.tag or nil - fragment = (proxy_table.fragment and node.protocol ~= "naive" and not node.hysteria2_realms) and true or nil - record_fragment = (proxy_table.record_fragment and node.protocol ~= "naive" and not node.hysteria2_realms) and true or nil + fragment = (proxy_table.fragment and node.protocol ~= "naive" and node.protocol ~= "masque" and not node.hysteria2_realms) and true or nil + record_fragment = (proxy_table.record_fragment and node.protocol ~= "naive" and node.protocol ~= "masque" and not node.hysteria2_realms) and true or nil end if node.type ~= "sing-box" then @@ -233,7 +233,14 @@ function gen_outbound(flag, node, tag, proxy_table) end local tls = nil - if node.protocol == "hysteria" or node.protocol == "hysteria2" or node.protocol == "tuic" or node.protocol == "naive" then + if node.protocol == "masque" then + node.alpn = nil + node.utls = nil + node.reality = nil + node.transport = nil + node.mux = nil + end + if node.protocol == "hysteria" or node.protocol == "hysteria2" or node.protocol == "tuic" or node.protocol == "naive" or node.protocol == "masque" then node.tls = "1" end if node.tls == "1" then @@ -254,7 +261,7 @@ function gen_outbound(flag, node, tag, proxy_table) fragment = fragment, record_fragment = record_fragment, certificate = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and api.split(node.tls_certificate_pem:gsub("\\n", "\n"), "\n") or nil, - cipher_suites = (node.cipherSuites and node.cipherSuites ~= "") and api.split(node.cipherSuites, ":") or nil, + cipher_suites = (node.cipherSuites and node.cipherSuites ~= "") and (type(node.cipherSuites) == "table" and node.cipherSuites or api.split(node.cipherSuites, ":")) or nil, ech = (node.ech == "1") and (function() local function get_ech_domain(s) --兼容xray "域名+DNS" 格式ech local domain, dns = s:match("^([^+]+)%+(.+)$") @@ -690,6 +697,25 @@ function gen_outbound(flag, node, tag, proxy_table) } end + if node.protocol == "masque" then + local headers = {} + if node.user_agent and node.user_agent ~= "" then + headers["user-agent"] = { node.user_agent } + end + for line in (node.masque_headers or ""):gsub("\\n", "\n"):gmatch("[^\r\n]+") do + local key, value = line:match("^%s*([^:]+):%s*(.-)%s*$") + if key then headers[api.trim(key):lower()] = { value } end + end + protocol_table = { + type = "masque-client", + username = not headers.authorization and node.username or nil, + password = not headers.authorization and node.password or nil, + path = (node.masque_path and node.masque_path ~= "") and node.masque_path or nil, + headers = next(headers) and headers or nil, + tls = tls + } + end + if node.protocol == "snell" then protocol_table = { version = tonumber(node.snell_version), @@ -719,6 +745,11 @@ function gen_config_server(node) { type = "direct", tag = "direct" } } + if node.protocol == "masque" then + node.tls = "1" + node.reality = nil + end + local tls = { enabled = true, certificate_path = (node.tls_use_pem ~= "1") and node.tls_certificateFile or nil, @@ -830,7 +861,7 @@ function gen_config_server(node) local user = api.uci_get_s(v) or {} if user[".type"] == "user" then local u = {} - if node.protocol == "mixed" or node.protocol == "socks" or node.protocol == "http" or node.protocol == "naive" then + if node.protocol == "mixed" or node.protocol == "socks" or node.protocol == "http" or node.protocol == "naive" or node.protocol == "masque" then u.username = user.username u.password = user.password end @@ -877,6 +908,18 @@ function gen_config_server(node) local protocol_table = nil + if node.protocol == "masque" then + tls.alpn = nil + protocol_table = { + type = "masque-server", + users = users, + path = (node.masque_path and node.masque_path ~= "") and node.masque_path or nil, + address = node.masque_address, + mtu = tonumber(node.masque_mtu or 1280), + tls = tls + } + end + if node.protocol == "mixed" then protocol_table = { users = users, @@ -1071,8 +1114,7 @@ function gen_config_server(node) end end - if node.protocol == "wireguard" then - inbound.listen = nil + if node.protocol == "wireguard" or node.protocol == "masque" then table.insert(endpoints, inbound) else table.insert(inbounds, inbound) @@ -1146,6 +1188,12 @@ function gen_config_server(node) end end end + for i = #config.outbounds, 1, -1 do + if config.outbounds[i].type == "masque-client" then + endpoints[#endpoints + 1] = config.outbounds[i] + table.remove(config.outbounds, i) + end + end return config end @@ -2341,6 +2389,9 @@ function gen_config(var) } endpoints[#endpoints + 1] = endpoint table.remove(config.outbounds, i) + elseif value.type == "masque-client" then + endpoints[#endpoints + 1] = value + table.remove(config.outbounds, i) end end if #endpoints > 0 then diff --git a/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/luci-app-passwall2/luasrc/passwall2/util_xray.lua index 38b816b8b..8a79f3853 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_xray.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_xray.lua @@ -145,6 +145,11 @@ function gen_outbound(flag, node, tag, proxy_table) node.transport = "hysteria" node.stream_security = "tls" end + if node.protocol == "masque" then + node.transport = "masque" + node.stream_security = "tls" + node.utls = nil + end if node.protocol == "http" and node.stream_security == "tls" then node.transport = "raw" @@ -162,12 +167,12 @@ function gen_outbound(flag, node, tag, proxy_table) _flag_proxy_tag = proxy_tag, tag = tag, protocol = node.protocol, - mux = { + mux = (node.protocol ~= "masque") and { enabled = (node.mux == "1") and true or false, concurrency = (node.mux == "1" and ((node.mux_concurrency) and tonumber(node.mux_concurrency) or -1)) or nil, xudpConcurrency = (node.mux == "1" and ((node.xudp_concurrency) and tonumber(node.xudp_concurrency) or 8)) or nil } or nil, - streamSettings = (node.streamSettings or dialer_proxy_tag or node.protocol == "vmess" or node.protocol == "vless" or node.protocol == "socks" or node.protocol == "shadowsocks" or node.protocol == "trojan" or node.protocol == "hysteria" or node.protocol == "http") and { + streamSettings = (node.streamSettings or dialer_proxy_tag or node.protocol == "vmess" or node.protocol == "vless" or node.protocol == "socks" or node.protocol == "shadowsocks" or node.protocol == "trojan" or node.protocol == "hysteria" or node.protocol == "http" or node.protocol == "masque") and { sockopt = { mark = 255, domainStrategy = node.domain_strategy or "UseIP", @@ -277,6 +282,28 @@ function gen_outbound(flag, node, tag, proxy_table) return api.cleanEmptyTables(extra) end)() } or nil, + masqueSettings = (node.protocol == "masque") and { + path = (node.masque_path and node.masque_path ~= "") and node.masque_path or nil, + warp = (node.masque_warp == "1") and { + privateKey = (node.masque_warp_private_key or ""):gsub("\\n", "\n"), + publicKey = (node.masque_warp_public_key or ""):gsub("\\n", "\n"), + address = node.masque_warp_address + } or nil, + headers = (function() + local headers = {} + if node.masque_warp ~= "1" and ((node.username and node.username ~= "") or (node.password and node.password ~= "")) then + headers["authorization"] = "Basic " .. api.base64Encode((node.username or "") .. ":" .. (node.password or "")) + end + if node.user_agent and node.user_agent ~= "" then + headers["user-agent"] = node.user_agent + end + for line in (node.masque_headers or ""):gsub("\\n", "\n"):gmatch("[^\r\n]+") do + local key, value = line:match("^%s*([^:]+):%s*(.-)%s*$") + if key then headers[api.trim(key):lower()] = value end + end + return next(headers) and headers or nil + end)() + } or nil, hysteriaSettings = (node.transport == "hysteria") and { version = 2, auth = node.hysteria2_auth_password @@ -521,6 +548,10 @@ function gen_config_server(node) u.user = user.username u.pass = user.password end + if node.protocol == "masque" then + u.email = user.username + u.pass = user.password + end if node.protocol == "shadowsocks" or node.protocol == "trojan" then u.email = user.username u.password = user.password @@ -591,6 +622,18 @@ function gen_config_server(node) version = 2, users = users } + elseif node.protocol == "masque" then + node.transport = "masque" + node.tls = "1" + node.reality = nil + node.alpn = nil + node.fallback = nil + node.finalmask = nil + settings = { + users = users, + address = node.masque_address, + mtu = tonumber(node.masque_mtu or 1280) + } elseif node.protocol == "tunnel" then settings = { allowedNetwork = node.d_protocol, @@ -698,6 +741,9 @@ function gen_config_server(node) streamSettings = { method = node.transport, security = "none", + masqueSettings = (node.protocol == "masque") and { + path = (node.masque_path and node.masque_path ~= "") and node.masque_path or nil + } or nil, tlsSettings = ("1" == node.tls) and { disableSystemRoot = false, certificates = { diff --git a/luci-app-passwall2/po/fa/passwall2.po b/luci-app-passwall2/po/fa/passwall2.po index f9d6ae621..58a73b213 100644 --- a/luci-app-passwall2/po/fa/passwall2.po +++ b/luci-app-passwall2/po/fa/passwall2.po @@ -2252,11 +2252,26 @@ msgstr "ورودی %s:%s" msgid "Node: %s:%s, Weight: %s" msgstr "گره: %s:%s، وزن: %s" -msgid "[Rule update] instance is running; please try again later." -msgstr "نمونه [به‌روزرسانی قانون] در حال اجرا است؛ لطفاً بعداً دوباره امتحان کنید." +msgid "Unable to open the subscription/rule update lock. Exiting." +msgstr "باز کردن قفل به‌روزرسانی اشتراک/قوانین ممکن نشد. خروج." -msgid "[Subscription] instance is running; [Rule Update] queue and wait." -msgstr "نمونه [اشتراک] در حال اجرا است؛ [به‌روزرسانی قانون] در صف انتظار قرار می‌گیرد." +msgid "A subscription or rule update is already running. Skipping this request." +msgstr "به‌روزرسانی اشتراک یا قوانین در حال اجرا است. این درخواست نادیده گرفته می‌شود." + +msgid "Unable to acquire the update lock. Scheduled task failed." +msgstr "دریافت قفل به‌روزرسانی ممکن نشد. وظیفه زمان‌بندی‌شده ناموفق بود." + +msgid "Subscription" +msgstr "اشتراک" + +msgid "Rule Update" +msgstr "به‌روزرسانی قوانین" + +msgid "Subscription/Rule Update" +msgstr "به‌روزرسانی اشتراک/قوانین" + +msgid "[%s] is running; [%s] is queued and waiting..." +msgstr "[%s] در حال اجرا است؛ [%s] در صف قرار گرفته و منتظر است..." msgid "Start updating the rules..." msgstr "شروع به‌روزرسانی قوانین..." @@ -2414,11 +2429,11 @@ msgstr "بدون پروکسی" msgid "Proxy" msgstr "پروکسی" -msgid "[Subscription] instance is running; please try again later." -msgstr "نمونه [اشتراک] در حال اجرا است؛ لطفاً بعداً دوباره امتحان کنید." +msgid "Unable to acquire the stop lock. Exiting." +msgstr "دریافت قفل توقف ممکن نشد. خروج." -msgid "[Rule Update] instance is running; [Subscription] queue and wait." -msgstr "نمونه [به‌روزرسانی قانون] در حال اجرا است؛ [اشتراک] در صف انتظار قرار دارد." +msgid "Failed or timed out waiting for subscription/rule updates. Restart cancelled." +msgstr "انتظار برای به‌روزرسانی اشتراک/قوانین ناموفق بود یا مهلت آن به پایان رسید. راه‌اندازی مجدد لغو شد." msgid "Start subscribing..." msgstr "شروع اشتراک‌گیری..." @@ -2432,8 +2447,8 @@ msgstr "اشتراک: [%s] بدون تغییر، نیازی به به‌روزر msgid "[%s] Subscription failed. This could be due to an invalid subscription address or a network issue. Please diagnose the problem! [%s]" msgstr "[%s] اشتراک ناموفق بود. این می‌تواند به دلیل آدرس اشتراک نامعتبر یا مشکل شبکه باشد. لطفاً مشکل را بررسی کنید! [%s]" -msgid "Error, restoring service." -msgstr "خطا، بازیابی سرویس." +msgid "An error occurred during subscription." +msgstr "هنگام به‌روزرسانی اشتراک خطایی رخ داد." msgid "Subscription complete..." msgstr "اشتراک کامل شد..." @@ -2470,3 +2485,18 @@ msgstr "فهرست مجموعه‌های رمز پشتیبانی‌شده را msgid "This username already exists." msgstr "این نام کاربری از قبل وجود دارد." + +msgid "MASQUE Path" +msgstr "مسیر MASQUE" + +msgid "Custom HTTP Headers" +msgstr "سرآیندهای سفارشی HTTP" + +msgid "MASQUE Address" +msgstr "آدرس تونل MASQUE" + +msgid "WARP Device Private Key" +msgstr "کلید خصوصی دستگاه WARP" + +msgid "WARP Endpoint Public Key" +msgstr "کلید عمومی سرور WARP" diff --git a/luci-app-passwall2/po/ru/passwall2.po b/luci-app-passwall2/po/ru/passwall2.po index 3941f856c..1784fe293 100644 --- a/luci-app-passwall2/po/ru/passwall2.po +++ b/luci-app-passwall2/po/ru/passwall2.po @@ -2253,11 +2253,26 @@ msgstr "Вход %s:%s" msgid "Node: %s:%s, Weight: %s" msgstr "Сервер: %s:%s, вес: %s" -msgid "[Rule update] instance is running; please try again later." -msgstr "Экземпляр [Обновление правила] запущен; пожалуйста, попробуйте позже." +msgid "Unable to open the subscription/rule update lock. Exiting." +msgstr "Не удалось открыть файл блокировки обновления подписок и правил. Завершение работы." -msgid "[Subscription] instance is running; [Rule Update] queue and wait." -msgstr "Экземпляр [Подписка] запущен; [Обновление правила] поставить в очередь и ждать." +msgid "A subscription or rule update is already running. Skipping this request." +msgstr "Обновление подписки или правил уже выполняется. Этот запрос пропущен." + +msgid "Unable to acquire the update lock. Scheduled task failed." +msgstr "Не удалось получить блокировку обновления. Запланированная задача не выполнена." + +msgid "Subscription" +msgstr "Подписка" + +msgid "Rule Update" +msgstr "Обновление правил" + +msgid "Subscription/Rule Update" +msgstr "Обновление подписки/правил" + +msgid "[%s] is running; [%s] is queued and waiting..." +msgstr "[%s] выполняется; [%s] находится в очереди и ожидает..." msgid "Start updating the rules..." msgstr "Начало обновления правил..." @@ -2415,11 +2430,11 @@ msgstr "Без прокси" msgid "Proxy" msgstr "Прокси" -msgid "[Subscription] instance is running; please try again later." -msgstr "Экземпляр [Подписки] запущен; пожалуйста, попробуйте позже." +msgid "Unable to acquire the stop lock. Exiting." +msgstr "Не удалось получить блокировку остановки. Завершение работы." -msgid "[Rule Update] instance is running; [Subscription] queue and wait." -msgstr "Экземпляр [Обновление правила] запущен; [Подписка] ставится в очередь и ожидает." +msgid "Failed or timed out waiting for subscription/rule updates. Restart cancelled." +msgstr "Ошибка или превышено время ожидания обновления подписок/правил. Перезапуск отменён." msgid "Start subscribing..." msgstr "Начало подписки..." @@ -2433,8 +2448,8 @@ msgstr "Подписка: [%s] изменений нет, обновление msgid "[%s] Subscription failed. This could be due to an invalid subscription address or a network issue. Please diagnose the problem! [%s]" msgstr "[%s] Ошибка подписки. Возможно, адрес подписки неверен или есть проблема с сетью. Пожалуйста, проведите диагностику! [%s]" -msgid "Error, restoring service." -msgstr "Ошибка, выполняется восстановление службы." +msgid "An error occurred during subscription." +msgstr "При обновлении подписки произошла ошибка." msgid "Subscription complete..." msgstr "Подписка завершена..." @@ -2471,3 +2486,18 @@ msgstr "Задает список поддерживаемых наборов ш msgid "This username already exists." msgstr "Это имя пользователя уже существует." + +msgid "MASQUE Path" +msgstr "Путь MASQUE" + +msgid "Custom HTTP Headers" +msgstr "Пользовательские HTTP-заголовки" + +msgid "MASQUE Address" +msgstr "Адрес туннеля MASQUE" + +msgid "WARP Device Private Key" +msgstr "Приватный ключ устройства WARP" + +msgid "WARP Endpoint Public Key" +msgstr "Публичный ключ сервера WARP" diff --git a/luci-app-passwall2/po/zh-cn/passwall2.po b/luci-app-passwall2/po/zh-cn/passwall2.po index 0e3dc0148..c77b99e15 100644 --- a/luci-app-passwall2/po/zh-cn/passwall2.po +++ b/luci-app-passwall2/po/zh-cn/passwall2.po @@ -2238,11 +2238,26 @@ msgstr "入口 %s:%s" msgid "Node: %s:%s, Weight: %s" msgstr "节点:%s:%s,权重:%s" -msgid "[Rule update] instance is running; please try again later." -msgstr "[规则更新]实例正在运行,请稍后再试..." +msgid "Unable to open the subscription/rule update lock. Exiting." +msgstr "无法打开订阅/规则更新锁,退出。" -msgid "[Subscription] instance is running; [Rule Update] queue and wait." -msgstr "[订阅]实例正在运行,[规则更新]进入队列等待..." +msgid "A subscription or rule update is already running. Skipping this request." +msgstr "订阅或规则更新已在运行,跳过本次请求。" + +msgid "Unable to acquire the update lock. Scheduled task failed." +msgstr "无法获取更新锁,定时任务失败。" + +msgid "Subscription" +msgstr "订阅" + +msgid "Rule Update" +msgstr "规则更新" + +msgid "Subscription/Rule Update" +msgstr "订阅/规则更新" + +msgid "[%s] is running; [%s] is queued and waiting..." +msgstr "[%s]正在运行,[%s]已进入队列等待..." msgid "Start updating the rules..." msgstr "开始更新规则..." @@ -2400,11 +2415,11 @@ msgstr "不代理" msgid "Proxy" msgstr "代理" -msgid "[Subscription] instance is running; please try again later." -msgstr "[订阅]实例正在运行,请稍后再试..." +msgid "Unable to acquire the stop lock. Exiting." +msgstr "无法获取停止锁,退出。" -msgid "[Rule Update] instance is running; [Subscription] queue and wait." -msgstr "[规则更新]实例正在运行,[订阅]进入队列等待..." +msgid "Failed or timed out waiting for subscription/rule updates. Restart cancelled." +msgstr "等待订阅/规则更新失败或超时,已取消重启。" msgid "Start subscribing..." msgstr "开始订阅..." @@ -2418,8 +2433,8 @@ msgstr "订阅:【%s】没有变化,无需更新。" msgid "[%s] Subscription failed. This could be due to an invalid subscription address or a network issue. Please diagnose the problem! [%s]" msgstr "【%s】订阅失败,可能是订阅地址无效,或是网络问题,请诊断![%s]" -msgid "Error, restoring service." -msgstr "发生错误, 正在恢复服务。" +msgid "An error occurred during subscription." +msgstr "订阅过程中发生错误。" msgid "Subscription complete..." msgstr "订阅完毕..." @@ -2460,3 +2475,18 @@ msgstr "该用户名已存在。" msgid "Update Options" msgstr "更新选项" +msgid "MASQUE Path" +msgstr "MASQUE 路径" + +msgid "Custom HTTP Headers" +msgstr "自定义 HTTP 请求头" + +msgid "MASQUE Address" +msgstr "MASQUE 隧道地址" + +msgid "WARP Device Private Key" +msgstr "WARP 设备私钥" + +msgid "WARP Endpoint Public Key" +msgstr "WARP 服务端公钥" + diff --git a/luci-app-passwall2/po/zh-tw/passwall2.po b/luci-app-passwall2/po/zh-tw/passwall2.po index 9e750ca09..1cd472d45 100644 --- a/luci-app-passwall2/po/zh-tw/passwall2.po +++ b/luci-app-passwall2/po/zh-tw/passwall2.po @@ -2244,11 +2244,26 @@ msgstr "入口 %s:%s" msgid "Node: %s:%s, Weight: %s" msgstr "節點:%s:%s,權重:%s" -msgid "[Rule update] instance is running; please try again later." -msgstr "[規則更新]實例正在運行,請稍後再試..." +msgid "Unable to open the subscription/rule update lock. Exiting." +msgstr "無法開啟訂閱/規則更新鎖,退出。" -msgid "[Subscription] instance is running; [Rule Update] queue and wait." -msgstr "[訂閱]實例正在運行,[規則更新]進入隊列等待..." +msgid "A subscription or rule update is already running. Skipping this request." +msgstr "訂閱或規則更新已在執行,略過本次請求。" + +msgid "Unable to acquire the update lock. Scheduled task failed." +msgstr "無法取得更新鎖,排程任務失敗。" + +msgid "Subscription" +msgstr "訂閱" + +msgid "Rule Update" +msgstr "規則更新" + +msgid "Subscription/Rule Update" +msgstr "訂閱/規則更新" + +msgid "[%s] is running; [%s] is queued and waiting..." +msgstr "[%s]正在執行,[%s]已進入佇列等待..." msgid "Start updating the rules..." msgstr "開始更新規則..." @@ -2406,11 +2421,11 @@ msgstr "不代理" msgid "Proxy" msgstr "代理" -msgid "[Subscription] instance is running; please try again later." -msgstr "[訂閱]實例正在運行,請稍後再試..." +msgid "Unable to acquire the stop lock. Exiting." +msgstr "無法取得停止鎖,退出。" -msgid "[Rule Update] instance is running; [Subscription] queue and wait." -msgstr "[規則更新]實例正在運行,[訂閱]進入隊列等待..." +msgid "Failed or timed out waiting for subscription/rule updates. Restart cancelled." +msgstr "等待訂閱/規則更新失敗或逾時,已取消重新啟動。" msgid "Start subscribing..." msgstr "開始訂閱..." @@ -2424,8 +2439,8 @@ msgstr "訂閱:【%s】沒有變化,無需更新。" msgid "[%s] Subscription failed. This could be due to an invalid subscription address or a network issue. Please diagnose the problem! [%s]" msgstr "【%s】訂閱失敗,可能是訂閱地址無效,或是網絡問題,請診斷![%s]" -msgid "Error, restoring service." -msgstr "發生錯誤, 正在恢復服務。" +msgid "An error occurred during subscription." +msgstr "訂閱過程中發生錯誤。" msgid "Subscription complete..." msgstr "訂閱完毕..." @@ -2462,3 +2477,18 @@ msgstr "配置支援的密碼套件列表。" msgid "This username already exists." msgstr "該用戶名已存在。" + +msgid "MASQUE Path" +msgstr "MASQUE 路徑" + +msgid "Custom HTTP Headers" +msgstr "自訂 HTTP 請求標頭" + +msgid "MASQUE Address" +msgstr "MASQUE 隧道位址" + +msgid "WARP Device Private Key" +msgstr "WARP 裝置私鑰" + +msgid "WARP Endpoint Public Key" +msgstr "WARP 伺服器公鑰" diff --git a/luci-app-passwall2/root/etc/init.d/passwall2 b/luci-app-passwall2/root/etc/init.d/passwall2 index ded32998f..c2a39f0c9 100755 --- a/luci-app-passwall2/root/etc/init.d/passwall2 +++ b/luci-app-passwall2/root/etc/init.d/passwall2 @@ -10,39 +10,30 @@ LOCK_FILE=${LOCK_PATH}/${CONFIG}.lock fi set_lock() { - [ ! -d "$LOCK_PATH" ] && mkdir -p $LOCK_PATH - exec 999>"$LOCK_FILE" - flock -xn 999 + mkdir -p "$LOCK_PATH" || return 1 + command exec 9>"$LOCK_FILE" || return 1 + flock -xn 9 +} + +set_lock_wait() { + mkdir -p "$LOCK_PATH" || return 1 + command exec 9>"$LOCK_FILE" || return 1 + flock -x 9 } unset_lock() { - flock -u 999 - rm -rf "$LOCK_FILE" -} - -unlock() { - failcount=1 - while [ "$failcount" -le 10 ]; do - if [ -f "$LOCK_FILE" ]; then - let "failcount++" - sleep 1s - [ "$failcount" -ge 10 ] && unset_lock - else - break - fi - done + flock -u 9 + exec 9>&- } boot_func() { rm -f ${LOCK_PATH}/${CONFIG}_ready.lock - rm -f ${LOCK_PATH}/${CONFIG}_rule_update.lock - rm -f ${LOCK_PATH}/${CONFIG}_subscribe.lock local delay=$(uci -q get ${CONFIG}.@global_delay[0].start_delay || echo 1) if [ "$delay" -gt 0 ]; then log_i18n 0 "Start after a delay of %s seconds!" "${delay}" sleep $delay fi - restart boot + restart boot || return 1 touch ${LOCK_PATH}/${CONFIG}_ready.lock } @@ -51,35 +42,55 @@ boot() { } start() { - set_lock - [ $? == 1 ] && log_i18n 0 "The script is already running, do not run it again. Exit." && exit 0 - $APP_FILE start + local action=${2:-$1} + [ "$action" = "start" ] && action= + set_lock || { log_i18n 0 "The script is already running, do not run it again. Exit."; return 0; } + "$APP_FILE" start "$action" 9>&- + local status=$? unset_lock + return "$status" } stop() { - unlock - set_lock - [ $? == 1 ] && log_i18n 0 "Stop the script and wait for a timeout, then exit without repeating the process." && exit 0 - $APP_FILE stop + local action=${2:-$1} + [ "$action" = "stop" ] && action= + set_lock_wait || { log_i18n 0 "Unable to acquire the stop lock. Exiting."; return 1; } + "$APP_FILE" stop "$action" 9>&- + local status=$? unset_lock + return "$status" } restart() { - set_lock - [ $? == 1 ] && log_i18n 0 "The script is already running, do not run it again. Exit." && exit 0 - rule_lock=${LOCK_PATH}/${CONFIG}_rule_update.lock - sub_lock=${LOCK_PATH}/${CONFIG}_subscribe.lock + local action=${2:-$1} + [ "$action" = "restart" ] && action= + set_lock || { + log_i18n 0 "The script is already running, do not run it again. Exit." + [ "$action" = "boot" ] && return 1 + return 0 + } + local update_status=1 for i in $(seq 1 300); do - if [ ! -f "$rule_lock" ] && [ ! -f "$sub_lock" ]; then - break - fi + PASSWALL2_UPDATE_LOCK="${LOCK_PATH}/${CONFIG}_sub_rule.lock" lua -e ' + local nixio = require "nixio" + local file = nixio.open(os.getenv("PASSWALL2_UPDATE_LOCK"), "a") + if not file then os.exit(2) end + os.exit(file:lock("tlock") and 0 or 1) + ' + update_status=$? + [ "$update_status" -ne 1 ] && break sleep 2 done - rm -f "$rule_lock" "$sub_lock" - $APP_FILE stop - $APP_FILE start $1 + if [ "$update_status" -ne 0 ]; then + log_i18n 0 "Failed or timed out waiting for subscription/rule updates. Restart cancelled." + unset_lock + return 1 + fi + "$APP_FILE" stop "$action" 9>&- + "$APP_FILE" start "$action" 9>&- + local status=$? unset_lock + return "$status" } reload() { diff --git a/luci-app-passwall2/root/usr/share/passwall2/app.sh b/luci-app-passwall2/root/usr/share/passwall2/app.sh index 8a5e79ed0..d2fff4e00 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/app.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/app.sh @@ -591,7 +591,6 @@ start_socks() { } clean_crontab() { - [ -f "${LOCK_PATH}/${CONFIG}_cron.lock" ] && return touch /etc/crontabs/root #sed -i "/${CONFIG}/d" /etc/crontabs/root >/dev/null 2>&1 sed -i "/$(echo "/etc/init.d/${CONFIG}" | sed 's#\/#\\\/#g')/d" /etc/crontabs/root >/dev/null 2>&1 @@ -608,11 +607,10 @@ start_crontab() { [ "$start_daemon" = "1" ] && { $APP_PATH/monitor.sh > /dev/null 2>&1 & } fi - [ -f "${LOCK_PATH}/${CONFIG}_cron.lock" ] && { - rm -rf "${LOCK_PATH}/${CONFIG}_cron.lock" + if [ "$1" = "cron" ]; then log_i18n 0 "The task is currently running automatically as a scheduled task; no reconfiguration of the scheduled task is required." return - } + fi clean_crontab @@ -628,7 +626,7 @@ start_crontab() { stop_time_mm=$(echo $stop_time_mode | awk -F ':' '{print $2}') local t="$stop_time_mm $stop_time_hh * * $stop_week_mode" [ "$stop_week_mode" = "7" ] && t="$stop_time_mm $stop_time_hh * * *" - echo "$t /etc/init.d/$CONFIG stop > /dev/null 2>&1 &" >>/etc/crontabs/root + echo "$t /etc/init.d/$CONFIG stop cron > /dev/null 2>&1 &" >>/etc/crontabs/root log_i18n 0 "Scheduled tasks: Auto stop service." fi @@ -639,7 +637,7 @@ start_crontab() { start_time_mm=$(echo $start_time_mode | awk -F ':' '{print $2}') local t="$start_time_mm $start_time_hh * * $start_week_mode" [ "$start_week_mode" = "7" ] && t="$start_time_mm $start_time_hh * * *" - echo "$t /etc/init.d/$CONFIG start > /dev/null 2>&1 &" >>/etc/crontabs/root + echo "$t /etc/init.d/$CONFIG start cron > /dev/null 2>&1 &" >>/etc/crontabs/root log_i18n 0 "Scheduled tasks: Auto start service." fi @@ -653,7 +651,7 @@ start_crontab() { if [ "$restart_week_mode" = "8" ]; then update_loop=1 else - echo "$t /etc/init.d/$CONFIG restart > /dev/null 2>&1 &" >>/etc/crontabs/root + echo "$t /etc/init.d/$CONFIG restart cron > /dev/null 2>&1 &" >>/etc/crontabs/root fi log_i18n 0 "Scheduled tasks: Auto restart service." fi @@ -716,7 +714,7 @@ start_crontab() { } stop_crontab() { - [ -f "${LOCK_PATH}/${CONFIG}_cron.lock" ] && return + [ "$1" = "cron" ] && return clean_crontab /etc/init.d/cron restart #log_i18n 0 "Clear scheduled commands." @@ -894,7 +892,7 @@ acl_node() { start() { busybox pgrep -f ${TMP_PATH}/bin > /dev/null 2>&1 && { logger -t PW2-RESTART "Upgrade or overload residue is detected, and the subprocess is being called to perform complete cleaning..." - (stop) + (stop "$1") sleep 2 } mkdir -p /tmp/etc /tmp/log $TMP_PATH $TMP_BIN_PATH $TMP_SCRIPT_FUNC_PATH $TMP_ROUTE_PATH $TMP_ACL_PATH $TMP_PATH2 @@ -936,10 +934,7 @@ start() { } fi run_process_queue - [ "$1" = "boot" ] && { - rm -f "${LOCK_PATH}/${CONFIG}_cron.lock" - } - start_crontab + start_crontab "$1" log_i18n 0 "Running complete!" echolog "\n" @@ -977,7 +972,7 @@ stop() { unset V2RAY_LOCATION_ASSET unset XRAY_LOCATION_ASSET unset SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN - stop_crontab + stop_crontab "$1" rm -rf $DEFAULT_DNSMASQ_CONF rm -rf $DEFAULT_DNSMASQ_CONF_PATH [ "1" = "1" ] && { @@ -1004,12 +999,6 @@ stop() { rm -f ${LOCK_PATH}/${CONFIG}_socks_auto_switch* rm -f ${LOCK_PATH}/${CONFIG}_lease2hosts* rm -f ${LOCK_PATH}/${CONFIG}_monitor* - if ! busybox pgrep -af "${CONFIG}/" | grep -q '/subscribe\.lua'; then - rm -f "${LOCK_PATH}/${CONFIG}_subscribe.lock" - fi - if ! busybox pgrep -af "${CONFIG}/" | grep -q '/rule_update\.lua'; then - rm -f "${LOCK_PATH}/${CONFIG}_rule_update.lock" - fi log_i18n 0 "Clearing and closing related programs and cache complete." exit 0 } @@ -1099,9 +1088,9 @@ socks_node_switch) socks_node_switch $@ ;; start) - start $@ + start "$@" ;; stop) - stop + stop "$@" ;; esac diff --git a/luci-app-passwall2/root/usr/share/passwall2/rule_update.lua b/luci-app-passwall2/root/usr/share/passwall2/rule_update.lua index 0ef8e387c..3deae806b 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/rule_update.lua +++ b/luci-app-passwall2/root/usr/share/passwall2/rule_update.lua @@ -1,7 +1,6 @@ #!/usr/bin/lua local api = require "luci.passwall2.api" -local appname = api.appname local fs = api.fs local log = api.log local sys = api.sys @@ -11,6 +10,35 @@ local arg1 = arg[1] local arg2 = arg[2] local arg3 = arg[3] +local update_lock = api.nixio.open(api.LOCK_PREFIX .. "_sub_rule.lock", api.nixio.open_flags("rdwr", "creat")) +if not update_lock then + log(0, api.i18n.translate("Unable to open the subscription/rule update lock. Exiting.")) + os.exit(1) +end + +if not update_lock:lock("tlock") then + if arg3 ~= "cron" then + log(0, api.i18n.translate("A subscription or rule update is already running. Skipping this request.")) + update_lock:close() + os.exit(0) + end + local running_task = update_lock:read(1) + update_lock:seek(0, "set") + local running_name = running_task == "s" and api.i18n.translate("Subscription") + or running_task == "r" and api.i18n.translate("Rule Update") + or api.i18n.translate("Subscription/Rule Update") + log(0, api.i18n.translatef("[%s] is running; [%s] is queued and waiting...", running_name, api.i18n.translate("Rule Update"))) + if not update_lock:lock("lock") then + log(0, api.i18n.translate("Unable to acquire the update lock. Scheduled task failed.")) + update_lock:close() + os.exit(1) + end +end +update_lock:write("r") +update_lock:seek(0, "set") +uci:revert(api.c_config) +math.randomseed(os.time() + math.floor(os.clock() * 1000)) + local reboot = 0 local geoip_update = "0" local geosite_update = "0" @@ -181,37 +209,10 @@ else geosite_update = uci_get("@global_rules[0]", "geosite_update") or "1" end if geoip_update == "0" and geosite_update == "0" then - os.exit(0) + update_lock:close() + return end -local function check_instance(action) - local rule_lock = "/var/lock/" .. appname .. "_rule_update.lock" - local sub_lock = "/var/lock/" .. appname .. "_subscribe.lock" - - if action == "start" then - math.randomseed(os.time() + math.floor(os.clock() * 1000)) - api.nixio.nanosleep(0, math.random(100, 1000) * 1000000) - if fs.access(rule_lock) then - log(0, api.i18n.translatef("[Rule update] instance is running; please try again later.") .. "\n") - os.exit(0) - else - luci.sys.call("touch " .. rule_lock) - end - elseif action == "end" then - luci.sys.call("rm -f " .. rule_lock) - return - end - - if fs.access(sub_lock) then - log(0, api.i18n.translatef("[Subscription] instance is running; [Rule Update] queue and wait.") .. "\n") - end - while fs.access(sub_lock) do - api.nixio.nanosleep(2, 0) - end -end - -check_instance("start") - log(0, api.i18n.translate("Start updating the rules...")) local function safe_call(func, err_msg) xpcall(func, function(e) @@ -235,19 +236,16 @@ end uci_set("@global_rules[0]", "geoip_update", geoip_update) uci_set("@global_rules[0]", "geosite_update", geosite_update) -uci_save(true) if reboot == 1 then - if arg3 == "cron" then - if not fs.access(api.LOCK_PREFIX .. ".lock") then - sys.call("touch %s_cron.lock" % api.LOCK_PREFIX) - end - end - log(1, api.i18n.translate("Restart the service and apply the new rules.")) uci_set("@global[0]", "flush_set", "1") - uci_save(true, true) end +uci_save(true) log(0, api.i18n.translate("The rules have been updated...")) -check_instance("end") +update_lock:close() +if reboot == 1 then + local action = arg3 == "cron" and " cron" or "" + sys.call("/etc/init.d/passwall2 restart" .. action .. " > /dev/null 2>&1 &") +end diff --git a/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua b/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua index a41d23319..85ac66660 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua +++ b/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua @@ -23,6 +23,35 @@ local uci, uci_get, uci_set, uci_del, uci_foreach, uci_save = api.uci, api.uci_g -- so caching them is worth the effort local tinsert = table.insert local ssub, slen, schar, sbyte, sformat, sgsub = string.sub, string.len, string.char, string.byte, string.format, string.gsub + +-- Both update scripts use the same lockf lock; the kernel releases it on exit. +if not arg[1] then os.exit(0) end +local update_lock = api.nixio.open(api.LOCK_PREFIX .. "_sub_rule.lock", api.nixio.open_flags("rdwr", "creat")) +if not update_lock then + log(0, i18n.translate("Unable to open the subscription/rule update lock. Exiting.")) + os.exit(1) +end +-- Scheduled updates wait for the lock; manual requests skip a busy lock. +if not update_lock:lock("tlock") then + if arg[3] ~= "cron" then + log(0, i18n.translate("A subscription or rule update is already running. Skipping this request.")) + os.exit(0) + end + local running = ({s = i18n.translate("Subscription"), r = i18n.translate("Rule Update")})[update_lock:read(1)] or i18n.translate("Subscription/Rule Update") + update_lock:seek(0, "set") + log(0, i18n.translatef("[%s] is running; [%s] is queued and waiting...", running, i18n.translate("Subscription"))) + if not update_lock:lock("lock") then + log(0, i18n.translate("Unable to acquire the update lock. Scheduled task failed.")) + os.exit(1) + end +end +-- Record the running task in the same file: s = subscription, r = rule update. +update_lock:write("s") +update_lock:seek(0, "set") +uci:revert(c_config) +math.randomseed(os.time() + math.floor(os.clock() * 1000)) +local restart_required = false + local lyaml = require "lyaml" local has_ss_rust = api.is_finded("sslocal") @@ -2487,15 +2516,7 @@ local function update_node(manual) uci_save(true) - if arg[3] == "cron" then - if not fs.access(api.LOCK_PREFIX .. ".lock") then - luci.sys.call("touch %s_cron.lock" % api.LOCK_PREFIX) - end - end - - if manual ~= 1 then - luci.sys.call("/etc/init.d/passwall2 restart > /dev/null 2>&1 &") - end + if manual ~= 1 then restart_required = true end end local function parse_link(raw, add_mode, group, sub_cfg) @@ -2687,54 +2708,29 @@ local execute = function() end end -local function check_instance(action) - local sub_lock = api.LOCK_PREFIX .. "_subscribe.lock" - local rule_lock = api.LOCK_PREFIX .. "_rule_update.lock" - - if action == "start" then - math.randomseed(os.time() + math.floor(os.clock() * 1000)) - api.nixio.nanosleep(0, math.random(100, 1000) * 1000000) - if fs.access(sub_lock) then - log(0, i18n.translatef("[Subscription] instance is running; please try again later.") .. "\n") - os.exit(0) - else - luci.sys.call("touch " .. sub_lock) - uci:revert(c_config) - end - elseif action == "end" then - luci.sys.call("rm -f " .. sub_lock) - return - end - - if fs.access(rule_lock) then - log(0, i18n.translatef("[Rule Update] instance is running; [Subscription] queue and wait.") .. "\n") - end - while fs.access(rule_lock) do - api.nixio.nanosleep(2, 0) - end -end - -if arg[1] then - check_instance("start") - - if arg[1] == "start" then - log(0, i18n.translatef("Start subscribing...")) - xpcall(execute, function(e) - log(1, e) +if arg[1] == "start" then + log(0, i18n.translate("Start subscribing...")) + xpcall(execute, function(e) + log(1, e) + if type(debug) == "table" and type(debug.traceback) == "function" then log(1, debug.traceback()) - log(1, i18n.translatef("Error, restoring service.")) - end) - log(0, i18n.translatef("Subscription complete...") .. "\n") - elseif arg[1] == "add" then - local f = assert(io.open("/tmp/links.conf", 'r')) - local raw = f:read('*all') - f:close() - parse_link(raw, "1", arg[2]) - update_node(1) - luci.sys.call("rm -f /tmp/links.conf") - elseif arg[1] == "truncate" then - truncate_nodes(arg[2]) - end + end + log(1, i18n.translate("An error occurred during subscription.")) + end) + log(0, i18n.translate("Subscription complete...") .. "\n") +elseif arg[1] == "add" then + local f = assert(io.open("/tmp/links.conf", 'r')) + local raw = f:read('*all') + f:close() + parse_link(raw, "1", arg[2]) + update_node(1) + luci.sys.call("rm -f /tmp/links.conf") +elseif arg[1] == "truncate" then + truncate_nodes(arg[2]) +end - check_instance("end") +update_lock:close() +if restart_required then + local action = arg[3] == "cron" and " cron" or "" + luci.sys.call("/etc/init.d/passwall2 restart" .. action .. " > /dev/null 2>&1 &") end diff --git a/luci-app-passwall2/root/usr/share/passwall2/tasks.sh b/luci-app-passwall2/root/usr/share/passwall2/tasks.sh index 296128fd8..c41882274 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/tasks.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/tasks.sh @@ -22,7 +22,7 @@ do restart_interval_mode=$(expr "$restart_interval_mode" \* 60) if [ -n "$restart_week_mode" ]; then [ "$restart_week_mode" = "8" ] && { - [ "$(expr "$CFG_UPDATE_INT" % "$restart_interval_mode")" -eq 0 ] && { /etc/init.d/$CONFIG restart > /dev/null 2>&1 & } + [ "$(expr "$CFG_UPDATE_INT" % "$restart_interval_mode")" -eq 0 ] && { /etc/init.d/$CONFIG restart cron > /dev/null 2>&1 & } } fi diff --git a/luci-app-smartsafehub/.gitignore b/luci-app-smartsafehub/.gitignore index 79493313a..ff614dc6d 100644 --- a/luci-app-smartsafehub/.gitignore +++ b/luci-app-smartsafehub/.gitignore @@ -27,3 +27,5 @@ frontend/.env.*.local *.orig *.rej /*.patch + +# frontend/demo-dist/ is intentionally committed for the Hub demo. diff --git a/luci-app-smartsafehub/CHANGELOG.md b/luci-app-smartsafehub/CHANGELOG.md index ddd1e5b3f..8ab0adba2 100644 --- a/luci-app-smartsafehub/CHANGELOG.md +++ b/luci-app-smartsafehub/CHANGELOG.md @@ -1,5 +1,155 @@ # 변경 기록 +## [0.2.29-r1] - 2026-10-17 + +### 추가 + +- 추천 사용자 규칙을 적용하는 기능을 제공합니다. +- 공유기 웹사이트의 미리보기 기능을 제공합니다. + +### 개선 + +- 펌웨어 정보에 관리 소프트웨어의 버전을 표시합니다. + +## [0.2.28-r9] - 2026-10-17 + +### 추가 + +- 추천 규칙의 허용 목록 반영을 확인하고 서버에 결과를 전달합니다. +- 직접 허용한 도메인과 추천 규칙이 추가한 도메인을 구분해 표시할 수 있습니다. + +### 호환성 + +- 기존 직접 추가한 허용 규칙과 오프라인 상태를 보존합니다. +- 확인 정보는 임시 메모리에 보관해 공유기의 저장장치 쓰기를 늘리지 않습니다. +- 확인 정보가 없어도 기존 기기 동기화가 계속 동작합니다. + +### 테스트 + +- 기존 허용 규칙 판정, 동기화 실패·재시도 및 잘못된 확인 응답을 검증합니다. + +## [0.2.28-r8] - 2026-10-16 + +### 추가 + +- 사용자 규칙 화면에서 추천 사용자 규칙을 확인하고 웹사이트로 이동할 수 있습니다. +- 웹사이트에서 공유기에 적용하거나 해제한 추천 허용 규칙이 자동으로 동기화됩니다. + +### 개선 + +- 추천 규칙으로 추가된 도메인에는 출처를 표시하고, 공유기에서 실수로 삭제하지 않도록 했습니다. +- 직접 추가한 허용 규칙은 추천 규칙을 해제해도 삭제하지 않습니다. +- 일시적인 동기화 실패 시 다음 연결에서 다시 적용합니다. + +### 호환성 + +- 인터넷 연결이 끊기거나 계정을 연결하지 않아도 기존 사용자 규칙은 정상 동작합니다. + +### 테스트 + +- 추천 규칙 추가·해제, 직접 등록한 허용 규칙 보존, 동기화 실패 및 잘못된 응답 처리를 확인합니다. + +## [0.2.28-r7] - 2026-10-16 + +### 개선 + +- 펌웨어·관리 소프트웨어의 마지막 확인 시간과 SafeShield의 최근 갱신 시간을 `몇 분 전`처럼 알아보기 쉽게 표시합니다. +- 표시된 시간 옆의 정보 아이콘을 누르면 정확한 날짜와 시각을 확인할 수 있습니다. +- 페이지를 새로고침하지 않아도 경과 시간이 자동으로 바뀌며, 오래된 기록은 날짜로 표시합니다. + +### 테스트 + +- 시간 경계값, 기록이 없는 경우, 오래된 기록 및 잘못된 보호 데이터 버전의 처리와 화면 연결을 검증합니다. + + +## [0.2.28-r6] - 2026-10-16 + +### 개선 + +- 설정을 '진단 및 지원', '시간 및 예약', '관리 및 복구' 탭으로 나눴습니다. 진단 화면이 가장 먼저 열립니다. +- 네트워크를 '인터넷 연결'과 'LAN 및 DHCP' 탭으로 나눠 필요한 설정을 더 쉽게 찾을 수 있습니다. +- 대시보드에서 확인할 수 있는 시스템 상태 요약 카드는 설정 화면에서 제거했습니다. +- 탭을 오가더라도 저장하지 않은 입력 내용은 유지됩니다. + +### 호환성 + +- 기존 인터넷·내부 네트워크 바로가기 주소를 계속 사용할 수 있으며, 선택한 탭의 주소를 공유할 수 있습니다. + +### 테스트 + +- 기본 탭, 주소 직접 접속, 이전 주소 호환성, 입력 상태 보존과 모바일 탭 구조를 확인합니다. + +## [0.2.28-r5] - 2026-10-16 + +### 추가 + +- 새 펌웨어에 포함된 SmartSafeHub 관리 소프트웨어 버전을 업데이트 화면에 표시합니다. + +### 개선 + +- 관리 소프트웨어가 이미 같은 버전일 때는 별도로 업데이트하지 않아도 된다고 안내합니다. +- 펌웨어에 포함된 관리 소프트웨어가 현재 설치 버전보다 오래된 경우 확인 안내를 표시합니다. +- 버전 정보가 제공되지 않는 펌웨어에서는 기존 업데이트 화면을 그대로 유지합니다. + +### 테스트 + +- 펌웨어의 포함 버전 전달과 동일·신규·이전 버전 비교 및 정보 누락 처리를 확인합니다. + +## [0.2.28-r4] - 2026-10-13 + +### 수정 + +- 사용자 규칙에서 도메인을 추가하면 저장 직후 입력창이 비워지도록 수정했습니다. +- 규칙 저장에 실패한 경우에는 입력한 도메인을 유지해 다시 시도할 수 있습니다. +- DNS 적용 진행 상황은 기존과 같이 별도로 안내합니다. + +### 개선 + +- 도메인을 연속 등록할 때 이전 규칙의 적용 결과가 새로운 안내를 덮어쓰지 않도록 했습니다. +- DNS 적용 상태를 확인할 수 없는 경우에도 저장 성공 여부를 구분해서 안내합니다. + +### 테스트 + +- 허용·차단 목록 모두 저장 성공 시에만 입력창을 초기화하는지 확인합니다. +- DNS 적용을 기다리는 동안에도 입력창을 초기화하고, 저장 실패 시 입력을 유지하는 동작을 확인합니다. +- 여러 규칙을 연속 등록해도 진행 안내가 뒤섞이지 않는지 확인합니다. + +## [0.2.28-r3] - 2026-10-11 + +### 수정 + +- 공유기 체험 화면의 다크 모드에서 안내 문구와 링크가 잘 보이지 않던 문제를 수정했습니다. + +### 추가 + +- 공유기 체험 메뉴에 최근 활동, 네트워크, Wi-Fi, 사용자 규칙, 업데이트 화면을 추가했습니다. +- 새 화면에서도 예시 데이터로 기능을 살펴볼 수 있도록 했습니다. + +### 개선 + +- 데모의 Wi-Fi QR 코드는 예시 정보만 사용하며 실제 공유기에는 연결하지 않습니다. +- 체험 화면에서는 저장 및 업데이트 설치 같은 변경 작업이 실행되지 않습니다. + +### 테스트 + +- 추가한 데모 메뉴와 데이터 및 실제 공유기 통신 차단을 확인합니다. + +## [0.2.28-r2] - 2026-10-10 + +### 추가 + +- SmartSafeHub 웹사이트에서 공유기 관리 화면의 대시보드, SafeShield, 연결 기기를 미리 체험할 수 있도록 했습니다. + +### 개선 + +- 데모에서는 실제 공유기에 접속하거나 설정을 변경하지 않도록 분리했습니다. +- 웹사이트 배포에서 자동으로 사용할 수 있도록 데모 실행 파일도 저장소에서 관리합니다. + +### 테스트 + +- 데모 화면에 실제 로그인 및 공유기 통신 코드가 포함되지 않도록 검사합니다. +- 데모를 새로 빌드했을 때 저장소에 등록된 파일과 일치하는지 확인합니다. + ## [0.2.28-r1] - 2026-10-10 ### 추가 diff --git a/luci-app-smartsafehub/Makefile b/luci-app-smartsafehub/Makefile index 679aee27a..367968519 100644 --- a/luci-app-smartsafehub/Makefile +++ b/luci-app-smartsafehub/Makefile @@ -6,8 +6,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-smartsafehub -PKG_VERSION:=0.2.28 -PKG_RELEASE:=8 +PKG_VERSION:=0.2.29 +PKG_RELEASE:=9 PKG_MAINTAINER:=Beomjun Kang PKG_LICENSE:=GPL-3.0-or-later @@ -23,6 +23,7 @@ LUCI_PKGARCH:=all define Package/luci-app-smartsafehub/conffiles /etc/config/smartsafehub /etc/smartsafehub/device-credential.json +/etc/smartsafehub/recommended-rules.state endef define Package/luci-app-smartsafehub/preinst diff --git a/luci-app-smartsafehub/docs/ROUTER_DEMO.md b/luci-app-smartsafehub/docs/ROUTER_DEMO.md new file mode 100644 index 000000000..d5ca6543f --- /dev/null +++ b/luci-app-smartsafehub/docs/ROUTER_DEMO.md @@ -0,0 +1,25 @@ +# 공유기 웹사이트 데모 번들 관리 + +웹사이트(`/router-demo/`)에 표시되는 공유기 대시보드 데모의 **원본 소스와 빌드 결과물**은 이 저장소에서 관리합니다. 실제 공유기 인증/RPC 코드는 데모에 포함되지 않습니다. + +```bash +cd frontend +npm ci +npm run build:demo +cd .. +git add frontend/demo-dist +# 기존 코드 변경과 함께 커밋하고 push +``` + +- `frontend/demo-dist/`는 `.gitignore` 대상이 **아닙니다**. 데모 빌드 결과물(`demo.js`, `demo.css`, 동적으로 생성된 파일)을 **모두 커밋**합니다. +- `.github/workflows/ci.yml`에서 동일한 빌드를 다시 실행하여 커밋된 결과물과의 일치 여부를 검사합니다. 커밋을 누락하거나 파일이 달라지면 CI가 실패합니다. +- **hub** 저장소에서는 `router-demo.ref`에 위 커밋의 전체 40자리 Git SHA를 지정합니다. Hub CI 및 배포 스크립트가 해당 커밋을 가져와 정적 자산으로 설치하므로 Hub 서버에서 Node/Vite 빌드는 필요하지 않습니다. +- 공유기 관리 페이지의 일반 빌드(`npm run build`)와 데모 전용 빌드(`npm run build:demo`)는 서로 독립적입니다. + +## 공개 데모 화면 + +- 대시보드, 최근 활동, 네트워크(WAN/LAN), Wi-Fi/게스트 Wi-Fi, 연결된 기기, SafeShield, 사용자 규칙, 업데이트를 체험할 수 있습니다. +- 저장, 차단 규칙 수정, WAN 재연결, 업데이트 확인/설치 등은 **실제 요청을 보내지 않고** 안내 메시지만 보여줍니다. +- QR 코드도 실제 자격 정보 요청 대신 **데모 전용 예시 Wi-Fi 정보**로 생성합니다. 실제 공유기에서는 기존 인증된 RPC를 계속 사용합니다. +- `frontend/src/app/routes.ts`의 `DEMO_ROUTES`는 데모 메뉴 및 허용 경로의 단일 기준입니다. +- 화면이나 데모 데이터를 수정하면 `npm run build:demo`로 데모 빌드 파일을 로컬에서 갱신해 커밋하세요. Hub는 고정 Git SHA로 해당 파일을 가져옵니다. diff --git a/luci-app-smartsafehub/frontend/demo-dist/demo.css b/luci-app-smartsafehub/frontend/demo-dist/demo.css new file mode 100644 index 000000000..1deeb8043 --- /dev/null +++ b/luci-app-smartsafehub/frontend/demo-dist/demo.css @@ -0,0 +1,3 @@ +/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */ +@layer properties{:host,*,:before,:after{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-outline-style:solid;--tw-ease:initial}::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-outline-style:solid;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:#fef2f2;--color-red-100:#ffe2e2;--color-red-200:#ffcaca;--color-red-300:#ffa3a3;--color-red-700:#bf000f;--color-red-800:#9f0712;--color-red-900:#82181a;--color-red-950:#460809;--color-amber-50:#fffbeb;--color-amber-100:#fef3c6;--color-amber-200:#fee685;--color-amber-300:#ffd236;--color-amber-500:#f99c00;--color-amber-600:#dd7400;--color-amber-700:#b75000;--color-amber-800:#953d00;--color-amber-900:#7b3306;--color-amber-950:#461901;--color-emerald-50:#ecfdf5;--color-emerald-100:#d0fae5;--color-emerald-200:#a4f4cf;--color-emerald-500:#00bb7f;--color-emerald-700:#007956;--color-emerald-800:#005f46;--color-emerald-900:#004e3b;--color-emerald-950:#002c22;--color-teal-50:#f0fdfa;--color-teal-100:#cbfbf1;--color-teal-200:#96f7e4;--color-teal-300:#46ecd5;--color-teal-400:#00d3bd;--color-teal-500:#00baa7;--color-teal-600:#009588;--color-teal-700:#00776e;--color-teal-800:#005f5a;--color-teal-900:#0b4f4a;--color-teal-950:#022f2e;--color-sky-50:#f0f9ff;--color-sky-100:#dff2fe;--color-sky-200:#b8e6fe;--color-sky-600:#0084cc;--color-sky-700:#0069a4;--color-sky-800:#005986;--color-sky-950:#052f4a;--color-blue-50:#eff6ff;--color-blue-200:#bedbff;--color-blue-700:#1447e6;--color-rose-50:#fff1f2;--color-rose-100:#ffe4e6;--color-rose-200:#ffccd3;--color-rose-300:#ffa2ae;--color-rose-600:#e70044;--color-rose-700:#c20039;--color-rose-800:#a30037;--color-rose-900:#8b0836;--color-rose-950:#4d0218;--color-slate-50:#f8fafc;--color-slate-100:#f1f5f9;--color-slate-200:#e2e8f0;--color-slate-300:#cad5e2;--color-slate-400:#90a1b9;--color-slate-500:#62748e;--color-slate-600:#45556c;--color-slate-700:#314158;--color-slate-800:#1d293d;--color-slate-900:#0f172b;--color-slate-950:#020618;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tight:-.025em;--tracking-wide:.025em;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--animate-bounce:bounce 1s infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}@supports (color:color(display-p3 0 0 0)){:root,:host{--color-red-50:color(display-p3 .988669 .951204 .950419);--color-red-100:color(display-p3 .980386 .889727 .887779);--color-red-200:color(display-p3 .969562 .798149 .794299);--color-red-300:color(display-p3 .956922 .651886 .645122);--color-red-700:color(display-p3 .692737 .116232 .104679);--color-red-800:color(display-p3 .569606 .121069 .108493);--color-red-900:color(display-p3 .466906 .133198 .12139);--color-red-950:color(display-p3 .25064 .0550983 .0468818);--color-amber-50:color(display-p3 .997804 .985047 .926312);--color-amber-100:color(display-p3 .989391 .954583 .796328);--color-amber-200:color(display-p3 .979824 .904554 .57325);--color-amber-300:color(display-p3 .974327 .83063 .33298);--color-amber-500:color(display-p3 .93994 .620584 .0585367);--color-amber-600:color(display-p3 .827144 .467166 .0336006);--color-amber-700:color(display-p3 .67989 .32771 .0520514);--color-amber-800:color(display-p3 .547533 .256058 .0728326);--color-amber-900:color(display-p3 .445958 .216422 .0823321);--color-amber-950:color(display-p3 .252662 .109091 .026881);--color-emerald-50:color(display-p3 .936818 .989882 .961937);--color-emerald-100:color(display-p3 .848335 .975974 .901691);--color-emerald-200:color(display-p3 .713164 .947563 .822283);--color-emerald-500:color(display-p3 .267113 .726847 .508397);--color-emerald-700:color(display-p3 .164041 .470229 .343508);--color-emerald-800:color(display-p3 .135396 .371401 .277561);--color-emerald-900:color(display-p3 .117821 .302975 .234501);--color-emerald-950:color(display-p3 .0549113 .170434 .13484);--color-teal-50:color(display-p3 .951444 .990904 .98112);--color-teal-100:color(display-p3 .834533 .980328 .946428);--color-teal-200:color(display-p3 .675869 .957879 .896029);--color-teal-300:color(display-p3 .484989 .914726 .835079);--color-teal-400:color(display-p3 .322787 .822056 .744388);--color-teal-500:color(display-p3 .266569 .721518 .655462);--color-teal-600:color(display-p3 .207114 .579584 .53668);--color-teal-700:color(display-p3 .174094 .461318 .433866);--color-teal-800:color(display-p3 .1457 .367051 .350749);--color-teal-900:color(display-p3 .135344 .303314 .290629);--color-teal-950:color(display-p3 .0664925 .181482 .179465);--color-sky-50:color(display-p3 .946501 .975402 .998336);--color-sky-100:color(display-p3 .889195 .946622 .992447);--color-sky-200:color(display-p3 .758153 .897447 .986665);--color-sky-600:color(display-p3 .162505 .509376 .796082);--color-sky-700:color(display-p3 .130314 .404568 .640847);--color-sky-800:color(display-p3 .120719 .343083 .524405);--color-sky-950:color(display-p3 .0726429 .180783 .28188);--color-blue-50:color(display-p3 .941826 .963151 .995385);--color-blue-200:color(display-p3 .76688 .855207 .987483);--color-blue-700:color(display-p3 .1379 .274983 .867624);--color-rose-50:color(display-p3 .989671 .946597 .949215);--color-rose-100:color(display-p3 .982241 .897232 .902568);--color-rose-200:color(display-p3 .96875 .808776 .827317);--color-rose-300:color(display-p3 .96017 .647703 .683715);--color-rose-600:color(display-p3 .848792 .102011 .269259);--color-rose-700:color(display-p3 .711801 .0770816 .226777);--color-rose-800:color(display-p3 .591248 .0929065 .220097);--color-rose-900:color(display-p3 .498064 .104884 .214595);--color-rose-950:color(display-p3 .275407 .0406065 .0990695);--color-slate-50:color(display-p3 .974377 .979815 .986207);--color-slate-100:color(display-p3 .947345 .959969 .97483);--color-slate-200:color(display-p3 .890322 .909405 .939294);--color-slate-300:color(display-p3 .800294 .834432 .882803);--color-slate-400:color(display-p3 .577446 .629622 .716602);--color-slate-500:color(display-p3 .397645 .452653 .547642);--color-slate-600:color(display-p3 .283418 .332214 .416355);--color-slate-700:color(display-p3 .205992 .253487 .336039);--color-slate-800:color(display-p3 .121994 .158688 .232363);--color-slate-900:color(display-p3 .0639692 .0891152 .163036);--color-slate-950:color(display-p3 .0102437 .0227555 .0878731)}}@supports (color:lab(0% 0 0)){:root,:host{--color-red-50:lab(96.5005% 4.18508 1.52328);--color-red-100:lab(92.243% 10.2865 3.83865);--color-red-200:lab(86.017% 19.8815 7.75869);--color-red-300:lab(76.5514% 36.422 15.5335);--color-red-700:lab(40.4273% 67.2623 53.7441);--color-red-800:lab(33.7174% 55.8993 41.0293);--color-red-900:lab(28.5139% 44.5539 29.0463);--color-red-950:lab(13.003% 29.04 16.7519);--color-amber-50:lab(98.6252% -.635922 8.42309);--color-amber-100:lab(95.916% -1.21653 23.111);--color-amber-200:lab(91.7203% -.505269 49.9084);--color-amber-300:lab(86.4156% 6.13147 78.3961);--color-amber-500:lab(72.7183% 31.8672 97.9407);--color-amber-600:lab(60.3514% 40.5624 87.1228);--color-amber-700:lab(47.2709% 42.9082 69.2966);--color-amber-800:lab(37.8822% 37.1699 52.2718);--color-amber-900:lab(31.2288% 30.2627 40.0378);--color-amber-950:lab(15.8111% 20.9107 23.3752);--color-emerald-50:lab(97.8462% -6.94966 1.85487);--color-emerald-100:lab(94.9004% -17.0769 5.63836);--color-emerald-200:lab(90.2247% -31.039 9.47084);--color-emerald-500:lab(66.9756% -58.27 19.5419);--color-emerald-700:lab(44.4871% -41.0396 11.0361);--color-emerald-800:lab(35.3675% -33.1188 8.04002);--color-emerald-900:lab(28.8637% -26.9249 5.45986);--color-emerald-950:lab(15.0582% -17.9507 2.38369);--color-teal-50:lab(98.3189% -4.74921 -.111711);--color-teal-100:lab(95.1845% -17.4212 -.425422);--color-teal-200:lab(90.7612% -33.1343 -.542295);--color-teal-300:lab(84.8977% -48.1516 -1.3321);--color-teal-400:lab(76.0109% -53.3483 -2.27906);--color-teal-500:lab(67.3859% -49.0983 -2.63511);--color-teal-600:lab(55.0223% -41.0774 -3.90277);--color-teal-700:lab(44.4134% -33.1436 -4.22149);--color-teal-800:lab(35.5975% -26.6648 -4.34487);--color-teal-900:lab(29.506% -21.4706 -3.59886);--color-teal-950:lab(16.6371% -15.3183 -3.81732);--color-sky-50:lab(97.3623% -2.33802 -4.13098);--color-sky-100:lab(94.3709% -4.56053 -8.23453);--color-sky-200:lab(88.6983% -11.3978 -16.8488);--color-sky-600:lab(51.7754% -11.4712 -49.8349);--color-sky-700:lab(41.6013% -9.10804 -42.5647);--color-sky-800:lab(35.164% -9.57692 -34.4068);--color-sky-950:lab(17.8299% -5.31271 -21.1584);--color-blue-50:lab(96.492% -1.14644 -5.11479);--color-blue-200:lab(86.15% -4.04379 -21.0797);--color-blue-700:lab(36.9089% 35.0961 -85.6872);--color-rose-50:lab(96.2369% 4.94155 1.28011);--color-rose-100:lab(92.8221% 9.86832 2.60075);--color-rose-200:lab(86.806% 19.1909 4.07754);--color-rose-300:lab(76.6339% 38.3549 9.68835);--color-rose-600:lab(49.1882% 81.577 36.0311);--color-rose-700:lab(41.1651% 71.6251 30.3087);--color-rose-800:lab(34.6481% 60.802 20.1957);--color-rose-900:lab(29.7104% 51.514 12.6253);--color-rose-950:lab(14.2323% 34.0086 9.80922);--color-slate-50:lab(98.1434% -.369519 -1.05966);--color-slate-100:lab(96.286% -.852436 -2.46847);--color-slate-200:lab(91.7353% -.998765 -4.76968);--color-slate-300:lab(84.7652% -1.94535 -7.93337);--color-slate-400:lab(65.5349% -2.25151 -14.5072);--color-slate-500:lab(48.0876% -2.03595 -16.5814);--color-slate-600:lab(35.5623% -1.74978 -15.4316);--color-slate-700:lab(26.9569% -1.47016 -15.6993);--color-slate-800:lab(16.132% -.318035 -14.6672);--color-slate-900:lab(7.78673% 1.82345 -15.0537);--color-slate-950:lab(1.76974% 1.32743 -9.28855)}}}@layer base{*,:after,:before{box-sizing:border-box;border:0 solid;margin:0;padding:0}::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::-webkit-file-upload-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::-webkit-file-upload-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:-webkit-any([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:-webkit-any([multiple],[size])) optgroup option{padding-inline-start:20px}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::-webkit-file-upload-button{margin-inline-end:4px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-year-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-month-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-day-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-hour-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-minute-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-second-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-millisecond-field{padding-block-start:0;padding-block-end:0}::-webkit-datetime-edit-meridiem-field{padding-block-start:0;padding-block-end:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button{-webkit-appearance:button;appearance:button}input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;appearance:button}::-webkit-file-upload-button{-webkit-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{top:0;bottom:0;left:0;right:0}.inset-y-0{top:0;bottom:0}.top-0{top:0}.top-0\.5{top:calc(var(--spacing) * .5)}.top-1{top:var(--spacing)}.top-\[4\.5rem\]{top:4.5rem}.right-0{right:0}.right-0\.5{right:calc(var(--spacing) * .5)}.right-1{right:var(--spacing)}.left-0{left:0}.left-1{left:var(--spacing)}.left-6{left:calc(var(--spacing) * 6)}.isolate{isolation:isolate}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.z-\[1000\]{z-index:1000}.m-0{margin:0}.mx-5{margin-inline:calc(var(--spacing) * 5)}.mx-auto{margin-inline-start:auto;margin-inline-end:auto}.my-5{margin-block:calc(var(--spacing) * 5)}.my-6{margin-block:calc(var(--spacing) * 6)}.my-auto{margin-block-start:auto;margin-block-end:auto}.mt-0{margin-top:0}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-7{margin-top:calc(var(--spacing) * 7)}.mb-0{margin-bottom:0}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.ml-0{margin-left:0}.ml-3{margin-left:calc(var(--spacing) * 3)}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-flex{display:inline-flex}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.size-2\.5{width:calc(var(--spacing) * 2.5);height:calc(var(--spacing) * 2.5)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-3\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-6{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.size-10{width:calc(var(--spacing) * 10);height:calc(var(--spacing) * 10)}.size-11{width:calc(var(--spacing) * 11);height:calc(var(--spacing) * 11)}.size-12{width:calc(var(--spacing) * 12);height:calc(var(--spacing) * 12)}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-10{height:calc(var(--spacing) * 10)}.h-44{height:calc(var(--spacing) * 44)}.h-\[4\.5rem\]{height:4.5rem}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.max-h-40{max-height:calc(var(--spacing) * 40)}.max-h-\[28rem\]{max-height:28rem}.min-h-9{min-height:calc(var(--spacing) * 9)}.min-h-10{min-height:calc(var(--spacing) * 10)}.min-h-11{min-height:calc(var(--spacing) * 11)}.min-h-16{min-height:calc(var(--spacing) * 16)}.min-h-52{min-height:calc(var(--spacing) * 52)}.min-h-\[4\.5rem\]{min-height:4.5rem}.min-h-screen{min-height:100vh}.w-10{width:calc(var(--spacing) * 10)}.w-11{width:calc(var(--spacing) * 11)}.w-12{width:calc(var(--spacing) * 12)}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-\[68\%\]{max-width:68%}.max-w-\[288px\]{max-width:288px}.max-w-\[1600px\]{max-width:1600px}.max-w-full{max-width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.min-w-0{min-width:0}.min-w-20{min-width:calc(var(--spacing) * 20)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.translate-x-1\/2{--tw-translate-x:calc(1 / 2 * 100%);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.rotate-180{rotate:180deg}.animate-bounce{animation:var(--animate-bounce)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.cursor-wait{cursor:wait}.resize{resize:both}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-7>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 7) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 7) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-5{column-gap:calc(var(--spacing) * 5)}.gap-y-1\.5{row-gap:calc(var(--spacing) * 1.5)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-slate-200>:not(:last-child)){border-color:var(--color-slate-200)}.self-start{align-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overscroll-contain{overscroll-behavior:contain}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:2147483647px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-amber-200{border-color:var(--color-amber-200)}.border-amber-300{border-color:var(--color-amber-300)}.border-current{border-color:currentColor}.border-emerald-200{border-color:var(--color-emerald-200)}.border-emerald-700{border-color:var(--color-emerald-700)}.border-red-200{border-color:var(--color-red-200)}.border-red-700{border-color:var(--color-red-700)}.border-rose-200{border-color:var(--color-rose-200)}.border-rose-300{border-color:var(--color-rose-300)}.border-sky-100{border-color:var(--color-sky-100)}.border-sky-200{border-color:var(--color-sky-200)}.border-slate-100{border-color:var(--color-slate-100)}.border-slate-200{border-color:var(--color-slate-200)}.border-slate-300{border-color:var(--color-slate-300)}.border-teal-200{border-color:var(--color-teal-200)}.border-teal-300{border-color:var(--color-teal-300)}.border-teal-600{border-color:var(--color-teal-600)}.border-teal-700{border-color:var(--color-teal-700)}.border-white\/80{border-color:#fffc}@supports (color:color-mix(in lab, red, red)){.border-white\/80{border-color:color-mix(in oklab, var(--color-white) 80%, transparent)}}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-50\/70{background-color:#fffbebb3}@supports (color:color-mix(in lab, red, red)){.bg-amber-50\/70{background-color:color-mix(in oklab, var(--color-amber-50) 70%, transparent)}}.bg-amber-100{background-color:var(--color-amber-100)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-amber-600{background-color:var(--color-amber-600)}.bg-amber-700{background-color:var(--color-amber-700)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-emerald-50\/70{background-color:#ecfdf5b3}@supports (color:color-mix(in lab, red, red)){.bg-emerald-50\/70{background-color:color-mix(in oklab, var(--color-emerald-50) 70%, transparent)}}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-emerald-700{background-color:var(--color-emerald-700)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-700{background-color:var(--color-red-700)}.bg-rose-50{background-color:var(--color-rose-50)}.bg-rose-50\/70{background-color:#fff1f2b3}@supports (color:color-mix(in lab, red, red)){.bg-rose-50\/70{background-color:color-mix(in oklab, var(--color-rose-50) 70%, transparent)}}.bg-rose-100{background-color:var(--color-rose-100)}.bg-rose-700{background-color:var(--color-rose-700)}.bg-sky-50{background-color:var(--color-sky-50)}.bg-sky-50\/60{background-color:#f0f9ff99}@supports (color:color-mix(in lab, red, red)){.bg-sky-50\/60{background-color:color-mix(in oklab, var(--color-sky-50) 60%, transparent)}}.bg-sky-600{background-color:var(--color-sky-600)}.bg-sky-700{background-color:var(--color-sky-700)}.bg-slate-50{background-color:var(--color-slate-50)}.bg-slate-50\/70{background-color:#f8fafcb3}@supports (color:color-mix(in lab, red, red)){.bg-slate-50\/70{background-color:color-mix(in oklab, var(--color-slate-50) 70%, transparent)}}.bg-slate-100{background-color:var(--color-slate-100)}.bg-slate-200{background-color:var(--color-slate-200)}.bg-slate-300{background-color:var(--color-slate-300)}.bg-slate-400{background-color:var(--color-slate-400)}.bg-slate-900{background-color:var(--color-slate-900)}.bg-slate-950{background-color:var(--color-slate-950)}.bg-slate-950\/70{background-color:#020618b3}@supports (color:color-mix(in lab, red, red)){.bg-slate-950\/70{background-color:color-mix(in oklab, var(--color-slate-950) 70%, transparent)}}.bg-teal-50{background-color:var(--color-teal-50)}.bg-teal-100{background-color:var(--color-teal-100)}.bg-teal-500{background-color:var(--color-teal-500)}.bg-teal-600{background-color:var(--color-teal-600)}.bg-teal-700{background-color:var(--color-teal-700)}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-white\/70{background-color:#ffffffb3}@supports (color:color-mix(in lab, red, red)){.bg-white\/70{background-color:color-mix(in oklab, var(--color-white) 70%, transparent)}}.bg-white\/95{background-color:#fffffff2}@supports (color:color-mix(in lab, red, red)){.bg-white\/95{background-color:color-mix(in oklab, var(--color-white) 95%, transparent)}}.p-0{padding:0}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-3\.5{padding:calc(var(--spacing) * 3.5)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-10{padding-block:calc(var(--spacing) * 10)}.pt-1{padding-top:var(--spacing)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pr-12{padding-right:calc(var(--spacing) * 12)}.pb-1{padding-bottom:var(--spacing)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.pb-8{padding-bottom:calc(var(--spacing) * 8)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-9{padding-left:calc(var(--spacing) * 9)}.pl-11{padding-left:calc(var(--spacing) * 11)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.7rem\]{font-size:.7rem}.text-\[0\.65rem\]{font-size:.65rem}.text-\[0\.68rem\]{font-size:.68rem}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-6{--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6)}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.08em\]{--tw-tracking:.08em;letter-spacing:.08em}.tracking-\[0\.12em\]{--tw-tracking:.12em;letter-spacing:.12em}.tracking-\[0\.14em\]{--tw-tracking:.14em;letter-spacing:.14em}.tracking-\[0\.16em\]{--tw-tracking:.16em;letter-spacing:.16em}.tracking-\[0\.18em\]{--tw-tracking:.18em;letter-spacing:.18em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-amber-900{color:var(--color-amber-900)}.text-amber-900\/80{color:#7b3306cc}@supports (color:color-mix(in lab, red, red)){.text-amber-900\/80{color:color-mix(in oklab, var(--color-amber-900) 80%, transparent)}}.text-amber-950{color:var(--color-amber-950)}.text-blue-700{color:var(--color-blue-700)}.text-current{color:currentColor}.text-emerald-700{color:var(--color-emerald-700)}.text-emerald-800{color:var(--color-emerald-800)}.text-emerald-900{color:var(--color-emerald-900)}.text-emerald-950{color:var(--color-emerald-950)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-red-900{color:var(--color-red-900)}.text-red-950{color:var(--color-red-950)}.text-rose-600{color:var(--color-rose-600)}.text-rose-700{color:var(--color-rose-700)}.text-rose-800{color:var(--color-rose-800)}.text-rose-900{color:var(--color-rose-900)}.text-rose-950{color:var(--color-rose-950)}.text-sky-700{color:var(--color-sky-700)}.text-sky-800{color:var(--color-sky-800)}.text-sky-950{color:var(--color-sky-950)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-slate-700{color:var(--color-slate-700)}.text-slate-800{color:var(--color-slate-800)}.text-slate-900{color:var(--color-slate-900)}.text-slate-950{color:var(--color-slate-950)}.text-teal-300{color:var(--color-teal-300)}.text-teal-600{color:var(--color-teal-600)}.text-teal-700{color:var(--color-teal-700)}.text-teal-800{color:var(--color-teal-800)}.text-teal-950{color:var(--color-teal-950)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.accent-teal-700{accent-color:var(--color-teal-700)}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-slate-900\/5{--tw-shadow-color:#0f172b0d}@supports (color:color-mix(in lab, red, red)){.shadow-slate-900\/5{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-slate-900) 5%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-slate-950\/10{--tw-shadow-color:#0206181a}@supports (color:color-mix(in lab, red, red)){.shadow-slate-950\/10{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-slate-950) 10%, transparent) var(--tw-shadow-alpha), transparent)}}.ring-amber-200{--tw-ring-color:var(--color-amber-200)}.ring-blue-200{--tw-ring-color:var(--color-blue-200)}.ring-emerald-200{--tw-ring-color:var(--color-emerald-200)}.ring-red-200{--tw-ring-color:var(--color-red-200)}.ring-rose-200{--tw-ring-color:var(--color-rose-200)}.ring-sky-100{--tw-ring-color:var(--color-sky-100)}.ring-sky-200{--tw-ring-color:var(--color-sky-200)}.ring-slate-100{--tw-ring-color:var(--color-slate-100)}.ring-slate-200{--tw-ring-color:var(--color-slate-200)}.ring-teal-100{--tw-ring-color:var(--color-teal-100)}.ring-teal-200{--tw-ring-color:var(--color-teal-200)}.ring-white{--tw-ring-color:var(--color-white)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[left\]{transition-property:left;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.outline-none{--tw-outline-style:none;outline-style:none}.select-all{-webkit-user-select:all;user-select:all}.ring-inset{--tw-ring-inset:inset}.group-open\:hidden:-webkit-any(:where(.group):-webkit-any([open],:popover-open,:open) *){display:none}.group-open\:hidden:is(:where(.group):is([open],:popover-open,:open) *){display:none}.group-open\:inline-block:-webkit-any(:where(.group):-webkit-any([open],:popover-open,:open) *){display:inline-block}.group-open\:inline-block:is(:where(.group):is([open],:popover-open,:open) *){display:inline-block}.group-open\:inline-flex:-webkit-any(:where(.group):-webkit-any([open],:popover-open,:open) *){display:inline-flex}.group-open\:inline-flex:is(:where(.group):is([open],:popover-open,:open) *){display:inline-flex}.placeholder\:text-slate-400::placeholder{color:var(--color-slate-400)}.first\:mt-0:first-child{margin-top:0}.first\:pt-0:first-child{padding-top:0}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.last\:pb-0:last-child{padding-bottom:0}@media (hover:hover){.hover\:border-emerald-800:hover{border-color:var(--color-emerald-800)}.hover\:border-red-200:hover{border-color:var(--color-red-200)}.hover\:border-red-300:hover{border-color:var(--color-red-300)}.hover\:border-red-800:hover{border-color:var(--color-red-800)}.hover\:border-slate-300:hover{border-color:var(--color-slate-300)}.hover\:border-teal-200:hover{border-color:var(--color-teal-200)}.hover\:border-teal-300:hover{border-color:var(--color-teal-300)}.hover\:border-teal-400:hover{border-color:var(--color-teal-400)}.hover\:border-teal-800:hover{border-color:var(--color-teal-800)}.hover\:bg-amber-50:hover{background-color:var(--color-amber-50)}.hover\:bg-amber-100:hover{background-color:var(--color-amber-100)}.hover\:bg-amber-700:hover{background-color:var(--color-amber-700)}.hover\:bg-amber-800:hover{background-color:var(--color-amber-800)}.hover\:bg-black\/5:hover{background-color:#0000000d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-black\/5:hover{background-color:color-mix(in oklab, var(--color-black) 5%, transparent)}}.hover\:bg-emerald-800:hover{background-color:var(--color-emerald-800)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:bg-red-800:hover{background-color:var(--color-red-800)}.hover\:bg-rose-50:hover{background-color:var(--color-rose-50)}.hover\:bg-rose-100:hover{background-color:var(--color-rose-100)}.hover\:bg-rose-800:hover{background-color:var(--color-rose-800)}.hover\:bg-sky-800:hover{background-color:var(--color-sky-800)}.hover\:bg-slate-50:hover{background-color:var(--color-slate-50)}.hover\:bg-slate-100:hover{background-color:var(--color-slate-100)}.hover\:bg-slate-800:hover{background-color:var(--color-slate-800)}.hover\:bg-teal-50:hover{background-color:var(--color-teal-50)}.hover\:bg-teal-100:hover{background-color:var(--color-teal-100)}.hover\:bg-teal-800:hover{background-color:var(--color-teal-800)}.hover\:text-red-700:hover{color:var(--color-red-700)}.hover\:text-rose-800:hover{color:var(--color-rose-800)}.hover\:text-slate-800:hover{color:var(--color-slate-800)}.hover\:text-slate-950:hover{color:var(--color-slate-950)}.hover\:text-teal-700:hover{color:var(--color-teal-700)}.hover\:text-teal-800:hover{color:var(--color-teal-800)}.hover\:text-teal-900:hover{color:var(--color-teal-900)}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:border-teal-500:focus{border-color:var(--color-teal-500)}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-4:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-teal-100:focus{--tw-ring-color:var(--color-teal-100)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-4:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-amber-100:focus-visible{--tw-ring-color:var(--color-amber-100)}.focus-visible\:ring-amber-200:focus-visible{--tw-ring-color:var(--color-amber-200)}.focus-visible\:ring-current:focus-visible{--tw-ring-color:currentcolor}.focus-visible\:ring-emerald-100:focus-visible{--tw-ring-color:var(--color-emerald-100)}.focus-visible\:ring-red-100:focus-visible{--tw-ring-color:var(--color-red-100)}.focus-visible\:ring-red-200:focus-visible{--tw-ring-color:var(--color-red-200)}.focus-visible\:ring-teal-100:focus-visible{--tw-ring-color:var(--color-teal-100)}.focus-visible\:ring-teal-200:focus-visible{--tw-ring-color:var(--color-teal-200)}.focus-visible\:ring-teal-500:focus-visible{--tw-ring-color:var(--color-teal-500)}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:cursor-wait:disabled{cursor:wait}.disabled\:bg-slate-100:disabled{background-color:var(--color-slate-100)}.disabled\:text-slate-400:disabled{color:var(--color-slate-400)}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-60:disabled{opacity:.6}@media (min-width:40rem){.sm\:mx-6{margin-inline:calc(var(--spacing) * 6)}.sm\:mb-6{margin-bottom:calc(var(--spacing) * 6)}.sm\:h-52{height:calc(var(--spacing) * 52)}.sm\:min-h-9{min-height:calc(var(--spacing) * 9)}.sm\:min-h-10{min-height:calc(var(--spacing) * 10)}.sm\:w-auto{width:auto}.sm\:max-w-\[392px\]{max-width:392px}.sm\:max-w-\[592px\]{max-width:592px}.sm\:min-w-80{min-width:calc(var(--spacing) * 80)}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:grid-cols-\[minmax\(0\,1fr\)_auto\]{grid-template-columns:minmax(0,1fr) auto}.sm\:flex-row{flex-direction:row}.sm\:flex-wrap{flex-wrap:wrap}.sm\:items-center{align-items:center}.sm\:items-end{align-items:flex-end}.sm\:items-start{align-items:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:justify-end{justify-content:flex-end}.sm\:gap-4{gap:calc(var(--spacing) * 4)}.sm\:self-auto{align-self:auto}.sm\:p-5{padding:calc(var(--spacing) * 5)}.sm\:p-6{padding:calc(var(--spacing) * 6)}.sm\:p-7{padding:calc(var(--spacing) * 7)}.sm\:p-8{padding:calc(var(--spacing) * 8)}.sm\:px-2{padding-inline:calc(var(--spacing) * 2)}.sm\:px-4{padding-inline:calc(var(--spacing) * 4)}.sm\:px-5{padding-inline:calc(var(--spacing) * 5)}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}.sm\:pt-2{padding-top:calc(var(--spacing) * 2)}.sm\:pt-6{padding-top:calc(var(--spacing) * 6)}.sm\:pb-6{padding-bottom:calc(var(--spacing) * 6)}.sm\:pb-10{padding-bottom:calc(var(--spacing) * 10)}.sm\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.sm\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}}@media (min-width:48rem){.md\:flex{display:flex}.md\:grid{display:grid}.md\:hidden{display:none}.md\:inline-flex{display:inline-flex}.md\:h-\[4\.5rem\]{height:4.5rem}.md\:h-full{height:100%}.md\:min-h-screen{min-height:100vh}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-\[5rem_minmax\(0\,1fr\)\]{grid-template-columns:5rem minmax(0,1fr)}.md\:grid-cols-\[16rem_minmax\(0\,1fr\)\]{grid-template-columns:16rem minmax(0,1fr)}.md\:flex-col{flex-direction:column}.md\:py-0{padding-block-start:0;padding-block-end:0}.md\:transition-\[grid-template-columns\]{transition-property:grid-template-columns;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.md\:duration-200{--tw-duration:.2s;transition-duration:.2s}.md\:ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}}@media (min-width:64rem){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-start-2{grid-column-start:2}.lg\:block{display:block}.lg\:max-w-sm{max-width:var(--container-sm)}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:items-start{align-items:flex-start}.lg\:justify-between{justify-content:space-between}.lg\:justify-end{justify-content:flex-end}.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}.lg\:pt-3{padding-top:calc(var(--spacing) * 3)}.lg\:pt-8{padding-top:calc(var(--spacing) * 8)}.lg\:pb-12{padding-bottom:calc(var(--spacing) * 12)}.lg\:whitespace-nowrap{white-space:nowrap}}@media (min-width:80rem){.xl\:col-start-1{grid-column-start:1}.xl\:col-start-2{grid-column-start:2}.xl\:row-span-2{grid-row:span 2/span 2}.xl\:row-start-1{grid-row-start:1}.xl\:row-start-2{grid-row-start:2}.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-\[minmax\(0\,0\.95fr\)_minmax\(0\,1\.05fr\)\]{grid-template-columns:minmax(0,.95fr) minmax(0,1.05fr)}.xl\:grid-cols-\[minmax\(0\,1\.2fr\)_minmax\(18rem\,0\.8fr\)\]{grid-template-columns:minmax(0,1.2fr) minmax(18rem,.8fr)}.xl\:grid-cols-\[minmax\(0\,1\.45fr\)_minmax\(20rem\,0\.75fr\)\]{grid-template-columns:minmax(0,1.45fr) minmax(20rem,.75fr)}.xl\:grid-cols-\[minmax\(0\,1\.55fr\)_minmax\(18rem\,0\.65fr\)\]{grid-template-columns:minmax(0,1.55fr) minmax(18rem,.65fr)}.xl\:self-stretch{align-self:stretch}.xl\:border-t-0{border-top-style:var(--tw-border-style);border-top-width:0}.xl\:border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.xl\:px-10{padding-inline:calc(var(--spacing) * 10)}.xl\:whitespace-nowrap{white-space:nowrap}}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none}}:host{color:#0f172a;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;-webkit-font-smoothing:antialiased;-webkit-tap-highlight-color:transparent;text-rendering:optimizelegibility;width:100%;min-width:0;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;display:block}.smartsafehub-shadow-root{width:100%;min-width:0;max-width:100%;display:block}.smartsafehub-shadow-root,.smartsafehub-shadow-root *,.smartsafehub-shadow-root :before,.smartsafehub-shadow-root :after{box-sizing:border-box}.ssh-app{width:100%;min-width:0;max-width:100%;overflow:clip}.ssh-app main,.ssh-app section,.ssh-app article,.ssh-app aside,.ssh-app nav,.ssh-app div{min-width:0}.ssh-product-hero{border-radius:0}.ssh-product-header-action{border-color:#e2e8f0!important}.ssh-product-header-action:hover{border-color:#5eead4!important}.ssh-app a,.ssh-app button,.ssh-app input,.ssh-app textarea{touch-action:manipulation}.ssh-app[data-theme=dark] .ssh-custom-select-trigger:disabled{color:#64748b;background-color:#1e293b;border-color:#334155}.ssh-app[data-theme=dark] .ssh-custom-select-trigger:focus-visible{--tw-ring-color:#134e4aa6}.ssh-app[data-theme=dark] .ssh-custom-select-menu{box-shadow:0 18px 45px #00000059}.ssh-mobile-navigation{padding-left:max(1rem, env(safe-area-inset-left));padding-right:max(1rem, env(safe-area-inset-right))}.ssh-mobile-menu{overscroll-behavior:contain;-webkit-overflow-scrolling:touch;max-height:calc(100dvh - 4.5rem);overflow-y:auto}.ssh-touch-scroll{scrollbar-width:none;overscroll-behavior-inline:contain;-webkit-overflow-scrolling:touch}.ssh-touch-scroll::-webkit-scrollbar{display:none}@media (max-width:767px){.ssh-app{min-height:100dvh;overflow-x:clip}.ssh-product-hero{padding-top:max(1.25rem, env(safe-area-inset-top));padding-left:max(1rem, env(safe-area-inset-left));padding-right:max(1rem, env(safe-area-inset-right))}.ssh-product-main{padding-right:max(1rem, env(safe-area-inset-right));padding-bottom:max(2rem, env(safe-area-inset-bottom));padding-left:max(1rem, env(safe-area-inset-left))}.ssh-app input,.ssh-app textarea{font-size:16px}.ssh-app button,.ssh-app a{min-height:44px}}.ssh-sidebar-toggle{color:#64748b;background:#fff;border:1px solid #e2e8f0;box-shadow:0 1px 3px #0f172a1f}.ssh-sidebar-toggle:hover{color:#0f766e;background:#f8fafc;border-color:#99f6e4}.ssh-app[data-theme=dark] .ssh-sidebar-toggle{color:#94a3b8;background:#1e293b;border-color:#334155;box-shadow:0 1px 3px #00000047}.ssh-app[data-theme=dark] .ssh-sidebar-toggle:hover{color:#5eead4;background:#334155;border-color:#0f766e}.ssh-update-nav-badge{color:#92400e;background:#fef3c7;border:1px solid #f59e0b;border-radius:9999px;justify-content:center;align-items:center;min-width:1.3rem;height:1.3rem;padding:0 .25rem;font-size:.75rem;font-weight:900;line-height:1;display:inline-flex;box-shadow:0 1px 2px #0f172a1f}.ssh-update-nav-badge[data-collapsed=true]{z-index:1;width:auto;min-width:1.25rem;height:1.25rem;padding:0 .25rem;position:absolute;top:.125rem;right:.125rem;box-shadow:0 0 0 2px #fff,0 2px 5px #0f172a33}.ssh-app[data-theme=dark] .ssh-update-nav-badge{color:#422006;background:#fbbf24;border-color:#fcd34d;box-shadow:0 1px 3px #0000004d}.ssh-app[data-theme=dark] .ssh-update-nav-badge[data-collapsed=true]{box-shadow:0 0 0 2px #0f172a,0 2px 6px #00000061}.ssh-update-progress-track{background:#bae6fd;border-radius:9999px;height:.375rem;position:relative;overflow:hidden}.ssh-update-progress-bar{border-radius:inherit;background:#0284c7;width:38%;animation:1.2s ease-in-out infinite ssh-update-progress;position:absolute;top:0;bottom:0;left:0}.ssh-app[data-theme=dark] .ssh-update-progress-track{background:#1e3a5f}.ssh-app[data-theme=dark] .ssh-update-progress-bar{background:#38bdf8}.ssh-app button.ssh-switch-control{box-sizing:border-box;flex:0 0 3rem;width:3rem;min-width:3rem;max-width:3rem;height:1.75rem;min-height:1.75rem;max-height:1.75rem;padding:0}.ssh-switch-thumb{aspect-ratio:1;box-sizing:border-box;background-color:#fff;border-radius:9999px;width:1.25rem;min-width:1.25rem;max-width:1.25rem;height:1.25rem;min-height:1.25rem;max-height:1.25rem}@keyframes ssh-update-progress{0%{transform:translate(-110%)}to{transform:translate(300%)}}.ssh-software-settings-grid{grid-template-columns:repeat(auto-fit,minmax(min(100%,18rem),1fr));gap:1rem;display:grid}.ssh-software-settings-channel{grid-column:1/-1}.ssh-time-select{grid-template-columns:repeat(2,minmax(0,1fr));gap:.5rem;width:100%;min-width:0;max-width:100%;display:grid}.ssh-time-select>*{min-width:0}.ssh-time-select .ssh-custom-select-trigger{gap:.5rem;min-width:0;max-width:100%;padding-inline-start:.75rem;padding-inline-end:.75rem}.ssh-ipv4-segments{grid-template-columns:repeat(4,minmax(0,1fr));gap:.75rem;width:100%;min-width:0;max-width:100%;display:grid}.ssh-ipv4-segment{min-width:0;position:relative}.ssh-ipv4-separator{pointer-events:none;position:absolute;top:50%;right:-.375rem;transform:translate(50%,-50%)}@media (min-width:640px){.ssh-ipv4-segments{gap:1rem}.ssh-ipv4-separator{right:-.5rem}}.ssh-app[data-theme=dark]{color:#f8fafc;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;background:#020617}.ssh-app[data-theme=dark] [class~=bg-white]{background-color:#0f172a}.ssh-app[data-theme=dark] [class~=bg-white\/95]{background-color:#0f172af2}.ssh-app[data-theme=dark] [class~=bg-white\/70]{background-color:#0f172ac7}.ssh-app[data-theme=dark] [class~=bg-slate-50]{background-color:#020617}.ssh-app[data-theme=dark] [class~=bg-slate-50\/70]{background-color:#020617b8}.ssh-app[data-theme=dark] [class~=bg-slate-100]{background-color:#1e293b}.ssh-app[data-theme=dark] [class~=bg-slate-200]{background-color:#334155}.ssh-app[data-theme=dark] [class~=bg-slate-300]{background-color:#475569}.ssh-app[data-theme=dark] [class~=bg-slate-400]{background-color:#64748b}.ssh-app[data-theme=dark] [class~=bg-black\/5]{background-color:#ffffff0d}.ssh-app[data-theme=dark] [class~=text-slate-950]{color:#f8fafc}.ssh-app[data-theme=dark] [class~=text-slate-900]{color:#f1f5f9}.ssh-app[data-theme=dark] [class~=text-slate-800]{color:#e2e8f0}.ssh-app[data-theme=dark] [class~=text-slate-700],.ssh-app[data-theme=dark] [class~=text-slate-600]{color:#cbd5e1}.ssh-app[data-theme=dark] [class~=text-slate-500],.ssh-app[data-theme=dark] [class~=text-slate-400]{color:#94a3b8}.ssh-app[data-theme=dark] [class~=text-slate-300]{color:#cbd5e1}.ssh-app[data-theme=dark] [class~=border-slate-100],.ssh-app[data-theme=dark] [class~=border-slate-200],.ssh-app[data-theme=dark] [class~=divide-slate-200]{border-color:#1e293b}.ssh-app[data-theme=dark] [class~=border-slate-300],.ssh-app[data-theme=dark] [class~=border-white\/80]{border-color:#334155}.ssh-app[data-theme=dark] [class~=ring-slate-100],.ssh-app[data-theme=dark] [class~=ring-slate-200],.ssh-app[data-theme=dark] [class~=ring-white]{--tw-ring-color:#334155}.ssh-app[data-theme=dark] [class~=bg-teal-50],.ssh-app[data-theme=dark] [class~=bg-teal-100]{background-color:#0d948829}.ssh-app[data-theme=dark] [class~=text-teal-700],.ssh-app[data-theme=dark] [class~=text-teal-800],.ssh-app[data-theme=dark] [class~=text-teal-950]{color:#5eead4}.ssh-app[data-theme=dark] [class~=border-teal-200],.ssh-app[data-theme=dark] [class~=border-teal-300]{border-color:#115e59}.ssh-app[data-theme=dark] [class~=ring-teal-100],.ssh-app[data-theme=dark] [class~=ring-teal-200]{--tw-ring-color:#134e4a}.ssh-app[data-theme=dark] [class~=bg-sky-50],.ssh-app[data-theme=dark] [class~=bg-sky-50\/60]{background-color:#0ea5e91f}.ssh-app[data-theme=dark] [class~=text-sky-700],.ssh-app[data-theme=dark] [class~=text-sky-800],.ssh-app[data-theme=dark] [class~=text-sky-950]{color:#7dd3fc}.ssh-app[data-theme=dark] [class~=border-sky-200]{border-color:#075985}.ssh-app[data-theme=dark] [class~=ring-sky-100],.ssh-app[data-theme=dark] [class~=ring-sky-200]{--tw-ring-color:#0c4a6e}.ssh-app[data-theme=dark] [class~=bg-emerald-50],.ssh-app[data-theme=dark] [class~=bg-emerald-100]{background-color:#10b98121}.ssh-app[data-theme=dark] [class~=bg-emerald-50\/70]{background-color:#10b9811a}.ssh-app[data-theme=dark] [class~=text-emerald-700],.ssh-app[data-theme=dark] [class~=text-emerald-800],.ssh-app[data-theme=dark] [class~=text-emerald-900],.ssh-app[data-theme=dark] [class~=text-emerald-950]{color:#6ee7b7}.ssh-app[data-theme=dark] [class~=border-emerald-200]{border-color:#065f46}.ssh-app[data-theme=dark] [class~=ring-emerald-100],.ssh-app[data-theme=dark] [class~=ring-emerald-200]{--tw-ring-color:#064e3b}.ssh-app[data-theme=dark] [class~=bg-amber-50],.ssh-app[data-theme=dark] [class~=bg-amber-50\/60],.ssh-app[data-theme=dark] [class~=bg-amber-100],.ssh-app[data-theme=dark] [class~=bg-amber-100\/70]{background-color:#f59e0b21}.ssh-app[data-theme=dark] [class~=bg-amber-50\/70]{background-color:#f59e0b1a}.ssh-app[data-theme=dark] [class~=text-amber-700],.ssh-app[data-theme=dark] [class~=text-amber-800],.ssh-app[data-theme=dark] [class~=text-amber-900],.ssh-app[data-theme=dark] [class~=text-amber-900\/80],.ssh-app[data-theme=dark] [class~=text-amber-950]{color:#fcd34d}.ssh-app[data-theme=dark] [class~=border-amber-200],.ssh-app[data-theme=dark] [class~=border-amber-300]{border-color:#92400e}.ssh-app[data-theme=dark] [class~=ring-amber-100],.ssh-app[data-theme=dark] [class~=ring-amber-200]{--tw-ring-color:#78350f}.ssh-app[data-theme=dark] [class~=bg-red-50],.ssh-app[data-theme=dark] [class~=bg-red-100]{background-color:#ef44441f}.ssh-app[data-theme=dark] [class~=text-red-700],.ssh-app[data-theme=dark] [class~=text-red-800],.ssh-app[data-theme=dark] [class~=text-red-900],.ssh-app[data-theme=dark] [class~=text-red-950]{color:#fca5a5}.ssh-app[data-theme=dark] [class~=border-red-200],.ssh-app[data-theme=dark] [class~=border-red-300]{border-color:#991b1b}.ssh-app[data-theme=dark] [class~=ring-red-100],.ssh-app[data-theme=dark] [class~=ring-red-200]{--tw-ring-color:#7f1d1d}.ssh-app[data-theme=dark] [class~=bg-rose-50]{background-color:#f43f5e1f}.ssh-app[data-theme=dark] [class~=bg-rose-50\/70]{background-color:#f43f5e1a}.ssh-app[data-theme=dark] [class~=text-rose-600],.ssh-app[data-theme=dark] [class~=text-rose-700],.ssh-app[data-theme=dark] [class~=text-rose-800],.ssh-app[data-theme=dark] [class~=text-rose-900],.ssh-app[data-theme=dark] [class~=text-rose-950]{color:#fda4af}.ssh-app[data-theme=dark] [class~=border-rose-200],.ssh-app[data-theme=dark] [class~=border-rose-300]{border-color:#9f1239}.ssh-app[data-theme=dark] [class~=ring-rose-200]{--tw-ring-color:#881337}.ssh-app[data-theme=dark] [class~=bg-blue-50]{background-color:#3b82f61f}.ssh-app[data-theme=dark] [class~=text-blue-700]{color:#93c5fd}.ssh-app[data-theme=dark] [class~=ring-blue-200]{--tw-ring-color:#1e3a8a}.ssh-app[data-theme=dark] .hover\:bg-slate-50:hover{background-color:#1e293b}.ssh-app[data-theme=dark] .hover\:bg-rose-50:hover{background-color:#f43f5e2e}.ssh-app[data-theme=dark] .hover\:text-slate-950:hover{color:#f8fafc}.ssh-app[data-theme=dark] .hover\:text-slate-700:hover{color:#e2e8f0}.ssh-app[data-theme=dark] .hover\:text-rose-800:hover{color:#fecdd3}.ssh-app[data-theme=dark] .hover\:border-slate-300:hover{border-color:#475569}.ssh-app[data-theme=dark] .ssh-product-header-action{color:#cbd5e1;background-color:#0f172a;border-color:#334155!important}.ssh-app[data-theme=dark] .ssh-product-header-action:hover{color:#5eead4;border-color:#0f766e!important}.ssh-app[data-theme=dark] .ssh-product-header-action:focus-visible{--tw-ring-color:#134e4a}.ssh-initial-setup-probe{color:#0f172a;background:#f8fafc;place-items:center;min-height:100dvh;padding:1.5rem;display:grid}.ssh-initial-setup-probe-card{text-align:center;background:#fff;border:1px solid #e2e8f0;border-radius:1.25rem;justify-items:center;gap:.7rem;width:min(100%,28rem);padding:2.25rem;display:grid;box-shadow:0 20px 55px #0f172a14}.ssh-initial-setup-probe-card>svg{color:#0f766e;width:2rem;height:2rem}.ssh-initial-setup-probe-card strong{font-size:1rem;font-weight:900}.ssh-initial-setup-probe-card span{color:#64748b;font-size:.84rem;line-height:1.55}.ssh-initial-setup-probe-card button{color:#fff;cursor:pointer;min-height:2.75rem;font:inherit;background:#0f766e;border:0;border-radius:.8rem;margin-top:.4rem;padding:0 1rem;font-size:.84rem;font-weight:900}.smartsafehub-login-shadow-root{background:#f8fafc;min-height:100dvh}.ssh-login-page,.ssh-login-page *,.ssh-login-page :before,.ssh-login-page :after{box-sizing:border-box}.ssh-login-page{--ssh-login-bg:#f8fafc;--ssh-login-surface:#fff;--ssh-login-surface-soft:#f8fafc;--ssh-login-border:#e2e8f0;--ssh-login-border-strong:#cbd5e1;--ssh-login-text:#0f172a;--ssh-login-muted:#64748b;--ssh-login-muted-soft:#94a3b8;--ssh-login-accent:#0f766e;--ssh-login-accent-hover:#115e59;--ssh-login-accent-soft:#f0fdfa;--ssh-login-accent-ring:#14b8a624;--ssh-login-card-shadow:0 24px 70px #0f172a14, 0 2px 10px #0f172a0a;background:var(--ssh-login-bg);min-height:100dvh;color:var(--ssh-login-text);grid-template-columns:minmax(0,1.08fr) minmax(420px,.92fr);display:grid}.ssh-login-page[data-theme=dark]{--ssh-login-bg:#020617;--ssh-login-surface:#0f172a;--ssh-login-surface-soft:#111827;--ssh-login-border:#1e293b;--ssh-login-border-strong:#334155;--ssh-login-text:#f8fafc;--ssh-login-muted:#94a3b8;--ssh-login-muted-soft:#64748b;--ssh-login-accent:#2dd4bf;--ssh-login-accent-hover:#5eead4;--ssh-login-accent-soft:#14b8a61a;--ssh-login-accent-ring:#2dd4bf2e;--ssh-login-card-shadow:0 28px 72px #0000003d, 0 0 0 1px #94a3b80a}.ssh-login-toast{top:max(1rem, env(safe-area-inset-top));z-index:20;color:#115e59;background:#f0fdfa;border:1px solid #99f6e4;border-radius:1rem;align-items:center;gap:.75rem;width:min(100% - 2rem,34rem);padding:.9rem 1rem;font-size:.88rem;font-weight:800;line-height:1.45;display:flex;position:fixed;left:50%;transform:translate(-50%);box-shadow:0 18px 45px #0f172a29}.ssh-login-toast svg{flex:none;width:1.25rem;height:1.25rem}.ssh-login-page[data-theme=dark] .ssh-login-toast{color:#99f6e4;background:#0f2f2e;border-color:#2dd4bf57;box-shadow:0 18px 45px #00000052}.ssh-login-brand{color:#f8fafc;background:radial-gradient(circle at 16% 14%,#2dd4bf29,#0000 28rem),linear-gradient(145deg,#020617 0%,#0f172a 56%,#042f2e 140%);justify-content:center;align-items:center;min-height:100dvh;padding:clamp(3rem,7vw,7rem);display:flex;position:relative;overflow:hidden}.ssh-login-brand:after{content:"";pointer-events:none;background-image:linear-gradient(#94a3b80e 1px,#0000 1px),linear-gradient(90deg,#94a3b80e 1px,#0000 1px);background-size:44px 44px;position:absolute;top:0;bottom:0;left:0;right:0;-webkit-mask-image:linear-gradient(to bottom right,#000,#0000 78%);mask-image:linear-gradient(to bottom right,#000,#0000 78%)}.ssh-login-brand-orb{filter:blur(1px);pointer-events:none;border-radius:9999px;position:absolute}.ssh-login-brand-orb-one{border:1px solid #2dd4bf29;width:30rem;height:30rem;top:-10rem;right:-8rem;box-shadow:inset 0 0 6rem #14b8a60d}.ssh-login-brand-orb-two{border:1px solid #5eead41c;width:36rem;height:36rem;bottom:-13rem;left:-10rem}.ssh-login-brand-content{z-index:1;width:min(100%,680px);position:relative}.ssh-login-brand-mark,.ssh-login-mobile-mark{color:#5eead4;background:#14b8a621;place-items:center;display:grid;box-shadow:inset 0 0 0 1px #5eead42e}.ssh-login-brand-mark{border-radius:1.5rem;width:4.5rem;height:4.5rem;margin-bottom:2rem}.ssh-login-brand-mark svg{width:2.25rem;height:2.25rem}.ssh-login-eyebrow,.ssh-login-kicker{color:#5eead4;letter-spacing:.22em;text-transform:uppercase;margin:0;font-size:.72rem;font-weight:900}.ssh-login-brand h1{letter-spacing:-.055em;margin:.7rem 0 0;font-size:clamp(2.7rem,5vw,5rem);font-weight:900;line-height:.98}.ssh-password-setup-brand-title-line{display:block}.ssh-login-brand-description{color:#cbd5e1;max-width:39rem;margin:1.6rem 0 0;font-size:clamp(1rem,1.5vw,1.16rem);line-height:1.85}.ssh-login-features{gap:.85rem;max-width:40rem;margin-top:3rem;display:grid}.ssh-login-feature{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background:#0f172a75;border:1px solid #94a3b81c;border-radius:1rem;align-items:center;gap:1rem;padding:.95rem 1rem;display:flex}.ssh-login-feature-icon{color:#5eead4;background:#14b8a61c;border-radius:.8rem;flex:none;place-items:center;width:2.65rem;height:2.65rem;display:grid}.ssh-login-feature-icon svg{width:1.25rem;height:1.25rem}.ssh-login-feature-copy{gap:.15rem;min-width:0;display:grid}.ssh-login-feature-copy strong{color:#f8fafc;font-size:.92rem;font-weight:800}.ssh-login-feature-copy>span{color:#94a3b8;font-size:.8rem;line-height:1.45}.ssh-login-panel{background:var(--ssh-login-bg);justify-content:center;align-items:center;min-height:100dvh;padding:clamp(2rem,5vw,5rem);transition:background-color .18s;display:flex;position:relative}.ssh-login-theme-toggle{top:max(1.25rem, env(safe-area-inset-top));right:max(1.25rem, env(safe-area-inset-right));z-index:2;border:1px solid var(--ssh-login-border);background:var(--ssh-login-surface);width:2.75rem;height:2.75rem;color:var(--ssh-login-muted);cursor:pointer;border-radius:.9rem;place-items:center;transition:border-color .15s,background-color .15s,color .15s,transform .15s;display:grid;position:absolute;box-shadow:0 5px 16px #0f172a0f}.ssh-login-theme-toggle svg{width:1.15rem;height:1.15rem}.ssh-login-theme-toggle:hover{border-color:var(--ssh-login-border-strong);color:var(--ssh-login-accent);transform:translateY(-1px)}.ssh-login-panel-inner{flex-direction:column;justify-content:center;width:min(100%,480px);min-height:min(40rem,100dvh - 4rem);display:flex}.ssh-login-card{border:1px solid var(--ssh-login-border);background:var(--ssh-login-surface);width:100%;box-shadow:var(--ssh-login-card-shadow);border-radius:1.5rem;padding:clamp(1.6rem,4vw,2.6rem);transition:background-color .18s,border-color .18s}.ssh-login-card-heading{align-items:flex-start;gap:1rem;display:flex}.ssh-login-card-heading>div{min-width:0}.ssh-login-mobile-mark{color:#fff;background:#0f766e;border-radius:1.1rem;flex:none;width:3.5rem;height:3.5rem;display:none;box-shadow:0 12px 28px #0f766e38}.ssh-login-mobile-mark svg{width:1.8rem;height:1.8rem}.ssh-login-kicker{color:var(--ssh-login-accent);letter-spacing:.18em}.ssh-login-card h2{color:var(--ssh-login-text);letter-spacing:-.035em;margin:.65rem 0 0;font-size:clamp(1.8rem,4vw,2.25rem);font-weight:900;line-height:1.15}.ssh-login-subtitle{color:var(--ssh-login-muted);margin:.7rem 0 0;font-size:.94rem;line-height:1.6}.ssh-login-alert{color:#9f1239;background:#fff1f2;border:1px solid #fecdd3;border-radius:.85rem;margin-top:1.3rem;padding:.85rem 1rem;font-size:.82rem;font-weight:700;line-height:1.55}.ssh-login-page[data-theme=dark] .ssh-login-alert{color:#fecdd3;background:#8813372e;border-color:#f43f5e59}.ssh-login-form{gap:1.1rem;margin-top:1.6rem;display:grid}.ssh-login-field{gap:.5rem;display:grid}.ssh-login-label{color:var(--ssh-login-text);font-size:.82rem;font-weight:800}.ssh-login-input-wrap{border:2px solid var(--ssh-login-border-strong);background:var(--ssh-login-surface-soft);min-height:3.3rem;color:var(--ssh-login-muted);border-radius:.9rem;align-items:center;transition:border-color .15s,background-color .15s,box-shadow .15s;display:flex;overflow:hidden;box-shadow:inset 0 1px 2px #0f172a09}.ssh-login-input-wrap:focus-within{border-color:var(--ssh-login-accent);background:var(--ssh-login-surface);box-shadow:0 0 0 4px var(--ssh-login-accent-ring)}.ssh-login-field-icon{background:var(--ssh-login-accent-soft);width:2.15rem;height:2.15rem;color:var(--ssh-login-accent);border-radius:.65rem;flex:none;place-items:center;margin-left:.55rem;transition:background-color .15s,color .15s,box-shadow .15s;display:grid}.ssh-login-field-icon svg{width:1.05rem;height:1.05rem}.ssh-login-input-wrap:focus-within .ssh-login-field-icon{box-shadow:inset 0 0 0 1px var(--ssh-login-accent-ring)}.ssh-login-input-wrap input{width:100%;min-width:0;height:3.15rem;color:var(--ssh-login-text);font:inherit;background:0 0;border:0;outline:0;padding:0 .8rem;font-size:.94rem;font-weight:700}.ssh-login-input-wrap input::placeholder{color:var(--ssh-login-muted-soft);opacity:.82;font-weight:600}.ssh-login-input-wrap input:-webkit-autofill{-webkit-text-fill-color:var(--ssh-login-text);caret-color:var(--ssh-login-text);box-shadow:0 0 0 1000px var(--ssh-login-surface-soft) inset}.ssh-login-input-wrap input:-webkit-autofill:hover{-webkit-text-fill-color:var(--ssh-login-text);caret-color:var(--ssh-login-text);box-shadow:0 0 0 1000px var(--ssh-login-surface-soft) inset}.ssh-login-input-wrap input:-webkit-autofill:focus{-webkit-text-fill-color:var(--ssh-login-text);caret-color:var(--ssh-login-text);box-shadow:0 0 0 1000px var(--ssh-login-surface-soft) inset}.ssh-login-input-wrap:focus-within input:-webkit-autofill{box-shadow:0 0 0 1000px var(--ssh-login-surface) inset}.ssh-login-input-wrap input:disabled{cursor:wait;opacity:.68}.ssh-login-password-toggle{width:2.35rem;height:2.35rem;color:var(--ssh-login-muted);cursor:pointer;background:0 0;border:1px solid #0000;border-radius:.7rem;flex:none;place-items:center;margin-right:.45rem;transition:border-color .15s,background-color .15s,color .15s;display:grid}.ssh-login-password-toggle svg{width:1.05rem;height:1.05rem}.ssh-login-password-toggle:hover:not(:disabled){border-color:var(--ssh-login-border);background:var(--ssh-login-accent-soft);color:var(--ssh-login-accent)}.ssh-login-password-toggle:disabled{cursor:wait;opacity:.6}.ssh-login-theme-toggle:focus-visible{outline:3px solid var(--ssh-login-accent-ring);outline-offset:2px}.ssh-login-password-toggle:focus-visible{outline:3px solid var(--ssh-login-accent-ring);outline-offset:2px}.ssh-login-submit:focus-visible{outline:3px solid var(--ssh-login-accent-ring);outline-offset:2px}.ssh-login-fallback:focus-visible{outline:3px solid var(--ssh-login-accent-ring);outline-offset:2px}.ssh-login-password-hint{color:var(--ssh-login-muted);margin:-.55rem 0 -.15rem;font-size:.76rem;line-height:1.5}.ssh-password-requirements{gap:.4rem;margin:-.25rem 0 0;padding:0;list-style:none;display:grid}.ssh-password-requirement{color:var(--ssh-login-muted);align-items:center;gap:.5rem;font-size:.78rem;font-weight:700;display:flex}.ssh-password-requirement svg{width:1rem;height:1rem;color:var(--ssh-login-muted-soft);flex:none}.ssh-password-requirement.is-met,.ssh-password-requirement.is-met svg,.ssh-password-match.is-met{color:var(--ssh-login-accent)}.ssh-password-match{color:#be123c;margin:-.5rem 0 -.1rem;font-size:.78rem;font-weight:800}.ssh-password-setup-summary{background:#14b8a617;border:1px solid #5eead42e;border-radius:1rem;align-items:flex-start;gap:.85rem;max-width:40rem;margin-top:2.4rem;padding:1rem 1.1rem;display:flex}.ssh-password-setup-summary>svg{color:#5eead4;flex:none;width:1.3rem;height:1.3rem;margin-top:.1rem}.ssh-password-setup-summary>div{gap:.25rem;display:grid}.ssh-password-setup-summary strong{color:#f8fafc;font-size:.9rem}.ssh-password-setup-summary span{color:#94a3b8;font-size:.8rem;line-height:1.5}.ssh-login-caps{color:#b45309;margin:-.45rem 0 -.15rem;font-size:.78rem;font-weight:800}.ssh-login-page[data-theme=dark] .ssh-login-caps{color:#fbbf24}.ssh-login-submit{color:#fff;cursor:pointer;min-height:3.3rem;font:inherit;background:#0f766e;border:0;border-radius:.9rem;justify-content:center;align-items:center;gap:.65rem;margin-top:.25rem;font-size:.94rem;font-weight:900;transition:background-color .15s,transform .15s,box-shadow .15s;display:inline-flex;box-shadow:0 12px 26px #0f766e33}.ssh-login-submit:hover:not(:disabled){background:#115e59;transform:translateY(-1px);box-shadow:0 16px 30px #0f766e40}.ssh-login-submit:disabled{cursor:wait;opacity:.72}.ssh-login-submit-spinner,.ssh-login-probe-spinner{flex:none;animation:.75s linear infinite ssh-login-spin;display:inline-block}.ssh-login-submit-spinner{color:#fff;width:1rem;height:1rem}.ssh-login-security-note{color:var(--ssh-login-muted);align-items:flex-start;gap:.55rem;margin-top:1.2rem;font-size:.76rem;line-height:1.5;display:flex}.ssh-login-security-note svg{width:1rem;height:1rem;color:var(--ssh-login-accent);flex:none;margin-top:.06rem}.ssh-login-fallback{border:1px solid var(--ssh-login-border-strong);background:var(--ssh-login-surface);min-height:2.7rem;color:var(--ssh-login-muted);border-radius:.8rem;justify-content:center;align-items:center;margin-top:.9rem;font-size:.8rem;font-weight:800;text-decoration:none;display:flex}.ssh-login-fallback:hover{border-color:var(--ssh-login-muted-soft);background:var(--ssh-login-surface-soft);color:var(--ssh-login-text)}.ssh-login-probe{color:var(--ssh-login-muted);text-align:center;justify-items:center;gap:.6rem;padding:2.5rem;display:grid}.ssh-login-probe-spinner{width:2rem;height:2rem;color:var(--ssh-login-accent);margin-bottom:.4rem}.ssh-login-probe strong{color:var(--ssh-login-text);font-size:1rem}.ssh-login-probe>span:last-child{font-size:.82rem}.ssh-login-footer{color:var(--ssh-login-muted-soft);flex-wrap:wrap;justify-content:center;gap:.4rem;margin-top:1.5rem;font-size:.72rem;display:flex}@keyframes ssh-login-spin{to{transform:rotate(360deg)}}@media (max-width:980px){.ssh-login-page{grid-template-columns:1fr}.ssh-login-brand{display:none}.ssh-login-panel{padding:max(1.25rem, env(safe-area-inset-top)) max(1.25rem, env(safe-area-inset-right)) max(1.25rem, env(safe-area-inset-bottom)) max(1.25rem, env(safe-area-inset-left))}.ssh-login-panel-inner{min-height:calc(100dvh - 2.5rem)}.ssh-login-mobile-mark{display:grid}}@media (max-width:520px){.ssh-login-panel{background:var(--ssh-login-surface);align-items:stretch;padding:0}.ssh-login-theme-toggle{top:max(1rem, env(safe-area-inset-top));right:max(1rem, env(safe-area-inset-right));box-shadow:none}.ssh-login-panel-inner{width:100%;min-height:100dvh}.ssh-login-card{min-height:100dvh;padding:max(4.75rem, calc(env(safe-area-inset-top) + 4.25rem)) max(1.25rem, env(safe-area-inset-right)) max(2rem, env(safe-area-inset-bottom)) max(1.25rem, env(safe-area-inset-left));box-shadow:none;border:0;border-radius:0;flex-direction:column;justify-content:center;display:flex}.ssh-login-card-heading{display:block}.ssh-login-mobile-mark{margin-bottom:1.25rem}.ssh-login-input-wrap input{font-size:16px}.ssh-login-footer{display:none}.ssh-login-probe{align-content:center;min-height:100dvh}}@media (prefers-reduced-motion:reduce){.ssh-login-panel,.ssh-login-card,.ssh-login-theme-toggle,.ssh-login-submit,.ssh-login-input-wrap{transition:none}.ssh-login-submit-spinner,.ssh-login-probe-spinner{animation-duration:1.4s}}.ssh-safeshield-plan-badge{isolation:isolate;letter-spacing:.085em;white-space:nowrap;border:1px solid #0000;border-radius:999px;align-items:center;gap:.5rem;min-height:2rem;padding:.3rem .82rem .3rem .36rem;font-size:.75rem;font-weight:950;line-height:1;display:inline-flex;position:relative;overflow:hidden}.ssh-safeshield-plan-badge[data-compact=true]{min-height:1.72rem;padding:.22rem .66rem .22rem .3rem;font-size:.69rem}.ssh-safeshield-plan-badge-mark{z-index:2;border-radius:999px;flex:none;place-items:center;width:1.2rem;height:1.2rem;font-size:.76rem;line-height:1;display:grid;position:relative}.ssh-safeshield-plan-badge[data-compact=true] .ssh-safeshield-plan-badge-mark{width:1.02rem;height:1.02rem;font-size:.66rem}.ssh-safeshield-plan-badge[data-tier=free]{color:#475569;background:linear-gradient(135deg,#fff 0%,#f1f5f9 100%);border-color:#cbd5e1;box-shadow:inset 0 1px #ffffffe6}.ssh-safeshield-plan-badge[data-tier=free] .ssh-safeshield-plan-badge-mark{color:#475569;background:#e2e8f0}.ssh-safeshield-plan-badge[data-tier=pro]{color:#f0fdfa;background:linear-gradient(135deg,#115e59 0%,#0f766e 62%,#0d9488 100%);border-color:#0f766e;box-shadow:0 1px 2px #0f766e29,inset 0 1px #ffffff24}.ssh-safeshield-plan-badge[data-tier=pro] .ssh-safeshield-plan-badge-mark{color:#ccfbf1;box-shadow:none;background:#f0fdfa1f;border:1px solid #99f6e475}.ssh-safeshield-plan-badge[data-tier=ultimate]{color:#fffbeb;background:linear-gradient(135deg,#78350f 0%,#92400e 58%,#b45309 100%);border-color:#b45309;box-shadow:0 1px 2px #78350f2e,inset 0 1px #ffffff1f}.ssh-safeshield-plan-badge[data-tier=ultimate] .ssh-safeshield-plan-badge-mark{color:#fde68a;box-shadow:none;background:#fffbeb1f;border:1px solid #fde68a75}.ssh-safeshield-plan-badge[data-tier=paid]{color:#f0f9ff;background:linear-gradient(135deg,#0c4a6e 0%,#075985 58%,#0369a1 100%);border-color:#0369a1;box-shadow:0 1px 2px #0369a129,inset 0 1px #ffffff1f}.ssh-safeshield-plan-badge[data-tier=paid] .ssh-safeshield-plan-badge-mark{color:#bae6fd;box-shadow:none;background:#f0f9ff1f;border:1px solid #bae6fd75}.ssh-safeshield-plan-caption{color:#64748b;white-space:nowrap;align-items:center;gap:.34rem;font-size:.68rem;font-weight:850;display:inline-flex}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=free]{color:#cbd5e1;background:#1e293b;border-color:#475569}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=free] .ssh-safeshield-plan-badge-mark{color:#e2e8f0;background:#475569}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=pro]{color:#99f6e4;background:linear-gradient(135deg,#042f2e 0%,#134e4a 100%);border-color:#0f766e;box-shadow:inset 0 1px #99f6e414}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=pro] .ssh-safeshield-plan-badge-mark{color:#99f6e4;background:#0d94881f;border-color:#2dd4bf61}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=ultimate]{color:#fde68a;background:linear-gradient(135deg,#451a03 0%,#78350f 100%);border-color:#b45309;box-shadow:inset 0 1px #fde68a14}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=ultimate] .ssh-safeshield-plan-badge-mark{color:#fde68a;background:#92400e29;border-color:#d9770675}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=paid]{color:#bae6fd;background:linear-gradient(135deg,#082f49 0%,#0c4a6e 100%);border-color:#0369a1;box-shadow:inset 0 1px #bae6fd14}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=paid] .ssh-safeshield-plan-badge-mark{color:#bae6fd;background:#0369a124;border-color:#0ea5e95c}.ssh-app[data-theme=dark] .ssh-safeshield-plan-caption{color:#94a3b8}.ssh-safeshield-upgrade-card{background:linear-gradient(120deg,#f0fdfa 0%,#ecfeff 58%,#fff7ed 100%);border:1px solid #99f6e4;border-radius:1rem;align-items:center;gap:.85rem;margin-top:.75rem;padding:.9rem 1rem;display:flex;box-shadow:0 10px 24px #0f766e14}.ssh-safeshield-upgrade-mark{color:#fff;background:linear-gradient(135deg,#0f766e,#0891b2);border-radius:.8rem;flex:none;place-items:center;width:2.5rem;height:2.5rem;font-size:1.05rem;display:grid;box-shadow:0 8px 18px #0d948838}.ssh-safeshield-upgrade-copy{flex:1;gap:.2rem;min-width:0;display:grid}.ssh-safeshield-upgrade-meta{color:#0f766e;letter-spacing:.12em;text-transform:uppercase;flex-wrap:wrap;align-items:center;gap:.45rem;font-size:.62rem;font-weight:900;display:flex}.ssh-safeshield-upgrade-soon{letter-spacing:.06em;background:#0f766e1a;border-radius:999px;padding:.18rem .45rem}.ssh-safeshield-upgrade-copy strong{color:#0f172a;font-size:.86rem;line-height:1.35rem}.ssh-safeshield-upgrade-copy>span{color:#64748b;font-size:.74rem;font-weight:600;line-height:1.15rem}.ssh-safeshield-upgrade-action{color:#fff;background:#0f766e;border:1px solid #0f766e;border-radius:.8rem;flex:none;justify-content:center;align-items:center;gap:.4rem;min-height:2.45rem;padding:.55rem .8rem;font-size:.74rem;font-weight:900;text-decoration:none;transition:background-color .15s,border-color .15s,transform .15s;display:inline-flex}.ssh-safeshield-upgrade-action:hover{background:#115e59;border-color:#115e59;transform:translateY(-1px)}.ssh-safeshield-upgrade-action:focus-visible{outline-offset:2px;outline:3px solid #14b8a63d}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-card{background:linear-gradient(120deg,#0d948821 0%,#0f172afa 58%,#92400e1f 100%);border-color:#134e4a;box-shadow:0 12px 28px #0000002e}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-mark{background:linear-gradient(135deg,#0d9488,#0891b2);box-shadow:0 8px 20px #2dd4bf21}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-meta{color:#5eead4}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-soon{background:#2dd4bf1f}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-copy strong{color:#f8fafc}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-copy>span{color:#94a3b8}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-action{color:#ccfbf1;background:#0d948838;border-color:#2dd4bf}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-action:hover{background:#0d948852;border-color:#5eead4}@media (max-width:640px){.ssh-safeshield-upgrade-card{flex-wrap:wrap;align-items:flex-start}.ssh-safeshield-upgrade-action{justify-content:center;width:100%}}.ssh-safeshield-refresh{background:#f0fdfab3;border:1px solid #ccfbf1;border-radius:1rem;align-items:center;gap:.875rem;width:27rem;max-width:100%;margin-top:1rem;padding:.75rem .875rem;display:flex}.ssh-safeshield-refresh[data-state=error]{background:#fef2f2c7;border-color:#fecaca}.ssh-safeshield-refresh-copy{min-width:0}.ssh-safeshield-refresh-kicker,.ssh-safeshield-refresh-title,.ssh-safeshield-refresh-description,.ssh-safeshield-refresh-error-code{margin:0}.ssh-safeshield-refresh-kicker{color:#0f766e;letter-spacing:.14em;text-transform:uppercase;font-size:.66rem;font-weight:900}.ssh-safeshield-refresh[data-state=error] .ssh-safeshield-refresh-kicker{color:#b91c1c}.ssh-safeshield-refresh-title{color:#0f172a;margin-top:.25rem;font-size:.875rem;font-weight:800}.ssh-safeshield-refresh-description{color:#475569;margin-top:.25rem;font-size:.75rem;font-weight:600;line-height:1.25rem}.ssh-safeshield-refresh-error-code{overflow-wrap:anywhere;color:#64748b;margin-top:.375rem;font-size:.68rem;font-weight:600}.ssh-safeshield-refresh-error-code code{font:inherit}.ssh-safeshield-refresh-donut{color:#0d9488;place-items:center;width:3.35rem;min-width:3.35rem;height:3.35rem;display:grid;position:relative}.ssh-safeshield-refresh-donut[data-state=error]{color:#dc2626}.ssh-safeshield-refresh-loader-svg{transform-origin:50%;width:100%;height:100%;animation:.82s linear infinite ssh-safeshield-refresh-loader-rotate;position:absolute;top:0;bottom:0;left:0;right:0;transform:rotate(-90deg)}.ssh-safeshield-refresh-loader-svg-error{animation:none}.ssh-safeshield-refresh-loader-track{color:#cbd5e1;stroke:#cbd5e1}.ssh-safeshield-refresh-loader-arc{color:#0f172a;stroke:#0f172a;filter:drop-shadow(0 0 .06rem #0f172a47)}.ssh-safeshield-refresh-donut[data-state=error] .ssh-safeshield-refresh-loader-track{color:#dc2626;stroke:#dc2626}.ssh-safeshield-refresh-donut-label{z-index:1;color:#0f766e;font-variant-numeric:tabular-nums;font-size:.68rem;font-weight:900;position:relative}.ssh-app[data-theme=dark] .ssh-safeshield-refresh{background:#0f766e1f;border-color:#134e4a}.ssh-app[data-theme=dark] .ssh-safeshield-refresh[data-state=error]{background:#7f1d1d2e;border-color:#7f1d1d}.ssh-app[data-theme=dark] .ssh-safeshield-refresh-title{color:#f8fafc}.ssh-app[data-theme=dark] .ssh-safeshield-refresh-description,.ssh-app[data-theme=dark] .ssh-safeshield-refresh-error-code{color:#94a3b8}.ssh-app[data-theme=dark] .ssh-safeshield-refresh-loader-track{color:#334155;stroke:#334155}.ssh-app[data-theme=dark] .ssh-safeshield-refresh-loader-arc{color:#fff;stroke:#fff;filter:drop-shadow(0 0 .08rem #ffffff8c)}.ssh-app[data-theme=dark] .ssh-safeshield-refresh-donut-label{color:#5eead4}.ssh-app[data-theme=dark] .ssh-safeshield-refresh-donut[data-state=error] .ssh-safeshield-refresh-loader-track{color:#ef4444;stroke:#ef4444}@keyframes ssh-safeshield-refresh-loader-rotate{to{transform:rotate(270deg)}}@media (prefers-reduced-motion:reduce){.ssh-safeshield-refresh-loader-svg{animation:none}}.ssh-dashboard-account-banner{background:#f0fdfa;border-color:#99e8df}.ssh-dashboard-account-banner-icon{color:#0f766e;background:#ccfbf1}.ssh-dashboard-account-banner-title{color:#0f172a}.ssh-dashboard-account-banner-description{color:#475569}.ssh-dashboard-account-banner-badge{color:#92400e;background:#fef3c7}.ssh-dashboard-account-banner-action{color:#fff;background:#0f766e}.ssh-dashboard-account-banner-action:hover{color:#fff;background:#115e59}.ssh-dashboard-account-banner-action:focus-visible{outline-color:#0d9488}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner{background:#102927;border-color:#115e59}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner-icon{color:#99f6e4;background:#134e4a}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner-title{color:#f1f5f9}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner-description{color:#cbd5e1}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner-badge{color:#fde68a;background:#78350f}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner-action{color:#fff;background:#0f766e}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner-action:hover{background:#0d9488}.ssh-account-nav-dot{width:10px;height:10px;box-shadow:0 0 0 2px var(--ssh-account-dot-ring,#fff);background-color:#d97706;border-radius:50%;flex:none;margin-left:auto;display:block}.ssh-account-nav-dot-collapsed{margin-left:0;position:absolute;top:5px;right:7px}.ssh-app[data-theme=dark] .ssh-account-nav-dot{--ssh-account-dot-ring:#0f172a;background-color:#fbbf24}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}} +/*$vite$:1*/ \ No newline at end of file diff --git a/luci-app-smartsafehub/frontend/demo-dist/demo.js b/luci-app-smartsafehub/frontend/demo-dist/demo.js new file mode 100644 index 000000000..e65e2d2e1 --- /dev/null +++ b/luci-app-smartsafehub/frontend/demo-dist/demo.js @@ -0,0 +1,4 @@ +var e,t,n,r,i,a,o,s,c,l,u={},d=[],f=/^m(i|n|o|s|text|space)$/,p=Array.isArray,m=d.slice,h=Object.assign;function g(e){e&&e.parentNode&&e.remove()}function _(n,r,i,a,o){var s={type:n,props:r,key:i,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o||++t,__i:-1,__u:0};return!o&&e.vnode&&e.vnode(s),s}function v(e){return e.children}function y(e,t){this.props=e,this.context=t,this.__g=0}function b(e,t){if(t==null)return e.__?b(e.__,e.__i+1):null;for(var n;tr&&n.sort(a),t=n.shift(),r=n.length,8&t.__g&&(s=void 0,c=void 0,l=(c=(o=t).__v).__e,u=[],d=[],(f=o.__P)&&((s=h({constructor:void 0},c)).__v=c.__v+1,e.vnode&&e.vnode(s),M(f,s,c,o.__n,f.namespaceURI,32&c.__u?[l]:null,u,l||b(c),32&c.__u,d),s.__v=c.__v,s.__.__k[s.__i]=s,te(u,s,d),c.__=c.__e=null,s.__e!=l&&x(s)))}finally{n.length=i=0}}function w(e,t,n,r,i,a,o,s,c,l,f){var p,m,h,g,_,v,y=r.__k||d,b=t.length;for(c=T(n,t,y,c,b),p=0;pc?x--:x++,S=!0)):(~l||(i>g?x--:i>1]+!!u){for(i=n-1,a=n+1;i>=0||a=0?i--:a++])&&!(2&l.__u)&&s==l.key&&c==l.type)return o}return-1}function k(e,t,n){n??(n=``),t[0]==`-`?e.setProperty(t,n):e[t]=n}function A(e,t,n,r,i){var a,o;n:if(t==`style`){if(typeof n==`string`)e.style.cssText=n;else{if(typeof r==`string`&&(e.style.cssText=r=``),r)for(t in r)n&&t in n||k(e.style,t,``);if(n)for(t in n)r&&n[t]==r[t]||k(e.style,t,n[t])}}else if(t[0]==`o`&&t[1]==`n`)(e.__e||(e.__e={}))[t]=n,n&&r||(o=l[t]||(l[t]=j(t)),(e.__a||(e.__a={}))[t]=c,a=t!=(t=t.replace(s,`$1`)),(t=t.slice(2))[0]<`a`&&(t=t.toLowerCase()),n?e.addEventListener(t,o,a):e.removeEventListener(t,o,a));else{if(i==`http://www.w3.org/2000/svg`)t=t.replace(/xlink(H|:h)/,`h`).replace(/sName$/,`s`);else if(t!=`width`&&t!=`height`&&t!=`href`&&t!=`list`&&t!=`form`&&t!=`tabIndex`&&t!=`download`&&t!=`rowSpan`&&t!=`colSpan`&&t!=`role`&&t!=`popover`&&t in e)try{e[t]=n??``;break n}catch{}typeof n==`function`||(n==null||!1===n&&t[4]!=`-`?e.removeAttribute(t):e.setAttribute(t,t==`popover`&&n==1?``:n))}}function j(t){return function(n){if(this.__e){var r=this.__e[t];if(n[o]==null)n[o]=c++;else if(n[o]=n.__.length&&n.__.push({}),n.__[e]}function F(e){return pe=1,Ce(Fe,e)}function Ce(e,t,n){var r=Se(le++,2);if(r.t=e,!r.__c&&(r.__=[n?n(t):Fe(void 0,t),function(e){var t=r.__N?r.__N[0]:r.__[0],n=r.t(t,e);fe(t,n)||(r.__N=[n,r.__[1]],r.__c.setState({}))}],r.__c=N,!N.__f)){N.__f=!0;var i=N.shouldComponentUpdate;N.shouldComponentUpdate=function(e,t,n){var r=this.__H;if(!r)return!0;var a=!1,o=this.props!=e;if(r.__.some(function(e){e.__N&&(a=!0,fe(e.__[0],e.__N[0])||(o=!0))}),i){var s=i.call(this,e,t,n);return a?s||o:s}return!a||o}}return r.__}function I(e,t){var n=Se(le++,3);!P.__s&&Pe(n.__H,t)&&(n.__P=!0,n.__=e,n.u=t,N.__H.__h.push(n))}function we(e){return pe=5,Te(function(){return{current:e}},[])}function Te(e,t){var n=Se(le++,7);return Pe(n.__H,t)&&(n.__=e(),n.__H=t),n.__}function Ee(e,t){return pe=8,Te(function(){return e},t)}function De(){var e=Se(le++,11);if(!e.__){for(var t=N.__v;!t.__m&&t.__;)t=t.__;var n=t.__m||(t.__m=[0,0]);e.__=`P`+n[0]+`-`+n[1]++}return e.__}function Oe(){var e;do{for(;e=he.shift();)try{Me(e)}catch(t){P.__e(t,{__:(e=e.__P)&&e.__v})}for(;e=me.shift();){var t=e.__H;if(e.__P&&t)try{t.__h.some(Me),t.__h.some(Ne),t.__h=[]}catch(n){t.__h=[],P.__e(n,e.__v)}}}while(he.length)}P.__b=function(e){N=null,ge&&ge(e)},P.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),xe&&xe(e,t)},P.__r=function(e){_e&&_e(e),le=0;var t=(N=e.__c).__H;t&&(ue==N?N.__h=[]:(t.__h.some(Me),t.__h.some(Ne),le=0),t.__h=[],t.__.some(function(e){e.__N&&(e.__=e.__N),e.u=e.__N=void 0})),ue=N},P.diffed=function(e){ve&&ve(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&je(me.push(t)),t.__H.__.some(function(e){e.u&&(e.__H=e.u)})),ue=N=null},P.__c=function(e,t){t.some(function(e){try{e.__h.some(Me),e.__h=e.__h.filter(function(e){return!e.__||Ne(e)})}catch(n){t.some(function(e){e.__h&&(e.__h=[])}),t=[],P.__e(n,e.__v)}}),ye&&ye(e,t)},P.unmount=function(e){be&&be(e);var t,n,r=e.__c;r&&r.__H&&(r.__H.__.some(function(r){try{if(r.__P&&r.__c){if(n===void 0){for(n=e.__;n&&(!n.__c||!n.__c.__P);)n=n.__;n=n&&n.__c}r.__P=n,je(he.push(r))}else Me(r)}catch(e){t=e}}),r.__H=void 0,t&&P.__e(t,r.__v))};var ke=typeof requestAnimationFrame==`function`;function Ae(e){var t,n=function(){clearTimeout(r),ke&&cancelAnimationFrame(t),setTimeout(e)},r=setTimeout(n,35);ke&&(t=requestAnimationFrame(n))}function je(e){e!=1&&de==P.requestAnimationFrame||((de=P.requestAnimationFrame)||Ae)(Oe)}function Me(e){var t=N,n=e.__c;typeof n==`function`&&(e.__c=void 0,n()),N=t}function Ne(e){var t=N;e.__c=e.__(),N=t}function Pe(e,t){return!e||e.length!=t.length||t.some(function(t,n){return!fe(t,e[n])})}function Fe(e,t){return typeof t==`function`?t(e):t}var Ie=Object.fromEntries([{route:`home`,hash:`#home`,label:`대시보드`,title:`대시보드`,description:`네트워크, 장치와 시스템 상태를 한눈에 확인합니다.`},{route:`activity`,hash:`#activity`,label:`최근 활동`,title:`최근 활동`,description:`현재 부팅 이후 인터넷, 보호, 업데이트와 진단 상태 변화를 확인합니다.`},{route:`network`,hash:`#network`,label:`네트워크`,title:`네트워크`,description:`인터넷 연결과 내부 네트워크 주소, DHCP 및 네트워크 충돌을 한 곳에서 관리합니다.`},{route:`wifi`,hash:`#wifi`,label:`Wi-Fi`,title:`Wi-Fi`,description:`기본 무선 네트워크의 이름, 보안과 사용 상태를 관리합니다.`},{route:`iptv`,hash:`#iptv`,label:`IPTV`,title:`IPTV`,description:`SK Broadband와 LG U+ 멀티캐스트 IPTV를 설정합니다.`},{route:`devices`,hash:`#devices`,label:`연결된 기기`,title:`연결된 기기`,description:`네트워크에서 확인된 기기와 연결 방식을 살펴봅니다.`},{route:`safeshield`,hash:`#safeshield`,label:`SafeShield`,title:`SafeShield`,description:`DNS 보호 상태를 확인하고 차단 목록을 관리합니다.`},{route:`rules`,hash:`#rules`,label:`사용자 규칙`,title:`사용자 규칙`,description:`직접 허용하거나 차단할 도메인을 관리합니다.`},{route:`account`,hash:`#account`,label:`SmartSafeHub 계정`,title:`SmartSafeHub 계정`,description:`이 기기의 SmartSafeHub 계정 연결 상태와 구독 플랜을 확인합니다.`},{route:`system`,hash:`#system`,label:`업데이트`,title:`업데이트`,description:`기기의 펌웨어와 관리 소프트웨어 업데이트를 관리합니다.`},{route:`settings`,hash:`#settings`,label:`설정`,title:`설정`,description:`시스템 상태와 시간대를 확인하고 장치 관리 및 진단 기능을 설정합니다.`}].map(e=>[e.route,e])),Le=[`home`,`activity`,`network`,`wifi`,`devices`,`safeshield`,`rules`,`system`],Re=`smartsafehub.theme`;function ze(){if(typeof window>`u`)return`light`;try{let e=window.localStorage.getItem(Re);if(e===`dark`||e===`light`)return e}catch{}return window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}function Be(e){try{window.localStorage.setItem(Re,e)}catch{}}function Ve(e){let t=document.querySelector(`meta[name="theme-color"]`);t&&(t.content=e===`dark`?`#0f172a`:`#f8fafc`),document.documentElement.style.colorScheme=e,document.body.style.backgroundColor=e===`dark`?`#020617`:`#f8fafc`}var He=0;Array.isArray;function L(t,n,r,i,a,o){n||(n={});var s,c,l=n;if(`ref`in l&&typeof t!=`function`)for(c in l={},n)c==`ref`?s=n[c]:l[c]=n[c];var u={type:t,props:l,key:r,ref:s,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--He,__i:-1,__u:0};return(a||o)&&(u.__source=a,u.__self=o),e.vnode&&e.vnode(u),u}function R({children:e,title:t,...n}){return L(`svg`,{"aria-hidden":!t||void 0,fill:`none`,role:t?`img`:void 0,stroke:`currentColor`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`1.8`,viewBox:`0 0 24 24`,...n,children:[t?L(`title`,{children:t}):null,e]})}function Ue(e){return L(R,{...e,children:[L(`path`,{d:`M12 3 5 6v5c0 4.6 2.8 8.1 7 10 4.2-1.9 7-5.4 7-10V6l-7-3Z`}),L(`path`,{d:`m9 12 2 2 4-4`})]})}function We(e){return L(R,{...e,children:[L(`rect`,{height:`7`,rx:`2`,width:`18`,x:`3`,y:`13`}),L(`path`,{d:`M7 16.5h.01M11 16.5h.01M7 10a7 7 0 0 1 10 0M9.5 7.5a3.5 3.5 0 0 1 5 0`})]})}function Ge(e){return L(R,{...e,children:[L(`rect`,{height:`11`,rx:`2`,width:`16`,x:`4`,y:`3`}),L(`path`,{d:`M8 21h8M12 14v7`})]})}function Ke(e){return L(R,{...e,children:[L(`path`,{d:`m3 11 9-8 9 8`}),L(`path`,{d:`M5 10v10h14V10M9 20v-6h6v6`})]})}function qe(e){return L(R,{...e,children:[L(`circle`,{cx:`12`,cy:`8`,r:`4`}),L(`path`,{d:`M4.5 21a7.5 7.5 0 0 1 15 0`})]})}function Je(e){return L(R,{...e,children:[L(`path`,{d:`M12 16V4`}),L(`path`,{d:`m7 9 5-5 5 5`}),L(`path`,{d:`M5 14v5h14v-5`})]})}function Ye(e){return L(R,{...e,children:[L(`circle`,{cx:`12`,cy:`12`,r:`3`}),L(`path`,{d:`M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2.8 2.8-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.6v.2h-4V21a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1L4.2 17l.1-.1a1.7 1.7 0 0 0 .3-1.9A1.7 1.7 0 0 0 3 14H2.8v-4H3a1.7 1.7 0 0 0 1.6-1 1.7 1.7 0 0 0-.3-1.9L4.2 7 7 4.2l.1.1a1.7 1.7 0 0 0 1.9.3A1.7 1.7 0 0 0 10 3V2.8h4V3a1.7 1.7 0 0 0 1 1.6 1.7 1.7 0 0 0 1.9-.3l.1-.1L19.8 7l-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.6 1h.2v4H21a1.7 1.7 0 0 0-1.6 1Z`})]})}function Xe(e){return L(R,{...e,children:[L(`path`,{d:`M5 9.5a10 10 0 0 1 14 0M8 13a6 6 0 0 1 8 0M11 16.5a2 2 0 0 1 2 0`}),L(`path`,{d:`M12 20h.01`})]})}function Ze(e){return L(R,{...e,children:[L(`path`,{d:`M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4z`}),L(`path`,{d:`M6.5 6.5h1M16.5 6.5h1M6.5 16.5h1`}),L(`path`,{d:`M14 14h2v2h-2zM18 14h2v2h-2zM16 16h2v2h-2zM18 18h2v2h-2zM14 18h1`})]})}function Qe(e){return L(R,{...e,children:[L(`path`,{d:`M7 3v4M17 3v4M5 7h14v4a7 7 0 0 1-14 0V7Z`}),L(`path`,{d:`M12 18v3`})]})}function $e(e){return L(R,{...e,children:[L(`circle`,{cx:`12`,cy:`12`,r:`9`}),L(`path`,{d:`M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18`})]})}function et(e){return L(R,{...e,children:[L(`rect`,{height:`13`,rx:`2`,width:`18`,x:`3`,y:`6`}),L(`path`,{d:`m8 2 4 4 4-4M8 22h8`})]})}function tt(e){return L(R,{...e,children:[L(`circle`,{cx:`12`,cy:`12`,r:`9`}),L(`path`,{d:`M12 7v5l3 2`})]})}function nt(e){return L(R,{...e,children:[L(`rect`,{height:`14`,rx:`2`,width:`14`,x:`5`,y:`5`}),L(`path`,{d:`M9 9h6v6H9zM9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M2 15h3M19 9h3M19 15h3`})]})}function z(e){return L(R,{...e,children:[L(`path`,{d:`M20 11a8 8 0 1 0-2.3 5.7`}),L(`path`,{d:`M20 4v7h-7`})]})}function rt(e){return L(R,{...e,children:[L(`path`,{d:`M12 2v10`}),L(`path`,{d:`M18.4 6.6a8 8 0 1 1-12.8 0`})]})}function it(e){return L(R,{...e,children:[L(`path`,{d:`M12 3v12`}),L(`path`,{d:`m7 10 5 5 5-5`}),L(`path`,{d:`M5 21h14`})]})}function at(e){return L(R,{...e,children:[L(`ellipse`,{cx:`12`,cy:`5`,rx:`8`,ry:`3`}),L(`path`,{d:`M4 5v6c0 1.7 3.6 3 8 3s8-1.3 8-3V5M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6`})]})}function ot(e){return L(R,{...e,children:[L(`circle`,{cx:`8`,cy:`15`,r:`4`}),L(`path`,{d:`m11 12 8-8M15 8l2 2M17 6l2 2`})]})}function st(e){return L(R,{...e,children:[L(`path`,{d:`M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z`}),L(`circle`,{cx:`12`,cy:`12`,r:`2.5`})]})}function ct(e){return L(R,{...e,children:[L(`path`,{d:`m3 3 18 18`}),L(`path`,{d:`M10.6 6.2A8.8 8.8 0 0 1 12 6c6 0 9.5 6 9.5 6a15.5 15.5 0 0 1-2.6 3.2`}),L(`path`,{d:`M6.2 6.3C3.8 8 2.5 12 2.5 12s3.5 6 9.5 6a9.8 9.8 0 0 0 3.1-.5`}),L(`path`,{d:`M10.3 10.3a2.5 2.5 0 0 0 3.4 3.4`})]})}function lt(e){return L(R,{...e,children:[L(`rect`,{height:`17`,rx:`2`,width:`18`,x:`3`,y:`4`}),L(`path`,{d:`M8 2v4M16 2v4M3 9h18`})]})}function B(e){return L(R,{...e,children:[L(`circle`,{cx:`12`,cy:`12`,r:`9`}),L(`path`,{d:`m8 12 2.5 2.5L16 9`})]})}function V(e){return L(R,{...e,children:[L(`path`,{d:`M10.3 4.2 2.7 17.5A2 2 0 0 0 4.4 20h15.2a2 2 0 0 0 1.7-2.5L13.7 4.2a2 2 0 0 0-3.4 0Z`}),L(`path`,{d:`M12 9v4M12 17h.01`})]})}function ut(e){return L(R,{...e,children:L(`path`,{d:`M12 5v14M5 12h14`})})}function dt(e){return L(R,{...e,children:L(`path`,{d:`M4 7h16M9 7V4h6v3M7 7l1 13h8l1-13M10 11v5M14 11v5`})})}function ft(e){return L(R,{...e,children:[L(`circle`,{cx:`11`,cy:`11`,r:`7`}),L(`path`,{d:`m20 20-4-4`})]})}function pt(e){return L(R,{...e,children:[L(`path`,{d:`M8 6h12M8 12h12M8 18h12`}),L(`path`,{d:`M4 6h.01M4 12h.01M4 18h.01`})]})}function mt(e){return L(R,{...e,children:[L(`path`,{d:`M10 5H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h4`}),L(`path`,{d:`M14 8l4 4-4 4M8 12h10`})]})}function ht(e){return L(R,{...e,children:L(`path`,{d:`M4 7h16M4 12h16M4 17h16`})})}function gt(e){return L(R,{...e,children:L(`path`,{d:`m6 9 6 6 6-6`})})}function _t(e){return L(R,{...e,children:L(`path`,{d:`m6 6 12 12M18 6 6 18`})})}function vt(e){return L(R,{...e,children:L(`path`,{d:`m15 18-6-6 6-6`})})}function yt(e){return L(R,{...e,children:L(`path`,{d:`m9 18 6-6-6-6`})})}function bt(e){return L(R,{...e,children:L(`path`,{d:`M20.7 13.2A8.5 8.5 0 1 1 10.8 3.3 6.7 6.7 0 0 0 20.7 13.2Z`})})}function xt(e){return L(R,{...e,children:[L(`circle`,{cx:`12`,cy:`12`,r:`4`}),L(`path`,{d:`M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41`})]})}function St({description:e,loading:t,onRefresh:n,onToggleTheme:r,refreshing:i,theme:a,title:o}){return L(`header`,{class:`ssh-product-hero border-b border-slate-200 bg-white px-4 py-4 sm:px-6 md:h-[4.5rem] md:py-0 lg:px-8 xl:px-10`,children:L(`div`,{class:`flex w-full items-center justify-between gap-4 md:h-full`,children:[L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-[0.68rem] font-extrabold uppercase tracking-[0.18em] text-teal-700`,children:`SmartSafeHub`}),L(`div`,{class:`mt-1 flex min-w-0 items-baseline gap-3`,children:[L(`h1`,{class:`m-0 truncate text-xl font-black tracking-tight text-slate-950 sm:text-2xl`,children:o}),L(`p`,{class:`m-0 hidden truncate text-sm text-slate-500 lg:block`,children:e})]})]}),L(`div`,{class:`flex shrink-0 items-center gap-2`,children:[L(`button`,{"aria-label":a===`dark`?`라이트 모드로 전환`:`다크 모드로 전환`,class:`ssh-product-header-action hidden h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-600 shadow-sm transition hover:border-teal-300 hover:text-teal-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 md:inline-flex`,onClick:r,title:a===`dark`?`라이트 모드`:`다크 모드`,type:`button`,children:L(a===`dark`?xt:bt,{class:`size-5`})}),L(`button`,{"aria-busy":i,"aria-label":i?`새로고침 중`:`현재 화면 새로고침`,class:`ssh-product-header-action hidden h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-white shadow-sm transition hover:border-teal-300 hover:text-teal-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-wait disabled:opacity-60 md:inline-flex ${i?`text-teal-700`:`text-slate-600`}`,disabled:t||i,onClick:n,title:i?`새로고침 중`:`새로고침`,type:`button`,children:i?L(z,{class:`size-5 animate-spin`}):L(z,{class:`size-5`})})]})]})})}var Ct=`/cgi-bin/luci`;function wt(){let e=window.location.pathname,t=e.indexOf(`/cgi-bin/luci`);return t>=0?e.slice(0,t+13):Ct}function Tt(e){let t=e.startsWith(`/`)?e:`/${e}`;return`${wt()}${t}`}function Et(e){return Tt(e)}var Dt=[{label:`Overview`,routes:[`home`,`activity`]},{label:`Network`,routes:[`network`,`wifi`,`iptv`,`devices`]},{label:`Security`,routes:[`safeshield`,`rules`]},{label:`System`,routes:[`account`,`system`,`settings`]}];function Ot({route:e}){switch(e){case`home`:return L(Ke,{class:`size-5`});case`activity`:return L(tt,{class:`size-5`});case`network`:return L($e,{class:`size-5`});case`wifi`:return L(Xe,{class:`size-5`});case`iptv`:return L(et,{class:`size-5`});case`devices`:return L(Ge,{class:`size-5`});case`safeshield`:return L(Ue,{class:`size-5`});case`rules`:return L(qe,{class:`size-5`});case`account`:return L(ot,{class:`size-5`});case`system`:return L(Je,{class:`size-5`});case`settings`:return L(Ye,{class:`size-5`})}}function kt({theme:e}){return L(e===`dark`?xt:bt,{class:`size-5`})}function At(e){return e===`dark`?`라이트 모드`:`다크 모드`}function jt(e,t=!1){return`relative flex min-h-11 min-w-0 items-center rounded-xl py-2.5 text-sm font-extrabold no-underline transition ${t?`justify-center px-2`:`gap-3 px-3`} ${e?`bg-teal-50 text-teal-800 ring-1 ring-inset ring-teal-200`:`text-slate-600 hover:bg-slate-50 hover:text-slate-950`}`}function Mt({accountRegistered:e,collapsed:t=!1,route:n,updateCount:r,demo:i=!1}){return L(`div`,{class:t?`space-y-3`:`space-y-5`,children:Dt.filter(e=>!i||e.routes.some(e=>Le.includes(e))).map((a,o)=>L(`section`,{"aria-label":a.label,class:t&&o>0?`border-t border-slate-100 pt-3`:void 0,children:[t?null:L(`p`,{class:`mb-2 px-3 text-[0.65rem] font-black uppercase tracking-[0.18em] text-slate-400`,children:a.label}),L(`div`,{class:`space-y-1`,children:a.routes.filter(e=>!i||Le.includes(e)).map(i=>{let a=Ie[i],o=n===i;return L(`a`,{"aria-current":o?`page`:void 0,"aria-label":i===`account`&&e===!1?`${a.label} (계정 연결 필요)`:t?a.label:void 0,class:jt(o,t),href:a.hash,title:i===`account`&&e===!1?`SmartSafeHub 계정 연결 필요`:t?a.label:void 0,children:[L(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg ${o?`bg-teal-700 text-white`:`bg-slate-100 text-slate-500`}`,children:L(Ot,{route:i})}),t?null:L(`span`,{class:`flex min-w-0 flex-1 items-center gap-2`,children:[L(`span`,{class:`truncate`,children:a.label}),i===`iptv`?L(`span`,{class:`rounded-full border border-amber-300 bg-amber-100 px-1.5 py-0.5 text-[9px] font-black uppercase tracking-[0.08em] text-amber-800`,children:`Beta`}):null]}),t&&i===`iptv`?L(`span`,{"aria-label":`Beta 기능`,class:`absolute right-0.5 top-0.5 inline-flex size-5 items-center justify-center rounded-full border border-amber-300 bg-amber-100 text-[13px] font-extrabold leading-none text-amber-900 shadow-sm ring-2 ring-white`,children:`β`}):null,i===`account`&&e===!1?L(`span`,{"aria-label":`계정 연결 필요`,class:`ssh-account-nav-dot ${t?`ssh-account-nav-dot-collapsed`:``}`,"aria-hidden":`true`,title:`계정 연결 필요`}):null,i===`system`&&r>0?t?L(`span`,{"aria-label":`${r}개의 업데이트 유형`,class:`ssh-update-nav-badge`,"data-collapsed":`true`,children:r}):L(`span`,{class:`ssh-update-nav-badge`,"data-collapsed":`false`,children:r}):null]},i)})})]},a.label))})}function Nt({accountRegistered:e,collapsed:t,loading:n,onRefresh:r,onToggleCollapsed:i,onToggleTheme:a,refreshing:o,route:s,theme:c,title:l,updateCount:u,demo:d=!1}){let[f,p]=F(!1),m=At(c);return I(()=>{p(!1)},[s]),I(()=>{if(!f)return;let e=e=>{e.key===`Escape`&&p(!1)};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[f]),L(v,{children:[L(`nav`,{"aria-label":`SmartSafeHub 모바일 메뉴`,class:`ssh-mobile-navigation sticky top-0 z-40 border-b border-slate-200 bg-white/95 px-4 shadow-sm shadow-slate-900/5 backdrop-blur md:hidden`,children:[L(`div`,{class:`flex min-h-16 items-center justify-between gap-3`,children:[L(`a`,{class:`flex min-w-0 items-center gap-3 no-underline`,href:`#home`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-950 text-teal-300`,children:L(Ue,{class:`size-5`})}),L(`span`,{class:`min-w-0`,children:[L(`strong`,{class:`block truncate text-sm font-black text-slate-950`,children:`SmartSafeHub`}),L(`span`,{class:`block truncate text-xs font-semibold text-slate-500`,children:l})]})]}),L(`div`,{class:`flex shrink-0 items-center gap-2`,children:[L(`button`,{"aria-label":`${m}로 전환`,class:`inline-flex size-11 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-600 transition hover:border-slate-300 hover:bg-slate-50 hover:text-teal-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100`,onClick:a,title:m,type:`button`,children:L(kt,{theme:c})}),L(`button`,{"aria-busy":o,"aria-label":o?`새로고침 중`:`현재 화면 새로고침`,class:`inline-flex size-11 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-white transition hover:border-slate-300 hover:bg-slate-50 hover:text-teal-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-wait disabled:opacity-60 ${o?`text-teal-700`:`text-slate-600`}`,disabled:n||o,onClick:r,title:o?`새로고침 중`:`새로고침`,type:`button`,children:o?L(z,{class:`size-5 animate-spin`}):L(z,{class:`size-5`})}),L(`button`,{"aria-controls":`smartsafehub-mobile-menu`,"aria-expanded":f,"aria-label":f?`모바일 메뉴 닫기`:`모바일 메뉴 열기`,class:`inline-flex size-11 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-700 transition hover:bg-slate-50 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100`,onClick:()=>p(e=>!e),type:`button`,children:L(f?_t:ht,{class:`size-5`})})]})]}),f?L(`div`,{class:`ssh-mobile-menu border-t border-slate-100 py-4`,id:`smartsafehub-mobile-menu`,children:[L(Mt,{accountRegistered:e,demo:d,route:s,updateCount:u}),d?null:L(`div`,{class:`mt-5 space-y-1 border-t border-slate-100 pt-4`,children:L(`a`,{class:`${jt(!1)} text-rose-700 hover:bg-rose-50 hover:text-rose-800`,href:Et(`/admin/logout`),children:[L(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg bg-rose-50 text-rose-600`,children:L(mt,{class:`size-5`})}),`로그아웃`]})})]}):null]}),L(`aside`,{class:`hidden min-h-screen border-r border-slate-200 bg-white md:flex md:flex-col`,"data-collapsed":t?`true`:`false`,children:L(`div`,{class:`sticky top-0 flex h-screen flex-col`,children:[L(`div`,{class:`flex h-[4.5rem] min-h-[4.5rem] shrink-0 items-center border-b border-slate-100 ${t?`justify-center px-2`:`gap-3 px-4`}`,children:[L(`a`,{"aria-label":`SmartSafeHub Dashboard`,class:`flex items-center no-underline ${t?`justify-center`:`min-w-0 flex-1 gap-3`}`,href:`#home`,title:t?`SmartSafeHub`:void 0,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-950 text-teal-300 shadow-sm shadow-slate-950/10`,children:L(Ue,{class:`size-5`})}),t?null:L(`div`,{class:`min-w-0`,children:[L(`strong`,{class:`block truncate text-sm font-black tracking-tight text-slate-950`,children:`SmartSafeHub`}),L(`span`,{class:`block text-[0.68rem] font-bold uppercase tracking-[0.14em] text-slate-400`,children:`Home Gateway`})]})]}),L(`button`,{"aria-controls":`smartsafehub-desktop-navigation`,"aria-expanded":!t,"aria-label":t?`사이드바 펼치기`:`사이드바 접기`,class:`ssh-sidebar-toggle absolute right-0 top-[4.5rem] z-20 inline-flex size-7 translate-x-1/2 -translate-y-1/2 shrink-0 items-center justify-center rounded-full transition focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100`,onClick:i,title:t?`사이드바 펼치기`:`사이드바 접기`,type:`button`,children:L(t?yt:vt,{class:`size-4`})})]}),L(`nav`,{"aria-label":`SmartSafeHub 메뉴`,class:`flex-1 overflow-y-auto py-5 ${t?`px-2`:`px-3`}`,id:`smartsafehub-desktop-navigation`,children:L(Mt,{accountRegistered:e,collapsed:t,demo:d,route:s,updateCount:u})}),d?null:L(`div`,{class:`border-t border-slate-100 ${t?`p-2`:`p-3`}`,children:L(`a`,{"aria-label":`SmartSafeHub에서 로그아웃`,class:`${jt(!1,t)} mt-1 text-rose-700 hover:bg-rose-50 hover:text-rose-800`,href:Et(`/admin/logout`),title:t?`로그아웃`:void 0,children:[L(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg bg-rose-50 text-rose-600`,children:L(mt,{class:`size-5`})}),t?null:`로그아웃`]})})]})})]})}var Pt=`smartsafehub.sidebar.collapsed`;function Ft(){if(typeof window>`u`)return!1;try{return window.localStorage.getItem(Pt)===`1`}catch{return!1}}function It({accountRegistered:e,children:t,loading:n,onRefresh:r,refreshing:i,route:a,updateCount:o,demo:s=!1}){let c=Ie[a],[l,u]=F(Ft),[d,f]=F(ze);return I(()=>{try{window.localStorage.setItem(Pt,l?`1`:`0`)}catch{}},[l]),I(()=>{Be(d),Ve(d)},[d]),L(`div`,{class:`ssh-app min-h-screen bg-slate-50 text-slate-950 md:grid md:transition-[grid-template-columns] md:duration-200 md:ease-out ${l?`md:grid-cols-[5rem_minmax(0,1fr)]`:`md:grid-cols-[16rem_minmax(0,1fr)]`}`,"data-theme":d,children:[L(Nt,{accountRegistered:e,demo:s,collapsed:l,loading:n,onRefresh:r,onToggleCollapsed:()=>u(e=>!e),onToggleTheme:()=>f(e=>e===`dark`?`light`:`dark`),refreshing:i,route:a,theme:d,title:c.title,updateCount:o}),L(`div`,{class:`min-w-0 md:min-h-screen`,children:[L(St,{description:c.description,loading:n,onRefresh:r,onToggleTheme:()=>f(e=>e===`dark`?`light`:`dark`),refreshing:i,theme:d,title:c.title}),L(`main`,{class:`ssh-product-main w-full max-w-[1600px] bg-slate-50 px-4 pb-8 sm:px-6 sm:pb-10 lg:px-8 lg:pb-12 xl:px-10 ${n?`pt-2 sm:pt-2 lg:pt-3`:`pt-5 sm:pt-6 lg:pt-8`}`,children:t})]})]})}var Lt=new Intl.NumberFormat(`ko-KR`),Rt=new Intl.DateTimeFormat(`ko-KR`,{dateStyle:`medium`,timeStyle:`short`});function zt(e){if(!Number.isFinite(e)||e<0)return`확인 불가`;let t=Math.floor(e/86400),n=Math.floor(e%86400/3600),r=Math.floor(e%3600/60);return t>0?`${t}일 ${n}시간`:n>0?`${n}시간 ${r}분`:`${Math.max(r,1)}분`}function Bt(e){if(!Number.isFinite(e)||e<0)return`확인 불가`;let t=[`B`,`KB`,`MB`,`GB`],n=e,r=0;for(;n>=1024&&r864e10?`기록 없음`:!Number.isFinite(t)||e>t+300||t-e>=604800?Ut(e):Wt(e,t)}function Kt(e){let t=/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(e??``);if(!t)return null;let[,n,r,i,a,o,s]=t,c=Date.UTC(Number(n),Number(r)-1,Number(i),Number(a),Number(o),Number(s)),l=new Date(c);return l.getUTCFullYear()!==Number(n)||l.getUTCMonth()+1!==Number(r)||l.getUTCDate()!==Number(i)||l.getUTCHours()!==Number(a)||l.getUTCMinutes()!==Number(o)||l.getUTCSeconds()!==Number(s)||c<=0?null:c/1e3}function qt(e){if(!Number.isFinite(e)||e<=0)return`확인 불가`;let t=Math.floor(e/3600),n=Math.floor(e%3600/60);return t>0?n>0?`${t}시간 ${n}분`:`${t}시간`:`${n}분`}function Jt(e){let t=Math.max(0,e.total),n=e.free+e.buffered+e.cached,r=Math.min(t,Math.max(0,e.available>0?e.available:n)),i=Math.max(0,t-r);return{used:i,percent:t>0?i/t*100:0}}function Yt(e,t){let n=e.metadata[t];return typeof n==`string`&&n.trim().length>0?n.trim():null}function Xt(e,t){let n=e.metadata[t];return typeof n==`number`&&Number.isFinite(n)?n:null}function Zt(e,t){let n=e.metadata[t];return typeof n==`boolean`?n:null}function Qt(e,t){let n=Yt(e,`error_code`);return n?`${t} (오류 코드: ${n})`:t}function $t(e){let t=Math.max(0,Math.floor(e));if(t<60)return`${t}초`;let n=Math.floor(t/60),r=t%60;if(n<60)return r>0?`${n}분 ${r}초`:`${n}분`;let i=Math.floor(n/60),a=n%60;return a>0?`${i}시간 ${a}분`:`${i}시간`}function en(e){let t=Yt(e,`from_version`),n=Yt(e,`to_version`);return t&&n?`${t} → ${n}`:n?`${n} 버전이 적용되었습니다.`:null}function tn(e){return e?e.toUpperCase():null}function nn(e){switch(e){case`dhcp`:return`자동 IP(DHCP)`;case`pppoe`:return`PPPoE`;case`static`:return`고정 IPv4`;default:return e.toUpperCase()}}function rn(e){switch(e.eventType){case`system.booted`:return{title:`공유기 부팅 감지`,description:`공유기가 켜지거나 재시작된 것을 확인했습니다.`};case`network.internet.disconnected`:return{title:`인터넷 연결 끊김 감지`,description:`공유기에서 인터넷 연결을 확인할 수 없었습니다.`};case`network.internet.recovered`:{let t=Xt(e,`downtime_seconds`);return{title:`인터넷 연결 복구`,description:t!==null&&t>0?`약 ${$t(t)} 동안 인터넷 연결이 확인되지 않았습니다.`:`공유기에서 인터넷 연결을 다시 확인했습니다.`}}case`safeshield.protection.enabled`:return{title:`SafeShield 보호 활성화`,description:`DNS 보호 기능이 활성화되었습니다.`};case`safeshield.protection.disabled`:return{title:`SafeShield 보호 비활성화`,description:`DNS 보호 기능이 비활성화되었습니다.`};case`safeshield.blocklist.updated`:{let t=Xt(e,`domain_count`),n=Yt(e,`artifact_version`),r=[];return t!==null&&t>0&&r.push(`${H(t)}개 도메인 적용`),n&&r.push(`목록 ${n}`),{title:`SafeShield 차단 목록 업데이트 완료`,description:r.length>0?r.join(` · `):`최신 차단 목록을 적용했습니다.`}}case`safeshield.blocklist.update_failed`:return{title:`SafeShield 차단 목록 업데이트 실패`,description:Qt(e,`차단 목록을 업데이트하지 못했습니다.`)};case`safeshield.statistics.enabled`:return{title:`SafeShield 차단 통계 수집 활성화`,description:`기기별 차단 통계 수집을 활성화했습니다.`};case`safeshield.statistics.disabled`:return{title:`SafeShield 차단 통계 수집 비활성화`,description:`기기별 차단 통계 수집을 비활성화했습니다.`};case`safeshield.rule.added`:return{title:Yt(e,`rule_action`)===`allow`?`사용자 허용 규칙 추가`:`사용자 차단 규칙 추가`,description:`SafeShield 사용자 규칙이 변경되었습니다.`};case`safeshield.rule.removed`:return{title:Yt(e,`rule_action`)===`allow`?`사용자 허용 규칙 삭제`:`사용자 차단 규칙 삭제`,description:`SafeShield 사용자 규칙이 변경되었습니다.`};case`settings.scheduled_reboot.updated`:{let t=Zt(e,`enabled`);return{title:`예약 재부팅 설정 변경`,description:t===!0?`예약 재부팅 설정을 저장하고 새 일정에 적용했습니다.`:t===!1?`예약 재부팅을 비활성화했습니다.`:`예약 재부팅 설정을 변경했습니다.`}}case`settings.software_updates.updated`:return{title:`관리 소프트웨어 업데이트 설정 변경`,description:Zt(e,`auto_install`)===!0?`자동 설치를 포함한 업데이트 설정을 저장했습니다.`:`관리 소프트웨어 업데이트 설정을 저장했습니다.`};case`settings.health_reporter.enabled`:return{title:`원격 상태 보고 활성화`,description:`장치 상태를 SmartSafeHub Cloud에 보고하도록 설정했습니다.`};case`settings.health_reporter.disabled`:return{title:`원격 상태 보고 비활성화`,description:`SmartSafeHub Cloud로의 장치 상태 보고를 중지했습니다.`};case`settings.activity_cloud_sync.enabled`:return{title:`Cloud 활동 기록 전송 활성화`,description:`이후 발생하는 활동을 SmartSafeHub Cloud에 전송하도록 설정했습니다.`};case`settings.activity_cloud_sync.disabled`:return{title:`Cloud 활동 기록 전송 비활성화`,description:`Cloud 전송을 중지했습니다. 로컬 최근 활동은 계속 기록됩니다.`};case`settings.timezone.updated`:{let t=Yt(e,`zonename`);return{title:`시간대 설정 변경`,description:t?`공유기 시간대를 ${t}(으)로 변경했습니다.`:`공유기 시간대를 변경했습니다.`}}case`settings.wifi.updated`:return{title:`Wi-Fi 설정 변경`,description:`Wi-Fi 설정을 저장하고 무선 네트워크에 적용했습니다.`};case`settings.wan.updated`:{let t=Yt(e,`protocol`);return{title:`인터넷 설정 변경`,description:t?`${nn(t)} 방식으로 WAN 설정을 변경했습니다.`:`WAN 인터넷 연결 설정을 변경했습니다.`}}case`settings.lan.updated`:return{title:`LAN 설정 변경`,description:`내부 네트워크 및 DHCP 설정을 변경했습니다.`};case`settings.iptv.updated`:{let t=Zt(e,`enabled`),n=Yt(e,`provider`),r=n===`skb`?`SK Broadband`:n===`lgu`?`LG U+`:null;return{title:`IPTV 설정 변경`,description:t===!1?`IPTV 기능을 비활성화했습니다.`:r?`${r} IPTV 설정을 적용했습니다.`:`IPTV 설정을 적용했습니다.`}}case`software.update.completed`:return{title:`관리 소프트웨어 업데이트 완료`,description:en(e)};case`software.update.failed`:return{title:`관리 소프트웨어 업데이트 실패`,description:Qt(e,`관리 소프트웨어를 업데이트하지 못했습니다.`)};case`firmware.update.started`:return{title:`펌웨어 업데이트 시작`,description:`펌웨어 설치를 시작했습니다. 완료 과정에서 공유기가 재부팅됩니다.`};case`firmware.update.failed`:return{title:`펌웨어 업데이트 실패`,description:Qt(e,`펌웨어 설치 준비 또는 검증에 실패했습니다.`)};case`license.activated`:{let t=tn(Yt(e,`to_plan`));return{title:`요금제 활성화`,description:t?`${t} 요금제가 활성화되었습니다.`:`요금제가 활성화되었습니다.`}}case`license.changed`:{let t=tn(Yt(e,`from_plan`)),n=tn(Yt(e,`to_plan`));return{title:`요금제 변경`,description:t&&n?`${t} → ${n}`:n?`${n} 요금제로 변경되었습니다.`:`요금제 상태가 변경되었습니다.`}}case`license.cleared`:return{title:`계정 권한 해제`,description:Yt(e,`reason`)===`manual`?`이 공유기에서 계정 권한 연결을 해제했습니다.`:`서버의 계정 권한 상태가 반영되어 이 공유기의 권한 연결이 해제되었습니다.`};case`health.issue.started`:{let t=Xt(e,`warning_count`)??0,n=Xt(e,`critical_count`)??0,r=[];return n>0&&r.push(`이상 ${H(n)}건`),t>0&&r.push(`주의 ${H(t)}건`),{title:`확인이 필요한 장치 상태 발견`,description:r.length>0?r.join(` · `):`장치 상태에서 확인이 필요한 항목이 발견되었습니다.`}}case`health.issue.changed`:{let t=Xt(e,`warning_count`)??0,n=Xt(e,`critical_count`)??0,r=[];return n>0&&r.push(`이상 ${H(n)}건`),t>0&&r.push(`주의 ${H(t)}건`),{title:`확인이 필요한 장치 상태 변경`,description:r.length>0?r.join(` · `):`확인이 필요한 장치 상태 항목이 변경되었습니다.`}}case`health.issue.resolved`:return{title:`장치 상태 정상으로 복구`,description:`이전에 확인이 필요했던 장치 상태가 정상으로 돌아왔습니다.`};default:return{title:`SmartSafeHub 활동`,description:`장치에서 새로운 활동이 기록되었습니다.`}}}function an(e){return e===`error`?{icon:L(V,{class:`size-4`}),surface:`bg-rose-50 text-rose-700 ring-rose-200`}:e===`warning`?{icon:L(V,{class:`size-4`}),surface:`bg-amber-50 text-amber-800 ring-amber-200`}:e===`success`?{icon:L(B,{class:`size-4`}),surface:`bg-emerald-50 text-emerald-700 ring-emerald-200`}:{icon:L(tt,{class:`size-4`}),surface:`bg-slate-100 text-slate-600 ring-slate-200`}}function on(e){let t=new Date(e*1e3);return new Intl.DateTimeFormat(`ko-KR`,{hour:`2-digit`,minute:`2-digit`,hour12:!1}).format(t)}function sn(e){let t=new Date(e*1e3);return`${t.getFullYear()}-${t.getMonth()+1}-${t.getDate()}`}function cn(e){let t=new Date(e*1e3),n=new Date,r=new Date(n.getFullYear(),n.getMonth(),n.getDate()),i=new Date(t.getFullYear(),t.getMonth(),t.getDate()),a=Math.round((r.getTime()-i.getTime())/864e5);return a===0?`오늘`:a===1?`어제`:new Intl.DateTimeFormat(`ko-KR`,{month:`long`,day:`numeric`,weekday:`short`}).format(t)}function ln({compact:e=!1,events:t}){if(t.length===0)return L(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 px-4 py-5 text-center`,children:[L(tt,{class:`mx-auto size-5 text-slate-400`}),L(`p`,{class:`mt-2 mb-0 text-sm font-extrabold text-slate-700`,children:`아직 기록된 활동이 없습니다.`}),L(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`상태가 실제로 변경되면 이곳에 시간순으로 기록됩니다.`})]});let n=``;return L(`div`,{class:e?`space-y-2`:`space-y-1`,children:t.map(t=>{let r=rn(t),i=an(t.severity),a=sn(t.occurredAt),o=!e&&a!==n;return n=a,L(`div`,{children:[o?L(`p`,{class:`mt-5 mb-2 first:mt-0 text-xs font-black text-slate-500`,children:cn(t.occurredAt)}):null,L(`article`,{class:`relative flex min-w-0 gap-3 ${e?`border-b border-slate-100 py-3 first:pt-0 last:border-b-0 last:pb-0`:`rounded-xl px-2 py-3 hover:bg-slate-50`}`,children:[L(`div`,{class:`w-12 shrink-0 pt-1 text-xs font-bold tabular-nums text-slate-400`,children:L(`time`,{dateTime:new Date(t.occurredAt*1e3).toISOString(),title:Ut(t.occurredAt),children:on(t.occurredAt)})}),L(`span`,{class:`mt-0.5 grid size-8 shrink-0 place-items-center rounded-full ring-1 ring-inset ${i.surface}`,children:i.icon}),L(`div`,{class:`min-w-0 flex-1`,children:[L(`p`,{class:`m-0 text-sm font-black leading-5 text-slate-950`,children:r.title}),r.description?L(`p`,{class:`mt-1 mb-0 break-words text-xs font-medium leading-5 text-slate-500`,children:r.description}):null]})]})]},t.eventId)})})}function un({error:e,loading:t,onRetry:n}){return t?L(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 px-4 py-5 text-center`,children:[L(z,{class:`mx-auto size-5 animate-spin text-teal-700`}),L(`p`,{class:`mt-2 mb-0 text-sm font-bold text-slate-500`,children:`최근 활동을 불러오는 중입니다.`})]}):e?L(`div`,{class:`rounded-xl border border-amber-200 bg-amber-50/70 px-4 py-4`,children:L(`div`,{class:`flex items-start gap-3`,children:[L(V,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),L(`div`,{class:`min-w-0 flex-1`,children:[L(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:`최근 활동을 불러오지 못했습니다.`}),L(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:e}),n?L(`button`,{class:`mt-3 inline-flex min-h-9 items-center justify-center rounded-lg border border-amber-300 bg-white px-3 text-xs font-extrabold text-amber-800 transition hover:bg-amber-50`,onClick:n,type:`button`,children:`다시 시도`}):null]})]})}):null}function dn(e){return e.enabled?e.eligible===!1||e.phase===`ineligible`?{label:`계정 연결 필요`,description:`SmartSafeHub 계정을 연결하면 이후 발생하는 활동을 Cloud에 자동으로 동기화할 수 있습니다.`,tone:`bg-slate-100 text-slate-600 ring-slate-200`}:e.phase===`error`?{label:`동기화 확인 필요`,description:`최근 Cloud 전송을 완료하지 못했습니다. 로컬 활동 기록은 계속 보관됩니다.`,tone:`bg-amber-50 text-amber-800 ring-amber-200`}:e.phase===`syncing`?{label:`동기화 중`,description:`새 활동을 SmartSafeHub Cloud에 전송하고 있습니다.`,tone:`bg-teal-50 text-teal-800 ring-teal-200`}:e.eligible===!0?{label:`자동 동기화`,description:`새 활동이 생기면 SmartSafeHub Cloud에 자동으로 안전하게 전송합니다.`,tone:`bg-emerald-50 text-emerald-800 ring-emerald-200`}:{label:`확인 중`,description:`Cloud 활동 기록 사용 가능 여부를 확인하고 있습니다.`,tone:`bg-slate-100 text-slate-600 ring-slate-200`}:e.eligible===!1?{label:`계정 연결 필요`,description:`SmartSafeHub 계정을 연결하면 Free에서도 Cloud 활동 기록을 사용할 수 있습니다.`,tone:`bg-slate-100 text-slate-600 ring-slate-200`}:{label:`Cloud 전송 꺼짐`,description:`활동 기록은 이 공유기에만 저장됩니다. Cloud에는 새로운 활동을 전송하지 않습니다.`,tone:`bg-slate-100 text-slate-700 ring-slate-200`}}function fn(e){if(e.eligible!==!0)return`—`;if(e.retentionDays>0)return`${e.retentionDays}일`;switch(e.plan?.toLowerCase()){case`free`:return`7일`;case`pro`:case`ultimate`:return`90일`;default:return`확인 중`}}function pn({actionError:e,actionMessage:t,cloud:n,onDismissFeedback:r,onSetEnabled:i,saving:a}){let o=dn(n),s=n.plan?.toUpperCase()??null,c=n.eligible===!0,l=n.enabled||c;return L(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:[L(`div`,{class:`flex flex-wrap items-start justify-between gap-4`,children:[L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700`,children:`Cloud history`}),L(`h2`,{class:`mt-2 mb-0 text-base font-black text-slate-950`,children:`Cloud 활동 기록`}),L(`p`,{class:`mt-2 mb-0 max-w-3xl text-sm leading-6 text-slate-500`,children:o.description})]}),L(`div`,{class:`flex shrink-0 items-center gap-3`,children:[L(`span`,{class:`rounded-full px-3 py-1.5 text-xs font-extrabold ring-1 ring-inset ${o.tone}`,children:a?`설정 중`:o.label}),L(`button`,{"aria-checked":n.enabled,"aria-label":`Cloud 활동 기록 전송 사용`,class:`ssh-switch-control relative inline-flex shrink-0 rounded-full border transition focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-not-allowed disabled:opacity-50 ${n.enabled?`border-teal-600 bg-teal-600`:`border-slate-300 bg-slate-200`}`,disabled:a||!l,onClick:()=>i(!n.enabled),role:`switch`,type:`button`,children:L(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${n.enabled?`left-6`:`left-1`}`})})]})]}),(e||t)&&L(`div`,{class:`mt-4 flex min-w-0 items-start justify-between gap-3 rounded-xl border px-4 py-3 text-sm font-bold ${e?`border-rose-200 bg-rose-50 text-rose-800`:`border-emerald-200 bg-emerald-50 text-emerald-800`}`,role:e?`alert`:`status`,children:[L(`span`,{class:`min-w-0`,children:e||t}),L(`button`,{class:`shrink-0 rounded-lg px-2 py-1 text-xs font-extrabold hover:bg-black/5`,onClick:r,type:`button`,children:`닫기`})]}),L(`dl`,{class:`mt-5 grid gap-3 sm:grid-cols-4`,children:[L(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[L(`dt`,{class:`text-xs font-bold text-slate-500`,children:`멤버십`}),L(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:s??(n.eligible===!1?`Free`:`확인 중`)})]}),L(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[L(`dt`,{class:`text-xs font-bold text-slate-500`,children:`전송 대기`}),L(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:[H(n.pendingEvents),`건`]})]}),L(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[L(`dt`,{class:`text-xs font-bold text-slate-500`,children:`마지막 동기화`}),L(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:n.lastSuccessAt>0?Wt(n.lastSuccessAt):`아직 없음`})]}),L(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[L(`dt`,{class:`text-xs font-bold text-slate-500`,children:`Cloud 보관`}),L(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:fn(n)})]})]}),n.enabled&&n.phase===`error`&&n.lastErrorCode?L(`p`,{class:`mt-3 mb-0 text-xs font-semibold text-amber-700`,children:[`오류 코드: `,L(`code`,{children:n.lastErrorCode})]}):null,L(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`SmartSafeHub 계정에 연결된 Free는 최근 7일, Pro와 Ultimate는 최근 90일 동안 Cloud 활동 기록을 보관합니다. 끄면 전송 대기 데이터와 임시 인증 정보를 삭제하며, 다시 켠 뒤 새로 발생한 활동부터 Cloud에 전송합니다. 로컬 최근 활동은 항상 유지됩니다.`})]})}function mn({actionError:e,actionMessage:t,data:n,error:r,loading:i,onDismissActionFeedback:a,onRetry:o,onSetCloudSync:s,savingCloudSync:c}){return L(`div`,{class:`min-w-0 space-y-5`,children:[L(`section`,{"aria-labelledby":`activity-history-title`,class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:[L(`div`,{class:`flex flex-wrap items-start justify-between gap-4`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700`,children:`Local history`}),L(`h2`,{class:`mt-2 mb-0 text-xl font-black tracking-tight text-slate-950`,id:`activity-history-title`,children:`현재 부팅 이후의 최근 활동`}),L(`p`,{class:`mt-2 mb-0 max-w-3xl text-sm leading-6 text-slate-500`,children:`인터넷, SafeShield, 업데이트, 계정 권한과 주요 설정에서 실제 상태가 변경된 시점만 기록합니다.`})]}),L(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5 text-xs font-extrabold text-slate-600 ring-1 ring-inset ring-slate-200`,children:[`최대 `,n?.maxEvents??128,`건`]})]}),L(`div`,{class:`mt-5`,children:i||r&&!n?L(un,{error:r,loading:i,onRetry:o}):L(ln,{events:n?.events??[]})})]}),L(pn,{actionError:e,actionMessage:t,cloud:n?.cloud??{enabled:!1,phase:`preparing`,eligible:null,plan:null,retentionDays:0,pendingEvents:0,lastAttemptAt:0,lastSuccessAt:0,lastUploadedCount:0,lastErrorCode:null,nextSyncAt:0},onDismissFeedback:a,onSetEnabled:s,saving:c}),L(`aside`,{class:`rounded-2xl border border-slate-200 bg-slate-50 px-5 py-4`,children:[L(`p`,{class:`m-0 text-sm font-extrabold text-slate-800`,children:`로컬 활동 기록 안내`}),L(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`이 기록은 공유기의 메모리 영역에만 보관되어 재부팅하면 초기화됩니다. 직접 설정을 변경한 작업은 성공 시점에 즉시 기록하고, 인터넷 및 장치 진단처럼 외부 상태를 확인해야 하는 항목은 실제 상태 변화가 관찰된 경우에만 추가합니다.`})]})]})}var hn=Object.assign,gn=/^(-|f[lo].*[^se]$|g.{5,}[^ps]$|z|o[pr]|(W.{5})?[lL]i.*(t|mp)$|an|(bo|s).{4}Im|sca|m.{6}[ds]|ta|c.*[st]$|wido|ini)/;function _n(e,t){var n=ce(e,t);return n.containerInfo=t,n}var vn=Symbol.for(`react.element`),yn=/^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|dominant|fill|flood|font|glyph(?!R)|horiz|image(?!S)|letter|lighting|marker(?!H|W|U)|overline|paint|pointer|shape|stop|strikethrough|stroke|text(?!L)|transform|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/,bn=/[A-Z0-9]/g,xn=typeof document<`u`,Sn=function(e){return/fil|che|rad/.test(e)};y.prototype.isReactComponent=!0,[`componentWillMount`,`componentWillReceiveProps`,`componentWillUpdate`].forEach(function(e){Object.defineProperty(y.prototype,e,{configurable:!0,get:function(){return this[`UNSAFE_`+e]},set:function(t){Object.defineProperty(this,e,{configurable:!0,writable:!0,value:t})}})});var Cn=e.event;e.event=function(e){return Cn&&(e=Cn(e)),e.persist=function(){},e.isPropagationStopped=function(){return this.cancelBubble},e.isDefaultPrevented=function(){return this.defaultPrevented},e.nativeEvent=e};var wn={configurable:!0,get:function(){return this.class}},Tn=e.vnode;e.vnode=function(e){if(typeof e.type==`string`)(function(e){var t=e.props,n=e.type,r={},i=n.indexOf(`-`)==-1;for(var a in t){var o=t[a];if(!(a==`value`&&`defaultValue`in t&&o==null||xn&&a==`children`&&n==`noscript`||a==`class`||a==`className`)){if(a==`style`&&typeof o==`object`){var s=void 0;for(var c in o)typeof o[c]!=`number`||gn.test(c)||(s||(s=o=hn({},o)),o[c]+=`px`)}else if(a==`defaultValue`&&`value`in t&&t.value==null)a=`value`;else if(a==`download`&&!0===o)o=``;else if(a==`translate`&&o===`no`)o=!1;else if(a[0]==`o`&&a[1]==`n`){var l=a.toLowerCase();l==`ondoubleclick`?a=`ondblclick`:l!=`onchange`||n!=`input`&&n!=`textarea`||Sn(t.type)?l==`onfocus`?a=`onfocusin`:l==`onblur`&&(a=`onfocusout`):l=a=`oninput`,l==`oninput`&&r[a=l]&&(a=`oninputCapture`)}else i&&yn.test(a)?a=a.replace(bn,`-$&`).toLowerCase():o===null&&(o=void 0);r[a]=o}}n==`select`&&(r.multiple&&Array.isArray(r.value)&&(r.value=D(t.children).forEach(function(e){e.props.selected=r.value.indexOf(e.props.value)!=-1})),r.defaultValue!=null&&(r.value=D(t.children).forEach(function(e){e.props.selected=r.multiple?r.defaultValue.indexOf(e.props.value)!=-1:r.defaultValue==e.props.value}))),t.class&&!t.className?(r.class=t.class,Object.defineProperty(r,"className",wn)):t.className&&(r.class=r.className=t.className),e.props=r})(e);else if(typeof e.type==`function`&&(`ref`in e.props&&`prototype`in e.type&&e.type.prototype.render&&(e.ref=e.props.ref,delete e.props.ref),e.type.defaultProps)){var t=hn({},e.props);for(var n in e.type.defaultProps)t[n]===void 0&&(t[n]=e.type.defaultProps[n]);e.props=t}e.ref&&!(`ref`in e.props)&&Object.defineProperty(e.props,"ref",{value:e.ref,configurable:!0,writable:!0}),e.$$typeof=vn,Tn&&Tn(e)};var En=8,Dn=256,On=8;function kn(e){return e.findIndex(e=>!e.disabled)}function An(e){for(let t=e.length-1;t>=0;--t)if(!e[t]?.disabled)return t;return-1}function jn(e,t,n){if(e.length===0)return-1;let r=t;for(let t=0;te.value===t&&!e.disabled);return n>=0?n:kn(e)}function Nn({ariaLabel:e,className:t=``,disabled:n=!1,id:r,onChange:i,options:a,value:o,variant:s=`default`}){let c=De(),l=r??`ssh-custom-select-${c}`,u=`${l}-menu`,d=we(null),f=we(null),p=we(null),m=we([]),[h,g]=F(!1),[_,v]=F(()=>Mn(a,o)),[y,b]=F(`bottom`),[x,S]=F(Dn),[C,w]=F({}),[T,E]=F(null),D=Te(()=>a.find(e=>e.value===o)??a[0]??null,[a,o]),O=()=>{let e=f.current;if(!e)return;let t=e.getBoundingClientRect(),n=Math.min(Dn,Math.max(44,a.length*42+12)),r=window.innerHeight-t.bottom-On,i=t.top-On,o=rr?`top`:`bottom`,s=Math.min(t.width,Math.max(0,window.innerWidth-16)),c=Math.max(On,Math.min(t.left,window.innerWidth-s-On)),l=o===`bottom`?r:i;b(o),S(Math.max(44,Math.min(Dn,Math.floor(l)))),w({left:`${c}px`,width:`${s}px`,...o===`bottom`?{top:`${t.bottom+En}px`}:{bottom:`${window.innerHeight-t.top+En}px`}})},k=(e=!1)=>{g(!1),e&&window.requestAnimationFrame(()=>f.current?.focus())},A=e=>{e<0||(v(e),window.requestAnimationFrame(()=>m.current[e]?.focus()))},j=e=>{let t=a[e];t&&!t.disabled&&(i(t.value),g(!1),window.requestAnimationFrame(()=>f.current?.focus()))},M=e=>{n||a.length===0||(O(),g(!0),A(e??Mn(a,o)))};I(()=>{let e=d.current?.closest(`.ssh-app`);if(e){E(e);return}let t=d.current?.getRootNode();t instanceof ShadowRoot?E(t):typeof document<`u`&&E(document.body)},[]),I(()=>{v(Mn(a,o)),m.current=m.current.slice(0,a.length)},[a,o]),I(()=>{if(!h)return;O();let e=e=>{let t=e.composedPath();d.current&&t.includes(d.current)||p.current&&t.includes(p.current)||k()},t=e=>{e.key===`Escape`&&(e.preventDefault(),k(!0))},n=()=>O();return document.addEventListener(`pointerdown`,e),document.addEventListener(`keydown`,t),window.addEventListener(`resize`,n),window.addEventListener(`scroll`,n,!0),()=>{document.removeEventListener(`pointerdown`,e),document.removeEventListener(`keydown`,t),window.removeEventListener(`resize`,n),window.removeEventListener(`scroll`,n,!0)}},[h,a.length]),I(()=>{n&&h&&k()},[n,h]);let ee=s===`emphasized`?`min-h-11 border-2 border-slate-300 bg-slate-50 px-4 py-2.5 text-sm font-semibold text-slate-950 shadow-inner focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100`:`min-h-11 border border-slate-300 bg-slate-50 px-3 py-2.5 text-sm font-bold text-slate-900 focus:border-teal-500 focus:ring-2 focus:ring-teal-100`,te=h?L(`div`,{"aria-label":`${e} 목록`,class:`ssh-custom-select-menu fixed z-[1000] overflow-hidden rounded-xl border border-slate-200 bg-white p-1.5 shadow-xl shadow-slate-950/10`,id:u,ref:p,role:`listbox`,style:C,children:L(`div`,{class:`overflow-y-auto overscroll-contain`,style:{maxHeight:`${x}px`},children:a.map((e,t)=>{let n=e.value===o;return L(`button`,{"aria-selected":n,class:`flex min-h-10 w-full items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-left text-sm font-bold transition ${n?`bg-teal-50 text-teal-800`:`text-slate-700 hover:bg-slate-50`} ${e.disabled?`cursor-not-allowed opacity-50`:``}`,disabled:e.disabled,onClick:()=>j(t),onFocus:()=>v(t),onKeyDown:e=>{switch(e.key){case`ArrowDown`:e.preventDefault(),A(jn(a,t,1));break;case`ArrowUp`:e.preventDefault(),A(jn(a,t,-1));break;case`Home`:e.preventDefault(),A(kn(a));break;case`End`:e.preventDefault(),A(An(a));break;case`Enter`:case` `:e.preventDefault(),j(t);break;case`Escape`:e.preventDefault(),k(!0);break;case`Tab`:g(!1)}},ref:e=>{m.current[t]=e},role:`option`,tabIndex:_===t?0:-1,type:`button`,children:[L(`span`,{class:`min-w-0 truncate`,children:e.label}),n?L(`span`,{"aria-hidden":`true`,class:`size-2 shrink-0 rounded-full bg-teal-500`}):null]},e.value)})})}):null;return L(`div`,{class:`relative ${t}`,ref:d,children:[L(`button`,{"aria-controls":u,"aria-expanded":h,"aria-haspopup":`listbox`,"aria-label":e,class:`ssh-custom-select-trigger flex w-full cursor-pointer items-center justify-between gap-3 rounded-xl text-left outline-none transition disabled:cursor-not-allowed disabled:bg-slate-100 disabled:text-slate-400 disabled:opacity-60 ${ee}`,disabled:n,id:l,onClick:()=>{h?k():(O(),g(!0))},onKeyDown:e=>{switch(e.key){case`ArrowDown`:e.preventDefault(),M(h?jn(a,_,1):Mn(a,o));break;case`ArrowUp`:e.preventDefault(),M(h?jn(a,_,-1):Mn(a,o));break;case`Home`:e.preventDefault(),M(kn(a));break;case`End`:e.preventDefault(),M(An(a));break;case`Enter`:case` `:e.preventDefault(),h?k():M();break;case`Escape`:h&&(e.preventDefault(),k())}},ref:f,type:`button`,children:[L(`span`,{class:`min-w-0 truncate`,children:D?.label??`선택하세요`}),L(gt,{"aria-hidden":`true`,class:`size-4 shrink-0 text-slate-400 transition-transform ${h?`rotate-180`:``}`})]}),te&&T?_n(te,T):null,L(`span`,{class:`sr-only`,"aria-live":`polite`,children:y===`top`?`선택 목록이 위쪽에 열렸습니다.`:``})]})}function Pn(e){let t=e.split(`.`);return[t[0]??``,t[1]??``,t[2]??``,t[3]??``]}function Fn(e){return e.replace(/\D/g,``).slice(0,3)}function In(e){return e.every(e=>e===``)?``:e.join(`.`)}function Ln({disabled:e=!1,label:t,value:n,onChange:r}){let i=Pn(n),a=(e,t)=>{let n=[...i];n[e]=Fn(t),r(In(n))};return L(`div`,{class:`ssh-ipv4-segments mt-2 min-h-11`,"aria-label":`${t} 입력`,children:i.map((n,r)=>L(`div`,{class:`ssh-ipv4-segment`,children:[L(`input`,{"aria-label":`${t} ${r+1}번째 옥텟`,autocomplete:`off`,class:`min-h-11 w-full min-w-0 max-w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-1.5 py-2.5 text-center text-sm font-semibold text-slate-950 shadow-inner outline-none transition focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:cursor-not-allowed disabled:bg-slate-100 sm:px-2`,disabled:e,inputMode:`numeric`,maxLength:3,onInput:e=>a(r,e.currentTarget.value),pattern:`[0-9]*`,spellcheck:!1,value:n}),rt);function Un(e){return Bn.includes(e)}function Wn(e){switch(e){case`dhcp`:return`자동 IP (DHCP)`;case`pppoe`:return`PPPoE`;case`static`:return`고정 IPv4`;default:return e?e.toUpperCase():`확인 전`}}function Gn(e){return{protocol:e.configuration.protocol,pppoeUsername:e.configuration.pppoe.username,pppoePassword:``,staticAddress:e.configuration.static.address??``,staticPrefixLength:e.configuration.static.prefixLength??24,staticGateway:e.configuration.static.gateway??``,dnsPrimary:e.configuration.static.dns[0]??``,dnsSecondary:e.configuration.static.dns[1]??``}}function Kn(e){return e.trim()}function qn(e,t){let n=Gn(t);return e.protocol===n.protocol?e.protocol===`dhcp`||!Un(e.protocol)?!1:e.protocol===`pppoe`?Kn(e.pppoeUsername)!==n.pppoeUsername||e.pppoePassword.length>0:Kn(e.staticAddress)!==n.staticAddress||e.staticPrefixLength!==n.staticPrefixLength||Kn(e.staticGateway)!==n.staticGateway||Kn(e.dnsPrimary)!==n.dnsPrimary||Kn(e.dnsSecondary)!==n.dnsSecondary:!0}function Jn(e){let t=e.trim().split(`.`);return t.length===4&&t.every(e=>{if(!/^(0|[1-9][0-9]{0,2})$/.test(e))return!1;let t=Number(e);return t>=0&&t<=255})}function Yn(e){let t=e;return Array.from({length:4},()=>{if(t>=8)return t-=8,255;if(t<=0)return 0;let e=256-2**(8-t);return t=0,e}).join(`.`)}function Xn(e){if(!Number.isFinite(e)||e<=0)return`연결 시간 확인 전`;let t=Math.floor(e/86400),n=Math.floor(e%86400/3600),r=Math.floor(e%3600/60);return t>0?`${t}일 ${n}시간 연결됨`:n>0?`${n}시간 ${r}분 연결됨`:`${Math.max(1,r)}분 연결됨`}function Zn({feedback:e,onDismiss:t}){return L(`div`,{"aria-live":`polite`,class:`mb-5 rounded-2xl border px-4 py-4 text-sm ${e.kind===`success`?`border-emerald-200 bg-emerald-50 text-emerald-900`:e.kind===`warning`?`border-amber-200 bg-amber-50 text-amber-950`:`border-red-200 bg-red-50 text-red-900`}`,children:L(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[L(`p`,{class:`m-0 font-bold leading-6`,children:e.message}),L(`button`,{class:`min-h-10 shrink-0 rounded-lg border border-current bg-transparent px-3 py-1.5 text-xs font-extrabold`,onClick:t,type:`button`,children:`닫기`})]})})}function Qn({data:e,busy:t,onReconnect:n}){let r=e.status,i=r.connected;return L(`section`,{class:`grid gap-4 lg:grid-cols-3`,"aria-label":`인터넷 연결 상태`,children:[L(`article`,{class:`rounded-2xl border p-5 shadow-sm shadow-slate-900/5 ${i?`border-emerald-200 bg-emerald-50 text-emerald-950`:`border-amber-200 bg-amber-50 text-amber-950`}`,children:[L(`div`,{class:`flex items-start justify-between gap-3`,children:[L(`span`,{class:`grid size-10 place-items-center rounded-xl ${i?`bg-emerald-100 text-emerald-700`:`bg-amber-100 text-amber-700`}`,children:L(i?B:V,{class:`size-5`})}),L(`button`,{class:`inline-flex min-h-10 items-center justify-center gap-2 rounded-xl border border-current bg-transparent px-3 py-2 text-xs font-extrabold transition disabled:cursor-wait disabled:opacity-60`,disabled:t,onClick:n,type:`button`,children:[L(z,{class:`size-4 ${t?`animate-spin`:``}`}),`재연결`]})]}),L(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] opacity-70`,children:`인터넷 상태`}),L(`p`,{class:`mt-2 mb-0 text-xl font-black`,children:r.pending?`연결 중`:i?`연결됨`:`연결 안 됨`}),L(`p`,{class:`mt-1 mb-0 text-sm font-semibold leading-6 opacity-80`,children:[Wn(r.protocol??e.configuration.protocol),` · `,Xn(r.uptimeSeconds)]})]}),L(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[L(`span`,{class:`grid size-10 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:L($e,{class:`size-5`})}),L(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`WAN IPv4`}),L(`p`,{class:`mt-2 mb-0 break-all text-xl font-black text-slate-950`,children:r.address?`${r.address}${r.prefixLength===null?``:`/${r.prefixLength}`}`:`주소 없음`}),L(`p`,{class:`mt-1 mb-0 text-sm font-semibold text-slate-600`,children:r.device?`인터페이스 ${r.device}`:`인터페이스 확인 전`})]}),L(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[L(`span`,{class:`grid size-10 place-items-center rounded-xl bg-slate-100 text-slate-700`,children:L(We,{class:`size-5`})}),L(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`Gateway · DNS`}),L(`p`,{class:`mt-2 mb-0 break-all text-base font-black text-slate-950`,children:r.gateway??`Gateway 확인 전`}),L(`p`,{class:`mt-1 mb-0 break-all text-sm font-semibold leading-6 text-slate-600`,children:r.dns.length>0?r.dns.join(` · `):`DNS 확인 전`})]})]})}function $n({action:e,data:t,error:n,feedback:r,loading:i,onDismissFeedback:a,onReconnect:o,onRetry:s,onSave:c}){let[l,u]=F({protocol:`dhcp`,pppoeUsername:``,pppoePassword:``,staticAddress:``,staticPrefixLength:24,staticGateway:``,dnsPrimary:``,dnsSecondary:``}),[d,f]=F(!1),[p,m]=F(!1),[h,g]=F(null);I(()=>{t&&!d&&(u(Gn(t)),g(null))},[t,d]);let _=Te(()=>{let e=[{value:`dhcp`,label:`자동 IP (DHCP)`},{value:`pppoe`,label:`PPPoE`},{value:`static`,label:`고정 IPv4`}];return t&&!t.configuration.supported&&e.unshift({value:t.configuration.protocol,label:`현재 설정 (${t.configuration.protocol})`}),e},[t]);if(i)return L(Rn,{});if(n)return L(zn,{message:n,onRetry:s});if(!t)return null;let y=e!==null,b=e=>{u(n=>{let r={...n,...e};return f(qn(r,t)),r}),g(null)},x=async e=>{if(e.preventDefault(),!Un(l.protocol))g(`DHCP, PPPoE 또는 고정 IPv4 중 하나를 선택해 주세요.`);else{if(l.protocol===`pppoe`){if(!Kn(l.pppoeUsername)){g(`PPPoE 사용자명을 입력해 주세요.`);return}if((t.configuration.protocol!==`pppoe`||!t.configuration.pppoe.passwordConfigured)&&l.pppoePassword.length===0){g(`PPPoE 연결을 시작하려면 비밀번호를 입력해 주세요.`);return}}if(l.protocol===`static`){if(!Jn(l.staticAddress)){g(`고정 IPv4 주소를 올바르게 입력해 주세요.`);return}if(!Jn(l.staticGateway)){g(`기본 게이트웨이를 올바르게 입력해 주세요.`);return}if(!Jn(l.dnsPrimary)){g(`기본 DNS 서버를 올바르게 입력해 주세요.`);return}if(Kn(l.dnsSecondary)&&!Jn(l.dnsSecondary)){g(`보조 DNS 서버를 올바르게 입력해 주세요.`);return}}window.confirm(`인터넷 연결 설정을 적용하면 WAN 연결이 다시 시작되어 인터넷이 잠시 끊길 수 있습니다. 계속하시겠습니까?`)&&await c({protocol:l.protocol,pppoeUsername:Kn(l.pppoeUsername),pppoePassword:l.pppoePassword,pppoePasswordChanged:l.pppoePassword.length>0,staticAddress:Kn(l.staticAddress),staticPrefixLength:l.staticPrefixLength,staticGateway:Kn(l.staticGateway),dnsPrimary:Kn(l.dnsPrimary),dnsSecondary:Kn(l.dnsSecondary)})&&(u(e=>({...e,pppoePassword:``})),f(!1))}};return L(v,{children:[r?L(Zn,{feedback:r,onDismiss:a}):null,L(Qn,{data:t,busy:y,onReconnect:()=>{window.confirm(`WAN 연결을 다시 시작합니다. 인터넷이 잠시 끊길 수 있습니다. 계속하시겠습니까?`)&&o()}}),t.configuration.supported?null:L(`section`,{class:`mt-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-amber-950 sm:p-6`,children:L(`div`,{class:`flex items-start gap-3`,children:[L(V,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),L(`div`,{children:[L(`h2`,{class:`m-0 text-base font-black`,children:`현재 WAN 방식은 직접 관리하지 않습니다`}),L(`p`,{class:`mt-2 mb-0 text-sm font-semibold leading-6 text-amber-900/80`,children:[`현재 설정은 `,t.configuration.protocol,`입니다. 아래에서 DHCP, PPPoE 또는 고정 IPv4를 선택해 저장하면 해당 방식으로 전환합니다.`]})]})]})}),L(`form`,{class:`mt-5 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,onSubmit:e=>void x(e),children:[L(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3`,children:[L(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:L($e,{class:`size-6`})}),L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Internet`}),L(`h2`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`인터넷 연결 설정`}),L(`p`,{class:`mt-2 mb-0 text-sm font-semibold leading-6 text-slate-600`,children:`통신사 또는 상위 네트워크 환경에 맞는 WAN 연결 방식을 선택합니다.`})]})]}),d?L(`span`,{"aria-live":`polite`,class:`inline-flex shrink-0 self-start items-center gap-1.5 rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs font-extrabold text-amber-800`,role:`status`,children:[L(V,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}):null]}),L(`div`,{class:`mt-6`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`연결 방식`}),L(Nn,{ariaLabel:`WAN 연결 방식`,className:`mt-2 max-w-xl`,disabled:y,id:`wan-protocol`,onChange:e=>b(e===`static`&&!Kn(l.dnsSecondary)?{protocol:e,dnsSecondary:Vn}:{protocol:e}),options:_,value:l.protocol,variant:`emphasized`}),L(`span`,{class:`mt-2 block text-xs font-semibold leading-5 text-slate-500`,children:`대부분의 가정용 인터넷과 상위 공유기 연결은 자동 IP(DHCP)를 사용합니다.`})]}),l.protocol===`pppoe`?L(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[L(`label`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`PPPoE 사용자명`}),L(`input`,{autocomplete:`username`,class:`mt-2 min-h-11 w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-4 py-2.5 text-sm font-semibold text-slate-950 shadow-inner outline-none transition focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:cursor-not-allowed disabled:bg-slate-100`,disabled:y,onInput:e=>b({pppoeUsername:e.currentTarget.value}),spellcheck:!1,value:l.pppoeUsername})]}),L(`label`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`PPPoE 비밀번호`}),L(`span`,{class:`relative mt-2 block`,children:[L(`input`,{autocomplete:`new-password`,class:`min-h-11 w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-4 py-2.5 pr-12 text-sm font-semibold text-slate-950 shadow-inner outline-none transition focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:cursor-not-allowed disabled:bg-slate-100`,disabled:y,onInput:e=>b({pppoePassword:e.currentTarget.value}),placeholder:t.configuration.protocol===`pppoe`&&t.configuration.pppoe.passwordConfigured?`변경할 때만 입력`:`PPPoE 비밀번호`,...p?{type:`text`}:{type:`password`},value:l.pppoePassword}),L(`button`,{"aria-label":p?`비밀번호 숨기기`:`비밀번호 표시`,class:`absolute inset-y-0 right-1 grid w-10 place-items-center rounded-lg border-0 bg-transparent text-slate-500`,disabled:y,onClick:()=>m(e=>!e),type:`button`,children:L(p?ct:st,{class:`size-5`})})]}),L(`span`,{class:`mt-2 block text-xs text-slate-500`,children:t.configuration.protocol===`pppoe`&&t.configuration.pppoe.passwordConfigured?`비워 두면 현재 저장된 비밀번호를 그대로 사용합니다.`:`통신사에서 제공한 PPPoE 비밀번호를 입력해 주세요.`})]})]}):null,l.protocol===`static`?L(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[L(`label`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`IPv4 주소`}),L(Ln,{disabled:y,label:`IPv4 주소`,onChange:e=>b({staticAddress:e}),value:l.staticAddress})]}),L(`div`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`서브넷 마스크`}),L(Nn,{ariaLabel:`WAN 서브넷 마스크`,className:`mt-2`,disabled:y,id:`wan-static-prefix-length`,onChange:e=>b({staticPrefixLength:Number(e)}),options:Hn.map(e=>({value:String(e),label:`/${e} · ${Yn(e)}`})),value:String(l.staticPrefixLength),variant:`emphasized`})]}),L(`label`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`기본 게이트웨이`}),L(Ln,{disabled:y,label:`기본 게이트웨이`,onChange:e=>b({staticGateway:e}),value:l.staticGateway})]}),L(`label`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`기본 DNS`}),L(Ln,{disabled:y,label:`기본 DNS`,onChange:e=>b({dnsPrimary:e}),value:l.dnsPrimary})]}),L(`label`,{class:`block lg:col-start-2`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`보조 DNS (선택)`}),L(Ln,{disabled:y,label:`보조 DNS`,onChange:e=>b({dnsSecondary:e}),value:l.dnsSecondary})]})]}):null,l.protocol===`dhcp`?L(`div`,{class:`mt-6 rounded-xl border border-slate-200 bg-slate-50 px-4 py-4 text-sm font-semibold leading-6 text-slate-600`,children:`상위 공유기 또는 통신사 장비에서 IP 주소, 기본 게이트웨이와 DNS 정보를 자동으로 받습니다. 별도의 계정이나 고정 주소가 없다면 이 방식을 사용하세요.`}):null,h?L(`p`,{class:`mt-5 mb-0 rounded-xl bg-red-50 px-4 py-3 text-sm font-bold text-red-800`,children:h}):null,L(`div`,{class:`mt-6 flex flex-col gap-3 border-t border-slate-200 pt-5 sm:flex-row sm:items-center sm:justify-between`,children:[L(`p`,{class:`m-0 text-xs font-semibold leading-5 text-slate-500`,children:`설정 저장 후 WAN만 다시 연결하며 LAN 관리 페이지와 내부 네트워크 설정은 유지됩니다.`}),L(`button`,{class:`inline-flex min-h-11 w-full shrink-0 items-center justify-center gap-2 rounded-xl border-0 bg-teal-700 px-5 py-2.5 text-sm font-extrabold text-white shadow-sm transition hover:bg-teal-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-200 disabled:cursor-wait disabled:opacity-60 sm:w-auto`,disabled:y||!d,type:`submit`,children:[L(B,{class:`size-5 ${e===`saving`?`animate-pulse`:``}`}),e===`saving`?`저장 중`:`설정 저장`]})]})]})]})}var er=[16,20,22,23,24,25,26,27,28],tr=[[`30m`,`30분`],[`1h`,`1시간`],[`6h`,`6시간`],[`12h`,`12시간`],[`24h`,`24시간`],[`3d`,`3일`],[`7d`,`7일`]];function nr(e){let t=e.trim().split(`.`);return t.length===4&&t.every(e=>{if(!/^(0|[1-9][0-9]{0,2})$/.test(e))return!1;let t=Number(e);return t>=0&&t<=255})}function rr(e){let t=Pn(e);return t.slice(0,3).some(e=>e===``||Number(e)>255)?null:t.slice(0,3).join(`.`)}function ir(e){return Pn(e)[3]}function ar(e){let t=rr(e.lan.address),n=ir(e.dhcp.start??``),r=ir(e.dhcp.end??``);return{ipAddress:e.lan.address,prefixLength:e.lan.prefixLength,dhcpEnabled:e.dhcp.enabled,dhcpStart:t&&n?`${t}.${n}`:e.dhcp.start??``,dhcpEnd:t&&r?`${t}.${r}`:e.dhcp.end??``,leaseTime:e.dhcp.leaseTime}}function or({disabled:e,label:t,lanAddress:n,value:r,onChange:i}){let a=rr(n),o=a?.split(`.`)??[``,``,``],s=ir(r),c=e=>{let t=Fn(e);i(a&&t!==``?`${a}.${t}`:t)};return L(`label`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:t}),L(`div`,{class:`ssh-ipv4-segments mt-2 min-h-11`,"aria-label":`${t} 입력`,children:[o.map((e,t)=>L(`div`,{class:`ssh-ipv4-segment`,children:[L(`span`,{class:`grid min-h-11 w-full min-w-0 max-w-full place-items-center rounded-xl border border-slate-200 bg-slate-100 px-1.5 py-2.5 text-center text-sm font-bold text-slate-500 sm:px-2`,children:e||`—`}),L(`span`,{"aria-hidden":`true`,class:`ssh-ipv4-separator text-sm font-black text-slate-400`,children:`.`})]},t)),L(`div`,{class:`ssh-ipv4-segment`,children:L(`input`,{"aria-label":`${t} 마지막 옥텟`,autocomplete:`off`,class:`min-h-11 w-full min-w-0 max-w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-1.5 py-2.5 text-center text-sm font-semibold text-slate-950 shadow-inner outline-none transition focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:cursor-not-allowed disabled:bg-slate-100 sm:px-2`,disabled:e||!a,inputMode:`numeric`,maxLength:3,onInput:e=>c(e.currentTarget.value),pattern:`[0-9]*`,spellcheck:!1,value:s})})]}),L(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`앞 3개 주소는 공유기 IP 주소와 동일하게 유지됩니다.`})]})}function sr(e){return`${window.location.protocol}//${e}${window.location.pathname}${window.location.search}${window.location.hash}`}function cr({feedback:e,onDismiss:t}){return L(`div`,{"aria-live":`polite`,class:`mb-5 rounded-2xl border px-4 py-4 text-sm ${e.kind===`success`?`border-emerald-200 bg-emerald-50 text-emerald-900`:e.kind===`warning`?`border-amber-200 bg-amber-50 text-amber-950`:`border-red-200 bg-red-50 text-red-900`}`,children:[L(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[L(`p`,{class:`m-0 font-bold leading-6`,children:e.message}),L(`button`,{class:`min-h-10 shrink-0 rounded-lg border border-current bg-transparent px-3 py-1.5 text-xs font-extrabold`,onClick:t,type:`button`,children:`닫기`})]}),e.newAddress?L(`a`,{class:`mt-3 inline-flex min-h-10 items-center rounded-lg border border-current px-3 py-2 font-extrabold underline underline-offset-2`,href:sr(e.newAddress),children:[`새 공유기 주소 `,e.newAddress,`로 연결`]}):null]})}function lr({data:e}){let t=e.conflict.detected;return L(`section`,{class:`grid gap-4 lg:grid-cols-3`,"aria-label":`내부 네트워크 상태`,children:[L(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[L(`span`,{class:`grid size-10 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:L(We,{class:`size-5`})}),L(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`SmartSafeHub LAN`}),L(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:e.lan.subnet}),L(`p`,{class:`mt-1 mb-0 text-sm font-semibold text-slate-600`,children:[`공유기 주소 `,e.lan.address]})]}),L(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[L(`span`,{class:`grid size-10 place-items-center rounded-xl bg-slate-100 text-slate-700`,children:L($e,{class:`size-5`})}),L(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`상위 네트워크`}),L(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:e.wan.subnet??`확인 전`}),L(`p`,{class:`mt-1 mb-0 text-sm font-semibold text-slate-600`,children:e.wan.connected?`WAN ${e.wan.address??`주소 확인 중`}`:`WAN 연결 후 자동으로 확인합니다.`})]}),L(`article`,{class:`rounded-2xl border p-5 shadow-sm shadow-slate-900/5 ${t?`border-red-200 bg-red-50 text-red-950`:`border-emerald-200 bg-emerald-50 text-emerald-950`}`,children:[L(`span`,{class:`grid size-10 place-items-center rounded-xl ${t?`bg-red-100 text-red-700`:`bg-emerald-100 text-emerald-700`}`,children:L(t?V:B,{class:`size-5`})}),L(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] opacity-70`,children:`대역 충돌`}),L(`p`,{class:`mt-2 mb-0 text-lg font-black`,children:t?`주소 대역이 겹칩니다`:`충돌이 감지되지 않았습니다`}),L(`p`,{class:`mt-1 mb-0 text-sm font-semibold leading-6 opacity-80`,children:t?`상위 네트워크 ${e.conflict.wanSubnet??``}와 다른 LAN 대역이 필요합니다.`:e.wan.connected?`현재 WAN과 LAN이 서로 다른 IPv4 대역을 사용합니다.`:`WAN이 연결되면 상위 네트워크와의 충돌 여부를 확인합니다.`})]})]})}function ur({action:e,data:t,error:n,feedback:r,loading:i,onApplyRecommendation:a,onDismissFeedback:o,onRetry:s,onSave:c}){let[l,u]=F(``),[d,f]=F(24),[p,m]=F(!0),[h,g]=F(``),[_,y]=F(``),[b,x]=F(`12h`),[S,C]=F(!1),[w,T]=F(null),E=(e={})=>{if(!t)return!1;let n=ar(t);return(e.ipAddress??l).trim()!==n.ipAddress||(e.prefixLength??d)!==n.prefixLength||(e.dhcpEnabled??p)!==n.dhcpEnabled||(e.dhcpStart??h).trim()!==n.dhcpStart||(e.dhcpEnd??_).trim()!==n.dhcpEnd||(e.leaseTime??b)!==n.leaseTime},D=e=>{let t=rr(e);if(u(e),!t){C(E({ipAddress:e}));return}let n=ir(h),r=ir(_),i=n?`${t}.${n}`:``,a=r?`${t}.${r}`:``;g(i),y(a),C(E({ipAddress:e,dhcpStart:i,dhcpEnd:a}))},O=e=>{g(e),C(E({dhcpStart:e}))},k=e=>{y(e),C(E({dhcpEnd:e}))},A=e=>{f(e),C(E({prefixLength:e}))},j=e=>{x(e),C(E({leaseTime:e}))},M=e=>{m(e),C(E({dhcpEnabled:e}))};I(()=>{if(!t||S)return;let e=ar(t);u(e.ipAddress),f(e.prefixLength),m(e.dhcpEnabled),g(e.dhcpStart),y(e.dhcpEnd),x(e.leaseTime),T(null)},[t,S]);let ee=Te(()=>{let e=[...er];return e.includes(d)||(e.push(d),e.sort((e,t)=>e-t)),e},[d]),te=Te(()=>{let e=[...tr];return e.some(([e])=>e===b)||e.push([b,`현재 설정 (${b})`]),e},[b]);if(i)return L(Rn,{});if(n)return L(zn,{message:n,onRetry:s});if(!t)return null;let ne=e!==null,re=async e=>{e.preventDefault(),nr(l)?!nr(h)||!nr(_)?T(`DHCP 시작/종료 주소를 올바른 IPv4 형식으로 입력해 주세요.`):window.confirm(`내부 네트워크 설정을 적용하면 연결된 기기가 새 DHCP 정보를 받는 동안 잠시 연결이 끊길 수 있습니다. 계속하시겠습니까?`)&&(T(null),await c({ipAddress:l.trim(),prefixLength:d,dhcpEnabled:p,dhcpStart:h.trim(),dhcpEnd:_.trim(),leaseTime:b})&&C(!1)):T(`공유기 주소를 올바른 IPv4 형식으로 입력해 주세요.`)},ie=async()=>{t.recommendation&&window.confirm(`상위 네트워크와 겹치지 않는 ${t.recommendation.subnet} 대역으로 자동 변경합니다. 현재 연결이 잠시 끊길 수 있습니다. 계속하시겠습니까?`)&&(T(null),await a()&&C(!1))};return L(v,{children:[r?L(cr,{feedback:r,onDismiss:o}):null,L(lr,{data:t}),t.conflict.detected?L(`section`,{class:`mt-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-amber-950 sm:p-6`,children:L(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between`,children:[L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-amber-700`,children:`자동 충돌 해결`}),L(`h2`,{class:`mt-2 mb-0 text-lg font-black`,children:`안전한 LAN 대역으로 변경할 수 있습니다`}),L(`p`,{class:`mt-2 mb-0 text-sm font-semibold leading-6 text-amber-900/80`,children:t.recommendation?`${t.recommendation.subnet} · DHCP ${t.recommendation.dhcpStart} ~ ${t.recommendation.dhcpEnd}`:`현재 활성 네트워크와 겹치지 않는 추천 대역을 찾지 못했습니다. 아래에서 직접 설정해 주세요.`})]}),t.recommendation?L(`button`,{class:`inline-flex min-h-11 shrink-0 items-center justify-center gap-2 rounded-xl border-0 bg-amber-700 px-5 py-2.5 text-sm font-extrabold text-white transition hover:bg-amber-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-amber-200 disabled:cursor-wait disabled:opacity-60`,disabled:ne,onClick:()=>void ie(),type:`button`,children:[L(B,{class:`size-5 ${e===`auto`?`animate-pulse`:``}`}),e===`auto`?`변경 중`:`추천 대역으로 자동 변경`]}):null]})}):null,L(`form`,{class:`mt-5 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,onSubmit:e=>void re(e),children:[L(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3`,children:[L(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:L(Qe,{class:`size-6`})}),L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`내부 네트워크`}),L(`h2`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`LAN 및 DHCP 설정`}),L(`p`,{class:`mt-2 mb-0 text-sm font-semibold leading-6 text-slate-600`,children:`상위 공유기와 다른 주소 대역을 사용해야 안정적으로 라우팅할 수 있습니다.`})]})]}),S?L(`span`,{"aria-live":`polite`,class:`inline-flex shrink-0 self-start items-center gap-1.5 rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs font-extrabold text-amber-800`,role:`status`,children:[L(V,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}):null]}),L(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[L(`div`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`공유기 IP 주소`}),L(Ln,{disabled:ne,label:`공유기 IP 주소`,onChange:D,value:l})]}),L(`div`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`현재 LAN 대역`}),L(`div`,{class:`mt-2 min-h-11 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm font-bold text-slate-700`,children:t.lan.subnet}),L(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`서브넷 마스크는 아래 고급 설정에서 변경할 수 있습니다.`})]}),L(or,{disabled:ne,label:`DHCP 시작 주소`,lanAddress:l,onChange:O,value:h}),L(or,{disabled:ne,label:`DHCP 종료 주소`,lanAddress:l,onChange:k,value:_})]}),L(`details`,{class:`mt-6 rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[L(`summary`,{class:`cursor-pointer text-sm font-black text-slate-900`,children:`고급 DHCP 설정`}),L(`div`,{class:`mt-5 grid gap-5 lg:grid-cols-2`,children:[L(`div`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`서브넷 마스크`}),L(Nn,{ariaLabel:`서브넷 마스크`,className:`mt-2`,disabled:ne,id:`lan-prefix-length`,onChange:e=>A(Number(e)),options:ee.map(e=>({value:String(e),label:`/${e} · ${e===t.lan.prefixLength?t.lan.netmask:`CIDR /${e}`}`})),value:String(d),variant:`emphasized`}),L(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`일반 가정용 네트워크는 /24 사용을 권장합니다.`})]}),L(`div`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`DHCP 임대 시간`}),L(Nn,{ariaLabel:`DHCP 임대 시간`,className:`mt-2`,disabled:ne,id:`lan-dhcp-lease-time`,onChange:j,options:te.map(([e,t])=>({value:e,label:t})),value:b,variant:`emphasized`})]}),L(`label`,{class:`inline-flex min-h-11 items-center gap-3 text-sm font-extrabold text-slate-800 lg:col-span-2`,children:[L(`input`,{checked:p,class:`size-5 accent-teal-700`,disabled:ne,onChange:e=>M(e.currentTarget.checked),type:`checkbox`}),`SmartSafeHub DHCP 서버 사용`]}),L(`p`,{class:`m-0 text-xs font-semibold leading-5 text-slate-500 lg:col-span-2`,children:`DHCP를 끄면 SmartSafeHub에 연결되는 장치에 IP 주소를 자동으로 할당하지 않습니다. 별도의 DHCP 서버를 사용하는 고급 구성에서만 비활성화하세요.`})]})]}),w?L(`p`,{class:`mt-5 mb-0 rounded-xl bg-red-50 px-4 py-3 text-sm font-bold text-red-800`,children:w}):null,L(`div`,{class:`mt-6 flex flex-col gap-3 border-t border-slate-200 pt-5 sm:flex-row sm:items-center sm:justify-between`,children:[L(`p`,{class:`m-0 text-xs font-semibold leading-5 text-slate-500`,children:`주소가 바뀌면 현재 관리 페이지 연결이 종료되며 새 IP를 받아 다시 접속해야 합니다.`}),L(`button`,{class:`inline-flex min-h-11 w-full shrink-0 items-center justify-center gap-2 rounded-xl border-0 bg-teal-700 px-5 py-2.5 text-sm font-extrabold text-white shadow-sm transition hover:bg-teal-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-200 disabled:cursor-wait disabled:opacity-60 sm:w-auto`,disabled:ne||!S,type:`submit`,children:[L(B,{class:`size-5 ${e===`saving`?`animate-pulse`:``}`}),e===`saving`?`저장 중`:`설정 저장`]})]})]})]})}var dr={};dr.QR8bitByte=(e,t)=>{var n=t("QRMode");function r(e){this.mode=n.MODE_8BIT_BYTE,this.data=e}r.prototype={getLength:function(){return this.data.length},write:function(e){for(var t=0;t{function n(){this.buffer=[],this.length=0}n.prototype={get:function(e){var t=Math.floor(e/8);return(this.buffer[t]>>>7-e%8&1)==1},put:function(e,t){for(var n=0;n>>t-n-1&1)==1)},getLengthInBits:function(){return this.length},putBit:function(e){var t=Math.floor(this.length/8);this.buffer.length<=t&&this.buffer.push(0),e&&(this.buffer[t]|=128>>>this.length%8),this.length++}},e.exports=n},dr.QRErrorCorrectLevel=(e,t)=>{e.exports={L:1,M:0,Q:3,H:2}},dr.QRMaskPattern=(e,t)=>{e.exports={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7}},dr.QRMath=(e,t)=>{for(var n={glog:function(e){if(e<1)throw Error(`glog(`+e+`)`);return n.LOG_TABLE[e]},gexp:function(e){for(;e<0;)e+=255;for(;e>=256;)e-=255;return n.EXP_TABLE[e]},EXP_TABLE:Array(256),LOG_TABLE:Array(256)},r=0;r<8;r++)n.EXP_TABLE[r]=1<{e.exports={MODE_NUMBER:1,MODE_ALPHA_NUM:2,MODE_8BIT_BYTE:4,MODE_KANJI:8}},dr.QRPolynomial=(e,t)=>{var n=t("QRMath");function r(e,t){if(e.length===void 0)throw Error(e.length+`/`+t);for(var n=0;n{var n=t("QRErrorCorrectLevel");function r(e,t){this.totalCount=e,this.dataCount=t}r.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],r.getRSBlocks=function(e,t){var n=r.getRsBlockTable(e,t);if(n===void 0)throw Error(`bad rs block @ typeNumber:`+e+`/errorCorrectLevel:`+t);for(var i=n.length/3,a=[],o=0;o{var n=t("QRMode"),r=t("QRPolynomial"),i=t("QRMath"),a=t("QRMaskPattern"),o={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:1335,G18:7973,G15_MASK:21522,getBCHTypeInfo:function(e){for(var t=e<<10;o.getBCHDigit(t)-o.getBCHDigit(o.G15)>=0;)t^=o.G15<=0;)t^=o.G18<>>=1;return t},getPatternPosition:function(e){return o.PATTERN_POSITION_TABLE[e-1]},getMask:function(e,t,n){switch(e){case a.PATTERN000:return(t+n)%2==0;case a.PATTERN001:return t%2==0;case a.PATTERN010:return n%3==0;case a.PATTERN011:return(t+n)%3==0;case a.PATTERN100:return(Math.floor(t/2)+Math.floor(n/3))%2==0;case a.PATTERN101:return t*n%2+t*n%3==0;case a.PATTERN110:return(t*n%2+t*n%3)%2==0;case a.PATTERN111:return(t*n%3+(t+n)%2)%2==0;default:throw Error(`bad maskPattern:`+e)}},getErrorCorrectPolynomial:function(e){for(var t=new r([1],0),n=0;n5&&(n+=3+a-5)}for(r=0;r{var n=t("QR8bitByte"),r=t("QRUtil"),i=t("QRPolynomial"),a=t("QRRSBlock"),o=t("QRBitBuffer");function s(e,t){this.typeNumber=e,this.errorCorrectLevel=t,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}s.prototype={addData:function(e){var t=new n(e);this.dataList.push(t),this.dataCache=null},isDark:function(e,t){if(e<0||this.moduleCount<=e||t<0||this.moduleCount<=t)throw Error(e+`,`+t);return this.modules[e][t]},getModuleCount:function(){return this.moduleCount},make:function(){if(this.typeNumber<1){var e=1;for(e=1;e<40;e++){for(var t=a.getRSBlocks(e,this.errorCorrectLevel),n=new o,i=0,s=0;s=7&&this.setupTypeNumber(e),this.dataCache===null&&(this.dataCache=s.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,t)},setupPositionProbePattern:function(e,t){for(var n=-1;n<=7;n++)if(!(e+n<=-1||this.moduleCount<=e+n))for(var r=-1;r<=7;r++)t+r<=-1||this.moduleCount<=t+r||(0<=n&&n<=6&&(r===0||r===6)||0<=r&&r<=6&&(n===0||n===6)||2<=n&&n<=4&&2<=r&&r<=4?this.modules[e+n][t+r]=!0:this.modules[e+n][t+r]=!1)},getBestMaskPattern:function(){for(var e=0,t=0,n=0;n<8;n++){this.makeImpl(!0,n);var i=r.getLostPoint(this);(n===0||e>i)&&(e=i,t=n)}return t},createMovieClip:function(e,t,n){var r=e.createEmptyMovieClip(t,n),i=1;this.make();for(var a=0;a>i&1)==1,this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=n;for(var a=0;a<18;a++)n=!e&&(t>>a&1)==1,this.modules[a%3+this.moduleCount-8-3][Math.floor(a/3)]=n},setupTypeInfo:function(e,t){for(var n=this.errorCorrectLevel<<3|t,i=r.getBCHTypeInfo(n),a,o=0;o<15;o++)a=!e&&(i>>o&1)==1,o<6?this.modules[o][8]=a:o<8?this.modules[o+1][8]=a:this.modules[this.moduleCount-15+o][8]=a;for(var s=0;s<15;s++)a=!e&&(i>>s&1)==1,s<8?this.modules[8][this.moduleCount-s-1]=a:s<9?this.modules[8][15-s-1+1]=a:this.modules[8][15-s-1]=a;this.modules[this.moduleCount-8][8]=!e},mapData:function(e,t){for(var n=-1,i=this.moduleCount-1,a=7,o=0,s=this.moduleCount-1;s>0;s-=2)for(s===6&&s--;;){for(var c=0;c<2;c++)if(this.modules[i][s-c]===null){var l=!1;o>>a&1)==1),r.getMask(t,i,s-c)&&(l=!l),this.modules[i][s-c]=l,a--,a===-1&&(o++,a=7)}if(i+=n,i<0||this.moduleCount<=i){i-=n,n=-n;break}}}},s.PAD0=236,s.PAD1=17,s.createData=function(e,t,n){for(var i=a.getRSBlocks(e,t),c=new o,l=0;ld*8)throw Error(`code length overflow. (`+c.getLengthInBits()+`>`+d*8+`)`);for(c.getLengthInBits()+4<=d*8&&c.put(0,4);c.getLengthInBits()%8!=0;)c.putBit(!1);for(;!(c.getLengthInBits()>=d*8||(c.put(s.PAD0,8),c.getLengthInBits()>=d*8));)c.put(s.PAD1,8);return s.createBytes(c,i)},s.createBytes=function(e,t){for(var n=0,a=0,o=0,s=Array(t.length),c=Array(t.length),l=0;l=0?m.get(g):0}}for(var _=0,v=0;vString.fromCharCode(e)).join(``),r=new(pr(`index`))(0,pr(`QRErrorCorrectLevel`).M);r.addData(n),r.make();let i=r.getModuleCount();return Array.from({length:i},(e,t)=>Array.from({length:i},(e,n)=>!!r.isDark(t,n)))}function hr(e,t,n){let r=e=>e.replace(/([\\;,:"])/g,`\\$1`);return`WIFI:T:${t===`none`?`nopass`:t===`sae`?`WPA3`:`WPA`};S:${r(e)};P:${r(n)};;`}function gr(e){return`${(e.replace(/[\\/:*?"<>|\x00-\x1f\x7f]/g,`_`).replace(/[. ]+$/g,``).trim()||`Wi-Fi`).slice(0,64)}-QR.png`}function _r(e,t,n=8){let r=(e.length+8)*n;t.fillStyle=`#ffffff`,t.fillRect(0,0,r,r),t.fillStyle=`#000000`,e.forEach((e,r)=>e.forEach((e,i)=>{e&&t.fillRect((i+4)*n,(r+4)*n,n,n)}))}function vr({load:e,onClose:t}){let[n,r]=F(null),[i,a]=F(null),[o,s]=F(null);I(()=>{let t=!0;return e().then(e=>{if(t)try{r({ssid:e.ssid,matrix:mr(hr(e.ssid,e.security,e.password))})}catch{a(`QR 코드를 생성할 수 없습니다.`)}}).catch(()=>{t&&a(`Wi-Fi 정보를 불러오지 못했습니다.`)}),()=>{t=!1,r(null)}},[e]);let c=n?.matrix.length??0;return L(`div`,{class:`fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70 p-4`,role:`presentation`,onClick:e=>{e.target===e.currentTarget&&t()},children:L(`section`,{role:`dialog`,"aria-modal":`true`,"aria-label":`Wi-Fi QR 코드`,class:`w-full max-w-sm rounded-2xl bg-white p-5 text-center text-slate-950 shadow-2xl sm:p-7`,children:[L(`h2`,{class:`m-0 text-xl font-extrabold`,children:`Wi-Fi QR 코드`}),L(`p`,{class:`mt-2 break-all text-sm font-semibold text-slate-600`,children:n?.ssid??`Wi-Fi 정보를 확인하는 중`}),i?L(`p`,{role:`alert`,class:`my-6 text-sm font-bold text-red-700`,children:i}):null,!n&&!i?L(`p`,{class:`my-6 text-sm`,children:`QR 코드를 만드는 중...`}):null,n?L(`div`,{class:`mx-auto my-5 w-full max-w-[288px] rounded-xl border border-slate-200 bg-white p-3`,children:L(`svg`,{class:`block h-auto w-full`,viewBox:`0 0 ${c+8} ${c+8}`,role:`img`,"aria-label":`${n.ssid} Wi-Fi 연결 QR 코드`,"shape-rendering":`crispEdges`,children:[L(`rect`,{width:c+8,height:c+8,fill:`white`}),n.matrix.map((e,t)=>e.map((e,n)=>e?L(`rect`,{x:n+4,y:t+4,width:`1`,height:`1`,fill:`black`},`${t}-${n}`):null))]})}):null,L(`p`,{class:`text-xs leading-5 text-slate-500`,children:[L(`span`,{class:`block`,children:`카메라로 스캔해 Wi-Fi에 연결하세요.`}),L(`span`,{class:`block`,children:`QR 코드에는 비밀번호가 포함됩니다.`})]}),o?L(`p`,{role:`alert`,class:`mt-3 text-sm font-bold text-red-700`,children:o}):null,n?L(`button`,{class:`mt-5 inline-flex min-h-11 w-full items-center justify-center gap-2 rounded-xl bg-teal-700 px-4 font-extrabold text-white transition hover:bg-teal-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-200`,onClick:()=>{if(n){s(null);try{let e=(n.matrix.length+8)*8,t=document.createElement(`canvas`);t.width=e,t.height=e;let r=t.getContext(`2d`);if(!r)throw Error(`Canvas is not supported`);_r(n.matrix,r,8);let i=document.createElement(`a`);i.href=t.toDataURL(`image/png`),i.download=gr(n.ssid),i.click()}catch{s(`QR 코드 이미지를 저장하지 못했습니다. 다시 시도해 주세요.`)}}},type:`button`,children:[L(it,{class:`size-5`}),`QR 코드 다운로드`]}):null,L(`button`,{class:`mt-2 min-h-11 w-full rounded-xl border border-slate-300 bg-white px-4 font-extrabold text-slate-700 transition hover:bg-slate-50`,onClick:t,type:`button`,children:`닫기`})]})})}function yr({guest:e,busy:t,saving:n,onUpdate:r,onLoadQr:i}){let[a,o]=F(!1),[s,c]=F(!1),[l,u]=F(e.ssid),[d,f]=F(``),[p,m]=F(e.enabled),[h,g]=F(null);if(I(()=>{u(e.ssid),m(e.enabled),f(``),g(null)},[e]),!e.supported)return L(`section`,{class:`mt-5 rounded-2xl border border-slate-200 bg-white p-5 sm:p-6`,children:[L(`h2`,{class:`m-0 text-lg font-extrabold text-slate-900`,children:`게스트 Wi-Fi`}),L(`p`,{class:`mt-2 mb-0 text-sm text-slate-600`,children:`2.4GHz 기본 Wi-Fi 장치를 찾지 못해 게스트 Wi-Fi를 사용할 수 없습니다.`})]});let _=l.trim()!==e.ssid||p!==e.enabled||d.length>0,y=t||_||!e.enabled||!e.passwordConfigured,b=async t=>{t.preventDefault();let n=l.trim(),i=new TextEncoder().encode(n).length;i<1||i>32?g(`Wi-Fi 이름은 UTF-8 기준 1~32바이트로 입력해 주세요.`):d&&!(d.length>=8&&d.length<=63||/^[0-9a-fA-F]{64}$/.test(d))?g(`비밀번호는 8~63자 또는 64자리 16진수여야 합니다.`):p&&!e.passwordConfigured&&!d?g(`게스트 전용 비밀번호를 입력해 주세요.`):window.confirm(`게스트 Wi-Fi 설정을 적용하면 무선 연결이 잠시 끊어질 수 있습니다. 계속하시겠습니까?`)&&(g(null),await r({ssid:n,enabled:p,password:d})&&f(``))};return L(v,{children:[L(`form`,{"aria-label":`게스트 Wi-Fi 설정`,class:`mt-5 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-6`,onSubmit:e=>void b(e),children:[L(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[L(`button`,{"aria-controls":`ssh-guest-settings`,"aria-expanded":a,"aria-label":`게스트 Wi-Fi 설정 ${a?`접기`:`펼치기`}`,class:`flex min-w-0 flex-1 items-start gap-3 rounded-xl text-left focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100`,onClick:()=>o(e=>!e),type:`button`,children:[L(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:L(We,{class:`size-6`})}),L(`span`,{class:`min-w-0 flex-1`,children:[L(`span`,{class:`block text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`2.4 GHz · 분리 네트워크`}),L(`span`,{class:`mt-2 block text-xl font-extrabold tracking-tight text-slate-950`,children:`게스트 Wi-Fi`}),L(`span`,{class:`mt-2 block break-all text-sm text-slate-600`,children:e.ssid})]}),L(gt,{class:`mt-3 size-5 shrink-0 text-slate-500 transition-transform ${a?`rotate-180`:``}`})]}),L(`div`,{class:`flex flex-wrap items-center gap-2 sm:justify-end`,children:[_&&L(`span`,{"aria-live":`polite`,class:`inline-flex items-center gap-1 rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs font-extrabold text-amber-800`,children:[L(V,{class:`size-3.5`}),`저장되지 않음`]}),L(`button`,{"aria-label":`게스트 Wi-Fi QR 코드 보기`,class:`inline-flex min-h-10 items-center gap-2 rounded-xl border border-slate-300 bg-white px-3.5 py-2 text-sm font-extrabold text-slate-800 transition hover:border-teal-300 hover:text-teal-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-not-allowed disabled:opacity-50`,disabled:y,onClick:()=>c(!0),title:e.enabled?void 0:`게스트 Wi-Fi를 켜고 저장하면 QR 코드를 사용할 수 있습니다.`,type:`button`,children:[L(Ze,{class:`size-4 shrink-0`}),`QR 코드 보기`]}),L(`span`,{class:`rounded-full px-3 py-1.5 text-xs font-extrabold ${e.enabled?`bg-emerald-50 text-emerald-700`:`bg-slate-100 text-slate-600`}`,children:e.enabled?`사용 중`:`꺼짐`})]})]}),L(`div`,{class:a?``:`hidden`,id:`ssh-guest-settings`,children:[L(`p`,{class:`mt-5 mb-0 text-sm leading-6 text-slate-600`,children:`방문자는 인터넷만 이용하며 내부 네트워크와 공유기 관리 화면에는 접근할 수 없습니다.`}),L(`div`,{class:`mt-5 grid gap-5 lg:grid-cols-2`,children:[L(`label`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`게스트 Wi-Fi 이름 (SSID)`}),L(`input`,{autocomplete:`off`,class:`mt-2 min-h-11 w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-4 py-2.5 text-sm font-semibold text-slate-950 outline-none focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:opacity-60`,disabled:t,maxLength:32,onInput:e=>u(e.currentTarget.value),value:l})]}),L(`label`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`게스트 Wi-Fi 비밀번호`}),L(`input`,{autocomplete:`new-password`,class:`mt-2 min-h-11 w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-4 py-2.5 text-sm font-semibold text-slate-950 outline-none focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:opacity-60`,disabled:t,onInput:e=>f(e.currentTarget.value),placeholder:e.passwordConfigured?`비워 두면 기존 비밀번호 유지`:`8자 이상의 별도 비밀번호 입력`,type:`password`,value:d}),L(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`WPA2/WPA3 혼합 · 기본 Wi-Fi와 다른 비밀번호를 권장합니다.`})]})]}),L(`div`,{class:`mt-5 rounded-xl bg-slate-50 px-4 py-3 text-sm leading-6 text-slate-700`,children:[`내부망 접근 차단 · 게스트 기기 간 통신 차단 · SafeShield DNS 사용`,e.subnet&&L(`span`,{class:`block text-xs text-slate-500`,children:[`게스트 게이트웨이: `,e.subnet]})]}),L(`div`,{class:`mt-5 flex flex-col gap-4 border-t border-slate-200 pt-5 sm:flex-row sm:items-center sm:justify-between`,children:[L(`label`,{class:`inline-flex min-h-11 items-center gap-3 text-sm font-extrabold text-slate-800`,children:[L(`input`,{checked:p,class:`size-5 accent-teal-700`,disabled:t,onChange:e=>m(e.currentTarget.checked),type:`checkbox`}),`게스트 Wi-Fi 사용`]}),L(`button`,{class:`inline-flex min-h-11 w-full items-center justify-center gap-2 rounded-xl bg-teal-700 px-5 py-2.5 text-sm font-extrabold text-white transition hover:bg-teal-800 disabled:opacity-60 sm:w-auto`,disabled:t||!_,type:`submit`,children:[L(B,{class:`size-5 ${n?`animate-pulse`:``}`}),n?`적용 중`:`설정 저장`]})]}),h&&L(`p`,{class:`mt-4 mb-0 rounded-xl bg-red-50 px-4 py-3 text-sm font-bold text-red-800`,children:h})]})]}),s&&L(vr,{load:i,onClose:()=>c(!1)})]})}var br=[{value:`sae-mixed`,label:`WPA2/WPA3 혼합 (권장)`},{value:`sae`,label:`WPA3-Personal`},{value:`psk2`,label:`WPA2-Personal`},{value:`none`,label:`암호화 없음`}];function xr(e){switch(e.security){case`sae-mixed`:return`WPA2/WPA3 혼합`;case`sae`:return`WPA3-Personal`;case`psk2`:return`WPA2-Personal`;case`none`:return`암호화 없음`;default:return`고급 설정 (${e.securityRaw})`}}function Sr(e){return e.length>=8&&e.length<=63||/^[0-9a-fA-F]{64}$/.test(e)}function Cr({network:e,busy:t,saving:n,onUpdate:r,onLoadQr:i}){let[a,o]=F(e.ssid),[s,c]=F(e.enabled),[l,u]=F(e.security===`custom`?`keep`:e.security),[d,f]=F(``),[p,m]=F(!1),h=Ee(()=>i(e.section),[i,e.section]),[g,_]=F(null);I(()=>{o(e.ssid),c(e.enabled),u(e.security===`custom`?`keep`:e.security),f(``),_(null)},[e]);let v=e.security===`custom`&&l===`keep`,y=l===`psk2`||l===`sae-mixed`||l===`sae`,b=e.security===`custom`?`keep`:e.security,x=a.trim()!==e.ssid||s!==e.enabled||l!==b||d.length>0,S=t||x||e.security===`custom`||!e.enabled||e.security!==`none`&&!e.passwordConfigured,C=[...e.security===`custom`?[{value:`keep`,label:`현재 고급 설정 유지 (${e.securityRaw})`}]:[],...br],w=async t=>{t.preventDefault();let n=a.trim(),i=new TextEncoder().encode(n).length;i<1||i>32?_(`SSID는 UTF-8 기준 1~32바이트로 입력해 주세요.`):d&&!Sr(d)?_(`비밀번호는 8~63자 또는 64자리 16진수여야 합니다.`):y&&!e.passwordConfigured&&d.length===0?_(`보안 Wi-Fi를 사용하려면 비밀번호를 입력해 주세요.`):window.confirm(`Wi-Fi 설정을 적용하면 현재 무선 연결이 잠시 끊어질 수 있습니다. 계속하시겠습니까?`)&&(_(null),await r({section:e.section,ssid:n,security:l,password:d,enabled:s})&&f(``))};return L(`form`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-6`,onSubmit:e=>void w(e),children:[L(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3`,children:[L(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:L(We,{class:`size-6`})}),L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:e.bandLabel}),L(`h2`,{class:`mt-2 mb-0 break-words text-xl font-extrabold tracking-tight text-slate-950`,children:e.ssid||`이름 없는 Wi-Fi`}),L(`p`,{class:`mt-2 mb-0 text-sm text-slate-600`,children:[`채널 `,e.channel??`자동`,` · 연결 기기 `,e.clientCount,`대`]})]})]}),L(`div`,{class:`flex flex-wrap items-center gap-2 sm:justify-end`,children:[x?L(`span`,{"aria-live":`polite`,class:`inline-flex shrink-0 items-center gap-1.5 rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs font-extrabold text-amber-800`,role:`status`,children:[L(V,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}):null,L(`button`,{"aria-label":`${e.bandLabel} Wi-Fi QR 코드 보기`,class:`inline-flex min-h-10 items-center gap-2 rounded-xl border border-slate-300 bg-white px-3.5 py-2 text-sm font-extrabold text-slate-800 shadow-sm transition hover:border-teal-300 hover:text-teal-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-not-allowed disabled:opacity-50`,disabled:S,onClick:()=>m(!0),type:`button`,children:[L(Ze,{class:`size-4 shrink-0`}),`QR 코드 보기`]}),L(`span`,{class:`inline-flex w-fit items-center gap-2 rounded-full px-3 py-1.5 text-xs font-extrabold ring-1 ring-inset ${e.runtimeUp?`bg-emerald-50 text-emerald-700 ring-emerald-200`:`bg-slate-100 text-slate-600 ring-slate-200`}`,children:[L(`span`,{class:`size-2 rounded-full ${e.runtimeUp?`bg-emerald-500`:`bg-slate-400`}`}),e.runtimeUp?`동작 중`:e.enabled?`시작 대기`:`꺼짐`]})]})]}),L(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[L(`label`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`Wi-Fi 이름 (SSID)`}),L(`input`,{autocomplete:`off`,class:`mt-2 min-h-11 w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-4 py-2.5 text-sm font-semibold text-slate-950 shadow-inner outline-none transition placeholder:text-slate-400 focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:cursor-not-allowed disabled:bg-slate-100`,disabled:t,maxLength:32,onInput:e=>o(e.currentTarget.value),value:a})]}),L(`div`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`보안 방식`}),L(Nn,{ariaLabel:`${e.bandLabel} Wi-Fi 보안 방식`,className:`mt-2`,disabled:t,id:`wifi-security-${e.section}`,onChange:e=>{let t=e;u(t),(t===`none`||t===`keep`)&&f(``)},options:C,value:l,variant:`emphasized`}),L(`span`,{class:`mt-2 block text-xs text-slate-500`,children:[`현재: `,xr(e)]})]}),L(`label`,{class:`block lg:col-span-2`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`새 비밀번호`}),L(`input`,{autocomplete:`new-password`,class:`mt-2 min-h-11 w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-4 py-2.5 text-sm font-semibold text-slate-950 shadow-inner outline-none transition placeholder:text-slate-400 focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:cursor-not-allowed disabled:bg-slate-100`,disabled:t||l===`none`||v,onInput:e=>f(e.currentTarget.value),placeholder:v?`고급 보안 설정은 기존 LuCI에서 변경합니다`:e.passwordConfigured?`비워 두면 기존 비밀번호를 유지합니다`:`8자 이상의 비밀번호를 입력하세요`,type:`password`,value:d}),L(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`저장된 비밀번호는 일반 설정 조회에 노출되지 않습니다. QR 코드에는 비밀번호가 포함됩니다.`})]})]}),L(`div`,{class:`mt-6 flex flex-col gap-4 border-t border-slate-200 pt-5 sm:flex-row sm:items-center sm:justify-between`,children:[L(`label`,{class:`inline-flex min-h-11 items-center gap-3 text-sm font-extrabold text-slate-800`,children:[L(`input`,{checked:s,class:`size-5 accent-teal-700`,disabled:t,onChange:e=>c(e.currentTarget.checked),type:`checkbox`}),`이 Wi-Fi 사용`]}),L(`button`,{class:`inline-flex min-h-11 w-full items-center justify-center gap-2 rounded-xl border-0 bg-teal-700 px-5 py-2.5 text-sm font-extrabold text-white shadow-sm transition hover:bg-teal-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-200 disabled:cursor-wait disabled:opacity-60 sm:w-auto`,disabled:t||!x,type:`submit`,children:[L(B,{class:`size-5 ${n?`animate-pulse`:``}`}),n?`적용 중`:`설정 저장`]})]}),p?L(vr,{load:h,onClose:()=>m(!1)}):null,g?L(`p`,{class:`mt-4 mb-0 rounded-xl bg-red-50 px-4 py-3 text-sm font-bold text-red-800`,children:g}):null]})}function wr({feedback:e,onDismiss:t}){return L(`div`,{"aria-live":`polite`,class:`mb-5 flex flex-col gap-3 rounded-2xl border px-4 py-3 text-sm sm:flex-row sm:items-center sm:justify-between ${e.kind===`success`?`border-emerald-200 bg-emerald-50 text-emerald-900`:e.kind===`warning`?`border-amber-200 bg-amber-50 text-amber-900`:`border-red-200 bg-red-50 text-red-900`}`,children:[L(`p`,{class:`m-0 font-bold leading-6`,children:e.message}),L(`button`,{class:`min-h-11 w-full shrink-0 rounded-lg border border-current bg-transparent sm:min-h-9 sm:w-auto px-3 py-1.5 text-xs font-extrabold`,onClick:t,type:`button`,children:`닫기`})]})}function Tr({data:e,error:t,feedback:n,loading:r,updatingSection:i,onDismissFeedback:a,onRetry:o,onUpdate:s,onUpdateGuest:c,onLoadQr:l,onLoadGuestQr:u}){return r?L(Rn,{}):t?L(zn,{message:t,onRetry:o}):e?L(v,{children:[n?L(wr,{feedback:n,onDismiss:a}):null,L(`section`,{class:`mb-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-amber-950 sm:p-6`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-amber-700`,children:`연결 주의`}),L(`h2`,{class:`mt-2 mb-0 text-lg font-extrabold`,children:`Wi-Fi 이름이나 비밀번호를 바꾸면 현재 연결이 끊어집니다`}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-amber-900/80`,children:`변경한 Wi-Fi 이름과 비밀번호로 다시 연결하면 SmartSafeHub를 계속 사용할 수 있습니다. VLAN 등 고급 무선 옵션은 기존 LuCI에서 관리합니다.`})]}),e.networks.length?L(`section`,{"aria-label":`기본 Wi-Fi 설정`,class:`grid gap-5 xl:grid-cols-2`,children:e.networks.map(e=>L(Cr,{busy:i!==null,network:e,onUpdate:s,onLoadQr:l,saving:i===e.section},e.section))}):L(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 text-center shadow-sm shadow-slate-900/5 sm:p-8`,children:[L(We,{class:`mx-auto size-10 text-slate-400`}),L(`h2`,{class:`mt-4 mb-0 text-lg font-extrabold text-slate-950`,children:`관리할 기본 Wi-Fi를 찾지 못했습니다`}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:`기존 LuCI에서 AP 모드의 무선 네트워크를 먼저 구성해 주세요.`})]}),e.guest&&L(v,{children:[L(`div`,{role:`separator`,"aria-label":`기본 Wi-Fi와 게스트 Wi-Fi 구분`,class:`mt-5 w-full border-t border-slate-200`}),L(yr,{guest:e.guest,busy:i!==null,saving:i===`ssh_guest`,onUpdate:c,onLoadQr:u})]}),e.guestError&&L(`section`,{class:`mt-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-sm text-amber-900`,role:`status`,children:[`게스트 Wi-Fi 정보를 불러오지 못했습니다. 패키지를 설치한 직후라면 로그아웃 후 다시 로그인해 주세요.`,L(`button`,{class:`ml-3 underline font-bold`,onClick:o,type:`button`,children:`다시 시도`})]})]}):null}function Er(e){return e.trim().toLowerCase().replace(/\.+$/,``)}function Dr(e){return e.length===0||e.length>253||!e.includes(`.`)||!/^[a-z0-9.-]+$/.test(e)?!1:e.split(`.`).every(e=>e.length>0&&e.length<=63&&/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(e))}function Or({action:e,kind:t,onAddRule:n,onDeleteRule:r,rules:i,recommendedDomains:a,recommendedKnown:o}){let[s,c]=F(``),[l,u]=F(``),[d,f]=F(null),p=t===`allow`,m=e!==null,h=Te(()=>{let e=l.trim().toLowerCase();return e.length>0?i.filter(t=>t.includes(e)):i},[i,l]);async function g(e){e.preventDefault();let r=Er(s);Dr(r)?(f(null),await n(t,r,()=>{c(e=>Er(e)===r?``:e)})):f(`example.com 형식의 도메인을 입력해 주세요. URL, IP 주소, 와일드카드는 사용할 수 없습니다.`)}let _=p?{badge:`bg-emerald-100 text-emerald-800`,button:`border-emerald-700 bg-emerald-700 text-white hover:border-emerald-800 hover:bg-emerald-800 focus-visible:ring-emerald-100`,icon:`bg-emerald-50 text-emerald-700`,label:`허용 목록`,description:`차단 목록에 포함되어도 허용할 도메인과 하위 도메인을 등록합니다.`,empty:`등록된 허용 도메인이 없습니다.`}:{badge:`bg-red-100 text-red-800`,button:`border-red-700 bg-red-700 text-white hover:border-red-800 hover:bg-red-800 focus-visible:ring-red-100`,icon:`bg-red-50 text-red-700`,label:`차단 목록`,description:`SafeShield 기본 목록에 없더라도 직접 차단할 도메인을 등록합니다.`,empty:`등록된 차단 도메인이 없습니다.`};return L(`article`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white shadow-sm shadow-slate-900/5`,children:[L(`div`,{class:`border-b border-slate-200 p-4 sm:p-6`,children:[L(`div`,{class:`flex items-start justify-between gap-4`,children:L(`div`,{class:`flex min-w-0 items-start gap-3`,children:[L(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl ${_.icon}`,children:L(p?B:Ue,{class:`size-6`})}),L(`div`,{class:`min-w-0`,children:[L(`div`,{class:`flex flex-wrap items-center gap-2`,children:[L(`h2`,{class:`m-0 text-xl font-extrabold tracking-tight text-slate-950`,children:_.label}),L(`span`,{class:`rounded-full px-2.5 py-1 text-xs font-extrabold ${_.badge}`,children:i.length})]}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:_.description})]})]})}),L(`form`,{class:`mt-5`,onSubmit:g,children:[L(`label`,{class:`block text-xs font-extrabold text-slate-600`,for:`${t}-domain`,children:`새 도메인`}),L(`div`,{class:`mt-2 flex flex-col gap-2 sm:flex-row`,children:[L(`input`,{"aria-describedby":d?`${t}-domain-error`:void 0,"aria-invalid":d?!0:void 0,autocomplete:`off`,class:`min-h-11 min-w-0 flex-1 rounded-xl border-2 border-slate-300 bg-slate-50 px-4 py-2.5 text-sm font-semibold text-slate-950 shadow-inner outline-none transition placeholder:text-slate-400 focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:cursor-not-allowed disabled:bg-slate-100`,disabled:m,id:`${t}-domain`,onInput:e=>c(e.currentTarget.value),placeholder:`example.com`,spellcheck:!1,type:`text`,value:s}),L(`button`,{class:`inline-flex min-h-11 w-full shrink-0 items-center justify-center gap-2 sm:w-auto rounded-xl border px-4 py-2.5 text-sm font-extrabold transition focus:outline-none focus-visible:ring-4 disabled:cursor-not-allowed disabled:opacity-50 ${_.button}`,disabled:m||s.trim().length===0,type:`submit`,children:[L(ut,{class:`size-4`}),e?.operation===`add`&&e.action===t?`추가 중…`:`추가`]})]}),d?L(`p`,{class:`mt-2 mb-0 text-xs font-semibold leading-5 text-red-700`,id:`${t}-domain-error`,children:d}):null]})]}),L(`div`,{class:`p-4 sm:p-6`,children:[L(`label`,{class:`relative block`,for:`${t}-search`,children:[L(`span`,{class:`sr-only`,children:[_.label,` 검색`]}),L(`span`,{class:`pointer-events-none absolute inset-y-0 left-0 flex w-11 items-center justify-center text-slate-400`,children:L(ft,{class:`size-4`})}),L(`input`,{class:`min-h-11 w-full rounded-xl border-2 border-slate-300 bg-slate-50 py-2.5 pr-4 pl-11 text-sm font-semibold text-slate-950 shadow-inner outline-none transition placeholder:text-slate-400 focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100`,id:`${t}-search`,onInput:e=>u(e.currentTarget.value),placeholder:`${_.label} 검색`,type:`search`,value:l})]}),L(`div`,{class:`mt-4 max-h-[28rem] overflow-y-auto rounded-xl border border-slate-200`,children:h.length>0?L(`ul`,{class:`m-0 list-none divide-y divide-slate-200 p-0`,children:h.map(n=>{let i=p&&a.includes(n),s=!p||o?i?`추천 규칙`:`직접 추가`:null,c=e?.operation===`delete`&&e.action===t&&e.domain===n;return L(`li`,{class:`flex min-w-0 items-center justify-between gap-3 px-3.5 py-3`,children:[L(`div`,{class:`flex min-w-0 flex-1 flex-wrap items-center gap-x-3 gap-y-1.5`,children:[L(`code`,{class:`min-w-0 break-all font-mono text-sm font-semibold text-slate-800`,children:n}),s?L(`span`,{class:`shrink-0 rounded-full px-2.5 py-1 text-[11px] font-bold ${i?`bg-teal-50 text-teal-800`:`bg-slate-100 text-slate-600`}`,title:i?`추천 규칙 동기화로 추가된 도메인입니다. 웹사이트에서 적용을 해제해 주세요.`:`공유기에 직접 등록되어 유지되는 규칙입니다.`,children:s}):null]}),L(`button`,{"aria-label":`${n} ${_.label}에서 삭제`,class:`inline-flex size-9 shrink-0 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-500 transition hover:border-red-200 hover:bg-red-50 hover:text-red-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-red-100 disabled:cursor-not-allowed disabled:opacity-50`,disabled:m||i,onClick:()=>void r(t,n),title:i?`웹사이트에서 적용 해제할 수 있습니다.`:`삭제`,type:`button`,children:L(dt,{class:`size-4 ${c?`animate-pulse`:``}`})})]},n)})}):L(`div`,{class:`px-5 py-10 text-center`,children:[L(pt,{class:`mx-auto size-8 text-slate-300`}),L(`p`,{class:`mt-3 mb-0 text-sm font-bold text-slate-600`,children:l.trim().length>0?`검색 결과가 없습니다.`:_.empty})]})})]})]})}function kr({action:e,data:t,error:n,feedback:r,loading:i,onAddRule:a,onDeleteRule:o,onDismissFeedback:s,onRetry:c,onLoadRecommendedRulesStatus:l}){let[u,d]=F([]),[f,p]=F(!1);return I(()=>{if(!l)return;let e=!0;return l().then(t=>{e&&(d(t.domains),p(!0))}).catch(()=>{e&&p(!1)}),()=>{e=!1}},[t?.allow.join(` +`),l]),i&&!t?L(Rn,{}):n&&!t?L(zn,{message:n,onRetry:c}):t?L(v,{children:[L(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-6`,children:L(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3 sm:gap-4`,children:[L(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-teal-50 text-teal-700`,children:L(pt,{class:`size-7`})}),L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700`,children:`Local overrides`}),L(`h2`,{class:`mt-2 mb-0 break-words text-xl font-black tracking-tight text-slate-950 sm:text-2xl`,children:`사용자 도메인 규칙`}),L(`p`,{class:`mt-2 mb-0 max-w-3xl text-sm leading-6 text-slate-600`,children:`허용 목록은 오탐을 해제하고, 차단 목록은 원하는 도메인을 SafeShield 보호 목록에 추가합니다.`})]})]}),L(`dl`,{class:`m-0 grid grid-cols-3 gap-2 sm:min-w-80`,children:[L(`div`,{class:`rounded-xl bg-emerald-50 px-3 py-3 text-center`,children:[L(`dt`,{class:`text-[0.68rem] font-extrabold uppercase tracking-wide text-emerald-700`,children:`허용`}),L(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-emerald-950`,children:t.counts.allow})]}),L(`div`,{class:`rounded-xl bg-red-50 px-3 py-3 text-center`,children:[L(`dt`,{class:`text-[0.68rem] font-extrabold uppercase tracking-wide text-red-700`,children:`차단`}),L(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-red-950`,children:t.counts.block})]}),L(`div`,{class:`rounded-xl bg-slate-100 px-3 py-3 text-center`,children:[L(`dt`,{class:`text-[0.68rem] font-extrabold uppercase tracking-wide text-slate-600`,children:`전체`}),L(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-slate-950`,children:t.counts.total})]})]})]})}),L(`section`,{class:`mt-4 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,"aria-label":`추천 사용자 규칙`,children:L(`div`,{class:`flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center`,children:[L(`div`,{class:`min-w-0`,children:[L(`div`,{class:`flex flex-wrap items-center gap-2`,children:[L(`span`,{class:`grid size-8 place-items-center rounded-lg bg-teal-50 text-teal-700`,children:L(B,{class:`size-4`})}),L(`h2`,{class:`m-0 text-base font-extrabold text-slate-950`,children:`추천 사용자 규칙`}),f&&u.length>0?L(`span`,{class:`rounded-full bg-teal-50 px-2.5 py-1 text-xs font-bold text-teal-800`,children:[`추천으로 추가 `,u.length,`개`]}):null]}),L(`div`,{class:`mt-2 flex flex-wrap items-center gap-2`,"aria-label":`추천 규칙 분류`,children:[L(`span`,{class:`inline-flex items-center rounded-full bg-teal-50 px-2.5 py-1 text-xs font-bold text-teal-800`,children:`공식 추천`}),L(`span`,{class:`inline-flex items-center rounded-full bg-slate-100 px-2.5 py-1 text-xs font-bold text-slate-700`,children:`커뮤니티`})]}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:`SmartSafeHub의 공식 추천과 관리자가 검토한 커뮤니티 허용 규칙을 확인하고, 필요한 규칙을 적용해 보세요.`}),L(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`공유기 선택과 적용 관리는 SmartSafeHub 웹사이트에서 진행합니다. 적용 결과는 다음 기기 동기화 후 반영되며, 연결된 모든 기기에 영향을 줍니다.`})]}),L(`a`,{class:`inline-flex min-h-10 w-full shrink-0 items-center justify-center rounded-xl border border-teal-700 bg-teal-700 px-4 py-2.5 text-sm font-extrabold text-white transition hover:bg-teal-800 sm:w-auto`,href:`https://www.smartsafehub.com/dashboard/recommended-rules/`,target:`_blank`,rel:`noopener noreferrer`,children:`추천 규칙 보기 ↗`})]})}),t.applyLocalOverrides?null:L(`section`,{class:`mt-4 flex items-start gap-3 rounded-2xl border border-amber-200 bg-amber-50 p-4`,role:`alert`,children:[L(V,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),L(`div`,{children:[L(`p`,{class:`m-0 text-sm font-extrabold text-amber-950`,children:`로컬 규칙 적용이 꺼져 있습니다`}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:`규칙은 저장되지만 SafeShield의 apply_local_overrides 설정을 켜기 전까지 DNS에 반영되지 않습니다.`})]})]}),t.safeshieldEnabled?null:L(`section`,{class:`mt-4 flex items-start gap-3 rounded-2xl border border-slate-200 bg-slate-100 p-4`,role:`status`,children:[L(Ue,{class:`mt-0.5 size-5 shrink-0 text-slate-600`}),L(`div`,{children:[L(`p`,{class:`m-0 text-sm font-extrabold text-slate-950`,children:`SafeShield가 비활성화되어 있습니다`}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`규칙을 미리 저장할 수 있으며, SafeShield를 켜고 차단 목록을 갱신하면 적용됩니다.`})]})]}),r?L(`section`,{"aria-live":`polite`,class:`mt-4 flex flex-col items-start gap-3 rounded-2xl border border-teal-200 bg-teal-50 sm:flex-row sm:justify-between p-4`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3`,children:[L(B,{class:`mt-0.5 size-5 shrink-0 text-teal-700`}),L(`p`,{class:`m-0 text-sm font-semibold leading-6 text-teal-950`,children:r})]}),L(`button`,{class:`shrink-0 rounded-lg border-0 bg-transparent px-2 py-1 text-xs font-extrabold text-teal-800 hover:bg-teal-100`,onClick:s,type:`button`,children:`닫기`})]}):null,n&&t?L(`section`,{"aria-live":`assertive`,class:`mt-4 flex items-start gap-3 rounded-2xl border border-red-200 bg-red-50 p-4`,children:[L(V,{class:`mt-0.5 size-5 shrink-0 text-red-700`}),L(`p`,{class:`m-0 text-sm font-semibold leading-6 text-red-900`,children:n})]}):null,L(`section`,{class:`mt-5 grid gap-5 xl:grid-cols-2`,children:[L(Or,{action:e,kind:`allow`,onAddRule:a,onDeleteRule:o,rules:t.allow,recommendedDomains:u,recommendedKnown:f}),L(Or,{action:e,kind:`block`,onAddRule:a,onDeleteRule:o,rules:t.block,recommendedDomains:u,recommendedKnown:f})]}),L(`p`,{class:`mt-5 mb-0 text-xs leading-5 text-slate-500`,children:`규칙 저장과 유효성 검사는 SafeShield API가 관리합니다. 도메인은 소문자로 정규화되고 중복 항목은 하나로 유지됩니다.`})]}):L(zn,{message:`사용자 규칙 응답이 비어 있습니다.`,onRetry:c})}function Ar({timestamp:e,fallback:t=`기록 없음`}){let[n,r]=F(()=>Date.now()/1e3),i=typeof e==`number`&&Number.isFinite(e)&&e>0&&e<=864e10;if(I(()=>{if(!i)return;r(Date.now()/1e3);let e=window.setInterval(()=>r(Date.now()/1e3),6e4);return()=>window.clearInterval(e)},[e,i]),!i||e==null)return L(`span`,{children:t});let a=Ut(e);return L(`span`,{class:`inline-flex max-w-full flex-wrap items-center gap-1`,children:[L(`time`,{class:`min-w-0`,dateTime:new Date(e*1e3).toISOString(),title:a,children:Gt(e,n)}),L(`details`,{class:`group inline-flex max-w-full flex-wrap items-center gap-1 align-middle`,children:[L(`summary`,{"aria-label":`정확한 날짜 및 시간 확인`,class:`inline-flex size-6 cursor-pointer list-none items-center justify-center rounded-full text-slate-400 transition hover:text-teal-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-500 [&::-webkit-details-marker]:hidden`,title:`정확한 날짜 및 시간 확인`,children:L(`svg`,{"aria-hidden":`true`,class:`size-3.5`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.8`,viewBox:`0 0 24 24`,children:[L(`circle`,{cx:`12`,cy:`12`,r:`9`}),L(`path`,{d:`M12 11v5m0-8h.01`,strokeLinecap:`round`})]})}),L(`span`,{class:`hidden max-w-full break-words rounded-md bg-slate-100 px-2 py-1 text-xs font-semibold text-slate-700 group-open:inline-block`,children:a})]})]})}function jr(e,t){if(!e||!t)return`unknown`;if(e===t)return`same`;let n=/^(\d+)\.(\d+)\.(\d+)(?:-r(\d+))?$/,r=n.exec(e),i=n.exec(t);if(!r||!i)return`different`;for(let e=1;e<=4;e+=1){let t=BigInt(r[e]||`0`),n=BigInt(i[e]||`0`);if(t>n)return`newer`;if(t=1048576?`${(e/1048576).toFixed(1)} MB`:`${(e/1024).toFixed(1)} KB`}function Nr(e){switch(e.phase){case`checking`:return`확인 중`;case`downloading`:return`다운로드 중`;case`verifying`:return`검증 중`;case`ready`:return`설치 준비 완료`;case`flashing`:return`설치 중`;case`error`:return`확인 필요`;default:return e.updateAvailable?`업데이트 가능`:e.lastCheckAt?`최신 상태`:`확인 전`}}function Pr(e){return e.phase===`error`?`bg-rose-50 text-rose-700 ring-rose-200`:e.phase===`checking`||e.phase===`downloading`||e.phase===`verifying`||e.phase===`flashing`?`bg-sky-50 text-sky-700 ring-sky-200`:e.phase===`ready`||e.updateAvailable?`bg-amber-50 text-amber-800 ring-amber-200`:e.lastCheckAt?`bg-emerald-50 text-emerald-700 ring-emerald-200`:`bg-slate-100 text-slate-600 ring-slate-200`}function Fr(e){return e.phase===`checking`||e.phase===`downloading`||e.phase===`verifying`||e.phase===`flashing`?L(z,{class:`size-3.5 animate-spin`}):e.phase===`error`?L(V,{class:`size-3.5`}):e.phase===`ready`||e.updateAvailable?L(it,{class:`size-3.5`}):e.lastCheckAt?L(B,{class:`size-3.5`}):L(tt,{class:`size-3.5`})}function Ir({checked:e,disabled:t,onChange:n}){return L(`button`,{"aria-checked":e,"aria-label":`현재 설정 유지`,class:`ssh-switch-control relative inline-flex shrink-0 rounded-full border transition focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-not-allowed disabled:opacity-50 ${e?`border-teal-600 bg-teal-600`:`border-slate-300 bg-slate-200`}`,disabled:t,onClick:()=>n(!e),role:`switch`,type:`button`,children:L(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${e?`left-6`:`left-1`}`})})}function Lr({action:e,actionError:t,data:n,software:r,error:i,loading:a,message:o,reconnecting:s,uploadProgress:c,onCheck:l,onDiscard:u,onDismissFeedback:d,onInstall:f,onPrepare:p,onRetry:m,onUpload:h}){let g=we(null),[_,y]=F(null),[b,x]=F(!0),[S,C]=F(!1);I(()=>{n?.prepared&&x(n.prepared.allowBackup)},[n?.prepared]);let w=e!==null||n?.phase===`checking`||n?.phase===`downloading`||n?.phase===`verifying`||n?.phase===`flashing`,T=n?.phase===`ready`?n.prepared:null,E=e===`check`||n?.phase===`checking`,D=e===`prepare`||n?.phase===`downloading`||n?.phase===`verifying`,O=e===`upload`||e===`validate-upload`,k=n?.release?.bundledSoftware?.smartsafehub??null,A=r?.packages.find(e=>e.name===`luci-app-smartsafehub`)?.installedVersion??null,j=jr(k,A),M=async()=>{_&&await h(_)&&(y(null),g.current&&(g.current.value=``))};return L(`article`,{class:`min-w-0 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm shadow-slate-900/5`,"data-component":`firmware-update-card`,children:[L(`div`,{class:`p-5 sm:p-6`,children:[L(`div`,{class:`flex flex-col gap-5 lg:flex-row lg:items-start lg:justify-between`,children:[L(`div`,{class:`flex min-w-0 gap-4`,children:[L(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-sky-50 text-sky-700`,children:L(We,{class:`size-6`})}),L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-sky-700`,children:`Firmware update`}),L(`h2`,{class:`mt-2 mb-0 text-xl font-black text-slate-950 sm:text-2xl`,children:`펌웨어 업데이트`}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500 lg:whitespace-nowrap`,children:`시스템 펌웨어를 확인하고 설치합니다. 온라인·수동 설치는 안전성 검증 후 진행됩니다.`})]})]}),n?L(`div`,{class:`flex shrink-0 flex-col gap-2 sm:flex-row lg:justify-end`,children:[L(`span`,{class:`inline-flex min-h-11 items-center justify-center gap-1.5 rounded-xl px-3 py-2 text-xs font-extrabold ring-1 ring-inset ${Pr(n)}`,children:[Fr(n),Nr(n)]}),L(`button`,{class:`inline-flex min-h-11 items-center justify-center gap-2 rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-extrabold text-slate-800 transition hover:bg-slate-50 disabled:cursor-wait disabled:opacity-60`,disabled:w||T!==null,onClick:l,type:`button`,children:[L(z,{class:`size-4 ${E?`animate-spin`:``}`}),E?`확인 중...`:`펌웨어 확인`]}),n.updateAvailable&&!T?L(`button`,{class:`inline-flex min-h-11 items-center justify-center gap-2 rounded-xl bg-sky-700 px-4 py-2.5 text-sm font-extrabold text-white transition hover:bg-sky-800 disabled:cursor-wait disabled:opacity-60`,disabled:w,onClick:p,type:`button`,children:[D?L(z,{class:`size-4 animate-spin`}):L(it,{class:`size-4`}),D?`준비 중...`:`다운로드 및 검증`]}):null]}):null]}),t||o?L(`div`,{class:`mt-5 flex flex-col gap-3 rounded-xl border px-4 py-3 text-sm font-bold sm:flex-row sm:items-center sm:justify-between ${t?`border-rose-200 bg-rose-50 text-rose-800`:`border-emerald-200 bg-emerald-50 text-emerald-800`}`,children:[L(`span`,{children:t||o}),L(`button`,{class:`shrink-0 self-start rounded-lg px-3 py-1.5 text-xs font-extrabold hover:bg-black/5 sm:self-auto`,onClick:d,type:`button`,children:`닫기`})]}):null,s||n?.phase===`flashing`?L(`div`,{class:`mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4 sm:p-5`,role:`status`,children:L(`div`,{class:`flex gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-amber-700 ring-1 ring-inset ring-amber-200`,children:L(z,{class:`size-5 animate-spin`})}),L(`div`,{children:[L(`strong`,{class:`block text-sm font-black text-amber-950`,children:`펌웨어를 설치하고 재부팅하고 있습니다.`}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:`전원을 끄지 마세요. 현재 주소에서 공유기가 다시 응답하면 SmartSafeHub가 자동으로 새로고침됩니다.`})]})]})}):null,i&&!n?L(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-800`,children:[L(`p`,{class:`m-0 font-bold`,children:i}),L(`button`,{class:`mt-3 rounded-lg border border-rose-300 bg-white px-3 py-2 text-xs font-extrabold text-rose-700`,onClick:m,type:`button`,children:`다시 시도`})]}):a&&!n?L(`div`,{class:`mt-5 flex items-center gap-3 rounded-xl bg-slate-50 p-4 text-sm font-bold text-slate-500`,children:[L(z,{class:`size-4 animate-spin`}),`펌웨어 상태를 확인하고 있습니다.`]}):n?L(v,{children:[n.lastError?L(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 sm:p-5`,children:L(`div`,{class:`flex gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-rose-700 ring-1 ring-inset ring-rose-200`,children:L(V,{class:`size-5`})}),L(`div`,{class:`min-w-0 flex-1`,children:[L(`strong`,{class:`block text-sm font-black text-rose-950`,children:`펌웨어 작업을 완료하지 못했습니다.`}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-rose-800`,children:n.lastError.message}),L(`details`,{class:`mt-3 rounded-lg border border-rose-200 bg-white p-3`,children:[L(`summary`,{class:`cursor-pointer text-xs font-extrabold text-rose-800`,children:`상세 정보 보기`}),L(`p`,{class:`mt-2 mb-0 break-all font-mono text-[11px] text-slate-600`,children:n.lastError.code})]})]})]})}):null,n.current.metadataAvailable?null:L(`div`,{class:`mt-5 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-xs leading-5 text-amber-900`,children:[L(`strong`,{class:`block font-extrabold`,children:`현재 펌웨어의 버전 정보를 완전히 확인할 수 없습니다.`}),L(`span`,{class:`mt-1 block text-amber-800`,children:`새 펌웨어를 설치하면 이후부터 정확한 설치 버전을 자동으로 확인할 수 있습니다.`}),L(`details`,{class:`mt-2`,children:[L(`summary`,{class:`cursor-pointer font-extrabold text-amber-800`,children:`기술 상세 보기`}),L(`p`,{class:`mt-2 mb-0 break-all font-mono text-[11px] leading-5 text-amber-800`,children:`SmartSafeHub build ID metadata is missing. Future images should include /usr/share/smartsafehub/firmware.json.`})]})]}),L(`dl`,{class:`mt-5 grid overflow-hidden rounded-xl border border-slate-200 bg-slate-200 sm:grid-cols-2 xl:grid-cols-4`,children:[L(`div`,{class:`bg-white p-4`,children:[L(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Device`}),L(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black text-slate-950`,children:n.current.deviceCode||n.current.boardName||`미확인`})]}),L(`div`,{class:`bg-white p-4`,children:[L(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Current version`}),L(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black text-slate-950`,children:n.current.releaseVersion||`미확인`}),n.current.buildId?L(`span`,{class:`mt-1 block break-all text-[11px] leading-5 text-slate-400`,children:[`빌드 ID `,n.current.buildId]}):n.current.openwrtVersion?L(`span`,{class:`mt-1 block text-[11px] leading-5 text-slate-400`,children:[`OpenWrt `,n.current.openwrtVersion]}):null]}),L(`div`,{class:`bg-white p-4`,children:[L(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Available version`}),L(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black ${n.updateAvailable?`text-amber-700`:`text-slate-950`}`,children:n.lastCheckAt?n.release?.version?`${n.release.version}${n.updateAvailable?``:` · 최신`}`:n.updateAvailable?`새 펌웨어`:`최신 버전`:`미확인`}),n.release?.buildId?L(`span`,{class:`mt-1 block break-all text-[11px] leading-5 text-slate-400`,children:[`빌드 ID `,n.release.buildId]}):null]}),L(`div`,{class:`bg-white p-4`,children:[L(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Last check`}),L(`dd`,{class:`mt-2 mb-0 ml-0 text-sm font-black text-slate-950`,children:L(Ar,{fallback:`아직 없음`,timestamp:n.lastCheckAt})})]})]})]}):null,n?.updateAvailable&&n.release?L(`section`,{class:`mt-5 rounded-xl border border-sky-200 bg-sky-50/60 p-4 sm:p-5`,"data-section":`available-firmware`,children:[L(`div`,{class:`flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-sky-700`,children:`Available firmware`}),L(`h3`,{class:`mt-2 mb-0 text-lg font-black text-slate-950`,children:`새 펌웨어가 있습니다.`}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:[`펌웨어 `,n.release.version||`미확인`,` · `,Mr(n.release.sysupgrade.sizeBytes),` · `,(n.release.channel||n.settings.channel)===`beta`?`Beta`:`Stable`]})]}),L(`span`,{class:`shrink-0 rounded-full bg-white px-3 py-1.5 text-xs font-extrabold text-sky-700 ring-1 ring-inset ring-sky-200`,children:n.release.version||`Latest`})]}),k?L(`div`,{class:`mt-4 rounded-xl border border-sky-100 bg-white p-4`,"data-section":`firmware-bundled-software`,children:[L(`div`,{class:`flex flex-wrap items-center justify-between gap-2`,children:[L(`span`,{class:`text-xs font-extrabold text-slate-800`,children:`포함된 관리 소프트웨어`}),L(`div`,{class:`flex flex-wrap items-center gap-2`,children:[L(`span`,{class:`text-sm font-black text-slate-950`,children:k}),j===`same`?L(`span`,{class:`rounded-full bg-emerald-50 px-2.5 py-1 text-[11px] font-extrabold text-emerald-700 ring-1 ring-inset ring-emerald-200`,children:`현재 버전과 동일`}):null]})]}),L(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-600`,children:j===`same`?`관리 소프트웨어는 이미 같은 버전입니다. 펌웨어 업데이트는 시스템 구성 요소를 갱신합니다.`:j===`older`?`현재 설치된 관리 소프트웨어(${A})보다 이전 버전이 포함되어 있습니다. 펌웨어 설치 전 버전을 확인해 주세요.`:j===`newer`?`현재 설치된 관리 소프트웨어(${A})보다 새 버전이 포함되어 있습니다.`:A?`현재 설치 버전 ${A} · 펌웨어 포함 버전 ${k}`:`이 펌웨어에 포함된 관리 소프트웨어 버전입니다.`})]}):null,n.release.releaseNotes.length?L(`ul`,{class:`mt-4 mb-0 space-y-2 rounded-xl bg-white p-4 pl-9 text-sm leading-6 text-slate-700 ring-1 ring-inset ring-sky-100`,children:n.release.releaseNotes.map((e,t)=>L(`li`,{children:e},`${t}-${e}`))}):null]}):null,T?L(`section`,{class:`mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4 sm:p-5`,"data-section":`prepared-firmware`,children:L(`div`,{class:`flex gap-4`,children:[L(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-white text-amber-700 ring-1 ring-inset ring-amber-200`,children:L(B,{class:`size-5`})}),L(`div`,{class:`min-w-0 flex-1`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-amber-700`,children:`Verified image`}),L(`h3`,{class:`mt-2 mb-0 text-lg font-black text-amber-950`,children:`설치할 펌웨어가 준비되었습니다.`}),L(`p`,{class:`mt-2 mb-0 break-all text-sm leading-6 text-amber-900`,children:[T.filename||`firmware.bin`,` · `,Mr(T.sizeBytes)]}),T.sha256?L(`p`,{class:`mt-2 mb-0 break-all font-mono text-[11px] leading-5 text-amber-800`,children:[`SHA-256 `,T.sha256]}):null,L(`div`,{class:`mt-5 flex items-start justify-between gap-4 rounded-xl border border-amber-200 bg-white p-4`,children:[L(`div`,{children:[L(`strong`,{class:`block text-sm font-black text-slate-950`,children:`현재 설정 유지`}),L(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`Wi-Fi, SafeShield, SmartSafeHub 설정을 유지합니다.`}),T.allowBackup?null:L(`span`,{class:`mt-2 block text-xs font-bold text-rose-700`,children:`이 이미지는 설정 유지 업그레이드를 지원하지 않습니다.`})]}),L(Ir,{checked:b,disabled:!T.allowBackup,onChange:x})]}),S?L(`div`,{class:`mt-4 rounded-xl border border-rose-200 bg-rose-50 p-4`,children:[L(`strong`,{class:`block text-sm font-black text-rose-950`,children:`펌웨어 설치를 시작할까요?`}),L(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-rose-800`,children:`설치 중 전원을 끄면 장치가 복구 불가능한 상태가 될 수 있습니다. 이미지 검증은 설치 직전에 다시 수행됩니다.`}),L(`div`,{class:`mt-4 flex flex-wrap justify-end gap-2`,children:[L(`button`,{class:`min-h-10 rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-extrabold text-slate-700`,onClick:()=>C(!1),type:`button`,children:`취소`}),L(`button`,{class:`min-h-10 rounded-lg bg-rose-700 px-3 py-2 text-xs font-extrabold text-white`,onClick:()=>{C(!1),f(b)},type:`button`,children:`설치 시작`})]})]}):L(`div`,{class:`mt-4 flex flex-wrap justify-end gap-2`,children:[L(`button`,{class:`min-h-10 rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-extrabold text-slate-700`,disabled:e===`discard`,onClick:()=>void u(),type:`button`,children:`파일 삭제`}),L(`button`,{class:`inline-flex min-h-10 items-center gap-2 rounded-lg bg-rose-700 px-4 py-2 text-xs font-extrabold text-white`,onClick:()=>C(!0),type:`button`,children:[L(Je,{class:`size-4`}),`펌웨어 설치`]})]})]})]})}):null]}),L(`details`,{class:`group mx-5 mb-5 rounded-xl border border-slate-200 bg-slate-50/70 sm:mx-6 sm:mb-6`,"data-layout":`firmware-card-subsection`,"data-section":`manual-firmware`,children:[L(`summary`,{class:`flex cursor-pointer list-none items-center justify-between gap-4 px-4 py-4 sm:px-5 [&::-webkit-details-marker]:hidden`,children:[L(`div`,{class:`flex min-w-0 items-center gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-slate-600 ring-1 ring-inset ring-slate-200`,children:L(it,{class:`size-5 rotate-180`})}),L(`div`,{class:`min-w-0`,children:[L(`strong`,{class:`block text-sm font-black text-slate-950`,children:`수동 펌웨어 설치`}),L(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`펌웨어 파일(.bin)을 직접 업로드하여 검증한 뒤 설치합니다.`})]})]}),L(`span`,{class:`shrink-0 rounded-full bg-white px-3 py-1.5 text-[11px] font-extrabold text-slate-600 ring-1 ring-inset ring-slate-200 group-open:hidden`,children:`열기`}),L(`span`,{class:`hidden shrink-0 rounded-full bg-white px-3 py-1.5 text-[11px] font-extrabold text-slate-600 ring-1 ring-inset ring-slate-200 group-open:inline-flex`,children:`닫기`})]}),L(`div`,{class:`border-t border-slate-200 px-5 pb-5 pt-3 sm:px-6`,children:[L(`p`,{class:`mt-0 mb-4 text-xs leading-5 text-slate-500 lg:whitespace-nowrap`,children:"이 장치에 맞는 Sysupgrade 이미지(.bin)만 사용하세요. 업로드한 파일은 이미지 검증과 `sysupgrade --test`를 모두 통과해야 설치할 수 있으며 강제 설치는 제공하지 않습니다."}),L(`div`,{class:`rounded-xl border border-slate-200 bg-white p-4 sm:p-5`,"data-layout":`firmware-file-picker`,children:[L(`input`,{accept:`.bin,application/octet-stream`,"aria-label":`펌웨어 파일 선택`,class:`sr-only`,disabled:w||T!==null,onChange:e=>y(e.currentTarget.files?.[0]??null),ref:g,type:`file`}),L(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between`,children:[L(`div`,{class:`flex min-w-0 items-center gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-50 text-slate-500 ring-1 ring-inset ring-slate-200`,children:L(it,{class:`size-5 rotate-180`})}),L(`div`,{class:`min-w-0`,children:[L(`strong`,{class:`block truncate text-sm font-black text-slate-900`,children:_?_.name:`펌웨어 파일을 선택하세요`}),L(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:_?`${Mr(_.size)} · Sysupgrade 이미지 (.bin)`:`이 장치에 맞는 Sysupgrade 이미지 (.bin)를 선택합니다.`})]})]}),L(`div`,{class:`flex shrink-0 flex-wrap gap-2 sm:justify-end`,children:[L(`button`,{class:`inline-flex min-h-10 items-center justify-center rounded-lg border border-slate-300 bg-white px-3.5 py-2 text-xs font-extrabold text-slate-700 transition hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50`,disabled:w||T!==null,onClick:()=>g.current?.click(),type:`button`,children:_?`다른 파일 선택`:`파일 선택`}),L(`button`,{class:`inline-flex min-h-10 items-center justify-center gap-2 rounded-lg bg-slate-900 px-3.5 py-2 text-xs font-extrabold text-white transition hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50`,disabled:!_||w||T!==null,onClick:()=>void M(),type:`button`,children:[O?L(z,{class:`size-4 animate-spin`}):L(it,{class:`size-4 rotate-180`}),O?`처리 중...`:`업로드 및 검증`]})]})]}),c===null?null:L(`div`,{class:`mt-4 border-t border-slate-100 pt-4`,children:[L(`div`,{class:`mb-2 flex items-center justify-between gap-3 text-xs font-bold text-slate-600`,children:[L(`span`,{children:`업로드 중`}),L(`span`,{children:[c,`%`]})]}),L(`div`,{class:`h-2 overflow-hidden rounded-full bg-slate-200`,children:L(`div`,{class:`h-full rounded-full bg-sky-600 transition-[width]`,style:{width:`${Math.max(0,Math.min(100,c))}%`}})})]})]})]})]})]})}var Rr=Array.from({length:24},(e,t)=>{let n=String(t).padStart(2,`0`);return{value:n,label:`${n}시`}}),zr=Array.from({length:60},(e,t)=>{let n=String(t).padStart(2,`0`);return{value:n,label:`${n}분`}});function Br(e){let t=/^([01]\d|2[0-3]):([0-5]\d)$/.exec(e);return{hour:t?.[1]??`03`,minute:t?.[2]??`00`}}function Vr({ariaLabel:e,className:t=``,disabled:n=!1,id:r,onChange:i,value:a,variant:o=`default`}){let{hour:s,minute:c}=Br(a),l=(e,t)=>{i(`${e}:${t}`)};return L(`div`,{"aria-label":e,class:`ssh-time-select ${t}`.trim(),"data-time-select":r,role:`group`,children:[L(Nn,{ariaLabel:`${e} 시`,disabled:n,id:`${r}-hour`,onChange:e=>l(e,c),options:Rr,value:s,variant:o}),L(Nn,{ariaLabel:`${e} 분`,disabled:n,id:`${r}-minute`,onChange:e=>l(s,e),options:zr,value:c,variant:o})]})}var Hr=7200;function Ur(e,t=Math.floor(Date.now()/1e3)){return!e||!e.settings.checkEnabled||e.phase===`checking`||!e.lastCheckAt?!1:Math.max(0,t-e.lastCheckAt)>Math.max(e.settings.checkIntervalSeconds*2,Hr)}var Wr=[{value:3600,label:`1시간마다`},{value:21600,label:`6시간마다`},{value:43200,label:`12시간마다`},{value:86400,label:`24시간마다`}],Gr=`luci-app-smartsafehub`;function Kr(e){return e?new Date(e*1e3).toLocaleString(`ko-KR`,{dateStyle:`medium`,timeStyle:`short`}):`아직 없음`}function qr(e){return e===`stable`?`Stable`:e===`beta`?`Beta`:`미확인`}function Jr(e){return e===`stable`?`bg-emerald-50 text-emerald-700 ring-emerald-200`:e===`beta`?`bg-amber-50 text-amber-700 ring-amber-200`:`bg-slate-100 text-slate-600 ring-slate-200`}function Yr(e){return e.code===`UPDATES_INDEX_REFRESH_FAILED`?{title:`패키지 저장소를 확인하지 못했습니다.`,description:`OpenWrt 패키지 저장소 동기화 또는 네트워크 상태가 일시적으로 불안정할 수 있습니다. 잠시 후 업데이트 확인을 다시 실행해 주세요.`}:e.code===`UPDATES_INSTALL_FAILED`?{title:`관리 소프트웨어 업데이트 설치에 실패했습니다.`,description:`패키지 설치가 완료되지 않았습니다. 네트워크 연결과 저장 공간을 확인한 뒤 다시 시도해 주세요.`}:{title:`최근 업데이트 작업을 완료하지 못했습니다.`,description:`잠시 후 다시 시도해 주세요. 문제가 반복되면 상세 정보를 확인해 주세요.`}}function Xr(e,t){return e.phase===`checking`?`업데이트 확인 중`:e.phase===`installing`?`업데이트 설치 중`:e.phase===`error`?`확인 필요`:e.updateCount>0?`업데이트 가능`:t?`업데이트 확인 지연`:e.lastCheckAt?`최신 상태`:`확인 전`}function Zr(e,t){return e.phase===`error`?`bg-rose-50 text-rose-700 ring-rose-200`:e.phase===`checking`||e.phase===`installing`?`bg-sky-50 text-sky-700 ring-sky-200`:e.updateCount>0||t?`bg-amber-50 text-amber-800 ring-amber-200`:e.lastCheckAt?`bg-emerald-50 text-emerald-700 ring-emerald-200`:`bg-slate-100 text-slate-600 ring-slate-200`}function Qr(e,t){return e.phase===`checking`||e.phase===`installing`?L(z,{class:`size-3.5 shrink-0 animate-spin`}):e.phase===`error`?L(V,{class:`size-3.5 shrink-0`}):e.updateCount>0?L(it,{class:`size-3.5 shrink-0`}):t?L(V,{class:`size-3.5 shrink-0`}):e.lastCheckAt?L(B,{class:`size-3.5 shrink-0`}):L(tt,{class:`size-3.5 shrink-0`})}function $r({checked:e,disabled:t=!1,label:n,onChange:r}){return L(`button`,{"aria-checked":e,"aria-label":n,class:`ssh-switch-control relative inline-flex shrink-0 rounded-full border transition focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-not-allowed disabled:opacity-50 ${e?`border-teal-600 bg-teal-600`:`border-slate-300 bg-slate-200`}`,disabled:t,onClick:()=>r(!e),role:`switch`,type:`button`,children:L(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${e?`left-6`:`left-1`}`})})}function ei({action:e,actionError:t,data:n,firmware:r,error:i,loading:a,message:o,onCheck:s,onDismissFeedback:c,onInstall:l,onRetry:u,onSaveSettings:d}){let[f,p]=F(!0),[m,h]=F(21600),[g,_]=F(!1),[y,b]=F(`03:00`),[x,S]=F(!1),[C,w]=F(!1);I(()=>{n&&!x&&(p(n.settings.checkEnabled),h(n.settings.checkIntervalSeconds),_(n.settings.autoInstall),b(n.settings.autoInstallTime))},[n,x]);let T=e===`check`||n?.phase===`checking`,E=e===`install`||n?.phase===`installing`,D=T||E,O=Ur(n),k=n?.phase===`error`&&n.lastError?.code===`UPDATES_INDEX_REFRESH_FAILED`,A=n?.lastError?Yr(n.lastError):null,j=n?.packages.find(e=>e.name===Gr)??n?.packages[0]??null,M=r?.updateAvailable?r.release?.bundledSoftware?.smartsafehub:null,ee=!(!n?.lastCheckAt||n.updateCount!==0||O||j?.name!==Gr)&&jr(M,j?.installedVersion)===`same`,te=(e={})=>n?(e.checkEnabled??f)!==n.settings.checkEnabled||(e.checkIntervalSeconds??m)!==n.settings.checkIntervalSeconds||(e.autoInstall??g)!==n.settings.autoInstall||(e.autoInstallTime??y)!==n.settings.autoInstallTime:!1,ne=async()=>{await d({checkEnabled:f,checkIntervalSeconds:m,autoInstall:g,autoInstallTime:y})&&S(!1)};return L(`article`,{class:`min-w-0 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm shadow-slate-900/5 ${n?`grid xl:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]`:``}`,"data-component":`management-software-update-card`,"data-layout":`management-software-sections`,children:[L(`section`,{class:`min-w-0 xl:col-start-1 xl:row-start-1`,"data-section":`software-update-status`,children:[L(`div`,{class:`p-5 sm:p-6`,children:[L(`div`,{class:`flex flex-col gap-5 lg:flex-row lg:items-start lg:justify-between`,children:[L(`div`,{class:`flex min-w-0 gap-4`,children:[L(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-teal-50 text-teal-700`,children:L(Je,{class:`size-6`})}),L(`div`,{class:`min-w-0`,children:[L(`div`,{class:`flex flex-wrap items-center gap-2`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700`,children:`Management software`}),n?L(`span`,{class:`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-extrabold ring-1 ring-inset ${Zr(n,O)}`,children:[L(`span`,{"aria-hidden":`true`,class:`flex items-center justify-center`,children:Qr(n,O)}),Xr(n,O)]}):null]}),L(`h2`,{class:`mt-2 mb-0 text-xl font-black text-slate-950 sm:text-2xl`,children:`관리 소프트웨어 업데이트`}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500 xl:whitespace-nowrap`,children:`새 버전을 확인하고 안전하게 설치합니다.`})]})]}),n?L(`div`,{class:`flex shrink-0 flex-col gap-2 sm:flex-row lg:justify-end`,children:[L(`button`,{class:`inline-flex min-h-11 items-center justify-center gap-2 rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-extrabold text-slate-800 transition hover:bg-slate-50 disabled:cursor-wait disabled:opacity-60`,disabled:D,onClick:s,type:`button`,children:[L(z,{class:`size-4 ${T?`animate-spin`:``}`}),T?`확인 중...`:`업데이트 확인`]}),n.updateCount>0&&!C?L(`button`,{class:`inline-flex min-h-11 items-center justify-center gap-2 rounded-xl bg-teal-700 px-4 py-2.5 text-sm font-extrabold text-white transition hover:bg-teal-800 disabled:cursor-wait disabled:opacity-60`,disabled:D,onClick:()=>w(!0),type:`button`,children:[E?L(z,{class:`size-4 animate-spin`}):L(it,{class:`size-4`}),E?`설치 중...`:`업데이트 설치`]}):null]}):null]}),(t||o)&&L(`div`,{class:`mt-5 flex flex-col gap-3 rounded-xl border px-4 py-3 text-sm font-bold sm:flex-row sm:items-center sm:justify-between ${t?`border-rose-200 bg-rose-50 text-rose-800`:`border-emerald-200 bg-emerald-50 text-emerald-800`}`,children:[L(`span`,{children:t||o}),L(`button`,{class:`shrink-0 self-start rounded-lg px-3 py-1.5 text-xs font-extrabold hover:bg-black/5 sm:self-auto`,onClick:c,type:`button`,children:`닫기`})]}),E?L(`div`,{"aria-busy":`true`,"aria-live":`polite`,class:`mt-5 rounded-xl border border-sky-200 bg-sky-50 p-4 sm:p-5`,children:[L(`div`,{class:`flex items-start gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-sky-700 ring-1 ring-inset ring-sky-200`,children:L(z,{"aria-hidden":`true`,class:`size-5 animate-spin`})}),L(`div`,{class:`min-w-0`,children:[L(`h3`,{class:`m-0 text-sm font-black text-sky-950`,children:`관리 소프트웨어를 업데이트하고 있습니다.`}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-sky-800`,children:`설치가 완료되면 이 화면이 자동으로 갱신됩니다. 작업 중에는 웹 연결이 잠시 끊길 수 있습니다.`})]})]}),L(`div`,{"aria-hidden":`true`,class:`ssh-update-progress-track mt-4`,children:L(`span`,{class:`ssh-update-progress-bar`})})]}):null,i&&!n?L(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-800`,children:[L(`p`,{class:`m-0 font-bold`,children:i}),L(`button`,{class:`mt-3 rounded-lg border border-rose-300 bg-white px-3 py-2 text-xs font-extrabold text-rose-700`,onClick:u,type:`button`,children:`다시 시도`})]}):a&&!n?L(`div`,{class:`mt-5 flex items-center gap-3 rounded-xl bg-slate-50 p-4 text-sm font-bold text-slate-500`,children:[L(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-xl bg-white text-teal-700 ring-1 ring-inset ring-slate-200`,children:L(z,{"aria-hidden":`true`,class:`size-4 animate-spin`})}),L(`span`,{children:`관리 소프트웨어 업데이트 상태를 확인하고 있습니다.`})]}):n?L(v,{children:[n.lastError&&A?L(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 sm:p-5`,children:L(`div`,{class:`flex items-start gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-rose-700 ring-1 ring-inset ring-rose-200`,children:L(V,{class:`size-5`})}),L(`div`,{class:`min-w-0 flex-1`,children:[L(`p`,{class:`m-0 text-sm font-extrabold text-rose-950`,children:A.title}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-rose-800`,children:A.description}),n.lastError.at?L(`p`,{class:`mt-2 mb-0 text-xs font-bold text-rose-700`,children:[`발생 시각 `,Kr(n.lastError.at)]}):null,L(`details`,{class:`mt-3 rounded-lg border border-rose-200 bg-white p-3`,children:[L(`summary`,{class:`cursor-pointer text-xs font-extrabold text-rose-800`,children:`상세 정보 보기`}),L(`div`,{class:`mt-3 grid gap-2 text-xs leading-5 text-slate-600`,children:[L(`p`,{class:`m-0`,children:[`오류 코드 `,L(`strong`,{class:`font-black text-slate-900`,children:n.lastError.code})]}),L(`pre`,{class:`m-0 max-h-40 overflow-auto whitespace-pre-wrap break-words rounded-lg bg-slate-50 p-3 font-mono text-[11px] leading-5 text-slate-700`,children:n.lastError.message})]})]})]})]})}):null,C?L(`div`,{class:`mt-5 flex flex-col gap-3 rounded-xl border border-amber-200 bg-amber-50 p-4 sm:flex-row sm:items-center sm:justify-between`,children:[L(`div`,{class:`min-w-0`,children:[L(`strong`,{class:`block text-sm font-black text-amber-900`,children:`관리 소프트웨어를 지금 업데이트할까요?`}),L(`span`,{class:`mt-1 block text-xs leading-5 text-amber-800`,children:`설치 중 SmartSafeHub 웹 화면 연결이 잠시 끊길 수 있습니다.`})]}),L(`div`,{class:`flex shrink-0 gap-2`,children:[L(`button`,{class:`min-h-10 rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-extrabold text-slate-700`,disabled:D,onClick:()=>w(!1),type:`button`,children:`취소`}),L(`button`,{class:`min-h-10 rounded-lg bg-amber-700 px-3 py-2 text-xs font-extrabold text-white disabled:opacity-60`,disabled:D,onClick:()=>{w(!1),l()},type:`button`,children:`설치 시작`})]})]}):null]}):null]}),n?L(`div`,{class:`px-5 pb-5 sm:px-6 sm:pb-6`,children:[L(`h3`,{class:`mb-3 text-sm font-black text-slate-950`,children:`현재 상태`}),L(`dl`,{class:`grid overflow-hidden rounded-xl border border-slate-200 bg-slate-200 sm:grid-cols-2`,children:[L(`div`,{class:`bg-white p-4`,children:[L(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Installed`}),L(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black text-slate-950`,children:j?.installedVersion||`미확인`})]}),L(`div`,{class:`bg-white p-4`,children:[L(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Available`}),L(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black ${j?.updateAvailable||O?`text-amber-700`:`text-slate-950`}`,children:!n.lastCheckAt||k&&!j?.updateAvailable?`미확인`:j?.updateAvailable&&j.availableVersion?j.availableVersion:O?`확인 필요`:`최신 버전`})]}),L(`div`,{class:`bg-white p-4`,children:[L(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Last check`}),L(`dd`,{class:`mt-2 mb-0 ml-0 text-sm font-black text-slate-950`,children:L(Ar,{fallback:`아직 없음`,timestamp:n.lastCheckAt})})]}),L(`div`,{class:`bg-white p-4`,children:[L(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Auto install`}),L(`dd`,{class:`mt-2 mb-0 ml-0 text-sm font-black text-slate-950`,children:n.settings.autoInstall?`켜짐 · ${n.settings.autoInstallTime}`:`꺼짐`})]})]})]}):null,n&&n.updateCount===0&&n.lastCheckAt?O?L(`section`,{class:`mx-5 mb-5 rounded-xl border border-amber-200 bg-amber-50 p-4 sm:mx-6 sm:mb-6 sm:p-5`,"data-section":`software-update-result`,children:L(`div`,{class:`flex gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-amber-700 ring-1 ring-inset ring-amber-200`,children:L(V,{class:`size-5`})}),L(`div`,{children:[L(`h3`,{class:`m-0 text-base font-black text-amber-950`,children:`업데이트 확인이 지연되고 있습니다.`}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:[`마지막 업데이트 확인: `,L(Ar,{timestamp:n.lastCheckAt}),`. 자동 확인 상태를 점검하거나 업데이트 확인을 실행해 주세요.`]})]})]})}):L(`section`,{class:`mx-5 mb-5 rounded-xl border border-emerald-200 bg-emerald-50 p-4 sm:mx-6 sm:mb-6 sm:p-5`,"data-section":`software-update-result`,children:L(`div`,{class:`flex gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-emerald-700`,children:L(B,{class:`size-5`})}),L(`div`,{children:[L(`h3`,{class:`m-0 text-base font-black text-emerald-950`,children:`최신 버전을 사용 중입니다.`}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-emerald-800`,children:[`마지막 업데이트 확인: `,L(Ar,{timestamp:n.lastCheckAt}),`.`]}),ee?L(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-emerald-800`,"data-section":`software-firmware-match`,children:[`새 펌웨어에도 동일한 관리 소프트웨어(`,M,`)가 포함되어 있어 별도로 업데이트할 필요가 없습니다.`]}):null]})]})}):null,n&&n.updateCount===0&&!n.lastCheckAt?L(`section`,{class:`mx-5 mb-5 rounded-xl border border-slate-200 bg-slate-50 p-4 sm:mx-6 sm:mb-6 sm:p-5`,"data-section":`software-update-result`,children:L(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between`,children:[L(`div`,{class:`flex min-w-0 gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-slate-500 ring-1 ring-inset ring-slate-200`,children:L(z,{class:`size-5`})}),L(`div`,{class:`min-w-0`,children:[L(`h3`,{class:`m-0 text-base font-black text-slate-900`,children:`업데이트 상태를 아직 확인하지 않았습니다.`}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`업데이트 확인을 실행하면 현재 설치 버전과 사용 가능한 최신 버전을 확인합니다.`})]})]}),L(`button`,{class:`inline-flex min-h-10 shrink-0 items-center justify-center gap-2 rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-extrabold text-slate-800 transition hover:border-teal-300 hover:text-teal-700 disabled:cursor-wait disabled:opacity-60`,disabled:D,onClick:s,type:`button`,children:[L(z,{class:`size-4`}),`지금 확인`]})]})}):null]}),n?L(v,{children:[L(`section`,{class:`min-w-0 border-t border-slate-200 p-5 sm:p-6 xl:col-start-2 xl:row-start-1 xl:row-span-2 xl:self-stretch xl:border-t-0 xl:border-l`,"data-section":`software-update-settings`,children:[L(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[L(`div`,{class:`flex min-w-0 gap-4`,children:[L(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-600`,children:L(Ye,{class:`size-5`})}),L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Settings`}),L(`h3`,{class:`mt-2 mb-0 text-lg font-black text-slate-950`,children:`자동 업데이트`}),L(`p`,{class:`mt-2 mb-0 max-w-3xl text-sm leading-6 text-slate-500`,children:`관리 소프트웨어의 자동 확인과 자동 설치를 설정합니다.`})]})]}),x?L(`span`,{"aria-live":`polite`,class:`inline-flex shrink-0 self-start items-center gap-1.5 rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs font-extrabold text-amber-800`,role:`status`,children:[L(V,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}):null]}),L(`div`,{class:`ssh-software-settings-grid mt-4`,children:[L(`div`,{class:`ssh-software-settings-channel rounded-xl border border-slate-200 p-4 sm:p-5`,children:L(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[L(`div`,{class:`min-w-0`,children:[L(`strong`,{class:`block text-sm font-black text-slate-950`,children:`업데이트 채널`}),L(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`현재 관리 소프트웨어 업데이트를 확인하는 배포 채널입니다.`})]}),L(`span`,{class:`inline-flex min-h-9 shrink-0 items-center justify-center rounded-full px-3 py-1.5 text-xs font-extrabold ring-1 ring-inset ${Jr(n.settings.channel)}`,title:`현재 업데이트 확인 채널`,children:qr(n.settings.channel)})]})}),L(`div`,{class:`min-w-0 overflow-hidden rounded-xl border border-slate-200 p-4 sm:p-5`,children:[L(`div`,{class:`flex min-w-0 items-start justify-between gap-4`,children:[L(`div`,{class:`min-w-0`,children:[L(`strong`,{class:`block text-sm font-black text-slate-950`,children:`자동으로 업데이트 확인`}),L(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`선택한 주기에 새 관리 소프트웨어 버전을 확인합니다.`})]}),L($r,{checked:f,label:`자동 업데이트 확인`,onChange:e=>{p(e),S(te({checkEnabled:e}))}})]}),L(`div`,{class:`mt-4 block`,children:[L(`span`,{class:`text-xs font-extrabold text-slate-700`,children:`확인 주기`}),L(Nn,{ariaLabel:`업데이트 확인 주기`,className:`mt-2`,disabled:!f,id:`software-update-check-interval`,onChange:e=>{let t=Number(e);h(t),S(te({checkIntervalSeconds:t}))},options:Wr.map(e=>({value:String(e.value),label:e.label})),value:String(m),variant:`emphasized`})]})]}),L(`div`,{class:`min-w-0 overflow-hidden rounded-xl border border-slate-200 p-4 sm:p-5`,children:[L(`div`,{class:`flex min-w-0 items-start justify-between gap-4`,children:[L(`div`,{class:`min-w-0`,children:[L(`strong`,{class:`block text-sm font-black text-slate-950`,children:`자동으로 업데이트 설치`}),L(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`새 버전이 있으면 지정한 시각에 자동으로 설치합니다.`})]}),L($r,{checked:g,label:`자동 업데이트 설치`,onChange:e=>{_(e),S(te({autoInstall:e}))}})]}),L(`div`,{class:`mt-4 min-w-0`,children:[L(`span`,{class:`text-xs font-extrabold text-slate-700`,children:`설치 시각`}),L(Vr,{ariaLabel:`자동 업데이트 설치 시각`,className:`mt-2`,disabled:!g,id:`software-update-auto-install-time`,onChange:e=>{b(e),S(te({autoInstallTime:e}))},value:y,variant:`emphasized`})]})]})]}),g?L(`p`,{class:`mt-4 mb-0 rounded-xl bg-amber-50 px-4 py-3 text-xs font-bold leading-5 text-amber-800`,children:`자동 설치 중에는 SmartSafeHub 웹 화면 연결이 잠시 끊길 수 있습니다.`}):null,L(`div`,{class:`mt-5 flex flex-col gap-3 border-t border-slate-100 pt-5 sm:flex-row sm:items-center sm:justify-between`,children:[L(`p`,{class:`m-0 text-xs leading-5 text-slate-500`,children:[`마지막 설치: `,Kr(n.lastInstallAt)]}),L(`button`,{class:`inline-flex min-h-11 items-center justify-center rounded-xl bg-slate-900 px-4 py-2.5 text-sm font-extrabold text-white transition hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50`,disabled:!x||e===`settings`||D,onClick:()=>void ne(),type:`button`,children:e===`settings`?`저장 중`:`자동 업데이트 설정 저장`})]})]}),n.updateCount>0?L(`section`,{class:`border-t border-slate-200 p-5 sm:p-6 xl:col-start-1 xl:row-start-2`,"data-section":`software-update-release-notes`,children:[L(`div`,{class:`flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700`,children:`Release notes`}),L(`h3`,{class:`mt-2 mb-0 text-lg font-black text-slate-950`,children:`업데이트 내용`}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500`,children:`현재 설치된 버전 이후 포함되는 주요 변경 사항입니다.`})]}),n.releaseNotes.length?L(`span`,{class:`shrink-0 rounded-full bg-slate-100 px-3 py-1.5 text-xs font-extrabold text-slate-600`,children:n.releaseNotes.length===1?`1개 릴리즈`:`${n.releaseNotes.length}개 릴리즈`}):null]}),n.releaseNotes.length?L(`div`,{class:`mt-5 grid gap-3`,children:[n.releaseNotes.map(e=>L(`article`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4 sm:p-5`,children:[L(`div`,{class:`flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-4`,children:[L(`strong`,{class:`break-all text-sm font-black text-slate-950`,children:e.version}),e.date?L(`span`,{class:`text-xs font-bold text-slate-500`,children:e.date}):null]}),e.summary?L(`p`,{class:`mt-3 mb-0 text-sm leading-6 text-slate-700`,children:e.summary}):null,e.sections.length?L(`details`,{class:`mt-4 rounded-lg border border-slate-200 bg-white p-3`,open:n.releaseNotes.length===1,children:[L(`summary`,{class:`cursor-pointer text-xs font-extrabold text-slate-800`,children:`자세한 변경 사항`}),L(`div`,{class:`mt-3 grid gap-4`,children:e.sections.map(t=>L(`section`,{children:[L(`h4`,{class:`m-0 text-xs font-extrabold text-slate-700`,children:t.title}),L(`ul`,{class:`mt-2 mb-0 space-y-1.5 pl-5 text-xs leading-5 text-slate-600`,children:t.items.map((n,r)=>L(`li`,{children:n},`${e.version}-${t.title}-${r}`))})]},`${e.version}-${t.title}`))})]}):null]},e.version)),n.releaseNotesComplete?null:L(`p`,{class:`m-0 rounded-xl bg-amber-50 px-4 py-3 text-xs font-bold leading-5 text-amber-800`,children:`일부 릴리즈 노트를 불러오지 못했습니다. 업데이트 확인과 설치는 계속 사용할 수 있습니다.`})]}):L(`p`,{class:`mt-4 mb-0 rounded-xl bg-slate-50 px-4 py-3 text-sm text-slate-500`,children:`릴리즈 노트를 불러오지 못했습니다. 업데이트 확인과 설치는 계속 사용할 수 있습니다.`})]}):null]}):null]})}function ti({action:e,actionError:t,data:n,error:r,firmware:i,loading:a,message:o,onCheck:s,onDismissFeedback:c,onInstall:l,onRetry:u,onSaveSettings:d}){return L(`section`,{class:`min-w-0 space-y-5`,children:[L(Lr,{action:i.action,actionError:i.actionError,data:i.data,software:n,error:i.error,loading:i.loading,message:i.message,reconnecting:i.reconnecting,uploadProgress:i.uploadProgress,onCheck:()=>void i.check(),onDiscard:i.discard,onDismissFeedback:i.dismissFeedback,onInstall:i.install,onPrepare:()=>void i.prepare(),onRetry:()=>void i.refresh(),onUpload:i.upload}),L(ei,{firmware:i.data,action:e,actionError:t,data:n,error:r,loading:a,message:o,onCheck:s,onDismissFeedback:c,onInstall:l,onRetry:u,onSaveSettings:d})]})}var ni={"#home":`home`,"#activity":`activity`,"#network":`network`,"#network/internet":`network`,"#network/lan":`network`,"#wan":`network`,"#lan":`network`,"#wifi":`wifi`,"#iptv":`iptv`,"#devices":`devices`,"#safeshield":`safeshield`,"#rules":`rules`,"#account":`account`,"#system":`system`,"#settings":`settings`,"#settings/diagnostics":`settings`,"#settings/time":`settings`,"#settings/management":`settings`};function ri(){return ni[window.location.hash]??`home`}function ii(){let[e,t]=F(ri);return I(()=>{let e=()=>t(ri());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),e}function ai(e){return e===`#lan`||e===`#network/lan`?`lan`:`internet`}function oi(e){return e===`#settings/time`?`time`:e===`#settings/management`?`management`:`diagnostics`}function si(){let e=window.location.hash;return{network:ai(e),settings:oi(e)}}function ci(){let[e,t]=F(si);return I(()=>{let e=()=>t(si());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),e}function li({active:e,label:t,tabs:n}){return L(`nav`,{"aria-label":t,class:`mb-5 min-w-0`,role:`tablist`,children:L(`div`,{class:`grid w-full min-w-0 gap-2 ${n.length===3?`grid-cols-3 sm:max-w-[592px]`:`grid-cols-2 sm:max-w-[392px]`}`,children:n.map(t=>{let r=t.id===e;return L(`a`,{"aria-controls":`${t.id}-panel`,"aria-selected":r,class:`flex min-h-11 min-w-0 items-center justify-center rounded-xl border px-2 py-2.5 text-center text-[11px] font-extrabold leading-5 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-500 sm:px-4 sm:text-sm ${r?`border-teal-200 bg-teal-50 text-teal-800`:`border-slate-200 bg-white text-slate-600 hover:border-teal-200 hover:text-teal-700`}`,href:t.href,id:`${t.id}-tab`,role:`tab`,tabIndex:r?0:-1,onKeyDown:e=>{if(![`ArrowLeft`,`ArrowRight`,`Home`,`End`].includes(e.key))return;e.preventDefault();let r=n.findIndex(e=>e.id===t.id),i=e.key===`Home`?0:e.key===`End`?n.length-1:(r+(e.key===`ArrowRight`?1:-1)+n.length)%n.length,a=e.currentTarget.parentElement?.children[i];a?.focus(),a?.click()},children:t.label},t.id)})})})}var ui=[{value:`all`,label:`전체`},{value:`online`,label:`현재 연결`},{value:`wifi`,label:`Wi-Fi`},{value:`ethernet`,label:`유선/기타`},{value:`offline`,label:`최근 기기`}],di=new Intl.Collator(`ko`),fi=new Intl.DateTimeFormat(`ko-KR`,{month:`numeric`,day:`numeric`,hour:`2-digit`,minute:`2-digit`});function pi(e){switch(e){case`wifi`:return`Wi-Fi`;case`ethernet`:return`유선/기타`;default:return`연결 방식 미확인`}}function mi(e){return e.hostname??e.ipv4Address??`이름 없는 기기`}function hi(e){return e===null?null:e>=-50?`매우 좋음`:e>=-60?`좋음`:e>=-70?`보통`:`약함`}function gi(e){if(e===null||e<0)return null;let t=Math.floor(e/86400),n=Math.floor(e%86400/3600),r=Math.floor(e%3600/60);return t>0?`${t}일 ${n}시간`:n>0?`${n}시간 ${r}분`:`${Math.max(r,1)}분`}function _i(e){return e.leaseActive?e.leaseExpiresAt===null?`DHCP 임대 활성`:`DHCP ${fi.format(new Date(e.leaseExpiresAt*1e3))}까지`:null}function vi(e,t){switch(t){case`online`:return e.online;case`wifi`:return e.online&&e.connection===`wifi`;case`ethernet`:return e.online&&e.connection===`ethernet`;case`offline`:return!e.online;default:return!0}}function yi({device:e}){return L(`span`,{class:`inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-extrabold ring-1 ring-inset ${e.online?`bg-emerald-50 text-emerald-700 ring-emerald-200`:`bg-slate-100 text-slate-600 ring-slate-200`}`,children:[L(`span`,{class:`size-2 rounded-full ${e.online?`bg-emerald-500`:`bg-slate-400`}`}),e.online?`현재 연결`:`최근 임대`]})}function bi({device:e}){let t=hi(e.signalDbm),n=gi(e.connectedSeconds),r=_i(e);return L(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:[L(`div`,{class:`flex min-w-0 flex-col items-start gap-4 sm:flex-row sm:justify-between`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3`,children:[L(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl ${e.connection===`wifi`?`bg-teal-50 text-teal-700`:e.connection===`ethernet`?`bg-blue-50 text-blue-700`:`bg-slate-100 text-slate-600`}`,children:e.connection===`wifi`?L(Xe,{class:`size-6`}):e.connection===`ethernet`?L(Qe,{class:`size-6`}):L(Ge,{class:`size-6`})}),L(`div`,{class:`min-w-0`,children:[L(`h3`,{class:`m-0 break-words text-base font-extrabold text-slate-950`,children:mi(e)}),L(`p`,{class:`mt-1 mb-0 break-all font-mono text-xs leading-5 text-slate-500`,children:e.mac})]})]}),L(`span`,{class:`shrink-0 self-start`,children:L(yi,{device:e})})]}),L(`dl`,{class:`mt-5 grid gap-3 text-sm sm:grid-cols-2`,children:[L(`div`,{class:`rounded-xl bg-slate-50 p-3.5`,children:[L(`dt`,{class:`text-xs font-bold text-slate-500`,children:`IP 주소`}),L(`dd`,{class:`mt-1.5 mb-0 ml-0 font-extrabold text-slate-900`,children:e.ipv4Address??`확인되지 않음`})]}),L(`div`,{class:`rounded-xl bg-slate-50 p-3.5`,children:[L(`dt`,{class:`text-xs font-bold text-slate-500`,children:`연결 방식`}),L(`dd`,{class:`mt-1.5 mb-0 ml-0 font-extrabold text-slate-900`,children:pi(e.connection)})]})]}),L(`div`,{class:`mt-4 flex flex-wrap gap-2 text-xs font-bold text-slate-600`,children:[e.ssid?L(`span`,{class:`rounded-full bg-teal-50 px-3 py-1.5 text-teal-800`,children:[e.ssid,e.bandLabel?` · ${e.bandLabel}`:``]}):null,t?L(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:[`신호 `,t,` · `,e.signalDbm,` dBm`]}):null,n?L(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:[`연결 `,n]}):null,r?L(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:r}):null,e.interface?L(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:e.interface}):null]})]})}function xi({data:e,error:t,loading:n,onRetry:r}){let[i,a]=F(``),[o,s]=F(`all`),c=Te(()=>{if(!e)return[];let t=i.trim().toLocaleLowerCase();return[...e.devices].filter(e=>vi(e,o)).filter(e=>!t||[e.hostname,e.ipv4Address,e.mac,e.ssid].filter(e=>!!e).some(e=>e.toLocaleLowerCase().includes(t))).sort((e,t)=>e.online===t.online?e.connection===t.connection?di.compare(mi(e),mi(t)):e.connection===`wifi`?-1:1:e.online?-1:1)},[e,o,i]);return n?L(Rn,{}):t?L(zn,{message:t,onRetry:r}):e?L(v,{children:[L(`section`,{"aria-label":`연결 기기 요약`,class:`grid gap-4 sm:grid-cols-2 xl:grid-cols-4`,children:[L(Si,{description:`${e.totals.offline}대는 최근 DHCP 임대에서 확인`,icon:L(Ge,{class:`size-6`}),label:`확인된 기기`,value:e.totals.known}),L(Si,{description:`Wi-Fi 또는 ARP에서 현재 확인됨`,icon:L(tt,{class:`size-6`}),label:`현재 연결`,value:e.totals.online}),L(Si,{description:`무선 AP에 연결된 기기`,icon:L(Xe,{class:`size-6`}),label:`Wi-Fi`,value:e.totals.wireless}),L(Si,{description:`LAN ARP에서 확인된 비무선 기기`,icon:L(Qe,{class:`size-6`}),label:`유선/기타`,value:e.totals.ethernet})]}),L(`section`,{class:`mt-5 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:[L(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-xs font-bold uppercase tracking-[0.16em] text-slate-500`,children:`Device inventory`}),L(`h2`,{class:`mt-2 mb-0 text-xl font-extrabold text-slate-950`,children:`기기 목록`})]}),L(`label`,{class:`relative block w-full lg:max-w-sm`,children:[L(`span`,{class:`sr-only`,children:`기기 검색`}),L(`span`,{class:`pointer-events-none absolute inset-y-0 left-0 flex w-11 items-center justify-center text-slate-400`,children:L(ft,{class:`size-5`})}),L(`input`,{class:`min-h-11 w-full rounded-xl border-2 border-slate-300 bg-slate-50 py-2.5 pr-4 pl-11 text-sm font-semibold text-slate-950 shadow-inner outline-none transition placeholder:text-slate-400 focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100`,onInput:e=>a(e.currentTarget.value),placeholder:`이름, IP, MAC 또는 SSID 검색`,type:`search`,value:i})]})]}),L(`div`,{class:`ssh-touch-scroll mt-4 flex gap-2 overflow-x-auto pb-1`,role:`group`,"aria-label":`기기 필터`,children:ui.map(e=>L(`button`,{"aria-pressed":o===e.value,class:`min-h-10 shrink-0 rounded-xl border px-4 py-2 text-sm font-extrabold transition ${o===e.value?`border-teal-700 bg-teal-700 text-white`:`border-slate-200 bg-white text-slate-600 hover:bg-slate-50 hover:text-slate-950`}`,onClick:()=>s(e.value),type:`button`,children:e.label},e.value))})]}),c.length?L(`section`,{"aria-live":`polite`,class:`mt-5 grid gap-4 lg:grid-cols-2`,children:c.map(e=>L(bi,{device:e},e.id))}):L(`section`,{class:`mt-5 rounded-2xl border border-dashed border-slate-300 bg-white p-5 text-center sm:p-8`,children:[L(Ge,{class:`mx-auto size-10 text-slate-400`}),L(`h2`,{class:`mt-4 mb-0 text-lg font-extrabold text-slate-950`,children:`조건에 맞는 기기가 없습니다`}),L(`p`,{class:`mt-2 mb-0 text-sm text-slate-600`,children:`검색어 또는 필터를 변경해 주세요.`})]}),L(`p`,{class:`mt-5 mb-0 rounded-xl bg-slate-100 px-4 py-3 text-xs leading-5 text-slate-600`,children:`유선/기타 상태는 ARP 테이블을 기준으로 하며, 최근 DHCP 임대는 기기가 연결 해제된 뒤에도 일정 시간 남을 수 있습니다.`})]}):null}function Si({description:e,icon:t,label:n,value:r}){return L(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:[L(`div`,{class:`flex items-start justify-between gap-4`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-xs font-bold uppercase tracking-[0.14em] text-slate-500`,children:n}),L(`p`,{class:`mt-3 mb-0 text-3xl font-black tracking-tight text-slate-950`,children:r})]}),L(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:t})]}),L(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:e})]})}function Ci(e){return e+.5|0}var wi=(e,t,n)=>Math.max(Math.min(e,n),t);function Ti(e){return wi(Ci(e*2.55),0,255)}function Ei(e){return wi(Ci(e*255),0,255)}function Di(e){return wi(Ci(e/2.55)/100,0,1)}function Oi(e){return wi(Ci(e*100),0,100)}var ki={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Ai=[...`0123456789ABCDEF`],ji=e=>Ai[e&15],Mi=e=>Ai[(e&240)>>4]+Ai[e&15],Ni=e=>(e&240)>>4==(e&15),Pi=e=>Ni(e.r)&&Ni(e.g)&&Ni(e.b)&&Ni(e.a);function Fi(e){var t=e.length,n;return e[0]===`#`&&(t===4||t===5?n={r:255&ki[e[1]]*17,g:255&ki[e[2]]*17,b:255&ki[e[3]]*17,a:t===5?ki[e[4]]*17:255}:(t===7||t===9)&&(n={r:ki[e[1]]<<4|ki[e[2]],g:ki[e[3]]<<4|ki[e[4]],b:ki[e[5]]<<4|ki[e[6]],a:t===9?ki[e[7]]<<4|ki[e[8]]:255})),n}var Ii=(e,t)=>e<255?t(e):``;function Li(e){var t=Pi(e)?ji:Mi;return e?`#`+t(e.r)+t(e.g)+t(e.b)+Ii(e.a,t):void 0}var Ri=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function zi(e,t,n){let r=t*Math.min(n,1-n),i=(t,i=(t+e/30)%12)=>n-r*Math.max(Math.min(i-3,9-i,1),-1);return[i(0),i(8),i(4)]}function Bi(e,t,n){let r=(r,i=(r+e/60)%6)=>n-n*t*Math.max(Math.min(i,4-i,1),0);return[r(5),r(3),r(1)]}function Vi(e,t,n){let r=zi(e,1,.5),i;for(t+n>1&&(i=1/(t+n),t*=i,n*=i),i=0;i<3;i++)r[i]*=1-t-n,r[i]+=t;return r}function Hi(e,t,n,r,i){return e===i?(t-n)/r+(t.5?l/(2-i-a):l/(i+a),s=Hi(t,n,r,l,i),s=s*60+.5),[s|0,c||0,o]}function Wi(e,t,n,r){return(Array.isArray(t)?e(t[0],t[1],t[2]):e(t,n,r)).map(Ei)}function Gi(e,t,n){return Wi(zi,e,t,n)}function Ki(e,t,n){return Wi(Vi,e,t,n)}function qi(e,t,n){return Wi(Bi,e,t,n)}function Ji(e){return(e%360+360)%360}function Yi(e){let t=Ri.exec(e),n=255,r;if(!t)return;t[5]!==r&&(n=t[6]?Ti(+t[5]):Ei(+t[5]));let i=Ji(+t[2]),a=t[3]/100,o=t[4]/100;return r=t[1]===`hwb`?Ki(i,a,o):t[1]===`hsv`?qi(i,a,o):Gi(i,a,o),{r:r[0],g:r[1],b:r[2],a:n}}function Xi(e,t){var n=Ui(e);n[0]=Ji(n[0]+t),n=Gi(n),e.r=n[0],e.g=n[1],e.b=n[2]}function Zi(e){if(!e)return;let t=Ui(e),n=t[0],r=Oi(t[1]),i=Oi(t[2]);return e.a<255?`hsla(${n}, ${r}%, ${i}%, ${Di(e.a)})`:`hsl(${n}, ${r}%, ${i}%)`}var Qi={x:`dark`,Z:`light`,Y:`re`,X:`blu`,W:`gr`,V:`medium`,U:`slate`,A:`ee`,T:`ol`,S:`or`,B:`ra`,C:`lateg`,D:`ights`,R:`in`,Q:`turquois`,E:`hi`,P:`ro`,O:`al`,N:`le`,M:`de`,L:`yello`,F:`en`,K:`ch`,G:`arks`,H:`ea`,I:`ightg`,J:`wh`},$i={OiceXe:`f0f8ff`,antiquewEte:`faebd7`,aqua:`ffff`,aquamarRe:`7fffd4`,azuY:`f0ffff`,beige:`f5f5dc`,bisque:`ffe4c4`,black:`0`,blanKedOmond:`ffebcd`,Xe:`ff`,XeviTet:`8a2be2`,bPwn:`a52a2a`,burlywood:`deb887`,caMtXe:`5f9ea0`,KartYuse:`7fff00`,KocTate:`d2691e`,cSO:`ff7f50`,cSnflowerXe:`6495ed`,cSnsilk:`fff8dc`,crimson:`dc143c`,cyan:`ffff`,xXe:`8b`,xcyan:`8b8b`,xgTMnPd:`b8860b`,xWay:`a9a9a9`,xgYF:`6400`,xgYy:`a9a9a9`,xkhaki:`bdb76b`,xmagFta:`8b008b`,xTivegYF:`556b2f`,xSange:`ff8c00`,xScEd:`9932cc`,xYd:`8b0000`,xsOmon:`e9967a`,xsHgYF:`8fbc8f`,xUXe:`483d8b`,xUWay:`2f4f4f`,xUgYy:`2f4f4f`,xQe:`ced1`,xviTet:`9400d3`,dAppRk:`ff1493`,dApskyXe:`bfff`,dimWay:`696969`,dimgYy:`696969`,dodgerXe:`1e90ff`,fiYbrick:`b22222`,flSOwEte:`fffaf0`,foYstWAn:`228b22`,fuKsia:`ff00ff`,gaRsbSo:`dcdcdc`,ghostwEte:`f8f8ff`,gTd:`ffd700`,gTMnPd:`daa520`,Way:`808080`,gYF:`8000`,gYFLw:`adff2f`,gYy:`808080`,honeyMw:`f0fff0`,hotpRk:`ff69b4`,RdianYd:`cd5c5c`,Rdigo:`4b0082`,ivSy:`fffff0`,khaki:`f0e68c`,lavFMr:`e6e6fa`,lavFMrXsh:`fff0f5`,lawngYF:`7cfc00`,NmoncEffon:`fffacd`,ZXe:`add8e6`,ZcSO:`f08080`,Zcyan:`e0ffff`,ZgTMnPdLw:`fafad2`,ZWay:`d3d3d3`,ZgYF:`90ee90`,ZgYy:`d3d3d3`,ZpRk:`ffb6c1`,ZsOmon:`ffa07a`,ZsHgYF:`20b2aa`,ZskyXe:`87cefa`,ZUWay:`778899`,ZUgYy:`778899`,ZstAlXe:`b0c4de`,ZLw:`ffffe0`,lime:`ff00`,limegYF:`32cd32`,lRF:`faf0e6`,magFta:`ff00ff`,maPon:`800000`,VaquamarRe:`66cdaa`,VXe:`cd`,VScEd:`ba55d3`,VpurpN:`9370db`,VsHgYF:`3cb371`,VUXe:`7b68ee`,VsprRggYF:`fa9a`,VQe:`48d1cc`,VviTetYd:`c71585`,midnightXe:`191970`,mRtcYam:`f5fffa`,mistyPse:`ffe4e1`,moccasR:`ffe4b5`,navajowEte:`ffdead`,navy:`80`,Tdlace:`fdf5e6`,Tive:`808000`,TivedBb:`6b8e23`,Sange:`ffa500`,SangeYd:`ff4500`,ScEd:`da70d6`,pOegTMnPd:`eee8aa`,pOegYF:`98fb98`,pOeQe:`afeeee`,pOeviTetYd:`db7093`,papayawEp:`ffefd5`,pHKpuff:`ffdab9`,peru:`cd853f`,pRk:`ffc0cb`,plum:`dda0dd`,powMrXe:`b0e0e6`,purpN:`800080`,YbeccapurpN:`663399`,Yd:`ff0000`,Psybrown:`bc8f8f`,PyOXe:`4169e1`,saddNbPwn:`8b4513`,sOmon:`fa8072`,sandybPwn:`f4a460`,sHgYF:`2e8b57`,sHshell:`fff5ee`,siFna:`a0522d`,silver:`c0c0c0`,skyXe:`87ceeb`,UXe:`6a5acd`,UWay:`708090`,UgYy:`708090`,snow:`fffafa`,sprRggYF:`ff7f`,stAlXe:`4682b4`,tan:`d2b48c`,teO:`8080`,tEstN:`d8bfd8`,tomato:`ff6347`,Qe:`40e0d0`,viTet:`ee82ee`,JHt:`f5deb3`,wEte:`ffffff`,wEtesmoke:`f5f5f5`,Lw:`ffff00`,LwgYF:`9acd32`};function ea(){let e={},t=Object.keys($i),n=Object.keys(Qi),r,i,a,o,s;for(r=0;r>16&255,a>>8&255,a&255]}return e}var ta;function na(e){ta||(ta=ea(),ta.transparent=[0,0,0,0]);let t=ta[e.toLowerCase()];return t&&{r:t[0],g:t[1],b:t[2],a:t.length===4?t[3]:255}}var ra=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;function ia(e){let t=ra.exec(e),n=255,r,i,a;if(t){if(t[7]!==r){let e=+t[7];n=t[8]?Ti(e):wi(e*255,0,255)}return r=+t[1],i=+t[3],a=+t[5],r=255&(t[2]?Ti(r):wi(r,0,255)),i=255&(t[4]?Ti(i):wi(i,0,255)),a=255&(t[6]?Ti(a):wi(a,0,255)),{r,g:i,b:a,a:n}}}function aa(e){return e&&(e.a<255?`rgba(${e.r}, ${e.g}, ${e.b}, ${Di(e.a)})`:`rgb(${e.r}, ${e.g}, ${e.b})`)}var oa=e=>e<=.0031308?e*12.92:e**(1/2.4)*1.055-.055,sa=e=>e<=.04045?e/12.92:((e+.055)/1.055)**2.4;function ca(e,t,n){let r=sa(Di(e.r)),i=sa(Di(e.g)),a=sa(Di(e.b));return{r:Ei(oa(r+n*(sa(Di(t.r))-r))),g:Ei(oa(i+n*(sa(Di(t.g))-i))),b:Ei(oa(a+n*(sa(Di(t.b))-a))),a:e.a+n*(t.a-e.a)}}function la(e,t,n){if(e){let r=Ui(e);r[t]=Math.max(0,Math.min(r[t]+r[t]*n,t===0?360:1)),r=Gi(r),e.r=r[0],e.g=r[1],e.b=r[2]}}function ua(e,t){return e&&Object.assign(t||{},e)}function da(e){var t={r:0,g:0,b:0,a:255};return Array.isArray(e)?e.length>=3&&(t={r:e[0],g:e[1],b:e[2],a:255},e.length>3&&(t.a=Ei(e[3]))):(t=ua(e,{r:0,g:0,b:0,a:1}),t.a=Ei(t.a)),t}function fa(e){return e.charAt(0)===`r`?ia(e):Yi(e)}var pa=class e{constructor(t){if(t instanceof e)return t;let n=typeof t,r;n===`object`?r=da(t):n===`string`&&(r=Fi(t)||na(t)||fa(t)),this._rgb=r,this._valid=!!r}get valid(){return this._valid}get rgb(){var e=ua(this._rgb);return e&&(e.a=Di(e.a)),e}set rgb(e){this._rgb=da(e)}rgbString(){return this._valid?aa(this._rgb):void 0}hexString(){return this._valid?Li(this._rgb):void 0}hslString(){return this._valid?Zi(this._rgb):void 0}mix(e,t){if(e){let n=this.rgb,r=e.rgb,i,a=t===i?.5:t,o=2*a-1,s=n.a-r.a,c=((o*s===-1?o:(o+s)/(1+o*s))+1)/2;i=1-c,n.r=255&c*n.r+i*r.r+.5,n.g=255&c*n.g+i*r.g+.5,n.b=255&c*n.b+i*r.b+.5,n.a=a*n.a+(1-a)*r.a,this.rgb=n}return this}interpolate(e,t){return e&&(this._rgb=ca(this._rgb,e._rgb,t)),this}clone(){return new e(this.rgb)}alpha(e){return this._rgb.a=Ei(e),this}clearer(e){let t=this._rgb;return t.a*=1-e,this}greyscale(){let e=this._rgb;return e.r=e.g=e.b=Ci(e.r*.3+e.g*.59+e.b*.11),this}opaquer(e){let t=this._rgb;return t.a*=1+e,this}negate(){let e=this._rgb;return e.r=255-e.r,e.g=255-e.g,e.b=255-e.b,this}lighten(e){return la(this._rgb,2,e),this}darken(e){return la(this._rgb,2,-e),this}saturate(e){return la(this._rgb,1,e),this}desaturate(e){return la(this._rgb,1,-e),this}rotate(e){return Xi(this._rgb,e),this}};function ma(){}var ha=(()=>{let e=0;return()=>e++})();function U(e){return e==null}function W(e){if(Array.isArray&&Array.isArray(e))return!0;let t=Object.prototype.toString.call(e);return t.slice(0,7)===`[object`&&t.slice(-6)===`Array]`}function G(e){return e!==null&&Object.prototype.toString.call(e)===`[object Object]`}function ga(e){return(typeof e==`number`||e instanceof Number)&&isFinite(+e)}function _a(e,t){return ga(e)?e:t}function K(e,t){return e===void 0?t:e}var va=(e,t)=>typeof e==`string`&&e.endsWith(`%`)?parseFloat(e)/100:+e/t,ya=(e,t)=>typeof e==`string`&&e.endsWith(`%`)?parseFloat(e)/100*t:+e;function q(e,t,n){if(e&&typeof e.call==`function`)return e.apply(n,t)}function J(e,t,n,r){let i,a,o;if(W(e)){if(a=e.length,r)for(i=a-1;i>=0;i--)t.call(n,e[i],i);else for(i=0;ie,x:e=>e.x,y:e=>e.y};function Oa(e){let t=e.split(`.`),n=[],r=``;for(let e of t)r+=e,r.endsWith(`\\`)?r=r.slice(0,-1)+`.`:(n.push(r),r=``);return n}function ka(e){let t=Oa(e);return e=>{for(let n of t){if(n===``)break;e=e&&e[n]}return e}}function Aa(e,t){return(Da[t]||(Da[t]=ka(t)))(e)}function ja(e){return e.charAt(0).toUpperCase()+e.slice(1)}var Ma=e=>e!==void 0,Na=e=>typeof e==`function`,Pa=(e,t)=>{if(e.size!==t.size)return!1;for(let n of e)if(!t.has(n))return!1;return!0};function Fa(e){return e.type===`mouseup`||e.type===`click`||e.type===`contextmenu`}var Y=Math.PI,X=2*Y,Ia=X+Y,La=1/0,Ra=Y/180,Z=Y/2,za=Y/4,Ba=Y*2/3,Va=Math.log10,Ha=Math.sign;function Ua(e,t,n){return Math.abs(e-t)e-t).pop(),t}function Ka(e){return typeof e==`symbol`||typeof e==`object`&&!!e&&!(Symbol.toPrimitive in e||`toString`in e||`valueOf`in e)}function qa(e){return!Ka(e)&&!isNaN(parseFloat(e))&&isFinite(e)}function Ja(e,t){let n=Math.round(e);return n-t<=e&&n+t>=e}function Ya(e,t,n){let r,i,a;for(r=0,i=e.length;rc&&l=Math.min(t,n)-r&&e<=Math.max(t,n)+r}function so(e,t,n){n=n||(n=>e[n]1;)a=i+r>>1,n(a)?i=a:r=a;return{lo:i,hi:r}}var co=(e,t,n,r)=>so(e,n,r?r=>{let i=e[r][t];return ie[r][t]so(e,n,r=>e[r][t]>=n);function uo(e,t,n){let r=0,i=e.length;for(;rr&&e[i-1]>n;)i--;return r>0||i{let n=`_onData`+ja(t),r=e[t];Object.defineProperty(e,t,{configurable:!0,enumerable:!1,value(...t){let i=r.apply(this,t);return e._chartjs.listeners.forEach(e=>{typeof e[n]==`function`&&e[n](...t)}),i}})}))}function mo(e,t){let n=e._chartjs;if(!n)return;let r=n.listeners,i=r.indexOf(t);i!==-1&&r.splice(i,1),!(r.length>0)&&(fo.forEach(t=>{delete e[t]}),delete e._chartjs)}function ho(e){let t=new Set(e);return t.size===e.length?e:Array.from(t)}var go=function(){return typeof window>`u`?function(e){return e()}:window.requestAnimationFrame}();function _o(e,t){let n=[],r=!1;return function(...i){n=i,r||(r=!0,go.call(window,()=>{r=!1,e.apply(t,n)}))}}function vo(e,t){let n;return function(...r){return t?(clearTimeout(n),n=setTimeout(e,t,r)):e.apply(this,r),t}}var yo=e=>e===`start`?`left`:e===`end`?`right`:`center`,bo=(e,t,n)=>e===`start`?t:e===`end`?n:(t+n)/2;function xo(e,t,n){let r=t.length,i=0,a=r;if(e._sorted){let{iScale:o,vScale:s,_parsed:c}=e,l=e.dataset&&e.dataset.options?e.dataset.options.spanGaps:null,u=o.axis,{min:d,max:f,minDefined:p,maxDefined:m}=o.getUserBounds();if(p){if(i=Math.min(co(c,u,d).lo,n?r:co(t,u,o.getPixelForValue(d)).lo),l){let e=c.slice(0,i+1).reverse().findIndex(e=>!U(e[s.axis]));i-=Math.max(0,e)}i=io(i,0,r-1)}if(m){let e=Math.max(co(c,o.axis,f,!0).hi+1,n?0:co(t,u,o.getPixelForValue(f),!0).hi+1);if(l){let t=c.slice(e-1).findIndex(e=>!U(e[s.axis]));e+=Math.max(0,t)}a=io(e,i,r)-i}else a=r-i}return{start:i,count:a}}function So(e){let{xScale:t,yScale:n,_scaleRanges:r}=e,i={xmin:t.min,xmax:t.max,ymin:n.min,ymax:n.max};if(!r)return e._scaleRanges=i,!0;let a=r.xmin!==t.min||r.xmax!==t.max||r.ymin!==n.min||r.ymax!==n.max;return Object.assign(r,i),a}var Co=e=>e===0||e===1,wo=(e,t,n)=>-(2**(10*--e)*Math.sin((e-t)*X/n)),To=(e,t,n)=>2**(-10*e)*Math.sin((e-t)*X/n)+1,Eo={linear:e=>e,easeInQuad:e=>e*e,easeOutQuad:e=>-e*(e-2),easeInOutQuad:e=>(e/=.5)<1?.5*e*e:-.5*(--e*(e-2)-1),easeInCubic:e=>e*e*e,easeOutCubic:e=>--e*e*e+1,easeInOutCubic:e=>(e/=.5)<1?.5*e*e*e:.5*((e-=2)*e*e+2),easeInQuart:e=>e*e*e*e,easeOutQuart:e=>-(--e*e*e*e-1),easeInOutQuart:e=>(e/=.5)<1?.5*e*e*e*e:-.5*((e-=2)*e*e*e-2),easeInQuint:e=>e*e*e*e*e,easeOutQuint:e=>--e*e*e*e*e+1,easeInOutQuint:e=>(e/=.5)<1?.5*e*e*e*e*e:.5*((e-=2)*e*e*e*e+2),easeInSine:e=>-Math.cos(e*Z)+1,easeOutSine:e=>Math.sin(e*Z),easeInOutSine:e=>-.5*(Math.cos(Y*e)-1),easeInExpo:e=>e===0?0:2**(10*(e-1)),easeOutExpo:e=>e===1?1:-(2**(-10*e))+1,easeInOutExpo:e=>Co(e)?e:e<.5?.5*2**(10*(e*2-1)):.5*(-(2**(-10*(e*2-1)))+2),easeInCirc:e=>e>=1?e:-(Math.sqrt(1-e*e)-1),easeOutCirc:e=>Math.sqrt(1- --e*e),easeInOutCirc:e=>(e/=.5)<1?-.5*(Math.sqrt(1-e*e)-1):.5*(Math.sqrt(1-(e-=2)*e)+1),easeInElastic:e=>Co(e)?e:wo(e,.075,.3),easeOutElastic:e=>Co(e)?e:To(e,.075,.3),easeInOutElastic(e){let t=.1125,n=.45;return Co(e)?e:e<.5?.5*wo(e*2,t,n):.5+.5*To(e*2-1,t,n)},easeInBack(e){return e*e*(2.70158*e-1.70158)},easeOutBack(e){return--e*e*(2.70158*e+1.70158)+1},easeInOutBack(e){let t=1.70158;return(e/=.5)<1?.5*(e*e*(((t*=1.525)+1)*e-t)):.5*((e-=2)*e*(((t*=1.525)+1)*e+t)+2)},easeInBounce:e=>1-Eo.easeOutBounce(1-e),easeOutBounce(e){let t=7.5625,n=2.75;return e<1/n?t*e*e:e<2/n?t*(e-=1.5/n)*e+.75:e<2.5/n?t*(e-=2.25/n)*e+.9375:t*(e-=2.625/n)*e+.984375},easeInOutBounce:e=>e<.5?Eo.easeInBounce(e*2)*.5:Eo.easeOutBounce(e*2-1)*.5+.5};function Do(e){if(e&&typeof e==`object`){let t=e.toString();return t===`[object CanvasPattern]`||t===`[object CanvasGradient]`}return!1}function Oo(e){return Do(e)?e:new pa(e)}function ko(e){return Do(e)?e:new pa(e).saturate(.5).darken(.1).hexString()}var Ao=[`x`,`y`,`borderWidth`,`radius`,`tension`],jo=[`color`,`borderColor`,`backgroundColor`];function Mo(e){e.set(`animation`,{delay:void 0,duration:1e3,easing:`easeOutQuart`,fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),e.describe(`animation`,{_fallback:!1,_indexable:!1,_scriptable:e=>e!==`onProgress`&&e!==`onComplete`&&e!==`fn`}),e.set(`animations`,{colors:{type:`color`,properties:jo},numbers:{type:`number`,properties:Ao}}),e.describe(`animations`,{_fallback:`animation`}),e.set(`transitions`,{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:`transparent`},visible:{type:`boolean`,duration:0}}},hide:{animations:{colors:{to:`transparent`},visible:{type:`boolean`,easing:`linear`,fn:e=>e|0}}}})}function No(e){e.set(`layout`,{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})}var Po=new Map;function Fo(e,t){t=t||{};let n=e+JSON.stringify(t),r=Po.get(n);return r||(r=new Intl.NumberFormat(e,t),Po.set(n,r)),r}function Io(e,t,n){return Fo(t,n).format(e)}var Lo={values(e){return W(e)?e:``+e},numeric(e,t,n){if(e===0)return`0`;let r=this.chart.options.locale,i,a=e;if(n.length>1){let t=Math.max(Math.abs(n[0].value),Math.abs(n[n.length-1].value));(t<1e-4||t>0x38d7ea4c68000)&&(i=`scientific`),a=Ro(e,n)}let o=Va(Math.abs(a)),s=isNaN(o)?1:Math.max(Math.min(-1*Math.floor(o),20),0),c={notation:i,minimumFractionDigits:s,maximumFractionDigits:s};return Object.assign(c,this.options.ticks.format),Io(e,r,c)},logarithmic(e,t,n){if(e===0)return`0`;let r=n[t].significand||e/10**Math.floor(Va(e));return[1,2,3,5,10,15].includes(r)||t>.8*n.length?Lo.numeric.call(this,e,t,n):``}};function Ro(e,t){let n=t.length>3?t[2].value-t[1].value:t[1].value-t[0].value;return Math.abs(n)>=1&&e!==Math.floor(e)&&(n=e-Math.floor(e)),n}var zo={formatters:Lo};function Bo(e){e.set(`scale`,{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:`ticks`,clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(e,t)=>t.lineWidth,tickColor:(e,t)=>t.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:``,padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:``,padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:zo.formatters.values,minor:{},major:{},align:`center`,crossAlign:`near`,showLabelBackdrop:!1,backdropColor:`rgba(255, 255, 255, 0.75)`,backdropPadding:2}}),e.route(`scale.ticks`,`color`,``,`color`),e.route(`scale.grid`,`color`,``,`borderColor`),e.route(`scale.border`,`color`,``,`borderColor`),e.route(`scale.title`,`color`,``,`color`),e.describe(`scale`,{_fallback:!1,_scriptable:e=>!e.startsWith(`before`)&&!e.startsWith(`after`)&&e!==`callback`&&e!==`parser`,_indexable:e=>e!==`borderDash`&&e!==`tickBorderDash`&&e!==`dash`}),e.describe(`scales`,{_fallback:`scale`}),e.describe(`scale.ticks`,{_scriptable:e=>e!==`backdropPadding`&&e!==`callback`,_indexable:e=>e!==`backdropPadding`})}var Vo=Object.create(null),Ho=Object.create(null);function Uo(e,t){if(!t)return e;let n=t.split(`.`);for(let t=0,r=n.length;te.chart.platform.getDevicePixelRatio(),this.elements={},this.events=[`mousemove`,`mouseout`,`click`,`touchstart`,`touchmove`],this.font={family:`'Helvetica Neue', 'Helvetica', 'Arial', sans-serif`,size:12,style:`normal`,lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(e,t)=>ko(t.backgroundColor),this.hoverBorderColor=(e,t)=>ko(t.borderColor),this.hoverColor=(e,t)=>ko(t.color),this.indexAxis=`x`,this.interaction={mode:`nearest`,intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(e),this.apply(t)}set(e,t){return Wo(this,e,t)}get(e){return Uo(this,e)}describe(e,t){return Wo(Ho,e,t)}override(e,t){return Wo(Vo,e,t)}route(e,t,n,r){let i=Uo(this,e),a=Uo(this,n),o=`_`+t;Object.defineProperties(i,{[o]:{value:i[t],writable:!0},[t]:{enumerable:!0,get(){let e=this[o],t=a[r];return G(e)?Object.assign({},t,e):K(e,t)},set(e){this[o]=e}}})}apply(e){e.forEach(e=>e(this))}}({_scriptable:e=>!e.startsWith(`on`),_indexable:e=>e!==`events`,hover:{_fallback:`interaction`},interaction:{_scriptable:!1,_indexable:!1}},[Mo,No,Bo]);function Go(e){return!e||U(e.size)||U(e.family)?null:(e.style?e.style+` `:``)+(e.weight?e.weight+` `:``)+e.size+`px `+e.family}function Ko(e,t,n,r,i){let a=t[i];return a||(a=t[i]=e.measureText(i).width,n.push(i)),a>r&&(r=a),r}function qo(e,t,n,r){r=r||{};let i=r.data=r.data||{},a=r.garbageCollect=r.garbageCollect||[];r.font!==t&&(i=r.data={},a=r.garbageCollect=[],r.font=t),e.save(),e.font=t;let o=0,s=n.length,c,l,u,d,f;for(c=0;cn.length){for(c=0;c0&&e.stroke()}}function Qo(e,t,n){return n=n||.5,!t||e&&e.x>t.left-n&&e.xt.top-n&&e.y0&&a.strokeColor!==``,c,l;for(e.save(),e.font=i.string,rs(e,a),c=0;c+e||0;function fs(e,t){let n={},r=G(t),i=r?Object.keys(t):t,a=G(e)?r?n=>K(e[n],e[t[n]]):t=>e[t]:()=>e;for(let e of i)n[e]=ds(a(e));return n}function ps(e){return fs(e,{top:`y`,right:`x`,bottom:`y`,left:`x`})}function ms(e){return fs(e,[`topLeft`,`topRight`,`bottomLeft`,`bottomRight`])}function hs(e){let t=ps(e);return t.width=t.left+t.right,t.height=t.top+t.bottom,t}function gs(e,t){e=e||{},t=t||Q.font;let n=K(e.size,t.size);typeof n==`string`&&(n=parseInt(n,10));let r=K(e.style,t.style);r&&!(``+r).match(ls)&&(console.warn(`Invalid font style specified: "`+r+`"`),r=void 0);let i={family:K(e.family,t.family),lineHeight:us(K(e.lineHeight,t.lineHeight),n),size:n,style:r,weight:K(e.weight,t.weight),string:``};return i.string=Go(i),i}function _s(e,t,n,r){let i=!0,a,o,s;for(a=0,o=e.length;an&&e===0?0:e+t;return{min:o(r,-Math.abs(a)),max:o(i,a)}}function ys(e,t){return Object.assign(Object.create(e),t)}function bs(e,t=[``],n,r,i=()=>e[0]){let a=n||e;return r===void 0&&(r=Is(`_fallback`,e)),new Proxy({[Symbol.toStringTag]:`Object`,_cacheable:!0,_scopes:e,_rootScopes:a,_fallback:r,_getTarget:i,override:n=>bs([n,...e],t,a,r)},{deleteProperty(t,n){return delete t[n],delete t._keys,delete e[0][n],!0},get(n,r){return Ts(n,r,()=>Fs(r,t,e,n))},getOwnPropertyDescriptor(e,t){return Reflect.getOwnPropertyDescriptor(e._scopes[0],t)},getPrototypeOf(){return Reflect.getPrototypeOf(e[0])},has(e,t){return Ls(e).includes(t)},ownKeys(e){return Ls(e)},set(e,t,n){let r=e._storage||(e._storage=i());return e[t]=r[t]=n,delete e._keys,!0}})}function xs(e,t,n,r){let i={_cacheable:!1,_proxy:e,_context:t,_subProxy:n,_stack:new Set,_descriptors:Ss(e,r),setContext:t=>xs(e,t,n,r),override:i=>xs(e.override(i),t,n,r)};return new Proxy(i,{deleteProperty(t,n){return delete t[n],delete e[n],!0},get(e,t,n){return Ts(e,t,()=>Es(e,t,n))},getOwnPropertyDescriptor(t,n){return t._descriptors.allKeys?Reflect.has(e,n)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(e,n)},getPrototypeOf(){return Reflect.getPrototypeOf(e)},has(t,n){return Reflect.has(e,n)},ownKeys(){return Reflect.ownKeys(e)},set(t,n,r){return e[n]=r,delete t[n],!0}})}function Ss(e,t={scriptable:!0,indexable:!0}){let{_scriptable:n=t.scriptable,_indexable:r=t.indexable,_allKeys:i=t.allKeys}=e;return{allKeys:i,scriptable:n,indexable:r,isScriptable:Na(n)?n:()=>n,isIndexable:Na(r)?r:()=>r}}var Cs=(e,t)=>e?e+ja(t):t,ws=(e,t)=>G(t)&&e!==`adapters`&&(Object.getPrototypeOf(t)===null||t.constructor===Object);function Ts(e,t,n){if(Object.prototype.hasOwnProperty.call(e,t)||t===`constructor`)return e[t];let r=n();return e[t]=r,r}function Es(e,t,n){let{_proxy:r,_context:i,_subProxy:a,_descriptors:o}=e,s=r[t];return Na(s)&&o.isScriptable(t)&&(s=Ds(t,s,e,n)),W(s)&&s.length&&(s=Os(t,s,e,o.isIndexable)),ws(t,s)&&(s=xs(s,i,a&&a[t],o)),s}function Ds(e,t,n,r){let{_proxy:i,_context:a,_subProxy:o,_stack:s}=n;if(s.has(e))throw Error(`Recursion detected: `+Array.from(s).join(`->`)+`->`+e);s.add(e);let c=t(a,o||r);return s.delete(e),ws(e,c)&&(c=Ms(i._scopes,i,e,c)),c}function Os(e,t,n,r){let{_proxy:i,_context:a,_subProxy:o,_descriptors:s}=n;if(a.index!==void 0&&r(e))return t[a.index%t.length];if(G(t[0])){let n=t,r=i._scopes.filter(e=>e!==n);t=[];for(let c of n){let n=Ms(r,i,e,c);t.push(xs(n,a,o&&o[e],s))}}return t}function ks(e,t,n){return Na(e)?e(t,n):e}var As=(e,t)=>e===!0?t:typeof e==`string`?Aa(t,e):void 0;function js(e,t,n,r,i){for(let a of t){let t=As(n,a);if(t){e.add(t);let a=ks(t._fallback,n,i);if(a!==void 0&&a!==n&&a!==r)return a}else if(t===!1&&r!==void 0&&n!==r)return null}return!1}function Ms(e,t,n,r){let i=t._rootScopes,a=ks(t._fallback,n,r),o=[...e,...i],s=new Set;s.add(r);let c=Ns(s,o,n,a||n,r);return c===null||a!==void 0&&a!==n&&(c=Ns(s,o,a,c,r),c===null)?!1:bs(Array.from(s),[``],i,a,()=>Ps(t,n,r))}function Ns(e,t,n,r,i){for(;n;)n=js(e,t,n,r,i);return n}function Ps(e,t,n){let r=e._getTarget();t in r||(r[t]={});let i=r[t];return W(i)&&G(n)?n:i||{}}function Fs(e,t,n,r){let i;for(let a of t)if(i=Is(Cs(a,e),n),i!==void 0)return ws(e,i)?Ms(n,r,e,i):i}function Is(e,t){for(let n of t){if(!n)continue;let t=n[e];if(t!==void 0)return t}}function Ls(e){let t=e._keys;return t||(t=e._keys=Rs(e._scopes)),t}function Rs(e){let t=new Set;for(let n of e)for(let e of Object.keys(n).filter(e=>!e.startsWith(`_`)))t.add(e);return Array.from(t)}function zs(e,t,n,r){let{iScale:i}=e,{key:a=`r`}=this._parsing,o=Array(r),s,c,l,u;for(s=0,c=r;ste===`x`?`y`:`x`;function Us(e,t,n,r){let i=e.skip?t:e,a=t,o=n.skip?t:n,s=eo(a,i),c=eo(o,a),l=s/(s+c),u=c/(s+c);l=isNaN(l)?0:l,u=isNaN(u)?0:u;let d=r*l,f=r*u;return{previous:{x:a.x-d*(o.x-i.x),y:a.y-d*(o.y-i.y)},next:{x:a.x+f*(o.x-i.x),y:a.y+f*(o.y-i.y)}}}function Ws(e,t,n){let r=e.length,i,a,o,s,c,l=Vs(e,0);for(let u=0;u!e.skip)),t.cubicInterpolationMode===`monotone`)Ks(e,i);else{let n=r?e[e.length-1]:e[0];for(a=0,o=e.length;ae.ownerDocument.defaultView.getComputedStyle(e,null);function ec(e,t){return $s(e).getPropertyValue(t)}var tc=[`top`,`right`,`bottom`,`left`];function nc(e,t,n){let r={};n=n?`-`+n:``;for(let i=0;i<4;i++){let a=tc[i];r[a]=parseFloat(e[t+`-`+a+n])||0}return r.width=r.left+r.right,r.height=r.top+r.bottom,r}var rc=(e,t,n)=>(e>0||t>0)&&(!n||!n.shadowRoot);function ic(e,t){let n=e.touches,r=n&&n.length?n[0]:e,{offsetX:i,offsetY:a}=r,o=!1,s,c;if(rc(i,a,e.target))s=i,c=a;else{let e=t.getBoundingClientRect();s=r.clientX-e.left,c=r.clientY-e.top,o=!0}return{x:s,y:c,box:o}}function ac(e,t){if(`native`in e)return e;let{canvas:n,currentDevicePixelRatio:r}=t,i=$s(n),a=i.boxSizing===`border-box`,o=nc(i,`padding`),s=nc(i,`border`,`width`),{x:c,y:l,box:u}=ic(e,n),d=o.left+(u&&s.left),f=o.top+(u&&s.top),{width:p,height:m}=t;return a&&(p-=o.width+s.width,m-=o.height+s.height),{x:Math.round((c-d)/p*n.width/r),y:Math.round((l-f)/m*n.height/r)}}function oc(e,t,n){let r,i;if(t===void 0||n===void 0){let a=e&&Zs(e);if(!a)t=e.clientWidth,n=e.clientHeight;else{let e=a.getBoundingClientRect(),o=$s(a),s=nc(o,`border`,`width`),c=nc(o,`padding`);t=e.width-c.width-s.width,n=e.height-c.height-s.height,r=Qs(o.maxWidth,a,`clientWidth`),i=Qs(o.maxHeight,a,`clientHeight`)}}return{width:t,height:n,maxWidth:r||La,maxHeight:i||La}}var sc=e=>Math.round(e*10)/10;function cc(e,t,n,r){let i=$s(e),a=nc(i,`margin`),o=Qs(i.maxWidth,e,`clientWidth`)||La,s=Qs(i.maxHeight,e,`clientHeight`)||La,c=oc(e,t,n),{width:l,height:u}=c;if(i.boxSizing===`content-box`){let e=nc(i,`border`,`width`),t=nc(i,`padding`);l-=t.width+e.width,u-=t.height+e.height}return l=Math.max(0,l-a.width),u=Math.max(0,r?l/r:u-a.height),l=sc(Math.min(l,o,c.maxWidth)),u=sc(Math.min(u,s,c.maxHeight)),l&&!u&&(u=sc(l/2)),(t!==void 0||n!==void 0)&&r&&c.height&&u>c.height&&(u=c.height,l=sc(Math.floor(u*r))),{width:l,height:u}}function lc(e,t,n){let r=t||1,i=sc(e.height*r),a=sc(e.width*r);e.height=sc(e.height),e.width=sc(e.width);let o=e.canvas;return o.style&&(n||!o.style.height&&!o.style.width)&&(o.style.height=`${e.height}px`,o.style.width=`${e.width}px`),e.currentDevicePixelRatio!==r||o.height!==i||o.width!==a?(e.currentDevicePixelRatio=r,o.height=i,o.width=a,e.ctx.setTransform(r,0,0,r,0,0),!0):!1}var uc=function(){let e=!1;try{let t={get passive(){return e=!0,!1}};Xs()&&(window.addEventListener(`test`,null,t),window.removeEventListener(`test`,null,t))}catch{}return e}();function dc(e,t){let n=ec(e,t),r=n&&n.match(/^(\d+)(\.\d+)?px$/);return r?+r[1]:void 0}function fc(e,t,n,r){return{x:e.x+n*(t.x-e.x),y:e.y+n*(t.y-e.y)}}function pc(e,t,n,r){return{x:e.x+n*(t.x-e.x),y:r===`middle`?n<.5?e.y:t.y:r===`after`?n<1?e.y:t.y:n>0?t.y:e.y}}function mc(e,t,n,r){let i={x:e.cp2x,y:e.cp2y},a={x:t.cp1x,y:t.cp1y},o=fc(e,i,n),s=fc(i,a,n),c=fc(a,t,n);return fc(fc(o,s,n),fc(s,c,n),n)}var hc=function(e,t){return{x(n){return e+e+t-n},setWidth(e){t=e},textAlign(e){return e===`center`?e:e===`right`?`left`:`right`},xPlus(e,t){return e-t},leftForLtr(e,t){return e-t}}},gc=function(){return{x(e){return e},setWidth(e){},textAlign(e){return e},xPlus(e,t){return e+t},leftForLtr(e,t){return e}}};function _c(e,t,n){return e?hc(t,n):gc()}function vc(e,t){let n,r;(t===`ltr`||t===`rtl`)&&(n=e.canvas.style,r=[n.getPropertyValue(`direction`),n.getPropertyPriority(`direction`)],n.setProperty(`direction`,t,`important`),e.prevTextDirection=r)}function yc(e,t){t!==void 0&&(delete e.prevTextDirection,e.canvas.style.setProperty(`direction`,t[0],t[1]))}function bc(e){return e===`angle`?{between:ro,compare:to,normalize:no}:{between:oo,compare:(e,t)=>e-t,normalize:e=>e}}function xc({start:e,end:t,count:n,loop:r,style:i}){return{start:e%n,end:t%n,loop:r&&(t-e+1)%n===0,style:i}}function Sc(e,t,n){let{property:r,start:i,end:a}=n,{between:o,normalize:s}=bc(r),c=t.length,{start:l,end:u,loop:d}=e,f,p;if(d){for(l+=c,u+=c,f=0,p=c;fc(i,y,_)&&s(i,y)!==0,x=()=>s(a,_)===0||c(a,y,_),S=()=>h||b(),C=()=>!h||x();for(let e=u,n=u;e<=d;++e)v=t[e%o],!v.skip&&(_=l(v[r]),_!==y&&(h=c(_,i,a),g===null&&S()&&(g=s(_,i)===0?e:n),g!==null&&C()&&(m.push(xc({start:g,end:e,loop:f,count:o,style:p})),g=null),n=e,y=_));return g!==null&&m.push(xc({start:g,end:d,loop:f,count:o,style:p})),m}function wc(e,t){let n=[],r=e.segments;for(let i=0;ii&&e[a%t].skip;)a--;return a%=t,{start:i,end:a}}function Ec(e,t,n,r){let i=e.length,a=[],o=t,s=e[t],c;for(c=t+1;c<=n;++c){let n=e[c%i];n.skip||n.stop?s.skip||(r=!1,a.push({start:t%i,end:(c-1)%i,loop:r}),t=o=n.stop?c:null):(o=c,s.skip&&(t=c)),s=n}return o!==null&&a.push({start:t%i,end:o%i,loop:r}),a}function Dc(e,t){let n=e.points,r=e.options.spanGaps,i=n.length;if(!i)return[];let a=!!e._loop,{start:o,end:s}=Tc(n,i,a,r);return r===!0?Oc(e,[{start:o,end:s,loop:a}],n,t):Oc(e,Ec(n,o,sr({chart:e,initial:t.initial,numSteps:a,currentStep:Math.min(n-t.start,a)}))}_refresh(){this._request||(this._running=!0,this._request=go.call(window,()=>{this._update(),this._request=null,this._running&&this._refresh()}))}_update(e=Date.now()){let t=0;this._charts.forEach((n,r)=>{if(!n.running||!n.items.length)return;let i=n.items,a=i.length-1,o=!1,s;for(;a>=0;--a)s=i[a],s._active?(s._total>n.duration&&(n.duration=s._total),s.tick(e),o=!0):(i[a]=i[i.length-1],i.pop());o&&(r.draw(),this._notify(r,n,e,`progress`)),i.length||(n.running=!1,this._notify(r,n,e,`complete`),n.initial=!1),t+=i.length}),this._lastDate=e,t===0&&(this._running=!1)}_getAnims(e){let t=this._charts,n=t.get(e);return n||(n={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},t.set(e,n)),n}listen(e,t,n){this._getAnims(e).listeners[t].push(n)}add(e,t){t&&t.length&&this._getAnims(e).items.push(...t)}has(e){return this._getAnims(e).items.length>0}start(e){let t=this._charts.get(e);t&&(t.running=!0,t.start=Date.now(),t.duration=t.items.reduce((e,t)=>Math.max(e,t._duration),0),this._refresh())}running(e){if(!this._running)return!1;let t=this._charts.get(e);return!(!t||!t.running||!t.items.length)}stop(e){let t=this._charts.get(e);if(!t||!t.items.length)return;let n=t.items,r=n.length-1;for(;r>=0;--r)n[r].cancel();t.items=[],this._notify(e,t,Date.now(),`complete`)}remove(e){return this._charts.delete(e)}},zc=`transparent`,Bc={boolean(e,t,n){return n>.5?t:e},color(e,t,n){let r=Oo(e||zc),i=r.valid&&Oo(t||zc);return i&&i.valid?i.mix(r,n).hexString():t},number(e,t,n){return e+(t-e)*n}},Vc=class{constructor(e,t,n,r){let i=t[n];r=_s([e.to,r,i,e.from]);let a=_s([e.from,i,r]);this._active=!0,this._fn=e.fn||Bc[e.type||typeof a],this._easing=Eo[e.easing]||Eo.linear,this._start=Math.floor(Date.now()+(e.delay||0)),this._duration=this._total=Math.floor(e.duration),this._loop=!!e.loop,this._target=t,this._prop=n,this._from=a,this._to=r,this._promises=void 0}active(){return this._active}update(e,t,n){if(this._active){this._notify(!1);let r=this._target[this._prop],i=n-this._start,a=this._duration-i;this._start=n,this._duration=Math.floor(Math.max(a,e.duration)),this._total+=i,this._loop=!!e.loop,this._to=_s([e.to,t,r,e.from]),this._from=_s([e.from,r,t])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(e){let t=e-this._start,n=this._duration,r=this._prop,i=this._from,a=this._loop,o=this._to,s;this._active=i!==o&&(a||t1?2-s:s,s=this._easing(Math.min(1,Math.max(0,s))),this._target[r]=this._fn(i,o,s)):(this._target[r]=o,this._notify(!0))}wait(){let e=this._promises||(this._promises=[]);return new Promise((t,n)=>{e.push({res:t,rej:n})})}_notify(e){let t=e?`res`:`rej`,n=this._promises||[];for(let e=0;e{let i=e[r];if(!G(i))return;let a={};for(let e of t)a[e]=i[e];(W(i.properties)&&i.properties||[r]).forEach(e=>{(e===r||!n.has(e))&&n.set(e,a)})})}_animateOptions(e,t){let n=t.options,r=Wc(e,n);if(!r)return[];let i=this._createAnimations(r,n);return n.$shared&&Uc(e.options.$animations,n).then(()=>{e.options=n},()=>{}),i}_createAnimations(e,t){let n=this._properties,r=[],i=e.$animations||(e.$animations={}),a=Object.keys(t),o=Date.now(),s;for(s=a.length-1;s>=0;--s){let c=a[s];if(c.charAt(0)===`$`)continue;if(c===`options`){r.push(...this._animateOptions(e,t));continue}let l=t[c],u=i[c],d=n.get(c);if(u){if(d&&u.active()){u.update(d,l,o);continue}u.cancel()}!d||!d.duration?e[c]=l:(i[c]=u=new Vc(d,e,c,l),r.push(u))}return r}update(e,t){if(this._properties.size===0){Object.assign(e,t);return}let n=this._createAnimations(e,t);if(n.length)return Rc.add(this._chart,n),!0}};function Uc(e,t){let n=[],r=Object.keys(t);for(let t=0;t0||!n&&t<0)return i.index}return null}function nl(e,t){let{chart:n,_cachedMeta:r}=e,i=n._stacks||(n._stacks={}),{iScale:a,vScale:o,index:s}=r,c=a.axis,l=o.axis,u=Qc(a,o,r),d=t.length,f;for(let e=0;en[e].axis===t).shift()}function il(e,t){return ys(e,{active:!1,dataset:void 0,datasetIndex:t,index:t,mode:`default`,type:`dataset`})}function al(e,t,n){return ys(e,{active:!1,dataIndex:t,parsed:void 0,raw:void 0,element:n,index:t,mode:`default`,type:`data`})}function ol(e,t){let n=e.controller.index,r=e.vScale&&e.vScale.axis;if(r){t=t||e._parsed;for(let e of t){let t=e._stacks;if(!t||t[r]===void 0||t[r][n]===void 0)return;delete t[r][n],t[r]._visualValues!==void 0&&t[r]._visualValues[n]!==void 0&&delete t[r]._visualValues[n]}}}var sl=e=>e===`reset`||e===`none`,cl=(e,t)=>t?e:Object.assign({},e),ll=(e,t,n)=>e&&!t.hidden&&t._stacked&&{keys:Jc(n,!0),values:null},ul=class{constructor(e,t){this.chart=e,this._ctx=e.ctx,this.index=t,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){let e=this._cachedMeta;this.configure(),this.linkScales(),e._stacked=Zc(e.vScale,e),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled(`filler`)&&console.warn(`Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options`)}updateIndex(e){this.index!==e&&ol(this._cachedMeta),this.index=e}linkScales(){let e=this.chart,t=this._cachedMeta,n=this.getDataset(),r=(e,t,n,r)=>e===`x`?t:e===`r`?r:n,i=t.xAxisID=K(n.xAxisID,rl(e,`x`)),a=t.yAxisID=K(n.yAxisID,rl(e,`y`)),o=t.rAxisID=K(n.rAxisID,rl(e,`r`)),s=t.indexAxis,c=t.iAxisID=r(s,i,a,o),l=t.vAxisID=r(s,a,i,o);t.xScale=this.getScaleForId(i),t.yScale=this.getScaleForId(a),t.rScale=this.getScaleForId(o),t.iScale=this.getScaleForId(c),t.vScale=this.getScaleForId(l)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(e){return this.chart.scales[e]}_getOtherScale(e){let t=this._cachedMeta;return e===t.iScale?t.vScale:t.iScale}reset(){this._update(`reset`)}_destroy(){let e=this._cachedMeta;this._data&&mo(this._data,this),e._stacked&&ol(e)}_dataCheck(){let e=this.getDataset(),t=e.data||(e.data=[]),n=this._data;if(G(t)){let e=this._cachedMeta;this._data=Xc(t,e)}else if(n!==t){if(n){mo(n,this);let e=this._cachedMeta;ol(e),e._parsed=[]}t&&Object.isExtensible(t)&&po(t,this),this._syncList=[],this._data=t}}addElements(){let e=this._cachedMeta;this._dataCheck(),this.datasetElementType&&(e.dataset=new this.datasetElementType)}buildOrUpdateElements(e){let t=this._cachedMeta,n=this.getDataset(),r=!1;this._dataCheck();let i=t._stacked;t._stacked=Zc(t.vScale,t),t.stack!==n.stack&&(r=!0,ol(t),t.stack=n.stack),this._resyncElements(e),(r||i!==t._stacked)&&(nl(this,t._parsed),t._stacked=Zc(t.vScale,t))}configure(){let e=this.chart.config,t=e.datasetScopeKeys(this._type),n=e.getOptionScopes(this.getDataset(),t,!0);this.options=e.createResolver(n,this.getContext()),this._parsing=this.options.parsing,this._cachedDataOpts={}}parse(e,t){let{_cachedMeta:n,_data:r}=this,{iScale:i,_stacked:a}=n,o=i.axis,s=e===0&&t===r.length||n._sorted,c=e>0&&n._parsed[e-1],l,u,d;if(this._parsing===!1)n._parsed=r,n._sorted=!0,d=r;else{d=W(r[e])?this.parseArrayData(n,r,e,t):G(r[e])?this.parseObjectData(n,r,e,t):this.parsePrimitiveData(n,r,e,t);let i=()=>u[o]===null||c&&u[o]t||u=0;--d)if(!p()){this.updateRangeFromParsed(c,e,f,s);break}}return c}getAllParsedValues(e){let t=this._cachedMeta._parsed,n=[],r,i,a;for(r=0,i=t.length;r=0&&ethis.getContext(n,r,t),u);return p.$shared&&(p.$shared=s,i[a]=Object.freeze(cl(p,s))),p}_resolveAnimations(e,t,n){let r=this.chart,i=this._cachedDataOpts,a=`animation-${t}`,o=i[a];if(o)return o;let s;if(r.options.animation!==!1){let r=this.chart.config,i=r.datasetAnimationScopeKeys(this._type,t),a=r.getOptionScopes(this.getDataset(),i);s=r.createResolver(a,this.getContext(e,n,t))}let c=new Hc(r,s&&s.animations);return s&&s._cacheable&&(i[a]=Object.freeze(c)),c}getSharedOptions(e){if(e.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},e))}includeOptions(e,t){return!t||sl(e)||this.chart._animationsDisabled}_getSharedOptions(e,t){let n=this.resolveDataElementOptions(e,t),r=this._sharedOptions,i=this.getSharedOptions(n),a=this.includeOptions(t,i)||i!==r;return this.updateSharedOptions(i,t,n),{sharedOptions:i,includeOptions:a}}updateElement(e,t,n,r){sl(r)?Object.assign(e,n):this._resolveAnimations(t,r).update(e,n)}updateSharedOptions(e,t,n){e&&!sl(t)&&this._resolveAnimations(void 0,t).update(e,n)}_setStyle(e,t,n,r){e.active=r;let i=this.getStyle(t,r);this._resolveAnimations(t,n,r).update(e,{options:!r&&this.getSharedOptions(i)||i})}removeHoverStyle(e,t,n){this._setStyle(e,n,`active`,!1)}setHoverStyle(e,t,n){this._setStyle(e,n,`active`,!0)}_removeDatasetHoverStyle(){let e=this._cachedMeta.dataset;e&&this._setStyle(e,void 0,`active`,!1)}_setDatasetHoverStyle(){let e=this._cachedMeta.dataset;e&&this._setStyle(e,void 0,`active`,!0)}_resyncElements(e){let t=this._data,n=this._cachedMeta.data;for(let[e,t,n]of this._syncList)this[e](t,n);this._syncList=[];let r=n.length,i=t.length,a=Math.min(i,r);a&&this.parse(0,a),i>r?this._insertElements(r,i-r,e):i{for(e.length+=t,o=e.length-1;o>=a;o--)e[o]=e[o-t]};for(s(i),o=e;oe-t))}return e._cache.$bar}function fl(e){let t=e.iScale,n=dl(t,e.type),r=t._length,i,a,o,s,c=()=>{o!==32767&&o!==-32768&&(Ma(s)&&(r=Math.min(r,Math.abs(o-s)||r)),s=o)};for(i=0,a=n.length;i0?i[e-1]:null,s=eMath.abs(s)&&(c=s,l=o),t[n.axis]=l,t._custom={barStart:c,barEnd:l,start:i,end:a,min:o,max:s}}function gl(e,t,n,r){return W(e)?hl(e,t,n,r):t[n.axis]=n.parse(e,r),t}function _l(e,t,n,r){let i=e.iScale,a=e.vScale,o=i.getLabels(),s=i===a,c=[],l,u,d,f;for(l=n,u=n+r;l=n?1:-1):Ha(e)}function bl(e){let t,n,r,i,a;return e.horizontal?(t=e.base>e.x,n=`left`,r=`right`):(t=e.basee.controller.options.grouped),i=n.options.stacked,a=[],o=this._cachedMeta.controller.getParsed(t),s=o&&o[n.axis],c=e=>{let t=e._parsed.find(e=>e[n.axis]===s),r=t&&t[e.vScale.axis];if(U(r)||isNaN(r))return!0};for(let n of r)if(!(t!==void 0&&c(n))&&((i===!1||a.indexOf(n.stack)===-1||i===void 0&&n.stack===void 0)&&a.push(n.stack),n.index===e))break;return a.length||a.push(void 0),a}_getStackCount(e){return this._getStacks(void 0,e).length}_getAxisCount(){return this._getAxis().length}getFirstScaleIdForIndexAxis(){let e=this.chart.scales,t=this.chart.options.indexAxis;return Object.keys(e).filter(n=>e[n].axis===t).shift()}_getAxis(){let e={},t=this.getFirstScaleIdForIndexAxis();for(let n of this.chart.data.datasets)e[K(this.chart.options.indexAxis===`x`?n.xAxisID:n.yAxisID,t)]=!0;return Object.keys(e)}_getStackIndex(e,t,n){let r=this._getStacks(e,n),i=t===void 0?-1:r.indexOf(t);return i===-1?r.length-1:i}_getRuler(){let e=this.options,t=this._cachedMeta,n=t.iScale,r=[],i,a;for(i=0,a=t.data.length;i=0;--n)t=Math.max(t,e[n].size(this.resolveDataElementOptions(n))/2);return t>0&&t}getLabelAndValue(e){let t=this._cachedMeta,n=this.chart.data.labels||[],{xScale:r,yScale:i}=t,a=this.getParsed(e),o=r.getLabelForValue(a.x),s=i.getLabelForValue(a.y),c=a._custom;return{label:n[e]||``,value:`(`+o+`, `+s+(c?`, `+c:``)+`)`}}update(e){let t=this._cachedMeta.data;this.updateElements(t,0,t.length,e)}updateElements(e,t,n,r){let i=r===`reset`,{iScale:a,vScale:o}=this._cachedMeta,{sharedOptions:s,includeOptions:c}=this._getSharedOptions(t,r),l=a.axis,u=o.axis;for(let d=t;dro(e,s,c,!0)?1:Math.max(t,t*n,r,r*n),m=(e,t,r)=>ro(e,s,c,!0)?-1:Math.min(t,t*n,r,r*n),h=p(0,l,d),g=p(Z,u,f),_=m(Y,l,d),v=m(Y+Z,u,f);r=(h-_)/2,i=(g-v)/2,a=-(h+_)/2,o=-(g+v)/2}return{ratioX:r,ratioY:i,offsetX:a,offsetY:o}}var kl=class extends ul{constructor(e,t){super(e,t),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(e,t){let n=this.getDataset().data,r=this._cachedMeta;if(this._parsing===!1)r._parsed=n;else{let i=e=>+n[e];if(G(n[e])){let{key:e=`value`}=this._parsing;i=t=>+Aa(n[t],e)}let a,o;for(a=e,o=e+t;a0&&!isNaN(e)?Math.abs(e)/t*X:0}getLabelAndValue(e){let t=this._cachedMeta,n=this.chart,r=n.data.labels||[],i=Io(t._parsed[e],n.options.locale);return{label:r[e]||``,value:i}}getMaxBorderWidth(e){let t=0,n=this.chart,r,i,a,o,s;if(!e){for(r=0,i=n.data.datasets.length;re!==`spacing`,_indexable:e=>e!==`spacing`&&!e.startsWith(`borderDash`)&&!e.startsWith(`hoverBorderDash`)}),$(kl,`overrides`,{aspectRatio:1,plugins:{legend:{labels:{generateLabels(e){let t=e.data,{labels:{pointStyle:n,textAlign:r,color:i,useBorderRadius:a,borderRadius:o}}=e.legend.options;return t.labels.length&&t.datasets.length?t.labels.map((t,s)=>{let c=e.getDatasetMeta(0).controller.getStyle(s);return{text:t,fillStyle:c.backgroundColor,fontColor:i,hidden:!e.getDataVisibility(s),lineDash:c.borderDash,lineDashOffset:c.borderDashOffset,lineJoin:c.borderJoinStyle,lineWidth:c.borderWidth,strokeStyle:c.borderColor,textAlign:r,pointStyle:n,borderRadius:a&&(o||c.borderRadius),index:s}}):[]}},onClick(e,t,n){n.chart.toggleDataVisibility(t.index),n.chart.update()}}}});var Al=class extends ul{initialize(){this.enableOptionSharing=!0,this.supportsDecimation=!0,super.initialize()}update(e){let t=this._cachedMeta,{dataset:n,data:r=[],_dataset:i}=t,a=this.chart._animationsDisabled,{start:o,count:s}=xo(t,r,a);this._drawStart=o,this._drawCount=s,So(t)&&(o=0,s=r.length),n._chart=this.chart,n._datasetIndex=this.index,n._decimated=!!i._decimated,n.points=r;let c=this.resolveDatasetElementOptions(e);this.options.showLine||(c.borderWidth=0),c.segment=this.options.segment,this.updateElement(n,void 0,{animated:!a,options:c},e),this.updateElements(r,o,s,e)}updateElements(e,t,n,r){let i=r===`reset`,{iScale:a,vScale:o,_stacked:s,_dataset:c}=this._cachedMeta,{sharedOptions:l,includeOptions:u}=this._getSharedOptions(t,r),d=a.axis,f=o.axis,{spanGaps:p,segment:m}=this.options,h=qa(p)?p:1/0,g=this.chart._animationsDisabled||i||r===`none`,_=t+n,v=e.length,y=t>0&&this.getParsed(t-1);for(let n=0;n=_){v.skip=!0;continue}let b=this.getParsed(n),x=U(b[f]),S=v[d]=a.getPixelForValue(b[d],n),C=v[f]=i||x?o.getBasePixel():o.getPixelForValue(s?this.applyStack(o,b,s):b[f],n);v.skip=isNaN(S)||isNaN(C)||x,v.stop=n>0&&Math.abs(b[d]-y[d])>h,m&&(v.parsed=b,v.raw=c.data[n]),u&&(v.options=l||this.resolveDataElementOptions(n,p.active?`active`:r)),g||this.updateElement(p,n,v,r),y=b}}getMaxOverflow(){let e=this._cachedMeta,t=e.dataset,n=t.options&&t.options.borderWidth||0,r=e.data||[];if(!r.length)return n;let i=r[0].size(this.resolveDataElementOptions(0)),a=r[r.length-1].size(this.resolveDataElementOptions(r.length-1));return Math.max(n,i,a)/2}draw(){let e=this._cachedMeta;e.dataset.updateControlPoints(this.chart.chartArea,e.iScale.axis),super.draw()}};$(Al,`id`,`line`),$(Al,`defaults`,{datasetElementType:`line`,dataElementType:`point`,showLine:!0,spanGaps:!1}),$(Al,`overrides`,{scales:{_index_:{type:`category`},_value_:{type:`linear`}}});var jl=class extends ul{constructor(e,t){super(e,t),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(e){let t=this._cachedMeta,n=this.chart,r=n.data.labels||[],i=Io(t._parsed[e].r,n.options.locale);return{label:r[e]||``,value:i}}parseObjectData(e,t,n,r){return zs.bind(this)(e,t,n,r)}update(e){let t=this._cachedMeta.data;this._updateRadius(),this.updateElements(t,0,t.length,e)}getMinMax(){let e=this._cachedMeta,t={min:1/0,max:-1/0};return e.data.forEach((e,n)=>{let r=this.getParsed(n).r;!isNaN(r)&&this.chart.getDataVisibility(n)&&(rt.max&&(t.max=r))}),t}_updateRadius(){let e=this.chart,t=e.chartArea,n=e.options,r=Math.min(t.right-t.left,t.bottom-t.top),i=Math.max(r/2,0),a=(i-Math.max(n.cutoutPercentage?i/100*n.cutoutPercentage:1,0))/e.getVisibleDatasetCount();this.outerRadius=i-a*this.index,this.innerRadius=this.outerRadius-a}updateElements(e,t,n,r){let i=r===`reset`,a=this.chart,o=a.options.animation,s=this._cachedMeta.rScale,c=s.xCenter,l=s.yCenter,u=s.getIndexAngle(0)-.5*Y,d=u,f,p=360/this.countVisibleElements();for(f=0;f{!isNaN(this.getParsed(n).r)&&this.chart.getDataVisibility(n)&&t++}),t}_computeAngle(e,t,n){return this.chart.getDataVisibility(e)?Xa(this.resolveDataElementOptions(e,t).angle||n):0}};$(jl,`id`,`polarArea`),$(jl,`defaults`,{dataElementType:`arc`,animation:{animateRotate:!0,animateScale:!0},animations:{numbers:{type:`number`,properties:[`x`,`y`,`startAngle`,`endAngle`,`innerRadius`,`outerRadius`]}},indexAxis:`r`,startAngle:0}),$(jl,`overrides`,{aspectRatio:1,plugins:{legend:{labels:{generateLabels(e){let t=e.data;if(t.labels.length&&t.datasets.length){let{labels:{pointStyle:n,color:r}}=e.legend.options;return t.labels.map((t,i)=>{let a=e.getDatasetMeta(0).controller.getStyle(i);return{text:t,fillStyle:a.backgroundColor,strokeStyle:a.borderColor,fontColor:r,lineWidth:a.borderWidth,pointStyle:n,hidden:!e.getDataVisibility(i),index:i}})}return[]}},onClick(e,t,n){n.chart.toggleDataVisibility(t.index),n.chart.update()}}},scales:{r:{type:`radialLinear`,angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}});var Ml=class extends kl{};$(Ml,`id`,`pie`),$(Ml,`defaults`,{cutout:0,rotation:0,circumference:360,radius:`100%`});var Nl=class extends ul{getLabelAndValue(e){let t=this._cachedMeta.vScale,n=this.getParsed(e);return{label:t.getLabels()[e],value:``+t.getLabelForValue(n[t.axis])}}parseObjectData(e,t,n,r){return zs.bind(this)(e,t,n,r)}update(e){let t=this._cachedMeta,n=t.dataset,r=t.data||[],i=t.iScale.getLabels();if(n.points=r,e!==`resize`){let t=this.resolveDatasetElementOptions(e);this.options.showLine||(t.borderWidth=0);let a={_loop:!0,_fullLoop:i.length===r.length,options:t};this.updateElement(n,void 0,a,e)}this.updateElements(r,0,r.length,e)}updateElements(e,t,n,r){let i=this._cachedMeta.rScale,a=r===`reset`;for(let o=t;o0&&this.getParsed(t-1);for(let l=t;l0&&Math.abs(n[f]-v[f])>g,h&&(m.parsed=n,m.raw=c.data[l]),d&&(m.options=u||this.resolveDataElementOptions(l,t.active?`active`:r)),_||this.updateElement(t,l,m,r),v=n}this.updateSharedOptions(u,r,l)}getMaxOverflow(){let e=this._cachedMeta,t=e.data||[];if(!this.options.showLine){let e=0;for(let n=t.length-1;n>=0;--n)e=Math.max(e,t[n].size(this.resolveDataElementOptions(n))/2);return e>0&&e}let n=e.dataset,r=n.options&&n.options.borderWidth||0;if(!t.length)return r;let i=t[0].size(this.resolveDataElementOptions(0)),a=t[t.length-1].size(this.resolveDataElementOptions(t.length-1));return Math.max(r,i,a)/2}};$(Pl,`id`,`scatter`),$(Pl,`defaults`,{datasetElementType:!1,dataElementType:`point`,showLine:!1,fill:!1}),$(Pl,`overrides`,{interaction:{mode:`point`},scales:{x:{type:`linear`},y:{type:`linear`}}});function Fl(){throw Error(`This method is not implemented: Check that a complete date adapter is provided.`)}var Il={_date:class e{static override(t){Object.assign(e.prototype,t)}constructor(e){$(this,`options`,void 0),this.options=e||{}}init(){}formats(){return Fl()}parse(){return Fl()}format(){return Fl()}add(){return Fl()}diff(){return Fl()}startOf(){return Fl()}endOf(){return Fl()}}};function Ll(e,t,n,r){let{controller:i,data:a,_sorted:o}=e,s=i._cachedMeta.iScale,c=e.dataset&&e.dataset.options?e.dataset.options.spanGaps:null;if(s&&t===s.axis&&t!==`r`&&o&&a.length){let o=s._reversePixels?lo:co;if(!r){let r=o(a,t,n);if(c){let{vScale:t}=i._cachedMeta,{_parsed:n}=e,a=n.slice(0,r.lo+1).reverse().findIndex(e=>!U(e[t.axis]));r.lo-=Math.max(0,a);let o=n.slice(r.hi).findIndex(e=>!U(e[t.axis]));r.hi+=Math.max(0,o)}return r}if(i._sharedOptions){let e=a[0],r=typeof e.getRange==`function`&&e.getRange(t);if(r){let e=o(a,t,n-r),i=o(a,t,n+r);return{lo:e.lo,hi:i.hi}}}}return{lo:0,hi:a.length-1}}function Rl(e,t,n,r,i){let a=e.getSortedVisibleDatasetMetas(),o=n[t];for(let e=0,n=a.length;e{e[o]&&e[o](t[n],i)&&(a.push({element:e,datasetIndex:r,index:c}),s=s||e.inRange(t.x,t.y,i))}),r&&!s?[]:a}var Gl={evaluateInteractionItems:Rl,modes:{index(e,t,n,r){let i=ac(t,e),a=n.axis||`x`,o=n.includeInvisible||!1,s=n.intersect?Bl(e,i,a,r,o):Ul(e,i,a,!1,r,o),c=[];return s.length?(e.getSortedVisibleDatasetMetas().forEach(e=>{let t=s[0].index,n=e.data[t];n&&!n.skip&&c.push({element:n,datasetIndex:e.index,index:t})}),c):[]},dataset(e,t,n,r){let i=ac(t,e),a=n.axis||`xy`,o=n.includeInvisible||!1,s=n.intersect?Bl(e,i,a,r,o):Ul(e,i,a,!1,r,o);if(s.length>0){let t=s[0].datasetIndex,n=e.getDatasetMeta(t).data;s=[];for(let e=0;ee.pos===t)}function Jl(e,t){return e.filter(e=>Kl.indexOf(e.pos)===-1&&e.box.axis===t)}function Yl(e,t){return e.sort((e,n)=>{let r=t?n:e,i=t?e:n;return r.weight===i.weight?r.index-i.index:r.weight-i.weight})}function Xl(e){let t=[],n,r,i,a,o,s;for(n=0,r=(e||[]).length;ne.box.fullSize),!0),r=Yl(ql(t,`left`),!0),i=Yl(ql(t,`right`)),a=Yl(ql(t,`top`),!0),o=Yl(ql(t,`bottom`)),s=Jl(t,`x`),c=Jl(t,`y`);return{fullSize:n,leftAndTop:r.concat(a),rightAndBottom:i.concat(c).concat(o).concat(s),chartArea:ql(t,`chartArea`),vertical:r.concat(i).concat(c),horizontal:a.concat(o).concat(s)}}function eu(e,t,n,r){return Math.max(e[n],t[n])+Math.max(e[r],t[r])}function tu(e,t){e.top=Math.max(e.top,t.top),e.left=Math.max(e.left,t.left),e.bottom=Math.max(e.bottom,t.bottom),e.right=Math.max(e.right,t.right)}function nu(e,t,n,r){let{pos:i,box:a}=n,o=e.maxPadding;if(!G(i)){n.size&&(e[i]-=n.size);let t=r[n.stack]||{size:0,count:1};t.size=Math.max(t.size,n.horizontal?a.height:a.width),n.size=t.size/t.count,e[i]+=n.size}a.getPadding&&tu(o,a.getPadding());let s=Math.max(0,t.outerWidth-eu(o,e,`left`,`right`)),c=Math.max(0,t.outerHeight-eu(o,e,`top`,`bottom`)),l=s!==e.w,u=c!==e.h;return e.w=s,e.h=c,n.horizontal?{same:l,other:u}:{same:u,other:l}}function ru(e){let t=e.maxPadding;function n(n){let r=Math.max(t[n]-e[n],0);return e[n]+=r,r}e.y+=n(`top`),e.x+=n(`left`),n(`right`),n(`bottom`)}function iu(e,t){let n=t.maxPadding;function r(e){let r={left:0,top:0,right:0,bottom:0};return e.forEach(e=>{r[e]=Math.max(t[e],n[e])}),r}return r(e?[`left`,`right`]:[`top`,`bottom`])}function au(e,t,n,r){let i=[],a,o,s,c,l,u;for(a=0,o=e.length,l=0;a{typeof e.beforeLayout==`function`&&e.beforeLayout()});let u=c.reduce((e,t)=>t.box.options&&t.box.options.display===!1?e:e+1,0)||1,d=Object.freeze({outerWidth:t,outerHeight:n,padding:i,availableWidth:a,availableHeight:o,vBoxMaxWidth:a/2/u,hBoxMaxHeight:o/2}),f=Object.assign({},i);tu(f,hs(r));let p=Object.assign({maxPadding:f,w:a,h:o,x:i.left,y:i.top},i),m=Ql(c.concat(l),d);au(s.fullSize,p,d,m),au(c,p,d,m),au(l,p,d,m)&&au(c,p,d,m),ru(p),su(s.leftAndTop,p,d,m),p.x+=p.w,p.y+=p.h,su(s.rightAndBottom,p,d,m),e.chartArea={left:p.left,top:p.top,right:p.left+p.w,bottom:p.top+p.h,height:p.h,width:p.w},J(s.chartArea,t=>{let n=t.box;Object.assign(n,e.chartArea),n.update(p.w,p.h,{left:0,top:0,right:0,bottom:0})})}},lu=class{acquireContext(e,t){}releaseContext(e){return!1}addEventListener(e,t,n){}removeEventListener(e,t,n){}getDevicePixelRatio(){return 1}getMaximumSize(e,t,n,r){return t=Math.max(0,t||e.width),n=n||e.height,{width:t,height:Math.max(0,r?Math.floor(t/r):n)}}isAttached(e){return!0}updateConfig(e){}},uu=class extends lu{acquireContext(e){return e&&e.getContext&&e.getContext(`2d`)||null}updateConfig(e){e.options.animation=!1}},du=`$chartjs`,fu={touchstart:`mousedown`,touchmove:`mousemove`,touchend:`mouseup`,pointerenter:`mouseenter`,pointerdown:`mousedown`,pointermove:`mousemove`,pointerup:`mouseup`,pointerleave:`mouseout`,pointerout:`mouseout`},pu=e=>e===null||e===``;function mu(e,t){let n=e.style,r=e.getAttribute(`height`),i=e.getAttribute(`width`);if(e[du]={initial:{height:r,width:i,style:{display:n.display,height:n.height,width:n.width}}},n.display=n.display||`block`,n.boxSizing=n.boxSizing||`border-box`,pu(i)){let t=dc(e,`width`);t!==void 0&&(e.width=t)}if(pu(r)){if(e.style.height===``)e.height=e.width/(t||2);else{let t=dc(e,`height`);t!==void 0&&(e.height=t)}}return e}var hu=uc?{passive:!0}:!1;function gu(e,t,n){e&&e.addEventListener(t,n,hu)}function _u(e,t,n){e&&e.canvas&&e.canvas.removeEventListener(t,n,hu)}function vu(e,t){let n=fu[e.type]||e.type,{x:r,y:i}=ac(e,t);return{type:n,chart:t,native:e,x:r===void 0?null:r,y:i===void 0?null:i}}function yu(e,t){for(let n of e)if(n===t||n.contains(t))return!0}function bu(e,t,n){let r=e.canvas,i=new MutationObserver(e=>{let t=!1;for(let n of e)t=t||yu(n.addedNodes,r),t=t&&!yu(n.removedNodes,r);t&&n()});return i.observe(document,{childList:!0,subtree:!0}),i}function xu(e,t,n){let r=e.canvas,i=new MutationObserver(e=>{let t=!1;for(let n of e)t=t||yu(n.removedNodes,r),t=t&&!yu(n.addedNodes,r);t&&n()});return i.observe(document,{childList:!0,subtree:!0}),i}var Su=new Map,Cu=0;function wu(){let e=window.devicePixelRatio;e!==Cu&&(Cu=e,Su.forEach((t,n)=>{n.currentDevicePixelRatio!==e&&t()}))}function Tu(e,t){Su.size||window.addEventListener(`resize`,wu),Su.set(e,t)}function Eu(e){Su.delete(e),Su.size||window.removeEventListener(`resize`,wu)}function Du(e,t,n){let r=e.canvas,i=r&&Zs(r);if(!i)return;let a=_o((e,t)=>{let r=i.clientWidth;n(e,t),r{let t=e[0],n=t.contentRect.width,r=t.contentRect.height;(n!==0||r!==0)&&a(n,r)});return o.observe(i),Tu(e,a),o}function Ou(e,t,n){n&&n.disconnect(),t===`resize`&&Eu(e)}function ku(e,t,n){let r=e.canvas,i=_o(t=>{e.ctx!==null&&n(vu(t,e))},e);return gu(r,t,i),i}var Au=class extends lu{acquireContext(e,t){let n=e&&e.getContext&&e.getContext(`2d`);return n&&n.canvas===e?(mu(e,t),n):null}releaseContext(e){let t=e.canvas;if(!t[du])return!1;let n=t[du].initial;[`height`,`width`].forEach(e=>{let r=n[e];U(r)?t.removeAttribute(e):t.setAttribute(e,r)});let r=n.style||{};return Object.keys(r).forEach(e=>{t.style[e]=r[e]}),t.width=t.width,delete t[du],!0}addEventListener(e,t,n){this.removeEventListener(e,t);let r=e.$proxies||(e.$proxies={});r[t]=({attach:bu,detach:xu,resize:Du}[t]||ku)(e,t,n)}removeEventListener(e,t){let n=e.$proxies||(e.$proxies={}),r=n[t];r&&(({attach:Ou,detach:Ou,resize:Ou}[t]||_u)(e,t,r),n[t]=void 0)}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(e,t,n,r){return cc(e,t,n,r)}isAttached(e){let t=e&&Zs(e);return!!(t&&t.isConnected)}};function ju(e){return!Xs()||typeof OffscreenCanvas<`u`&&e instanceof OffscreenCanvas?uu:Au}var Mu=class{constructor(){$(this,`x`,void 0),$(this,`y`,void 0),$(this,`active`,!1),$(this,`options`,void 0),$(this,`$animations`,void 0)}tooltipPosition(e){let{x:t,y:n}=this.getProps([`x`,`y`],e);return{x:t,y:n}}hasValue(){return qa(this.x)&&qa(this.y)}getProps(e,t){let n=this.$animations;if(!t||!n)return this;let r={};return e.forEach(e=>{r[e]=n[e]&&n[e].active()?n[e]._to:this[e]}),r}};$(Mu,`defaults`,{}),$(Mu,`defaultRoutes`,void 0);function Nu(e,t){let n=e.options.ticks,r=Pu(e),i=Math.min(n.maxTicksLimit||r,r),a=n.major.enabled?Iu(t):[],o=a.length,s=a[0],c=a[o-1],l=[];if(o>i)return Lu(t,l,a,o/i),l;let u=Fu(a,t,i);if(o>0){let e,n,r=o>1?Math.round((c-s)/(o-1)):null;for(Ru(t,l,u,U(r)?0:s-r,s),e=0,n=o-1;ei)return t}return Math.max(i,1)}function Iu(e){let t=[],n,r;for(n=0,r=e.length;ne===`left`?`right`:e===`right`?`left`:e,Vu=(e,t,n)=>t===`top`||t===`left`?e[t]+n:e[t]-n,Hu=(e,t)=>Math.min(t||e,e);function Uu(e,t){let n=[],r=e.length/t,i=e.length,a=0;for(;ao+s)))return c}function Gu(e,t){J(e,e=>{let n=e.gc,r=n.length/2,i;if(r>t){for(i=0;in?n:t,n=r&&t>n?t:n,{min:_a(t,_a(n,t)),max:_a(n,_a(t,n))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){let e=this.chart.data;return this.options.labels||(this.isHorizontal()?e.xLabels:e.yLabels)||e.labels||[]}getLabelItems(e=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(e))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){q(this.options.beforeUpdate,[this])}update(e,t,n){let{beginAtZero:r,grace:i,ticks:a}=this.options,o=a.sampleSize;this.beforeUpdate(),this.maxWidth=e,this.maxHeight=t,this._margins=n=Object.assign({left:0,right:0,top:0,bottom:0},n),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+n.left+n.right:this.height+n.top+n.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=vs(this,i,r),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();let s=o=i||n<=1||!this.isHorizontal()){this.labelRotation=r;return}let l=this._getLabelSizes(),u=l.widest.width,d=l.highest.height,f=io(this.chart.width-u,0,this.maxWidth);o=e.offset?this.maxWidth/n:f/(n-1),u+6>o&&(o=f/(n-(e.offset?.5:1)),s=this.maxHeight-Ku(e.grid)-t.padding-qu(e.title,this.chart.options.font),c=Math.sqrt(u*u+d*d),a=Za(Math.min(Math.asin(io((l.highest.height+6)/o,-1,1)),Math.asin(io(s/c,-1,1))-Math.asin(io(d/c,-1,1)))),a=Math.max(r,Math.min(i,a))),this.labelRotation=a}afterCalculateLabelRotation(){q(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){q(this.options.beforeFit,[this])}fit(){let e={width:0,height:0},{chart:t,options:{ticks:n,title:r,grid:i}}=this,a=this._isVisible(),o=this.isHorizontal();if(a){let a=qu(r,t.options.font);if(o?(e.width=this.maxWidth,e.height=Ku(i)+a):(e.height=this.maxHeight,e.width=Ku(i)+a),n.display&&this.ticks.length){let{first:t,last:r,widest:i,highest:a}=this._getLabelSizes(),s=n.padding*2,c=Xa(this.labelRotation),l=Math.cos(c),u=Math.sin(c);if(o){let t=n.mirror?0:u*i.width+l*a.height;e.height=Math.min(this.maxHeight,e.height+t+s)}else{let t=n.mirror?0:l*i.width+u*a.height;e.width=Math.min(this.maxWidth,e.width+t+s)}this._calculatePadding(t,r,u,l)}}this._handleMargins(),o?(this.width=this._length=t.width-this._margins.left-this._margins.right,this.height=e.height):(this.width=e.width,this.height=this._length=t.height-this._margins.top-this._margins.bottom)}_calculatePadding(e,t,n,r){let{ticks:{align:i,padding:a},position:o}=this.options,s=this.labelRotation!==0,c=o!==`top`&&this.axis===`x`;if(this.isHorizontal()){let o=this.getPixelForTick(0)-this.left,l=this.right-this.getPixelForTick(this.ticks.length-1),u=0,d=0;s?c?(u=r*e.width,d=n*t.height):(u=n*e.height,d=r*t.width):i===`start`?d=t.width:i===`end`?u=e.width:i!==`inner`&&(u=e.width/2,d=t.width/2),this.paddingLeft=Math.max((u-o+a)*this.width/(this.width-o),0),this.paddingRight=Math.max((d-l+a)*this.width/(this.width-l),0)}else{let n=t.height/2,r=e.height/2;i===`start`?(n=0,r=e.height):i===`end`&&(n=t.height,r=0),this.paddingTop=n+a,this.paddingBottom=r+a}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){q(this.options.afterFit,[this])}isHorizontal(){let{axis:e,position:t}=this.options;return t===`top`||t===`bottom`||e===`x`}isFullSize(){return this.options.fullSize}_convertTicksToLabels(e){this.beforeTickToLabelConversion(),this.generateTickLabels(e);let t,n;for(t=0,n=e.length;t({width:a[e]||0,height:o[e]||0});return{first:C(0),last:C(t-1),widest:C(x),highest:C(S),widths:a,heights:o}}getLabelForValue(e){return e}getPixelForValue(e,t){return NaN}getValueForPixel(e){}getPixelForTick(e){let t=this.ticks;return e<0||e>t.length-1?null:this.getPixelForValue(t[e].value)}getPixelForDecimal(e){this._reversePixels&&(e=1-e);let t=this._startPixel+e*this._length;return ao(this._alignToPixels?Jo(this.chart,t,0):t)}getDecimalForPixel(e){let t=(e-this._startPixel)/this._length;return this._reversePixels?1-t:t}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){let{min:e,max:t}=this;return e<0&&t<0?t:e>0&&t>0?e:0}getContext(e){let t=this.ticks||[];if(e>=0&&eo*r?o/n:s/r:s*r0:!!e}_computeGridLineItems(e){let t=this.axis,n=this.chart,r=this.options,{grid:i,position:a,border:o}=r,s=i.offset,c=this.isHorizontal(),l=this.ticks.length+ +!!s,u=Ku(i),d=[],f=o.setContext(this.getContext()),p=f.display?f.width:0,m=p/2,h=function(e){return Jo(n,e,p)},g,_,v,y,b,x,S,C,w,T,E,D;if(a===`top`)g=h(this.bottom),x=this.bottom-u,C=g-m,T=h(e.top)+m,D=e.bottom;else if(a===`bottom`)g=h(this.top),T=e.top,D=h(e.bottom)-m,x=g+m,C=this.top+u;else if(a===`left`)g=h(this.right),b=this.right-u,S=g-m,w=h(e.left)+m,E=e.right;else if(a===`right`)g=h(this.left),w=e.left,E=h(e.right)-m,b=g+m,S=this.left+u;else if(t===`x`){if(a===`center`)g=h((e.top+e.bottom)/2+.5);else if(G(a)){let e=Object.keys(a)[0],t=a[e];g=h(this.chart.scales[e].getPixelForValue(t))}T=e.top,D=e.bottom,x=g+m,C=x+u}else if(t===`y`){if(a===`center`)g=h((e.left+e.right)/2);else if(G(a)){let e=Object.keys(a)[0],t=a[e];g=h(this.chart.scales[e].getPixelForValue(t))}b=g-m,S=b-u,w=e.left,E=e.right}let O=K(r.ticks.maxTicksLimit,l),k=Math.max(1,Math.ceil(l/O));for(_=0;_0&&(a-=r/2)}f={left:a,top:i,width:r+t.width,height:n+t.height,color:e.backdropColor}}h.push({label:y,font:w,textOffset:D,options:{rotation:m,color:n,strokeColor:s,strokeWidth:l,textAlign:d,textBaseline:O,translation:[b,x],backdrop:f}})}return h}_getXAxisLabelAlignment(){let{position:e,ticks:t}=this.options;if(-Xa(this.labelRotation))return e===`top`?`left`:`right`;let n=`center`;return t.align===`start`?n=`left`:t.align===`end`?n=`right`:t.align===`inner`&&(n=`inner`),n}_getYAxisLabelAlignment(e){let{position:t,ticks:{crossAlign:n,mirror:r,padding:i}}=this.options,a=this._getLabelSizes(),o=e+i,s=a.widest.width,c,l;return t===`left`?r?(l=this.right+i,n===`near`?c=`left`:n===`center`?(c=`center`,l+=s/2):(c=`right`,l+=s)):(l=this.right-o,n===`near`?c=`right`:n===`center`?(c=`center`,l-=s/2):(c=`left`,l=this.left)):t===`right`?r?(l=this.left+i,n===`near`?c=`right`:n===`center`?(c=`center`,l-=s/2):(c=`left`,l-=s)):(l=this.left+o,n===`near`?c=`left`:n===`center`?(c=`center`,l+=s/2):(c=`right`,l=this.right)):c=`right`,{textAlign:c,x:l}}_computeLabelArea(){if(this.options.ticks.mirror)return;let e=this.chart,t=this.options.position;if(t===`left`||t===`right`)return{top:0,left:this.left,bottom:e.height,right:this.right};if(t===`top`||t===`bottom`)return{top:this.top,left:0,bottom:this.bottom,right:e.width}}drawBackground(){let{ctx:e,options:{backgroundColor:t},left:n,top:r,width:i,height:a}=this;t&&(e.save(),e.fillStyle=t,e.fillRect(n,r,i,a),e.restore())}getLineWidthForValue(e){let t=this.options.grid;if(!this._isVisible()||!t.display)return 0;let n=this.ticks.findIndex(t=>t.value===e);return n>=0?t.setContext(this.getContext(n)).lineWidth:0}drawGrid(e){let t=this.options.grid,n=this.ctx,r=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(e)),i,a,o=(e,t,r)=>{r.width&&r.color&&(n.save(),n.lineWidth=r.width,n.strokeStyle=r.color,n.setLineDash(r.borderDash||[]),n.lineDashOffset=r.borderDashOffset,n.beginPath(),n.moveTo(e.x,e.y),n.lineTo(t.x,t.y),n.stroke(),n.restore())};if(t.display)for(i=0,a=r.length;i{this.draw(e)}}]:[{z:r,draw:e=>{this.drawBackground(),this.drawGrid(e),this.drawTitle()}},{z:i,draw:()=>{this.drawBorder()}},{z:n,draw:e=>{this.drawLabels(e)}}]}getMatchingVisibleMetas(e){let t=this.chart.getSortedVisibleDatasetMetas(),n=this.axis+`AxisID`,r=[],i,a;for(i=0,a=t.length;i{let r=n.split(`.`),i=r.pop(),a=[e].concat(r).join(`.`),o=t[n].split(`.`),s=o.pop(),c=o.join(`.`);Q.route(a,i,c,s)})}function nd(e){return`id`in e&&`defaults`in e}var rd=new class{constructor(){this.controllers=new $u(ul,`datasets`,!0),this.elements=new $u(Mu,`elements`),this.plugins=new $u(Object,`plugins`),this.scales=new $u(Qu,`scales`),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...e){this._each(`register`,e)}remove(...e){this._each(`unregister`,e)}addControllers(...e){this._each(`register`,e,this.controllers)}addElements(...e){this._each(`register`,e,this.elements)}addPlugins(...e){this._each(`register`,e,this.plugins)}addScales(...e){this._each(`register`,e,this.scales)}getController(e){return this._get(e,this.controllers,`controller`)}getElement(e){return this._get(e,this.elements,`element`)}getPlugin(e){return this._get(e,this.plugins,`plugin`)}getScale(e){return this._get(e,this.scales,`scale`)}removeControllers(...e){this._each(`unregister`,e,this.controllers)}removeElements(...e){this._each(`unregister`,e,this.elements)}removePlugins(...e){this._each(`unregister`,e,this.plugins)}removeScales(...e){this._each(`unregister`,e,this.scales)}_each(e,t,n){[...t].forEach(t=>{let r=n||this._getRegistryForType(t);n||r.isForType(t)||r===this.plugins&&t.id?this._exec(e,r,t):J(t,t=>{let r=n||this._getRegistryForType(t);this._exec(e,r,t)})})}_exec(e,t,n){let r=ja(e);q(n[`before`+r],[],n),t[e](n),q(n[`after`+r],[],n)}_getRegistryForType(e){for(let t=0;te.filter(e=>!t.some(t=>e.plugin.id===t.plugin.id));this._notify(r(t,n),e,`stop`),this._notify(r(n,t),e,`start`)}};function ad(e){let t={},n=[],r=Object.keys(rd.plugins.items);for(let e=0;e1&&fd(e[0].toLowerCase());if(t)return t}throw Error(`Cannot determine type of '${e}' axis. Please provide 'axis' or 'position' option.`)}function hd(e,t,n){if(n[t+`AxisID`]===e)return{axis:t}}function gd(e,t){if(t.data&&t.data.datasets){let n=t.data.datasets.filter(t=>t.xAxisID===e||t.yAxisID===e);if(n.length)return hd(e,`x`,n[0])||hd(e,`y`,n[0])}return{}}function _d(e,t){let n=Vo[e.type]||{scales:{}},r=t.scales||{},i=ld(e.type,t),a=Object.create(null);return Object.keys(r).forEach(t=>{let o=r[t];if(!G(o))return console.error(`Invalid scale configuration for scale: ${t}`);if(o._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${t}`);let s=md(t,o,gd(t,e),Q.scales[o.type]),c=dd(s,i),l=n.scales||{};a[t]=Ta(Object.create(null),[{axis:s},o,l[s],l[c]])}),e.data.datasets.forEach(n=>{let i=n.type||e.type,o=n.indexAxis||ld(i,t),s=(Vo[i]||{}).scales||{};Object.keys(s).forEach(e=>{let t=ud(e,o),i=n[t+`AxisID`]||t;a[i]=a[i]||Object.create(null),Ta(a[i],[{axis:t},r[i],s[e]])})}),Object.keys(a).forEach(e=>{let t=a[e];Ta(t,[Q.scales[t.type],Q.scale])}),a}function vd(e){let t=e.options||(e.options={});t.plugins=K(t.plugins,{}),t.scales=_d(e,t)}function yd(e){return e=e||{},e.datasets=e.datasets||[],e.labels=e.labels||[],e}function bd(e){return e=e||{},e.data=yd(e.data),vd(e),e}var xd=new Map,Sd=new Set;function Cd(e,t){let n=xd.get(e);return n||(n=t(),xd.set(e,n),Sd.add(n)),n}var wd=(e,t,n)=>{let r=Aa(t,n);r!==void 0&&e.add(r)},Td=class{constructor(e){this._config=bd(e),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(e){this._config.type=e}get data(){return this._config.data}set data(e){this._config.data=yd(e)}get options(){return this._config.options}set options(e){this._config.options=e}get plugins(){return this._config.plugins}update(){let e=this._config;this.clearCache(),vd(e)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(e){return Cd(e,()=>[[`datasets.${e}`,``]])}datasetAnimationScopeKeys(e,t){return Cd(`${e}.transition.${t}`,()=>[[`datasets.${e}.transitions.${t}`,`transitions.${t}`],[`datasets.${e}`,``]])}datasetElementScopeKeys(e,t){return Cd(`${e}-${t}`,()=>[[`datasets.${e}.elements.${t}`,`datasets.${e}`,`elements.${t}`,``]])}pluginScopeKeys(e){let t=e.id,n=this.type;return Cd(`${n}-plugin-${t}`,()=>[[`plugins.${t}`,...e.additionalOptionScopes||[]]])}_cachedScopes(e,t){let n=this._scopeCache,r=n.get(e);return(!r||t)&&(r=new Map,n.set(e,r)),r}getOptionScopes(e,t,n){let{options:r,type:i}=this,a=this._cachedScopes(e,n),o=a.get(t);if(o)return o;let s=new Set;t.forEach(t=>{e&&(s.add(e),t.forEach(t=>wd(s,e,t))),t.forEach(e=>wd(s,r,e)),t.forEach(e=>wd(s,Vo[i]||{},e)),t.forEach(e=>wd(s,Q,e)),t.forEach(e=>wd(s,Ho,e))});let c=Array.from(s);return c.length===0&&c.push(Object.create(null)),Sd.has(t)&&a.set(t,c),c}chartOptionScopes(){let{options:e,type:t}=this;return[e,Vo[t]||{},Q.datasets[t]||{},{type:t},Q,Ho]}resolveNamedOptions(e,t,n,r=[``]){let i={$shared:!0},{resolver:a,subPrefixes:o}=Ed(this._resolverCache,e,r),s=a;if(Od(a,t)){i.$shared=!1,n=Na(n)?n():n;let t=this.createResolver(e,n,o);s=xs(a,n,t)}for(let e of t)i[e]=s[e];return i}createResolver(e,t,n=[``],r){let{resolver:i}=Ed(this._resolverCache,e,n);return G(t)?xs(i,t,void 0,r):i}};function Ed(e,t,n){let r=e.get(t);r||(r=new Map,e.set(t,r));let i=n.join(),a=r.get(i);return a||(a={resolver:bs(t,n),subPrefixes:n.filter(e=>!e.toLowerCase().includes(`hover`))},r.set(i,a)),a}var Dd=e=>G(e)&&Object.getOwnPropertyNames(e).some(t=>Na(e[t]));function Od(e,t){let{isScriptable:n,isIndexable:r}=Ss(e);for(let i of t){let t=n(i),a=r(i),o=(a||t)&&e[i];if(t&&(Na(o)||Dd(o))||a&&W(o))return!0}return!1}var kd=`4.5.1`,Ad=[`top`,`bottom`,`left`,`right`,`chartArea`];function jd(e,t){return e===`top`||e===`bottom`||Ad.indexOf(e)===-1&&t===`x`}function Md(e,t){return function(n,r){return n[e]===r[e]?n[t]-r[t]:n[e]-r[e]}}function Nd(e){let t=e.chart,n=t.options.animation;t.notifyPlugins(`afterRender`),q(n&&n.onComplete,[e],t)}function Pd(e){let t=e.chart,n=t.options.animation;q(n&&n.onProgress,[e],t)}function Fd(e){return Xs()&&typeof e==`string`?e=document.getElementById(e):e&&e.length&&(e=e[0]),e&&e.canvas&&(e=e.canvas),e}var Id={},Ld=e=>{let t=Fd(e);return Object.values(Id).filter(e=>e.canvas===t).pop()};function Rd(e,t,n){let r=Object.keys(e);for(let i of r){let r=+i;if(r>=t){let a=e[i];delete e[i],(n>0||r>t)&&(e[r+n]=a)}}}function zd(e,t,n,r){return!n||e.type===`mouseout`?null:r?t:e}var Bd=class{static register(...e){rd.add(...e),Vd()}static unregister(...e){rd.remove(...e),Vd()}constructor(e,t){let n=this.config=new Td(t),r=Fd(e),i=Ld(r);if(i)throw Error(`Canvas is already in use. Chart with ID '`+i.id+`' must be destroyed before the canvas with ID '`+i.canvas.id+`' can be reused.`);let a=n.createResolver(n.chartOptionScopes(),this.getContext());this.platform=new(n.platform||(ju(r))),this.platform.updateConfig(n);let o=this.platform.acquireContext(r,a.aspectRatio),s=o&&o.canvas,c=s&&s.height,l=s&&s.width;this.id=ha(),this.ctx=o,this.canvas=s,this.width=l,this.height=c,this._options=a,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new id,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=vo(e=>this.update(e),a.resizeDelay||0),this._dataChanges=[],Id[this.id]=this,!o||!s?console.error(`Failed to create chart: can't acquire context from the given item`):(Rc.listen(this,`complete`,Nd),Rc.listen(this,`progress`,Pd),this._initialize(),this.attached&&this.update())}get aspectRatio(){let{options:{aspectRatio:e,maintainAspectRatio:t},width:n,height:r,_aspectRatio:i}=this;return U(e)?t&&i?i:r?n/r:null:e}get data(){return this.config.data}set data(e){this.config.data=e}get options(){return this._options}set options(e){this.config.options=e}get registry(){return rd}_initialize(){return this.notifyPlugins(`beforeInit`),this.options.responsive?this.resize():lc(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins(`afterInit`),this}clear(){return Yo(this.canvas,this.ctx),this}stop(){return Rc.stop(this),this}resize(e,t){Rc.running(this)?this._resizeBeforeDraw={width:e,height:t}:this._resize(e,t)}_resize(e,t){let n=this.options,r=this.canvas,i=n.maintainAspectRatio&&this.aspectRatio,a=this.platform.getMaximumSize(r,e,t,i),o=n.devicePixelRatio||this.platform.getDevicePixelRatio(),s=this.width?`resize`:`attach`;this.width=a.width,this.height=a.height,this._aspectRatio=this.aspectRatio,lc(this,o,!0)&&(this.notifyPlugins(`resize`,{size:a}),q(n.onResize,[this,a],this),this.attached&&this._doResize(s)&&this.render())}ensureScalesHaveIDs(){J(this.options.scales||{},(e,t)=>{e.id=t})}buildOrUpdateScales(){let e=this.options,t=e.scales,n=this.scales,r=Object.keys(n).reduce((e,t)=>(e[t]=!1,e),{}),i=[];t&&(i=i.concat(Object.keys(t).map(e=>{let n=t[e],r=md(e,n),i=r===`r`,a=r===`x`;return{options:n,dposition:i?`chartArea`:a?`bottom`:`left`,dtype:i?`radialLinear`:a?`category`:`linear`}}))),J(i,t=>{let i=t.options,a=i.id,o=md(a,i),s=K(i.type,t.dtype);(i.position===void 0||jd(i.position,o)!==jd(t.dposition))&&(i.position=t.dposition),r[a]=!0;let c=null;a in n&&n[a].type===s?c=n[a]:(c=new(rd.getScale(s))({id:a,type:s,ctx:this.ctx,chart:this}),n[c.id]=c),c.init(i,e)}),J(r,(e,t)=>{e||delete n[t]}),J(n,e=>{cu.configure(this,e,e.options),cu.addBox(this,e)})}_updateMetasets(){let e=this._metasets,t=this.data.datasets.length,n=e.length;if(e.sort((e,t)=>e.index-t.index),n>t){for(let e=t;et.length&&delete this._stacks,e.forEach((e,n)=>{t.filter(t=>t===e._dataset).length===0&&this._destroyDatasetMeta(n)})}buildOrUpdateControllers(){let e=[],t=this.data.datasets,n,r;for(this._removeUnreferencedMetasets(),n=0,r=t.length;n{this.getDatasetMeta(t).controller.reset()},this)}reset(){this._resetElements(),this.notifyPlugins(`reset`)}update(e){let t=this.config;t.update();let n=this._options=t.createResolver(t.chartOptionScopes(),this.getContext()),r=this._animationsDisabled=!n.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),this.notifyPlugins(`beforeUpdate`,{mode:e,cancelable:!0})===!1)return;let i=this.buildOrUpdateControllers();this.notifyPlugins(`beforeElementsUpdate`);let a=0;for(let e=0,t=this.data.datasets.length;e{e.reset()}),this._updateDatasets(e),this.notifyPlugins(`afterUpdate`,{mode:e}),this._layers.sort(Md(`z`,`_idx`));let{_active:o,_lastEvent:s}=this;s?this._eventHandler(s,!0):o.length&&this._updateHoverStyles(o,o,!0),this.render()}_updateScales(){J(this.scales,e=>{cu.removeBox(this,e)}),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){let e=this.options;(!Pa(new Set(Object.keys(this._listeners)),new Set(e.events))||!!this._responsiveListeners!==e.responsive)&&(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){let{_hiddenIndices:e}=this,t=this._getUniformDataChanges()||[];for(let{method:n,start:r,count:i}of t)Rd(e,r,n===`_removeElements`?-i:i)}_getUniformDataChanges(){let e=this._dataChanges;if(!e||!e.length)return;this._dataChanges=[];let t=this.data.datasets.length,n=t=>new Set(e.filter(e=>e[0]===t).map((e,t)=>t+`,`+e.splice(1).join(`,`))),r=n(0);for(let e=1;ee.split(`,`)).map(e=>({method:e[1],start:+e[2],count:+e[3]}))}_updateLayout(e){if(this.notifyPlugins(`beforeLayout`,{cancelable:!0})===!1)return;cu.update(this,this.width,this.height,e);let t=this.chartArea,n=t.width<=0||t.height<=0;this._layers=[],J(this.boxes,e=>{n&&e.position===`chartArea`||(e.configure&&e.configure(),this._layers.push(...e._layers()))},this),this._layers.forEach((e,t)=>{e._idx=t}),this.notifyPlugins(`afterLayout`)}_updateDatasets(e){if(this.notifyPlugins(`beforeDatasetsUpdate`,{mode:e,cancelable:!0})!==!1){for(let e=0,t=this.data.datasets.length;e=0;--t)this._drawDataset(e[t]);this.notifyPlugins(`afterDatasetsDraw`)}_drawDataset(e){let t=this.ctx,n={meta:e,index:e.index,cancelable:!0},r=Pc(this,e);this.notifyPlugins(`beforeDatasetDraw`,n)!==!1&&(r&&$o(t,r),e.controller.draw(),r&&es(t),n.cancelable=!1,this.notifyPlugins(`afterDatasetDraw`,n))}isPointInArea(e){return Qo(e,this.chartArea,this._minPadding)}getElementsAtEventForMode(e,t,n,r){let i=Gl.modes[t];return typeof i==`function`?i(this,e,n,r):[]}getDatasetMeta(e){let t=this.data.datasets[e],n=this._metasets,r=n.filter(e=>e&&e._dataset===t).pop();return r||(r={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:t&&t.order||0,index:e,_dataset:t,_parsed:[],_sorted:!1},n.push(r)),r}getContext(){return this.$context||(this.$context=ys(null,{chart:this,type:`chart`}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(e){let t=this.data.datasets[e];if(!t)return!1;let n=this.getDatasetMeta(e);return typeof n.hidden==`boolean`?!n.hidden:!t.hidden}setDatasetVisibility(e,t){let n=this.getDatasetMeta(e);n.hidden=!t}toggleDataVisibility(e){this._hiddenIndices[e]=!this._hiddenIndices[e]}getDataVisibility(e){return!this._hiddenIndices[e]}_updateVisibility(e,t,n){let r=n?`show`:`hide`,i=this.getDatasetMeta(e),a=i.controller._resolveAnimations(void 0,r);Ma(t)?(i.data[t].hidden=!n,this.update()):(this.setDatasetVisibility(e,n),a.update(i,{visible:n}),this.update(t=>t.datasetIndex===e?r:void 0))}hide(e,t){this._updateVisibility(e,t,!1)}show(e,t){this._updateVisibility(e,t,!0)}_destroyDatasetMeta(e){let t=this._metasets[e];t&&t.controller&&t.controller._destroy(),delete this._metasets[e]}_stop(){let e,t;for(this.stop(),Rc.remove(this),e=0,t=this.data.datasets.length;e{t.addEventListener(this,n,r),e[n]=r},r=(e,t,n)=>{e.offsetX=t,e.offsetY=n,this._eventHandler(e)};J(this.options.events,e=>n(e,r))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});let e=this._responsiveListeners,t=this.platform,n=(n,r)=>{t.addEventListener(this,n,r),e[n]=r},r=(n,r)=>{e[n]&&(t.removeEventListener(this,n,r),delete e[n])},i=(e,t)=>{this.canvas&&this.resize(e,t)},a,o=()=>{r(`attach`,o),this.attached=!0,this.resize(),n(`resize`,i),n(`detach`,a)};a=()=>{this.attached=!1,r(`resize`,i),this._stop(),this._resize(0,0),n(`attach`,o)},t.isAttached(this.canvas)?o():a()}unbindEvents(){J(this._listeners,(e,t)=>{this.platform.removeEventListener(this,t,e)}),this._listeners={},J(this._responsiveListeners,(e,t)=>{this.platform.removeEventListener(this,t,e)}),this._responsiveListeners=void 0}updateHoverStyle(e,t,n){let r=n?`set`:`remove`,i,a,o,s;for(t===`dataset`&&(i=this.getDatasetMeta(e[0].datasetIndex),i.controller[`_`+r+`DatasetHoverStyle`]()),o=0,s=e.length;o{let n=this.getDatasetMeta(e);if(!n)throw Error(`No dataset found at index `+e);return{datasetIndex:e,element:n.data[t],index:t}});ba(n,t)||(this._active=n,this._lastEvent=null,this._updateHoverStyles(n,t))}notifyPlugins(e,t,n){return this._plugins.notify(this,e,t,n)}isPluginEnabled(e){return this._plugins._cache.filter(t=>t.plugin.id===e).length===1}_updateHoverStyles(e,t,n){let r=this.options.hover,i=(e,t)=>e.filter(e=>!t.some(t=>e.datasetIndex===t.datasetIndex&&e.index===t.index)),a=i(t,e),o=n?e:i(e,t);a.length&&this.updateHoverStyle(a,r.mode,!1),o.length&&r.mode&&this.updateHoverStyle(o,r.mode,!0)}_eventHandler(e,t){let n={event:e,replay:t,cancelable:!0,inChartArea:this.isPointInArea(e)},r=t=>(t.options.events||this.options.events).includes(e.native.type);if(this.notifyPlugins(`beforeEvent`,n,r)===!1)return;let i=this._handleEvent(e,t,n.inChartArea);return n.cancelable=!1,this.notifyPlugins(`afterEvent`,n,r),(i||n.changed)&&this.render(),this}_handleEvent(e,t,n){let{_active:r=[],options:i}=this,a=t,o=this._getActiveElements(e,r,n,a),s=Fa(e),c=zd(e,this._lastEvent,n,s);n&&(this._lastEvent=null,q(i.onHover,[e,o,this],this),s&&q(i.onClick,[e,o,this],this));let l=!ba(o,r);return(l||t)&&(this._active=o,this._updateHoverStyles(o,r,t)),this._lastEvent=c,l}_getActiveElements(e,t,n,r){if(e.type===`mouseout`)return[];if(!n)return t;let i=this.options.hover;return this.getElementsAtEventForMode(e,i.mode,i,r)}};$(Bd,`defaults`,Q),$(Bd,`instances`,Id),$(Bd,`overrides`,Vo),$(Bd,`registry`,rd),$(Bd,`version`,kd),$(Bd,`getChart`,Ld);function Vd(){return J(Bd.instances,e=>e._plugins.invalidate())}function Hd(e,t,n){let{startAngle:r,x:i,y:a,outerRadius:o,innerRadius:s,options:c}=t,{borderWidth:l,borderJoinStyle:u}=c,d=Math.min(l/o,no(r-n));if(e.beginPath(),e.arc(i,a,o-l/2,r+d/2,n-d/2),s>0){let t=Math.min(l/s,no(r-n));e.arc(i,a,s+l/2,n-t/2,r+t/2,!0)}else{let t=Math.min(l/2,o*no(r-n));if(u===`round`)e.arc(i,a,t,n-Y/2,r+Y/2,!0);else if(u===`bevel`){let o=2*t*t,s=-o*Math.cos(n+Y/2)+i,c=-o*Math.sin(n+Y/2)+a,l=o*Math.cos(r+Y/2)+i,u=o*Math.sin(r+Y/2)+a;e.lineTo(s,c),e.lineTo(l,u)}}e.closePath(),e.moveTo(0,0),e.rect(0,0,e.canvas.width,e.canvas.height),e.clip(`evenodd`)}function Ud(e,t,n){let{startAngle:r,pixelMargin:i,x:a,y:o,outerRadius:s,innerRadius:c}=t,l=i/s;e.beginPath(),e.arc(a,o,s,r-l,n+l),c>i?(l=i/c,e.arc(a,o,c,n+l,r-l,!0)):e.arc(a,o,i,n+Z,r-Z),e.closePath(),e.clip()}function Wd(e){return fs(e,[`outerStart`,`outerEnd`,`innerStart`,`innerEnd`])}function Gd(e,t,n,r){let i=Wd(e.options.borderRadius),a=(n-t)/2,o=Math.min(a,r*t/2),s=e=>{let t=(n-Math.min(a,e))*r/2;return io(e,0,Math.min(a,t))};return{outerStart:s(i.outerStart),outerEnd:s(i.outerEnd),innerStart:io(i.innerStart,0,o),innerEnd:io(i.innerEnd,0,o)}}function Kd(e,t,n,r){return{x:n+e*Math.cos(t),y:r+e*Math.sin(t)}}function qd(e,t,n,r,i,a){let{x:o,y:s,startAngle:c,pixelMargin:l,innerRadius:u}=t,d=Math.max(t.outerRadius+r+n-l,0),f=u>0?u+r+n+l:0,p=0,m=i-c;if(r){let e=((u>0?u-r:0)+(d>0?d-r:0))/2;p=(m-(e===0?m:m*e/(e+r)))/2}let h=(m-Math.max(.001,m*d-n/Y)/d)/2,g=c+h+p,_=i-h-p,{outerStart:v,outerEnd:y,innerStart:b,innerEnd:x}=Gd(t,f,d,_-g),S=d-v,C=d-y,w=g+v/S,T=_-y/C,E=f+b,D=f+x,O=g+b/E,k=_-x/D;if(e.beginPath(),a){let t=(w+T)/2;if(e.arc(o,s,d,w,t),e.arc(o,s,d,t,T),y>0){let t=Kd(C,T,o,s);e.arc(t.x,t.y,y,T,_+Z)}let n=Kd(D,_,o,s);if(e.lineTo(n.x,n.y),x>0){let t=Kd(D,k,o,s);e.arc(t.x,t.y,x,_+Z,k+Math.PI)}let r=(_-x/f+(g+b/f))/2;if(e.arc(o,s,f,_-x/f,r,!0),e.arc(o,s,f,r,g+b/f,!0),b>0){let t=Kd(E,O,o,s);e.arc(t.x,t.y,b,O+Math.PI,g-Z)}let i=Kd(S,g,o,s);if(e.lineTo(i.x,i.y),v>0){let t=Kd(S,w,o,s);e.arc(t.x,t.y,v,g-Z,w)}}else{e.moveTo(o,s);let t=Math.cos(w)*d+o,n=Math.sin(w)*d+s;e.lineTo(t,n);let r=Math.cos(T)*d+o,i=Math.sin(T)*d+s;e.lineTo(r,i)}e.closePath()}function Jd(e,t,n,r,i){let{fullCircles:a,startAngle:o,circumference:s}=t,c=t.endAngle;if(a){qd(e,t,n,r,c,i);for(let t=0;t=Y&&p===0&&u!==`miter`&&Hd(e,t,h),a||(qd(e,t,n,r,h,i),e.stroke())}var Xd=class extends Mu{constructor(e){super(),$(this,`circumference`,void 0),$(this,`endAngle`,void 0),$(this,`fullCircles`,void 0),$(this,`innerRadius`,void 0),$(this,`outerRadius`,void 0),$(this,`pixelMargin`,void 0),$(this,`startAngle`,void 0),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,e&&Object.assign(this,e)}inRange(e,t,n){let{angle:r,distance:i}=$a(this.getProps([`x`,`y`],n),{x:e,y:t}),{startAngle:a,endAngle:o,innerRadius:s,outerRadius:c,circumference:l}=this.getProps([`startAngle`,`endAngle`,`innerRadius`,`outerRadius`,`circumference`],n),u=(this.options.spacing+this.options.borderWidth)/2,d=K(l,o-a),f=ro(r,a,o)&&a!==o,p=d>=X||f,m=oo(i,s+u,c+u);return p&&m}getCenterPoint(e){let{x:t,y:n,startAngle:r,endAngle:i,innerRadius:a,outerRadius:o}=this.getProps([`x`,`y`,`startAngle`,`endAngle`,`innerRadius`,`outerRadius`],e),{offset:s,spacing:c}=this.options,l=(r+i)/2,u=(a+o+c+s)/2;return{x:t+Math.cos(l)*u,y:n+Math.sin(l)*u}}tooltipPosition(e){return this.getCenterPoint(e)}draw(e){let{options:t,circumference:n}=this,r=(t.offset||0)/4,i=(t.spacing||0)/2,a=t.circular;if(this.pixelMargin=t.borderAlign===`inner`?.33:0,this.fullCircles=n>X?Math.floor(n/X):0,n===0||this.innerRadius<0||this.outerRadius<0)return;e.save();let o=(this.startAngle+this.endAngle)/2;e.translate(Math.cos(o)*r,Math.sin(o)*r);let s=r*(1-Math.sin(Math.min(Y,n||0)));e.fillStyle=t.backgroundColor,e.strokeStyle=t.borderColor,Jd(e,this,s,i,a),Yd(e,this,s,i,a),e.restore()}};$(Xd,`id`,`arc`),$(Xd,`defaults`,{borderAlign:`center`,borderColor:`#fff`,borderDash:[],borderDashOffset:0,borderJoinStyle:void 0,borderRadius:0,borderWidth:2,offset:0,spacing:0,angle:void 0,circular:!0,selfJoin:!1}),$(Xd,`defaultRoutes`,{backgroundColor:`backgroundColor`}),$(Xd,`descriptors`,{_scriptable:!0,_indexable:e=>e!==`borderDash`});function Zd(e,t,n=t){e.lineCap=K(n.borderCapStyle,t.borderCapStyle),e.setLineDash(K(n.borderDash,t.borderDash)),e.lineDashOffset=K(n.borderDashOffset,t.borderDashOffset),e.lineJoin=K(n.borderJoinStyle,t.borderJoinStyle),e.lineWidth=K(n.borderWidth,t.borderWidth),e.strokeStyle=K(n.borderColor,t.borderColor)}function Qd(e,t,n){e.lineTo(n.x,n.y)}function $d(e){return e.stepped?ts:e.tension||e.cubicInterpolationMode===`monotone`?ns:Qd}function ef(e,t,n={}){let r=e.length,{start:i=0,end:a=r-1}=n,{start:o,end:s}=t,c=Math.max(i,o),l=Math.min(a,s),u=is&&a>s;return{count:r,start:c,loop:t.loop,ilen:l(o+(l?s-e:e))%a,y=()=>{h!==g&&(e.lineTo(u,g),e.lineTo(u,h),e.lineTo(u,_))};for(c&&(p=i[v(0)],e.moveTo(p.x,p.y)),f=0;f<=s;++f){if(p=i[v(f)],p.skip)continue;let t=p.x,n=p.y,r=t|0;r===m?(ng&&(g=n),u=(d*u+t)/++d):(y(),e.lineTo(t,n),m=r,d=0,h=g=n),_=n}y()}function rf(e){let t=e.options,n=t.borderDash&&t.borderDash.length;return!e._decimated&&!e._loop&&!t.tension&&t.cubicInterpolationMode!==`monotone`&&!t.stepped&&!n?nf:tf}function af(e){return e.stepped?pc:e.tension||e.cubicInterpolationMode===`monotone`?mc:fc}function of(e,t,n,r){let i=t._path;i||(i=t._path=new Path2D,t.path(i,n,r)&&i.closePath()),Zd(e,t.options),e.stroke(i)}function sf(e,t,n,r){let{segments:i,options:a}=t,o=rf(t);for(let s of i)Zd(e,a,s.style),e.beginPath(),o(e,t,s,{start:n,end:n+r-1})&&e.closePath(),e.stroke()}var cf=typeof Path2D==`function`;function lf(e,t,n,r){cf&&!t.options.segment?of(e,t,n,r):sf(e,t,n,r)}var uf=class extends Mu{constructor(e){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,e&&Object.assign(this,e)}updateControlPoints(e,t){let n=this.options;if((n.tension||n.cubicInterpolationMode===`monotone`)&&!n.stepped&&!this._pointsUpdated){let r=n.spanGaps?this._loop:this._fullLoop;Ys(this._points,n,e,r,t),this._pointsUpdated=!0}}set points(e){this._points=e,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=Dc(this,this.options.segment))}first(){let e=this.segments,t=this.points;return e.length&&t[e[0].start]}last(){let e=this.segments,t=this.points,n=e.length;return n&&t[e[n-1].end]}interpolate(e,t){let n=this.options,r=e[t],i=this.points,a=wc(this,{property:t,start:r,end:r});if(!a.length)return;let o=[],s=af(n),c,l;for(c=0,l=a.length;ce!==`borderDash`&&e!==`fill`});function df(e,t,n,r){let i=e.options,{[n]:a}=e.getProps([n],r);return Math.abs(t-a)e+t)/r.size,y:i/a}},nearest(e,t){if(!e.length)return!1;let n=t.x,r=t.y,i=1/0,a,o,s;for(a=0,o=e.length;a-1?e.split(` +`):e}function Ef(e,t){let{element:n,datasetIndex:r,index:i}=t,a=e.getDatasetMeta(r).controller,{label:o,value:s}=a.getLabelAndValue(i);return{chart:e,label:o,parsed:a.getParsed(i),raw:e.data.datasets[r].data[i],formattedValue:s,dataset:a.getDataset(),dataIndex:i,datasetIndex:r,element:n}}function Df(e,t){let n=e.chart.ctx,{body:r,footer:i,title:a}=e,{boxWidth:o,boxHeight:s}=t,c=gs(t.bodyFont),l=gs(t.titleFont),u=gs(t.footerFont),d=a.length,f=i.length,p=r.length,m=hs(t.padding),h=m.height,g=0,_=r.reduce((e,t)=>e+t.before.length+t.lines.length+t.after.length,0);if(_+=e.beforeBody.length+e.afterBody.length,d&&(h+=d*l.lineHeight+(d-1)*t.titleSpacing+t.titleMarginBottom),_){let e=t.displayColors?Math.max(s,c.lineHeight):c.lineHeight;h+=p*e+(_-p)*c.lineHeight+(_-1)*t.bodySpacing}f&&(h+=t.footerMarginTop+f*u.lineHeight+(f-1)*t.footerSpacing);let v=0,y=function(e){g=Math.max(g,n.measureText(e).width+v)};return n.save(),n.font=l.string,J(e.title,y),n.font=c.string,J(e.beforeBody.concat(e.afterBody),y),v=t.displayColors?o+2+t.boxPadding:0,J(r,e=>{J(e.before,y),J(e.lines,y),J(e.after,y)}),v=0,n.font=u.string,J(e.footer,y),n.restore(),g+=m.width,{width:g,height:h}}function Of(e,t){let{y:n,height:r}=t;return ne.height-r/2?`bottom`:`center`}function kf(e,t,n,r){let{x:i,width:a}=r,o=n.caretSize+n.caretPadding;if(e===`left`&&i+a+o>t.width||e===`right`&&i-a-o<0)return!0}function Af(e,t,n,r){let{x:i,width:a}=n,{width:o,chartArea:{left:s,right:c}}=e,l=`center`;return r===`center`?l=i<=(s+c)/2?`left`:`right`:i<=a/2?l=`left`:i>=o-a/2&&(l=`right`),kf(l,e,t,n)&&(l=`center`),l}function jf(e,t,n){let r=n.yAlign||t.yAlign||Of(e,n);return{xAlign:n.xAlign||t.xAlign||Af(e,t,n,r),yAlign:r}}function Mf(e,t){let{x:n,width:r}=e;return t===`right`?n-=r:t===`center`&&(n-=r/2),n}function Nf(e,t,n){let{y:r,height:i}=e;return t===`top`?r+=n:r-=t===`bottom`?i+n:i/2,r}function Pf(e,t,n,r){let{caretSize:i,caretPadding:a,cornerRadius:o}=e,{xAlign:s,yAlign:c}=n,l=i+a,{topLeft:u,topRight:d,bottomLeft:f,bottomRight:p}=ms(o),m=Mf(t,s),h=Nf(t,c,l);return c===`center`?s===`left`?m+=l:s===`right`&&(m-=l):s===`left`?m-=Math.max(u,f)+i:s===`right`&&(m+=Math.max(d,p)+i),{x:io(m,0,r.width-t.width),y:io(h,0,r.height-t.height)}}function Ff(e,t,n){let r=hs(n.padding);return t===`center`?e.x+e.width/2:t===`right`?e.x+e.width-r.right:e.x+r.left}function If(e){return wf([],Tf(e))}function Lf(e,t,n){return ys(e,{tooltip:t,tooltipItems:n,type:`tooltip`})}function Rf(e,t){let n=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return n?e.override(n):e}var zf={beforeTitle:ma,title(e){if(e.length>0){let t=e[0],n=t.chart.data.labels,r=n?n.length:0;if(this&&this.options&&this.options.mode===`dataset`)return t.dataset.label||``;if(t.label)return t.label;if(r>0&&t.dataIndex{let t={before:[],lines:[],after:[]},i=Rf(n,e);wf(t.before,Tf(Bf(i,`beforeLabel`,this,e))),wf(t.lines,Bf(i,`label`,this,e)),wf(t.after,Tf(Bf(i,`afterLabel`,this,e))),r.push(t)}),r}getAfterBody(e,t){return If(Bf(t.callbacks,`afterBody`,this,e))}getFooter(e,t){let{callbacks:n}=t,r=Bf(n,`beforeFooter`,this,e),i=Bf(n,`footer`,this,e),a=Bf(n,`afterFooter`,this,e),o=[];return o=wf(o,Tf(r)),o=wf(o,Tf(i)),o=wf(o,Tf(a)),o}_createItems(e){let t=this._active,n=this.chart.data,r=[],i=[],a=[],o=[],s,c;for(s=0,c=t.length;se.filter(t,r,i,n))),e.itemSort&&(o=o.sort((t,r)=>e.itemSort(t,r,n))),J(o,t=>{let n=Rf(e.callbacks,t);r.push(Bf(n,`labelColor`,this,t)),i.push(Bf(n,`labelPointStyle`,this,t)),a.push(Bf(n,`labelTextColor`,this,t))}),this.labelColors=r,this.labelPointStyles=i,this.labelTextColors=a,this.dataPoints=o,o}update(e,t){let n=this.options.setContext(this.getContext()),r=this._active,i,a=[];if(!r.length)this.opacity!==0&&(i={opacity:0});else{let e=Cf[n.position].call(this,r,this._eventPosition);a=this._createItems(n),this.title=this.getTitle(a,n),this.beforeBody=this.getBeforeBody(a,n),this.body=this.getBody(a,n),this.afterBody=this.getAfterBody(a,n),this.footer=this.getFooter(a,n);let t=this._size=Df(this,n),o=Object.assign({},e,t),s=jf(this.chart,n,o),c=Pf(n,o,s,this.chart);this.xAlign=s.xAlign,this.yAlign=s.yAlign,i={opacity:1,x:c.x,y:c.y,width:t.width,height:t.height,caretX:e.x,caretY:e.y}}this._tooltipItems=a,this.$context=void 0,i&&this._resolveAnimations().update(this,i),e&&n.external&&n.external.call(this,{chart:this.chart,tooltip:this,replay:t})}drawCaret(e,t,n,r){let i=this.getCaretPosition(e,n,r);t.lineTo(i.x1,i.y1),t.lineTo(i.x2,i.y2),t.lineTo(i.x3,i.y3)}getCaretPosition(e,t,n){let{xAlign:r,yAlign:i}=this,{caretSize:a,cornerRadius:o}=n,{topLeft:s,topRight:c,bottomLeft:l,bottomRight:u}=ms(o),{x:d,y:f}=e,{width:p,height:m}=t,h,g,_,v,y,b;return i===`center`?(y=f+m/2,r===`left`?(h=d,g=h-a,v=y+a,b=y-a):(h=d+p,g=h+a,v=y-a,b=y+a),_=h):(g=r===`left`?d+Math.max(s,l)+a:r===`right`?d+p-Math.max(c,u)-a:this.caretX,i===`top`?(v=f,y=v-a,h=g-a,_=g+a):(v=f+m,y=v+a,h=g+a,_=g-a),b=v),{x1:h,x2:g,x3:_,y1:v,y2:y,y3:b}}drawTitle(e,t,n){let r=this.title,i=r.length,a,o,s;if(i){let c=_c(n.rtl,this.x,this.width);for(e.x=Ff(this,n.titleAlign,n),t.textAlign=c.textAlign(n.titleAlign),t.textBaseline=`middle`,a=gs(n.titleFont),o=n.titleSpacing,t.fillStyle=n.titleColor,t.font=a.string,s=0;se!==0)?(e.beginPath(),e.fillStyle=i.multiKeyBackground,ss(e,{x:t,y:p,w:c,h:s,radius:o}),e.fill(),e.stroke(),e.fillStyle=a.backgroundColor,e.beginPath(),ss(e,{x:n,y:p+1,w:c-2,h:s-2,radius:o}),e.fill()):(e.fillStyle=i.multiKeyBackground,e.fillRect(t,p,c,s),e.strokeRect(t,p,c,s),e.fillStyle=a.backgroundColor,e.fillRect(n,p+1,c-2,s-2))}e.fillStyle=this.labelTextColors[n]}drawBody(e,t,n){let{body:r}=this,{bodySpacing:i,bodyAlign:a,displayColors:o,boxHeight:s,boxWidth:c,boxPadding:l}=n,u=gs(n.bodyFont),d=u.lineHeight,f=0,p=_c(n.rtl,this.x,this.width),m=function(n){t.fillText(n,p.x(e.x+f),e.y+d/2),e.y+=d+i},h=p.textAlign(a),g,_,v,y,b,x,S;for(t.textAlign=a,t.textBaseline=`middle`,t.font=u.string,e.x=Ff(this,h,n),t.fillStyle=n.bodyColor,J(this.beforeBody,m),f=o&&h!==`right`?a===`center`?c/2+l:c+2+l:0,y=0,x=r.length;y0&&t.stroke()}_updateAnimationTarget(e){let t=this.chart,n=this.$animations,r=n&&n.x,i=n&&n.y;if(r||i){let n=Cf[e.position].call(this,this._active,this._eventPosition);if(!n)return;let a=this._size=Df(this,e),o=Object.assign({},n,this._size),s=jf(t,e,o),c=Pf(e,o,s,t);(r._to!==c.x||i._to!==c.y)&&(this.xAlign=s.xAlign,this.yAlign=s.yAlign,this.width=a.width,this.height=a.height,this.caretX=n.x,this.caretY=n.y,this._resolveAnimations().update(this,c))}}_willRender(){return!!this.opacity}draw(e){let t=this.options.setContext(this.getContext()),n=this.opacity;if(!n)return;this._updateAnimationTarget(t);let r={width:this.width,height:this.height},i={x:this.x,y:this.y};n=Math.abs(n)<.001?0:n;let a=hs(t.padding),o=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;t.enabled&&o&&(e.save(),e.globalAlpha=n,this.drawBackground(i,e,r,t),vc(e,t.textDirection),i.y+=a.top,this.drawTitle(i,e,t),this.drawBody(i,e,t),this.drawFooter(i,e,t),yc(e,t.textDirection),e.restore())}getActiveElements(){return this._active||[]}setActiveElements(e,t){let n=this._active,r=e.map(({datasetIndex:e,index:t})=>{let n=this.chart.getDatasetMeta(e);if(!n)throw Error(`Cannot find a dataset at index `+e);return{datasetIndex:e,element:n.data[t],index:t}}),i=!ba(n,r),a=this._positionChanged(r,t);(i||a)&&(this._active=r,this._eventPosition=t,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(e,t,n=!0){if(t&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;let r=this.options,i=this._active||[],a=this._getActiveElements(e,i,t,n),o=this._positionChanged(a,e),s=t||!ba(a,i)||o;return s&&(this._active=a,(r.enabled||r.external)&&(this._eventPosition={x:e.x,y:e.y},this.update(!0,t))),s}_getActiveElements(e,t,n,r){let i=this.options;if(e.type===`mouseout`)return[];if(!r)return t.filter(e=>this.chart.data.datasets[e.datasetIndex]&&this.chart.getDatasetMeta(e.datasetIndex).controller.getParsed(e.index)!==void 0);let a=this.chart.getElementsAtEventForMode(e,i.mode,i,n);return i.reverse&&a.reverse(),a}_positionChanged(e,t){let{caretX:n,caretY:r,options:i}=this,a=Cf[i.position].call(this,e,t);return a!==!1&&(n!==a.x||r!==a.y)}};$(Vf,`positioners`,Cf);var Hf={id:`tooltip`,_element:Vf,positioners:Cf,afterInit(e,t,n){n&&(e.tooltip=new Vf({chart:e,options:n}))},beforeUpdate(e,t,n){e.tooltip&&e.tooltip.initialize(n)},reset(e,t,n){e.tooltip&&e.tooltip.initialize(n)},afterDraw(e){let t=e.tooltip;if(t&&t._willRender()){let n={tooltip:t};if(e.notifyPlugins(`beforeTooltipDraw`,{...n,cancelable:!0})===!1)return;t.draw(e.ctx),e.notifyPlugins(`afterTooltipDraw`,n)}},afterEvent(e,t){if(e.tooltip){let n=t.replay;e.tooltip.handleEvent(t.event,n,t.inChartArea)&&(t.changed=!0)}},defaults:{enabled:!0,external:null,position:`average`,backgroundColor:`rgba(0,0,0,0.8)`,titleColor:`#fff`,titleFont:{weight:`bold`},titleSpacing:2,titleMarginBottom:6,titleAlign:`left`,bodyColor:`#fff`,bodySpacing:2,bodyFont:{},bodyAlign:`left`,footerColor:`#fff`,footerSpacing:2,footerMarginTop:6,footerFont:{weight:`bold`},footerAlign:`left`,padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(e,t)=>t.bodyFont.size,boxWidth:(e,t)=>t.bodyFont.size,multiKeyBackground:`#fff`,displayColors:!0,boxPadding:0,borderColor:`rgba(0,0,0,0)`,borderWidth:0,animation:{duration:400,easing:`easeOutQuart`},animations:{numbers:{type:`number`,properties:[`x`,`y`,`width`,`height`,`caretX`,`caretY`]},opacity:{easing:`linear`,duration:200}},callbacks:zf},defaultRoutes:{bodyFont:`font`,footerFont:`font`,titleFont:`font`},descriptors:{_scriptable:e=>e!==`filter`&&e!==`itemSort`&&e!==`external`,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:`animation`}},additionalOptionScopes:[`interaction`]},Uf=(e,t,n,r)=>(typeof t==`string`?(n=e.push(t)-1,r.unshift({index:n,label:t})):isNaN(t)&&(n=null),n);function Wf(e,t,n,r){let i=e.indexOf(t);return i===-1?Uf(e,t,n,r):i===e.lastIndexOf(t)?i:n}var Gf=(e,t)=>e===null?null:io(Math.round(e),0,t);function Kf(e){let t=this.getLabels();return e>=0&&et.length-1?null:this.getPixelForValue(t[e].value)}getValueForPixel(e){return Math.round(this._startValue+this.getDecimalForPixel(e)*this._valueRange)}getBasePixel(){return this.bottom}};$(qf,`id`,`category`),$(qf,`defaults`,{ticks:{callback:Kf}});function Jf(e,t){let n=[],{bounds:r,step:i,min:a,max:o,precision:s,count:c,maxTicks:l,maxDigits:u,includeBounds:d}=e,f=i||1,p=l-1,{min:m,max:h}=t,g=!U(a),_=!U(o),v=!U(c),y=(h-m)/(u+1),b=Wa((h-m)/p/f)*f,x,S,C,w;if(b<1e-14&&!g&&!_)return[{value:m},{value:h}];w=Math.ceil(h/b)-Math.floor(m/b),w>p&&(b=Wa(w*b/p/f)*f),U(s)||(x=10**s,b=Math.ceil(b*x)/x),r===`ticks`?(S=Math.floor(m/b)*b,C=Math.ceil(h/b)*b):(S=m,C=h),g&&_&&i&&Ja((o-a)/i,b/1e3)?(w=Math.round(Math.min((o-a)/b,l)),b=(o-a)/w,S=a,C=o):v?(S=g?a:S,C=_?o:C,w=c-1,b=(C-S)/w):(w=(C-S)/b,w=Ua(w,Math.round(w),b/1e3)?Math.round(w):Math.ceil(w));let T=Math.max(Qa(b),Qa(S));x=10**(U(s)?T:s),S=Math.round(S*x)/x,C=Math.round(C*x)/x;let E=0;for(g&&(d&&S!==a?(n.push({value:a}),So)break;n.push({value:e})}return _&&d&&C!==o?n.length&&Ua(n[n.length-1].value,o,Yf(o,y,e))?n[n.length-1].value=o:n.push({value:o}):(!_||C===o)&&n.push({value:C}),n}function Yf(e,t,{horizontal:n,minRotation:r}){let i=Xa(r),a=(n?Math.sin(i):Math.cos(i))||.001,o=.75*t*(``+e).length;return Math.min(t/a,o)}var Xf=class extends Qu{constructor(e){super(e),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(e,t){return U(e)||(typeof e==`number`||e instanceof Number)&&!isFinite(+e)?null:+e}handleTickRangeOptions(){let{beginAtZero:e}=this.options,{minDefined:t,maxDefined:n}=this.getUserBounds(),{min:r,max:i}=this,a=e=>r=t?r:e,o=e=>i=n?i:e;if(e){let e=Ha(r),t=Ha(i);e<0&&t<0?o(0):e>0&&t>0&&a(0)}if(r===i){let t=i===0?1:Math.abs(i*.05);o(i+t),e||a(r-t)}this.min=r,this.max=i}getTickLimit(){let{maxTicksLimit:e,stepSize:t}=this.options.ticks,n;return t?(n=Math.ceil(this.max/t)-Math.floor(this.min/t)+1,n>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${t} would result generating up to ${n} ticks. Limiting to 1000.`),n=1e3)):(n=this.computeTickLimit(),e=e||11),e&&(n=Math.min(e,n)),n}computeTickLimit(){return 1/0}buildTicks(){let e=this.options,t=e.ticks,n=this.getTickLimit();n=Math.max(2,n);let r=Jf({maxTicks:n,bounds:e.bounds,min:e.min,max:e.max,precision:t.precision,step:t.stepSize,count:t.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:t.minRotation||0,includeBounds:t.includeBounds!==!1},this._range||this);return e.bounds===`ticks`&&Ya(r,this,`value`),e.reverse?(r.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),r}configure(){let e=this.ticks,t=this.min,n=this.max;if(super.configure(),this.options.offset&&e.length){let r=(n-t)/Math.max(e.length-1,1)/2;t-=r,n+=r}this._startValue=t,this._endValue=n,this._valueRange=n-t}getLabelForValue(e){return Io(e,this.chart.options.locale,this.options.ticks.format)}},Zf=class extends Xf{determineDataLimits(){let{min:e,max:t}=this.getMinMax(!0);this.min=ga(e)?e:0,this.max=ga(t)?t:1,this.handleTickRangeOptions()}computeTickLimit(){let e=this.isHorizontal(),t=e?this.width:this.height,n=Xa(this.options.ticks.minRotation),r=(e?Math.sin(n):Math.cos(n))||.001,i=this._resolveTickFontOptions(0);return Math.ceil(t/Math.min(40,i.lineHeight/r))}getPixelForValue(e){return e===null?NaN:this.getPixelForDecimal((e-this._startValue)/this._valueRange)}getValueForPixel(e){return this._startValue+this.getDecimalForPixel(e)*this._valueRange}};$(Zf,`id`,`linear`),$(Zf,`defaults`,{ticks:{callback:zo.formatters.numeric}});var Qf=e=>Math.floor(Va(e)),$f=(e,t)=>10**(Qf(e)+t);function ep(e){return e/10**Qf(e)==1}function tp(e,t,n){let r=10**n,i=Math.floor(e/r);return Math.ceil(t/r)-i}function np(e,t){let n=Qf(t-e);for(;tp(e,t,n)>10;)n++;for(;tp(e,t,n)<10;)n--;return Math.min(n,Qf(e))}function rp(e,{min:t,max:n}){t=_a(e.min,t);let r=[],i=Qf(t),a=np(t,n),o=a<0?10**Math.abs(a):1,s=10**a,c=i>a?10**i:0,l=Math.round((t-c)*o)/o,u=Math.floor((t-c)/s/10)*s*10,d=Math.floor((l-u)/10**a),f=_a(e.min,Math.round((c+u+d*10**a)*o)/o);for(;f=10?d=d<15?15:20:d++,d>=20&&(a++,d=2,o=a>=0?1:o),f=Math.round((c+u+d*10**a)*o)/o;let p=_a(e.max,f);return r.push({value:p,major:ep(p),significand:d}),r}var ip=class extends Qu{constructor(e){super(e),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(e,t){let n=Xf.prototype.parse.apply(this,[e,t]);if(n===0)this._zero=!0;else return ga(n)&&n>0?n:null}determineDataLimits(){let{min:e,max:t}=this.getMinMax(!0);this.min=ga(e)?Math.max(0,e):null,this.max=ga(t)?Math.max(0,t):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!ga(this._userMin)&&(this.min=e===$f(this.min,0)?$f(this.min,-1):$f(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){let{minDefined:e,maxDefined:t}=this.getUserBounds(),n=this.min,r=this.max,i=t=>n=e?n:t,a=e=>r=t?r:e;n===r&&(n<=0?(i(1),a(10)):(i($f(n,-1)),a($f(r,1)))),n<=0&&i($f(r,-1)),r<=0&&a($f(n,1)),this.min=n,this.max=r}buildTicks(){let e=this.options,t=rp({min:this._userMin,max:this._userMax},this);return e.bounds===`ticks`&&Ya(t,this,`value`),e.reverse?(t.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),t}getLabelForValue(e){return e===void 0?`0`:Io(e,this.chart.options.locale,this.options.ticks.format)}configure(){let e=this.min;super.configure(),this._startValue=Va(e),this._valueRange=Va(this.max)-Va(e)}getPixelForValue(e){return(e===void 0||e===0)&&(e=this.min),e===null||isNaN(e)?NaN:this.getPixelForDecimal(e===this.min?0:(Va(e)-this._startValue)/this._valueRange)}getValueForPixel(e){let t=this.getDecimalForPixel(e);return 10**(this._startValue+t*this._valueRange)}};$(ip,`id`,`logarithmic`),$(ip,`defaults`,{ticks:{callback:zo.formatters.logarithmic,major:{enabled:!0}}});function ap(e){let t=e.ticks;if(t.display&&e.display){let e=hs(t.backdropPadding);return K(t.font&&t.font.size,Q.font.size)+e.height}return 0}function op(e,t,n){return n=W(n)?n:[n],{w:qo(e,t.string,n),h:n.length*t.lineHeight}}function sp(e,t,n,r,i){return e===r||e===i?{start:t-n/2,end:t+n/2}:ei?{start:t-n,end:t}:{start:t,end:t+n}}function cp(e){let t={l:e.left+e._padding.left,r:e.right-e._padding.right,t:e.top+e._padding.top,b:e.bottom-e._padding.bottom},n=Object.assign({},t),r=[],i=[],a=e._pointLabels.length,o=e.options.pointLabels,s=o.centerPointLabels?Y/a:0;for(let c=0;ct.r&&(s=(r.end-t.r)/a,e.r=Math.max(e.r,t.r+s)),i.startt.b&&(c=(i.end-t.b)/o,e.b=Math.max(e.b,t.b+c))}function up(e,t,n){let r=e.drawingArea,{extra:i,additionalAngle:a,padding:o,size:s}=n,c=e.getPointPosition(t,r+i+o,a),l=Math.round(Za(no(c.angle+Z))),u=hp(c.y,s.h,l),d=pp(l),f=mp(c.x,s.w,d);return{visible:!0,x:c.x,y:u,textAlign:d,left:f,top:u,right:f+s.w,bottom:u+s.h}}function dp(e,t){if(!t)return!0;let{left:n,top:r,right:i,bottom:a}=e;return!(Qo({x:n,y:r},t)||Qo({x:n,y:a},t)||Qo({x:i,y:r},t)||Qo({x:i,y:a},t))}function fp(e,t,n){let r=[],i=e._pointLabels.length,a=e.options,{centerPointLabels:o,display:s}=a.pointLabels,c={extra:ap(a)/2,additionalAngle:o?Y/i:0},l;for(let a=0;a270||n<90)&&(e-=t),e}function gp(e,t,n){let{left:r,top:i,right:a,bottom:o}=n,{backdropColor:s}=t;if(!U(s)){let n=ms(t.borderRadius),c=hs(t.backdropPadding);e.fillStyle=s;let l=r-c.left,u=i-c.top,d=a-r+c.width,f=o-i+c.height;Object.values(n).some(e=>e!==0)?(e.beginPath(),ss(e,{x:l,y:u,w:d,h:f,radius:n}),e.fill()):e.fillRect(l,u,d,f)}}function _p(e,t){let{ctx:n,options:{pointLabels:r}}=e;for(let i=t-1;i>=0;i--){let t=e._pointLabelItems[i];if(!t.visible)continue;let a=r.setContext(e.getPointLabelContext(i));gp(n,a,t);let o=gs(a.font),{x:s,y:c,textAlign:l}=t;os(n,e._pointLabels[i],s,c+o.lineHeight/2,o,{color:a.color,textAlign:l,textBaseline:`middle`})}}function vp(e,t,n,r){let{ctx:i}=e;if(n)i.arc(e.xCenter,e.yCenter,t,0,X);else{let n=e.getPointPosition(0,t);i.moveTo(n.x,n.y);for(let a=1;a{let n=q(this.options.pointLabels.callback,[e,t],this);return n||n===0?n:``}).filter((e,t)=>this.chart.getDataVisibility(t))}fit(){let e=this.options;e.display&&e.pointLabels.display?cp(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(e,t,n,r){this.xCenter+=Math.floor((e-t)/2),this.yCenter+=Math.floor((n-r)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(e,t,n,r))}getIndexAngle(e){let t=X/(this._pointLabels.length||1),n=this.options.startAngle||0;return no(e*t+Xa(n))}getDistanceFromCenterForValue(e){if(U(e))return NaN;let t=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-e)*t:(e-this.min)*t}getValueForDistanceFromCenter(e){if(U(e))return NaN;let t=e/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-t:this.min+t}getPointLabelContext(e){let t=this._pointLabels||[];if(e>=0&&e{if(t!==0||t===0&&this.min<0){s=this.getDistanceFromCenterForValue(e.value);let n=this.getContext(t),o=r.setContext(n),c=i.setContext(n);yp(this,o,s,a,c)}}),n.display){for(e.save(),o=a-1;o>=0;o--){let r=n.setContext(this.getPointLabelContext(o)),{color:i,lineWidth:a}=r;a&&i&&(e.lineWidth=a,e.strokeStyle=i,e.setLineDash(r.borderDash),e.lineDashOffset=r.borderDashOffset,s=this.getDistanceFromCenterForValue(t.reverse?this.min:this.max),c=this.getPointPosition(o,s),e.beginPath(),e.moveTo(this.xCenter,this.yCenter),e.lineTo(c.x,c.y),e.stroke())}e.restore()}}drawBorder(){}drawLabels(){let e=this.ctx,t=this.options,n=t.ticks;if(!n.display)return;let r=this.getIndexAngle(0),i,a;e.save(),e.translate(this.xCenter,this.yCenter),e.rotate(r),e.textAlign=`center`,e.textBaseline=`middle`,this.ticks.forEach((r,o)=>{if(o===0&&this.min>=0&&!t.reverse)return;let s=n.setContext(this.getContext(o)),c=gs(s.font);if(i=this.getDistanceFromCenterForValue(this.ticks[o].value),s.showLabelBackdrop){e.font=c.string,a=e.measureText(r.label).width,e.fillStyle=s.backdropColor;let t=hs(s.backdropPadding);e.fillRect(-a/2-t.left,-i-c.size/2-t.top,a+t.width,c.size+t.height)}os(e,r.label,0,-i,c,{color:s.color,strokeColor:s.textStrokeColor,strokeWidth:s.textStrokeWidth})}),e.restore()}drawTitle(){}};$(xp,`id`,`radialLinear`),$(xp,`defaults`,{display:!0,animate:!0,position:`chartArea`,angleLines:{display:!0,lineWidth:1,borderDash:[],borderDashOffset:0},grid:{circular:!1},startAngle:0,ticks:{showLabelBackdrop:!0,callback:zo.formatters.numeric},pointLabels:{backdropColor:void 0,backdropPadding:2,display:!0,font:{size:10},callback(e){return e},padding:5,centerPointLabels:!1}}),$(xp,`defaultRoutes`,{"angleLines.color":`borderColor`,"pointLabels.color":`color`,"ticks.color":`color`}),$(xp,`descriptors`,{angleLines:{_fallback:`grid`}});var Sp={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},Cp=Object.keys(Sp);function wp(e,t){return e-t}function Tp(e,t){if(U(t))return null;let n=e._adapter,{parser:r,round:i,isoWeekday:a}=e._parseOpts,o=t;return typeof r==`function`&&(o=r(o)),ga(o)||(o=typeof r==`string`?n.parse(o,r):n.parse(o)),o===null?null:(i&&(o=i===`week`&&(qa(a)||a===!0)?n.startOf(o,`isoWeek`,a):n.startOf(o,i)),+o)}function Ep(e,t,n,r){let i=Cp.length;for(let a=Cp.indexOf(e);a=Cp.indexOf(n);a--){let n=Cp[a];if(Sp[n].common&&e._adapter.diff(i,r,n)>=t-1)return n}return Cp[n?Cp.indexOf(n):0]}function Op(e){for(let t=Cp.indexOf(e)+1,n=Cp.length;t=t?n[r]:n[i];e[a]=!0}}function Ap(e,t,n,r){let i=e._adapter,a=+i.startOf(t[0].value,r),o=t[t.length-1].value,s,c;for(s=a;s<=o;s=+i.add(s,1,r))c=n[s],c>=0&&(t[c].major=!0);return t}function jp(e,t,n){let r=[],i={},a=t.length,o,s;for(o=0;o+e.value))}initOffsets(e=[]){let t=0,n=0,r,i;this.options.offset&&e.length&&(r=this.getDecimalForValue(e[0]),t=e.length===1?1-r:(this.getDecimalForValue(e[1])-r)/2,i=this.getDecimalForValue(e[e.length-1]),n=e.length===1?i:(i-this.getDecimalForValue(e[e.length-2]))/2);let a=e.length<3?.5:.25;t=io(t,0,a),n=io(n,0,a),this._offsets={start:t,end:n,factor:1/(t+1+n)}}_generate(){let e=this._adapter,t=this.min,n=this.max,r=this.options,i=r.time,a=i.unit||Ep(i.minUnit,t,n,this._getLabelCapacity(t)),o=K(r.ticks.stepSize,1),s=a===`week`&&i.isoWeekday,c=qa(s)||s===!0,l={},u=t,d,f;if(c&&(u=+e.startOf(u,`isoWeek`,s)),u=+e.startOf(u,c?`day`:a),e.diff(n,t,a)>1e5*o)throw Error(t+` and `+n+` are too far apart with stepSize of `+o+` `+a);let p=r.ticks.source===`data`&&this.getDataTimestamps();for(d=u,f=0;d+e)}getLabelForValue(e){let t=this._adapter,n=this.options.time;return n.tooltipFormat?t.format(e,n.tooltipFormat):t.format(e,n.displayFormats.datetime)}format(e,t){let n=this.options.time.displayFormats,r=this._unit,i=t||n[r];return this._adapter.format(e,i)}_tickFormatFunction(e,t,n,r){let i=this.options,a=i.ticks.callback;if(a)return q(a,[e,t,n],this);let o=i.time.displayFormats,s=this._unit,c=this._majorUnit,l=s&&o[s],u=c&&o[c],d=n[t],f=c&&u&&d&&d.major;return this._adapter.format(e,r||(f?u:l))}generateTickLabels(e){let t,n,r;for(t=0,n=e.length;t0?o:1}getDataTimestamps(){let e=this._cache.data||[],t,n;if(e.length)return e;let r=this.getMatchingVisibleMetas();if(this._normalized&&r.length)return this._cache.data=r[0].controller.getAllParsedValues(this);for(t=0,n=r.length;t=e[r].pos&&t<=e[i].pos&&({lo:r,hi:i}=co(e,`pos`,t)),{pos:a,time:s}=e[r],{pos:o,time:c}=e[i]):(t>=e[r].time&&t<=e[i].time&&({lo:r,hi:i}=co(e,`time`,t)),{time:a,pos:s}=e[r],{time:o,pos:c}=e[i]);let l=o-a;return l?s+(c-s)*(t-a)/l:s}var Pp=class extends Mp{constructor(e){super(e),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){let e=this._getTimestampsForTable(),t=this._table=this.buildLookupTable(e);this._minPos=Np(t,this.min),this._tableRange=Np(t,this.max)-this._minPos,super.initOffsets(e)}buildLookupTable(e){let{min:t,max:n}=this,r=[],i=[],a,o,s,c,l;for(a=0,o=e.length;a=t&&c<=n&&r.push(c);if(r.length<2)return[{time:t,pos:0},{time:n,pos:1}];for(a=0,o=r.length;ae-t)}_getTimestampsForTable(){let e=this._cache.all||[];if(e.length)return e;let t=this.getDataTimestamps(),n=this.getLabelTimestamps();return e=t.length&&n.length?this.normalize(t.concat(n)):t.length?t:n,e=this._cache.all=e,e}getDecimalForValue(e){return(Np(this._table,e)-this._minPos)/this._tableRange}getValueForPixel(e){let t=this._offsets,n=this.getDecimalForPixel(e)/t.factor-t.end;return Np(this._table,n*this._tableRange+this._minPos,!0)}};$(Pp,`id`,`timeseries`),$(Pp,`defaults`,Mp.defaults),Bd.register(El,Sf,qf,Zf,Hf);var Fp=new Intl.DateTimeFormat(`ko-KR`,{hour:`2-digit`,hour12:!1}),Ip=`#94a3b8`,Lp=`rgba(148, 163, 184, 0.18)`,Rp=`#0d9488`,zp=`#0f766e`;function Bp(e){return Fp.format(new Date(e*1e3))}function Vp(e){return e.map((t,n)=>n%3==0||n===e.length-1?Bp(t.bucketStart):``)}function Hp({buckets:e}){let t=we(null),n=we(null),r=we(e);return r.current=e,I(()=>{let e=t.current;if(!e)return;let i=window.matchMedia(`(prefers-reduced-motion: reduce)`).matches,a=window.getComputedStyle(e).fontFamily,o={animation:!i&&{duration:500,easing:`easeOutQuart`},maintainAspectRatio:!1,responsive:!0,interaction:{intersect:!1,mode:`index`},plugins:{legend:{display:!1},tooltip:{displayColors:!1,callbacks:{title:e=>{let t=e[0]?.dataIndex??-1,n=r.current[t];return n?Bp(n.bucketStart):``},label:e=>`차단 ${H(Number(e.raw??0))}`,afterLabel:e=>{let t=r.current[e.dataIndex];return t?`DNS 요청 ${H(t.queries)}`:``}}}},scales:{x:{border:{display:!1},grid:{display:!1},ticks:{autoSkip:!1,color:Ip,font:{family:a,size:11,weight:700},maxRotation:0,minRotation:0}},y:{beginAtZero:!0,border:{display:!1},grid:{color:Lp},ticks:{color:Ip,font:{family:a,size:11,weight:700},maxTicksLimit:4,precision:0}}}};return n.current=new Bd(e,{type:`bar`,data:{labels:Vp(r.current),datasets:[{data:r.current.map(e=>e.blocked),backgroundColor:Rp,hoverBackgroundColor:zp,borderRadius:4,borderSkipped:!1,barPercentage:.82,categoryPercentage:.9,maxBarThickness:24}]},options:o}),()=>{n.current?.destroy(),n.current=null}},[]),I(()=>{let t=n.current;t&&(t.data.labels=Vp(e),t.data.datasets[0].data=e.map(e=>e.blocked),t.update())},[e]),L(`div`,{class:`mt-5 h-44 sm:h-52`,children:L(`canvas`,{"aria-label":`최근 24시간 시간대별 SafeShield 차단 요청`,ref:t,role:`img`,children:`최근 24시간 시간대별 SafeShield 차단 요청 차트`})})}var Up=3600,Wp=24;function Gp(e,t){return e<=0?`0.0%`:`${(t/e*100).toFixed(1)}%`}function Kp(e){let t=e.hourly[e.hourly.length-1];if(t?.bucketStart)return t.bucketStart;let n=e.updatedAt>0?e.updatedAt:Math.floor(Date.now()/1e3);return Math.floor(n/Up)*Up}function qp(e){let t=new Map(e.hourly.map(e=>[e.bucketStart,e])),n=Kp(e)-23*Up;return Array.from({length:Wp},(e,r)=>{let i=n+r*Up;return t.get(i)??{bucketStart:i,queries:0,blocked:0}})}function Jp({children:e}){return L(`div`,{class:`mt-5 flex min-h-52 items-center justify-center rounded-xl border border-slate-200 bg-slate-50 px-5 text-center`,children:L(`p`,{class:`m-0 max-w-lg text-sm font-semibold leading-6 text-slate-500`,children:e})})}function Yp({data:e,error:t,loading:n,nowTimestamp:r,refreshing:i}){let a;if(n&&!e)a=L(Jp,{children:`SafeShield 보호 활동을 불러오는 중입니다.`});else if(t&&!e)a=L(Jp,{children:`SafeShield 보호 활동을 불러오지 못했습니다. SafeShield 페이지에서 상태를 확인해 주세요.`});else if(!e||!e.available)a=L(Jp,{children:`현재 SafeShield에서 차단 통계를 제공하지 않습니다. 통계 기능을 확인해 주세요.`});else if(!e.enabled)a=L(Jp,{children:`통계 수집이 꺼져 있습니다. SafeShield 페이지에서 통계를 켜면 최근 24시간 활동이 표시됩니다.`});else{let t=qp(e),n=t.reduce((e,t)=>({queries:e.queries+t.queries,blocked:e.blocked+t.blocked}),{queries:0,blocked:0});a=L(v,{children:[L(`dl`,{class:`mt-5 grid gap-3 sm:grid-cols-3`,children:[L(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3`,children:[L(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:`DNS 요청`}),L(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-slate-950`,children:H(n.queries)})]}),L(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3`,children:[L(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:`차단`}),L(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-teal-700`,children:H(n.blocked)})]}),L(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3`,children:[L(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:`차단율`}),L(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-slate-950`,children:Gp(n.queries,n.blocked)})]})]}),L(Hp,{buckets:t})]})}return L(`article`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:[L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`SafeShield activity`}),L(`div`,{class:`mt-2 flex min-w-0 items-center justify-between gap-3`,children:[L(`h3`,{class:`m-0 min-w-0 text-xl font-black text-slate-950`,children:`최근 24시간 보호 활동`}),L(`a`,{class:`shrink-0 whitespace-nowrap text-xs font-extrabold text-teal-700 no-underline hover:text-teal-800`,href:`#safeshield`,children:`상세 통계 →`})]}),e?.updatedAt?L(`p`,{class:`mt-2 mb-0 text-[0.68rem] font-semibold text-slate-400`,title:Ut(e.updatedAt),children:i?`갱신 중…`:`집계: ${Wt(e.updatedAt,r)}`}):null]}),a,t&&e?L(`p`,{class:`mt-3 mb-0 text-xs font-bold text-amber-700`,children:`최근 통계 갱신에 실패해 마지막 정상 데이터를 표시하고 있습니다.`}):null]})}function Xp(e){return e===`healthy`?`bg-emerald-50 text-emerald-700 ring-emerald-200`:e===`warning`?`bg-amber-50 text-amber-800 ring-amber-200`:`bg-slate-100 text-slate-600 ring-slate-200`}function Zp({detail:e,eyebrow:t,href:n,icon:r,linkLabel:i=`자세히 보기 →`,meta:a,metaState:o,metaWarning:s=!1,state:c=`neutral`,value:l}){let u=L(v,{children:[L(`div`,{class:`flex items-start justify-between gap-4`,children:[L(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.16em] text-slate-500`,children:t}),L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl ring-1 ring-inset ${Xp(c)}`,children:r})]}),L(`p`,{class:`mt-4 mb-0 break-words text-2xl font-black tracking-tight text-slate-950`,children:l}),L(`p`,{class:`mt-2 mb-0 min-h-10 text-sm font-medium leading-5 text-slate-500`,children:e}),a?L(`p`,{class:`mt-3 mb-0 text-xs font-bold ${o===`healthy`?`text-emerald-700`:o===`warning`||s?`text-amber-700`:`text-slate-400`}`,children:a}):null,n?L(`span`,{class:`mt-4 inline-flex text-xs font-extrabold text-teal-700`,children:i}):null]}),d=`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 transition`;return n?L(`a`,{class:`${d} no-underline hover:border-slate-300 hover:shadow-md`,href:n,children:u}):L(`article`,{class:d,children:u})}function Qp({action:e,description:t,eyebrow:n,id:r,title:i}){return L(`div`,{class:`mb-4 min-w-0`,children:[L(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.18em] text-teal-700`,children:n}),L(`div`,{class:`mt-2 flex min-w-0 items-center justify-between gap-3`,children:[L(`h2`,{class:`m-0 min-w-0 text-xl font-black tracking-tight text-slate-950`,id:r,children:i}),e?L(`div`,{class:`shrink-0`,children:e}):null]}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500`,children:t})]})}var $p=6e4;function em(e,t){return!e||!Number.isFinite(e)||e<=0?null:Math.max(0,t-e)}function tm(e,t,n,r){return!t||t<=0?`${e}: ${r}`:L(`span`,{title:Ut(t),children:[e,`: `,Wt(t,n)]})}function nm(e){if(!e)return null;switch(e.toLowerCase()){case`dhcp`:return`DHCP`;case`static`:return`Static`;case`pppoe`:return`PPPoE`;default:return e.toUpperCase()}}function rm(e){if(!e)return!1;let t=e.split(`.`).map(e=>Number(e));if(t.length!==4||t.some(e=>!Number.isInteger(e)||e<0||e>255))return!1;let[n,r]=t;return n===void 0||r===void 0?!1:n===10||n===172&&r>=16&&r<=31||n===192&&r===168}function im({label:e,value:t}){return L(`div`,{class:`flex min-w-0 items-start justify-between gap-4 border-b border-slate-100 py-3 last:border-b-0`,children:[L(`dt`,{class:`text-sm font-semibold text-slate-500`,children:e}),L(`dd`,{class:`m-0 min-w-0 max-w-[68%] break-words text-right text-sm font-extrabold text-slate-950`,children:t})]})}function am(e){return e===`initializing`?{badge:`bg-teal-100 text-teal-800`,icon:`text-teal-700`,label:`준비 중`,panel:`border-teal-200 bg-teal-50`}:e===`critical`?{badge:`bg-rose-100 text-rose-800`,icon:`text-rose-700`,label:`이상`,panel:`border-rose-200 bg-rose-50/70`}:e===`warning`?{badge:`bg-amber-100 text-amber-800`,icon:`text-amber-700`,label:`주의`,panel:`border-amber-200 bg-amber-50/70`}:e===`unknown`?{badge:`bg-slate-200 text-slate-700`,icon:`text-slate-500`,label:`확인 불가`,panel:`border-slate-200 bg-slate-50`}:{badge:`bg-emerald-100 text-emerald-800`,icon:`text-emerald-700`,label:`정상`,panel:`border-emerald-200 bg-emerald-50/70`}}function om({data:e,error:t,loading:n,nowTimestamp:r}){let i=!!(e&&e.generatedAt>0),a=am(i&&e?e.overall:`unknown`),o=i&&e?e.checks.filter(e=>e.status!==`ok`):[],s=o.slice(0,2),c=Math.max(0,o.length-s.length);return L(`div`,{class:`mt-5 border-t border-slate-200 pt-5`,children:[L(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[L(`div`,{class:`flex items-center gap-2`,children:[L(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:`장치 진단`}),i?L(`span`,{class:`rounded-full px-2.5 py-1 text-[11px] font-extrabold ${a.badge}`,children:a.label}):null]}),L(`a`,{class:`text-xs font-extrabold text-teal-700 no-underline hover:text-teal-900`,href:`#settings`,children:`상세 보기 →`})]}),t&&!i?L(`div`,{class:`mt-3 rounded-xl border border-amber-200 bg-amber-50/70 px-4 py-3`,children:L(`div`,{class:`flex items-start gap-2`,children:[L(V,{class:`mt-0.5 size-4 shrink-0 text-amber-700`}),L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-sm font-extrabold text-slate-900`,children:`진단 상태를 확인하지 못했습니다.`}),L(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`설정에서 장치 진단 상태를 다시 확인해 주세요.`})]})]})}):i&&e?L(`div`,{class:`mt-3 rounded-xl border px-4 py-3 ${a.panel}`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3`,children:[e.overall===`ok`?L(B,{class:`mt-0.5 size-5 shrink-0 ${a.icon}`}):e.overall===`initializing`?L(tt,{class:`mt-0.5 size-5 shrink-0 ${a.icon}`}):L(V,{class:`mt-0.5 size-5 shrink-0 ${a.icon}`}),L(`div`,{class:`min-w-0 flex-1`,children:[L(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:e.summary.message}),L(`p`,{class:`mt-1 mb-0 text-xs font-semibold leading-5 text-slate-500`,children:[`마지막 진단: `,Wt(e.generatedAt,r),` · 검사 항목:`,` `,e.summary.total,`개`]})]})]}),s.length>0?L(`div`,{class:`mt-3 grid gap-2 sm:grid-cols-2`,children:s.map(e=>L(`div`,{class:`min-w-0 rounded-lg border border-white/80 bg-white/70 px-3 py-2`,children:[L(`p`,{class:`m-0 text-xs font-extrabold text-slate-900`,children:e.label}),L(`p`,{class:`mt-1 mb-0 line-clamp-2 text-xs leading-5 text-slate-500`,children:e.message})]},e.id))}):null,c>0?L(`p`,{class:`mt-2 mb-0 text-xs font-extrabold text-slate-600`,children:[`외 `,c,`건의 확인 항목이 있습니다.`]}):null]}):L(`div`,{class:`mt-3 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3`,children:[L(`p`,{class:`m-0 text-sm font-extrabold text-slate-900`,children:n?`진단 상태를 확인하고 있습니다.`:`아직 생성된 진단 결과가 없습니다.`}),L(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`진단 결과가 생성되면 현재 장치 상태를 이곳에 요약해서 표시합니다.`})]})]})}function sm(e,t,n){if(t&&!e)return{detail:`보호 상태를 확인하고 있습니다.`,state:`neutral`,value:`확인 중`};if(n&&!e)return{detail:`SafeShield 상태를 불러오지 못했습니다.`,state:`warning`,value:`확인 필요`};if(!e||!e.available)return{detail:`SafeShield 서비스를 사용할 수 없습니다.`,state:`warning`,value:`사용 불가`};if(!e.enabled||e.status===`disabled`)return{detail:`DNS 보호가 비활성화되어 있습니다.`,state:`neutral`,value:`꺼짐`};if(e.status===`running`)return{detail:`차단 목록을 갱신하고 있습니다.`,state:`neutral`,value:`갱신 중`};if(!(e.active&&e.runtime.dnsmasqRunning&&e.runtime.dnsRuntimeOk&&e.health.overall!==`error`&&e.issueCounts.errors===0))return{detail:e.runtime.lastErrorCode?`최근 오류: ${e.runtime.lastErrorCode}`:`DNS 보호 상태를 확인해 주세요.`,state:`warning`,value:`확인 필요`};let r=e.entitlement.plan?.trim(),i=e.blocklist.validLineCount>0?`${H(e.blocklist.validLineCount)}개 차단 규칙`:`차단 목록 적용됨`;return{detail:r?`${i} · ${r.toUpperCase()}`:i,state:`healthy`,value:`보호 중`}}function cm({accountRegistered:e,activity:t,activityError:n,activityLoading:r,data:i,devices:a,devicesError:o,devicesLoading:s,error:c,firmware:l,firmwareError:u,firmwareLoading:d,health:f,healthError:p,healthLoading:m,lan:h,lanError:g,lanLoading:_,loading:v,onRetry:y,safeshield:b,safeshieldError:x,safeshieldLoading:S,statistics:C,statisticsError:w,statisticsLoading:T,statisticsRefreshing:E,updates:D,updatesError:O,updatesLoading:k}){let A=Te(()=>i?Jt(i.runtime.memory):null,[i]),[j,M]=F(()=>Math.floor(Date.now()/1e3));if(I(()=>{let e=window.setInterval(()=>{M(Math.floor(Date.now()/1e3))},$p);return()=>window.clearInterval(e)},[]),v&&!i)return L(Rn,{});if(c&&!i)return L(zn,{message:c,onRetry:y});if(!i)return null;let ee=A?Math.round(A.percent):0,te=sm(b,S,x),ne=D?.packages.find(e=>e.name===`luci-app-smartsafehub`)??D?.packages[0]??null,re=!!(D&&D.updateCount>0),ie=D?re?`${D.updateCount}개 업데이트`:D.lastCheckAt?`최신 상태`:`확인 전`:O?`확인 필요`:`확인 중`,ae=ne?re&&ne.availableVersion?`${ne.installedVersion} → ${ne.availableVersion}`:`설치 버전 ${ne.installedVersion}`:O?`업데이트 상태를 불러오지 못했습니다.`:k?`업데이트 상태를 확인하고 있습니다.`:`업데이트 상태를 확인할 수 없습니다.`,oe=a?`${H(a.totals.online)}대`:s?`확인 중`:`확인 필요`,se=a?`Wi-Fi ${H(a.totals.wireless)}대 · 유선/기타 ${H(a.totals.ethernet)}대`:o||`연결 기기 요약을 불러오고 있습니다.`,ce=nm(i.network.protocol),le=i.network.available?i.network.ipv4Address?`WAN ${i.network.ipv4Address}${ce?` · ${ce}`:``}`:ce?`${ce} · WAN 주소를 받지 못했습니다.`:`WAN 주소를 받지 못했습니다.`:`WAN 인터페이스를 찾을 수 없습니다.`,N=!!h?.conflict.detected,ue=!i.network.up||N||g!==null&&!h?`warning`:h?`healthy`:`neutral`,de=N?`네트워크 충돌`:i.network.up?`정상 연결`:`연결 확인`,fe=i.network.up?N?`⚠ LAN 대역과 충돌합니다`:_&&!h?`네트워크 대역 확인 중`:g&&!h?`네트워크 대역을 확인하지 못했습니다.`:h?`✓ 네트워크 충돌 없음`:`네트워크 대역 확인 필요`:`WAN 연결 상태를 확인해 주세요.`,pe=!i.network.up||N||g!==null&&!h?`warning`:h?`healthy`:`neutral`,me=i.network.ipv4Address?rm(i.network.ipv4Address)?`${i.network.ipv4Address} · 사설 네트워크`:i.network.ipv4Address:`할당되지 않음`,he=em(b?.timestamps.lastSuccess,j),P=b?Math.max(b.timestamps.refreshIntervalS*2,86400):null,ge=!!(b?.enabled&&b.status!==`running`&&he!==null&&P!==null&&he>P),_e=Ur(D,j),ve=!!l?.current.metadataAvailable,ye=d&&!l?`확인 중`:u&&!l?`확인 필요`:ve?l?.current.releaseVersion?`SmartSafeHub ${l.current.releaseVersion}`:`SmartSafeHub 펌웨어`:`${i.software.distribution} ${i.software.version}`,be=d&&!l?`확인 중`:u&&!l?`확인 필요`:ve&&l?.current.buildId||i.software.revision,xe=ve?`빌드 ID`:`리비전`;return L(`div`,{class:`min-w-0 space-y-6`,children:[L(`section`,{"aria-labelledby":`dashboard-overview-title`,children:[L(Qp,{description:`인터넷, SafeShield, 연결 기기와 소프트웨어 상태를 한눈에 확인합니다.`,eyebrow:`Overview`,id:`dashboard-overview-title`,title:`시스템 개요`}),L(`div`,{class:`grid min-w-0 gap-4 sm:grid-cols-2 xl:grid-cols-4`,children:[L(Zp,{detail:le,eyebrow:`Internet`,href:`#network`,icon:L($e,{class:`size-5`}),linkLabel:N?`해결하기 →`:`자세히 보기 →`,meta:fe,metaState:pe,state:ue,value:de}),L(Zp,{detail:te.detail,eyebrow:`SafeShield`,href:`#safeshield`,icon:L(Ue,{class:`size-5`}),meta:tm(ge?`차단 목록 갱신 지연`:`차단 목록 갱신`,b?.timestamps.lastSuccess,j,S?`확인 중`:`기록 없음`),metaWarning:ge,state:te.state===`healthy`&&ge?`warning`:te.state,value:te.value}),L(Zp,{detail:se,eyebrow:`Connected devices`,href:`#devices`,icon:L(Ge,{class:`size-5`}),meta:tm(`목록 확인`,a?.generatedAt,j,s?`확인 중`:`기록 없음`),state:a?`healthy`:o?`warning`:`neutral`,value:oe}),L(Zp,{detail:ae,eyebrow:`Software update`,href:`#system`,icon:L(Je,{class:`size-5`}),meta:D&&!D.settings.checkEnabled?tm(`자동 확인 꺼짐 · 마지막 확인`,D.lastCheckAt,j,`확인 기록 없음`):tm(_e?`업데이트 확인 지연`:`마지막 확인`,D?.lastCheckAt,j,k?`확인 중`:`아직 없음`),metaWarning:_e,state:O||_e||re?`warning`:D?.lastCheckAt?`healthy`:`neutral`,value:ie})]}),e===!1?L(`div`,{class:`ssh-dashboard-account-banner mt-4 flex flex-col gap-4 rounded-2xl border p-4 sm:flex-row sm:items-center sm:justify-between sm:p-5`,role:`status`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3`,children:[L(`span`,{class:`ssh-dashboard-account-banner-icon grid size-10 shrink-0 place-items-center rounded-xl`,"aria-hidden":`true`,children:L(qe,{class:`size-5`})}),L(`div`,{class:`min-w-0`,children:[L(`div`,{class:`flex flex-wrap items-center gap-2`,children:[L(`p`,{class:`ssh-dashboard-account-banner-title m-0 font-bold`,children:`SmartSafeHub 계정을 연결해 주세요`}),L(`span`,{class:`ssh-dashboard-account-banner-badge rounded-full px-2 py-0.5 text-[0.7rem] font-bold`,children:`연결 필요`})]}),L(`p`,{class:`ssh-dashboard-account-banner-description mt-1 mb-0 text-sm leading-relaxed`,children:`계정을 연결하면 웹사이트에서 기기를 관리하고 구독 정보를 확인할 수 있습니다. DNS 보호는 연결하지 않아도 계속 작동합니다.`})]})]}),L(`a`,{href:`#account`,class:`ssh-dashboard-account-banner-action inline-flex min-h-10 shrink-0 items-center justify-center rounded-xl px-4 py-2 text-sm font-bold no-underline transition-colors focus-visible:outline-2 focus-visible:outline-offset-2`,children:`계정 연결하기 →`})]}):null]}),L(`section`,{"aria-labelledby":`dashboard-activity-title`,children:[L(Qp,{description:`최근 SafeShield 차단 활동과 현재 네트워크 연결 구성을 함께 확인합니다.`,eyebrow:`Activity`,id:`dashboard-activity-title`,title:`네트워크 보호 활동`}),L(`div`,{class:`grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1.55fr)_minmax(18rem,0.65fr)]`,children:[L(Yp,{data:C,error:w,loading:T,nowTimestamp:j,refreshing:E}),L(`article`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:[L(`div`,{class:`flex items-start justify-between gap-4`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Network`}),L(`h3`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`연결 상태`})]}),L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl ${i.network.up&&!N?`bg-emerald-50 text-emerald-700`:`bg-amber-50 text-amber-800`}`,children:L($e,{class:`size-5`})})]}),L(`dl`,{class:`mt-4 mb-0`,children:[L(im,{label:`인터넷`,value:N?`네트워크 충돌`:i.network.up?`정상 연결`:`연결 확인`}),L(im,{label:`WAN IP`,value:me}),L(im,{label:`프로토콜`,value:ce||`확인되지 않음`}),L(im,{label:`상위 네트워크`,value:h?.wan.subnet||(_?`확인 중`:g?`확인 필요`:`확인되지 않음`)}),L(im,{label:`LAN 네트워크`,value:h?.lan.subnet||(_?`확인 중`:g?`확인 필요`:`확인되지 않음`)}),L(im,{label:`대역 충돌`,value:h?L(`span`,{class:h.conflict.detected?`text-amber-700`:`text-emerald-700`,children:h.conflict.detected?`충돌 감지`:`없음`}):_?`확인 중`:`확인 필요`})]}),L(`a`,{class:`mt-5 inline-flex min-h-10 w-full items-center justify-center rounded-xl border border-slate-200 bg-slate-50 px-4 py-2 text-sm font-extrabold text-slate-700 no-underline transition hover:border-teal-200 hover:bg-teal-50 hover:text-teal-800`,href:`#network`,children:`네트워크 보기`})]})]})]}),L(`section`,{"aria-labelledby":`dashboard-recent-activity-title`,children:[L(Qp,{action:L(`a`,{class:`inline-flex whitespace-nowrap text-xs font-extrabold text-teal-700 no-underline hover:text-teal-900`,href:`#activity`,children:`전체 보기 →`}),description:`인터넷, 보호, 업데이트와 진단에서 실제 상태가 바뀐 시점을 확인합니다.`,eyebrow:`History`,id:`dashboard-recent-activity-title`,title:`최근 활동`}),L(`article`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:r||n&&!t?L(un,{error:n,loading:r}):L(ln,{compact:!0,events:(t?.events??[]).slice(0,3)})})]}),L(`section`,{"aria-labelledby":`dashboard-system-title`,children:[L(Qp,{description:`메모리, 시스템 부하와 장치 정보를 빠르게 점검합니다.`,eyebrow:`System`,id:`dashboard-system-title`,title:`시스템 상태`}),L(`div`,{class:`grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1.45fr)_minmax(20rem,0.75fr)]`,children:[L(`article`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:[L(`div`,{class:`flex flex-wrap items-start justify-between gap-4`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Runtime`}),L(`h3`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`리소스 사용량`})]}),L(`span`,{class:`inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-black ring-1 ring-inset ${ee>=90?`bg-amber-50 text-amber-800 ring-amber-200`:`bg-emerald-50 text-emerald-700 ring-emerald-200`}`,children:[L(`span`,{class:`size-2 rounded-full ${ee>=90?`bg-amber-500`:`bg-emerald-500`}`}),ee>=90?`메모리 확인`:`정상 실행`]})]}),L(`div`,{class:`mt-5 grid gap-3 sm:grid-cols-3`,children:[L(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[L(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[L(nt,{class:`size-4`}),L(`span`,{class:`text-xs font-extrabold`,children:`메모리`})]}),L(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:[ee,`%`]}),L(`p`,{class:`mt-1 mb-0 text-xs font-semibold text-slate-500`,children:[A?Bt(A.used):`-`,` /`,` `,Bt(i.runtime.memory.total)]}),L(`div`,{"aria-label":`메모리 사용률 ${ee}%`,class:`mt-3 h-1.5 overflow-hidden rounded-full bg-slate-200`,role:`progressbar`,"aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":ee,children:L(`div`,{class:`h-full rounded-full transition-[width] duration-300 ${ee>=90?`bg-amber-500`:`bg-teal-600`}`,style:{width:`${Math.min(100,ee)}%`}})})]}),L(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[L(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[L(We,{class:`size-4`}),L(`span`,{class:`text-xs font-extrabold`,children:`시스템 부하`})]}),L(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:Vt(i.runtime.load[0])}),L(`p`,{class:`mt-1 mb-0 text-xs font-semibold text-slate-500`,children:[`5분 `,Vt(i.runtime.load[1]),` · 15분`,` `,Vt(i.runtime.load[2])]})]}),L(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[L(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[L(tt,{class:`size-4`}),L(`span`,{class:`text-xs font-extrabold`,children:`실행 시간`})]}),L(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:zt(i.runtime.uptime)}),L(`p`,{class:`mt-1 mb-0 text-xs font-semibold text-slate-500`,children:[`부팅 `,Ht(i.runtime.localtime,i.runtime.uptime)]})]})]}),L(om,{data:f,error:p,loading:m,nowTimestamp:j})]}),L(`article`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:[L(`div`,{class:`flex items-start justify-between gap-4`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Device`}),L(`h3`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`장치 정보`})]}),L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:L(We,{class:`size-5`})})]}),L(`dl`,{class:`mt-4 mb-0`,children:[L(im,{label:`모델`,value:i.device.model}),L(im,{label:`호스트 이름`,value:i.device.hostname}),L(im,{label:`펌웨어`,value:L(`span`,{title:ve?l?.current.releaseVersion?`SmartSafeHub 펌웨어 ${l.current.releaseVersion}`:`SmartSafeHub 커스텀 펌웨어`:u||void 0,children:ye})}),L(im,{label:xe,value:be}),L(im,{label:`커널`,value:i.software.kernel}),L(im,{label:`WAN IP`,value:i.network.ipv4Address||`할당되지 않음`})]})]})]})]})]})}var lm=3,um=10;function dm(e,t){return e<=0?`0.0%`:`${(t/e*100).toFixed(1)}%`}function fm(e){return e.id===`other`?`기타 기기`:e.hostname?e.hostname:e.identified?`이름 없는 기기`:`미식별 기기`}function pm(e){return[...e].sort((e,t)=>e.id===`other`?1:t.id===`other`?-1:t.blocked-e.blocked||t.queries-e.queries||fm(e).localeCompare(fm(t),`ko`))}function mm({devices:e,deviceLimit:t,truncated:n}){let r=pm(e),[i,a]=F(!1),[o,s]=F(1),c=Math.max(1,Math.ceil(r.length/um)),l=Math.min(o,c),u=(l-1)*um,d=Math.min(u+um,r.length),f=i?r.slice(u,d):r.slice(0,lm),p=r.length>lm;return I(()=>{s(e=>Math.min(e,c))},[c]),L(`div`,{class:`mt-6 rounded-2xl border border-slate-100 bg-slate-50/70 p-4 sm:p-5`,children:[L(`div`,{class:`flex flex-wrap items-start justify-between gap-3`,children:[L(`div`,{children:[L(`div`,{class:`flex flex-wrap items-center gap-2`,children:[L(`p`,{class:`m-0 text-xs font-bold text-slate-500`,children:`기기별 통계`}),p&&!i?L(`span`,{class:`inline-flex items-center rounded-full bg-teal-50 px-2 py-1 text-[10px] font-extrabold tracking-wide text-teal-700 ring-1 ring-teal-100`,children:`차단 TOP 3`}):null]}),L(`h3`,{class:`mt-1 mb-0 text-base font-extrabold text-slate-950`,children:`어떤 기기에서 차단되었는지 확인`}),L(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-500`,children:`DHCP 정보가 있는 기기는 MAC 주소를 기준으로 식별하며, 기기별 숫자만 현재 부팅 세션 동안 메모리에 집계합니다.`})]}),L(`div`,{class:`flex flex-wrap items-center justify-end gap-2`,children:[L(`span`,{class:`inline-flex items-center gap-2 rounded-full bg-white px-3 py-1.5 text-xs font-extrabold text-slate-600 ring-1 ring-slate-200`,children:[L(Ge,{class:`size-4 text-teal-700`}),H(r.length),`개 항목`]}),p?L(`button`,{"aria-controls":`safeshield-device-statistics-list`,"aria-expanded":i,class:`rounded-full bg-white px-3 py-1.5 text-xs font-extrabold text-teal-700 ring-1 ring-slate-200 transition hover:bg-slate-50`,onClick:()=>{a(e=>!e),s(1)},type:`button`,children:i?`간단히 보기 ↑`:`전체 ${H(r.length)}개 기기 보기 ↓`}):null]})]}),r.length===0?L(`p`,{class:`mt-4 mb-0 rounded-xl bg-white p-4 text-sm leading-6 text-slate-500 ring-1 ring-slate-100`,children:`아직 기기별 DNS 통계가 없습니다. 클라이언트에서 DNS 요청이 발생하면 다음 통계 갱신 시 표시됩니다.`}):L(`div`,{class:`mt-4 grid gap-3`,id:`safeshield-device-statistics-list`,children:f.map(e=>L(`article`,{class:`grid gap-4 rounded-xl bg-white p-4 ring-1 ring-slate-100 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center`,children:[L(`div`,{class:`min-w-0`,children:L(`div`,{class:`flex min-w-0 flex-wrap items-center gap-2`,children:[L(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg bg-teal-50 text-teal-700`,children:L(Ge,{class:`size-4`})}),L(`div`,{class:`min-w-0`,children:[L(`h4`,{class:`m-0 truncate text-sm font-extrabold text-slate-950`,children:fm(e)}),L(`p`,{class:`mt-1 mb-0 break-all text-xs text-slate-500`,children:[e.ip||`IP 미확인`,e.mac?` · ${e.mac}`:``]})]}),L(`span`,{class:`rounded-full px-2 py-1 text-[11px] font-extrabold ${e.identified?`bg-emerald-50 text-emerald-700`:`bg-slate-100 text-slate-600`}`,children:e.id===`other`?`합산`:e.identified?`DHCP 식별`:`IP 임시 식별`})]})}),L(`dl`,{class:`m-0 grid grid-cols-3 gap-2 text-right`,children:[L(`div`,{class:`min-w-20`,children:[L(`dt`,{class:`text-[11px] font-bold text-slate-400`,children:`DNS 요청`}),L(`dd`,{class:`mt-1 mb-0 ml-0 text-sm font-extrabold text-slate-900`,children:H(e.queries)})]}),L(`div`,{class:`min-w-20`,children:[L(`dt`,{class:`text-[11px] font-bold text-slate-400`,children:`차단`}),L(`dd`,{class:`mt-1 mb-0 ml-0 text-sm font-extrabold text-teal-700`,children:H(e.blocked)})]}),L(`div`,{class:`min-w-20`,children:[L(`dt`,{class:`text-[11px] font-bold text-slate-400`,children:`차단율`}),L(`dd`,{class:`mt-1 mb-0 ml-0 text-sm font-extrabold text-slate-900`,children:dm(e.queries,e.blocked)})]})]})]},e.id))}),p&&!i?L(`p`,{class:`mt-3 mb-0 text-xs font-semibold leading-5 text-slate-500`,children:[`전체 `,H(r.length),`개 기기 중 차단 수 기준 상위 `,lm,`개를 표시하고 있습니다.`]}):null,i&&r.length>um?L(`nav`,{"aria-label":`기기별 통계 페이지`,class:`mt-4 flex flex-wrap items-center justify-between gap-3`,children:[L(`p`,{class:`m-0 text-xs font-semibold text-slate-500`,children:[H(u+1),`–`,H(d),` /`,` `,H(r.length),`개`]}),L(`div`,{class:`flex items-center gap-2`,children:[L(`button`,{class:`rounded-lg bg-white px-3 py-2 text-xs font-extrabold text-slate-700 ring-1 ring-slate-200 transition hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50`,disabled:l<=1,onClick:()=>s(e=>Math.max(1,e-1)),type:`button`,children:`이전`}),L(`span`,{"aria-current":`page`,class:`min-w-20 text-center text-xs font-extrabold text-slate-600`,children:[H(l),` / `,H(c)]}),L(`button`,{class:`rounded-lg bg-white px-3 py-2 text-xs font-extrabold text-slate-700 ring-1 ring-slate-200 transition hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50`,disabled:l>=c,onClick:()=>s(e=>Math.min(c,e+1)),type:`button`,children:`다음`})]})]}):null,n?L(`p`,{class:`mt-4 mb-0 rounded-xl border border-amber-200 bg-amber-50 p-3 text-xs font-semibold leading-5 text-amber-800`,children:[`개별 통계는 최대 `,H(t),`개 기기까지 추적합니다. 한도를 초과한 기기의 요청은 기타 기기로 합산됩니다.`]}):null]})}var hm=3600,gm=24;function _m(e,t){return e<=0?`0.0%`:`${(t/e*100).toFixed(1)}%`}function vm(e){let t=e.hourly[e.hourly.length-1];if(t?.bucketStart)return t.bucketStart;let n=e.updatedAt>0?e.updatedAt:Math.floor(Date.now()/1e3);return Math.floor(n/hm)*hm}function ym(e){let t=new Map(e.hourly.map(e=>[e.bucketStart,e])),n=vm(e)-23*hm;return Array.from({length:gm},(e,r)=>{let i=n+r*hm;return t.get(i)??{bucketStart:i,queries:0,blocked:0}})}function bm({emphasized:e=!1,label:t,value:n}){return L(`div`,{class:`rounded-xl border border-slate-100 bg-slate-50 p-4 sm:p-5`,children:[L(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:t}),L(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight ${e?`text-teal-700`:`text-slate-950`}`,children:n})]})}function xm({action:e,data:t,error:n,loading:r,onRetry:i,onSetEnabled:a,refreshing:o}){if(r&&t===null)return L(`section`,{class:`mt-5 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:L(`p`,{class:`m-0 text-sm font-bold text-slate-500`,children:`차단 통계를 불러오는 중입니다…`})});if(n&&t===null)return L(`section`,{class:`mt-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 sm:p-6`,children:[L(`p`,{class:`m-0 text-sm font-extrabold text-amber-950`,children:`차단 통계를 불러오지 못했습니다.`}),L(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-amber-800`,children:n}),L(`button`,{class:`mt-4 inline-flex min-h-9 items-center justify-center rounded-xl border border-amber-300 bg-white px-3 py-2 text-xs font-extrabold text-amber-900 transition hover:bg-amber-100 focus:outline-none focus-visible:ring-4 focus-visible:ring-amber-100`,onClick:i,type:`button`,children:`다시 시도`})]});if(!t)return L(`section`,{class:`mt-5 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:[L(`p`,{class:`m-0 text-xs font-bold uppercase tracking-[0.16em] text-slate-500`,children:`Activity`}),L(`h2`,{class:`mt-3 mb-0 text-xl font-extrabold text-slate-950`,children:`보호 활동`}),L(`p`,{class:`mt-3 mb-0 text-sm leading-6 text-slate-600`,children:`현재 SafeShield에서 통계 데이터를 제공하지 않습니다. SafeShield 0.3.15 버전 이상이 설치되어 있고, 통계 수집이 활성화되어 있는지 확인해 주세요.`})]});let s=ym(t),c=s[s.length-1],l=s.reduce((e,t)=>({queries:e.queries+t.queries,blocked:e.blocked+t.blocked}),{queries:0,blocked:0}),u=e===`statistics-enable`||e===`statistics-disable`,d=e===`statistics-enable`||e!==`statistics-disable`&&t.enabled,f=u?d?`활성화하는 중…`:`비활성화하는 중…`:t.enabled?t.collectorRunning?`활성화 · 수집 중`:`활성화 · 시작 중`:`비활성화`;return L(`section`,{"aria-busy":u,class:`mt-5 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6 ${u?`cursor-wait`:``}`,children:[L(`div`,{class:`flex flex-wrap items-start justify-between gap-4`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.18em] text-teal-700`,children:`Activity`}),L(`h2`,{class:`mt-2 mb-0 text-xl font-black tracking-tight text-slate-950`,children:`최근 24시간 보호 활동`}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500`,children:`DNS 요청 원본은 저장하지 않고 숫자만 로컬 메모리에 집계합니다.`})]}),L(`div`,{class:`flex items-center gap-3 rounded-xl border border-slate-100 bg-slate-50 px-3 py-2.5`,children:[L(`span`,{class:`grid size-9 place-items-center rounded-lg bg-slate-100 text-slate-500`,children:L(at,{class:`size-5`})}),L(`div`,{class:`text-right`,children:[L(`p`,{class:`m-0 text-[0.68rem] font-bold text-slate-400`,children:`통계 수집`}),L(`p`,{class:`mt-1 mb-0 text-xs font-extrabold ${u||d?`text-emerald-700`:`text-slate-500`}`,role:`status`,children:f})]}),L(`button`,{"aria-checked":d,"aria-label":d?`차단 통계 수집 끄기`:`차단 통계 수집 켜기`,class:`ssh-switch-control relative inline-flex shrink-0 items-center rounded-full border-0 p-0 transition focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 ${u?`cursor-wait opacity-80`:`disabled:cursor-not-allowed disabled:opacity-50`} ${d?`bg-teal-600`:`bg-slate-300`}`,disabled:e!==null,onClick:()=>a(!t.enabled),role:`switch`,type:`button`,children:L(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 grid place-items-center shadow-sm transition-all ${d?`left-6`:`left-1`}`,children:u?L(z,{"aria-hidden":`true`,class:`size-3 animate-spin text-teal-600`}):null})})]})]}),t.enabled?null:L(`div`,{class:`mt-5 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm leading-6 text-slate-600`,children:`통계 수집이 꺼져 있습니다. 활성화하면 DNS 요청 원본은 저장하지 않고 요청 수와 차단 수만 로컬 메모리에 집계합니다.`}),L(`dl`,{class:`mt-6 grid gap-3 sm:grid-cols-2 xl:grid-cols-4`,children:[L(bm,{label:`최근 24시간 DNS 요청`,value:H(l.queries)}),L(bm,{emphasized:!0,label:`최근 24시간 차단`,value:H(l.blocked)}),L(bm,{label:`최근 24시간 차단율`,value:_m(l.queries,l.blocked)}),L(bm,{label:`현재 시간 차단`,value:H(c?.blocked??0)})]}),L(`div`,{class:`mt-5 rounded-2xl border border-slate-100 bg-slate-50/70 p-4 sm:p-5`,children:[L(`div`,{class:`flex flex-wrap items-end justify-between gap-3`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-xs font-bold text-slate-500`,children:`최근 24시간`}),L(`h3`,{class:`mt-1 mb-0 text-base font-extrabold text-slate-950`,children:`시간대별 차단 요청`})]}),L(`span`,{class:`text-xs font-bold text-slate-500`,children:o?`갱신 중…`:`마지막 집계 ${Ut(t.updatedAt)}`})]}),L(Hp,{buckets:s})]}),L(mm,{deviceLimit:t.deviceLimit,devices:t.devices,truncated:t.devicesTruncated}),L(`div`,{class:`mt-5 flex flex-wrap gap-x-5 gap-y-2 border-t border-slate-100 pt-4 text-xs leading-5 text-slate-500`,children:[L(`span`,{children:[`수집 누적 DNS 요청 `,H(t.totals.queries)]}),L(`span`,{children:[`수집 누적 차단 `,H(t.totals.blocked)]}),L(`span`,{children:[`수집 시작 `,Ut(t.startedAt)]}),L(`span`,{children:[`보존 `,H(t.retentionHours),`시간`]}),t.volatile?L(`span`,{children:`재부팅 시 초기화되는 메모리 통계`}):null]}),n?L(`p`,{class:`mt-3 mb-0 text-xs font-bold text-amber-700`,children:`최근 자동 갱신에 실패했습니다. 현재 표시된 마지막 정상 통계를 유지합니다.`}):null]})}var Sm=[{label:`갱신 준비`,description:`차단 목록 갱신을 준비하고 있습니다.`,stages:[`starting`,`boot_delay`,`boot_refresh`,`boot_refresh_skipped`,`scheduled_refresh`,`init`]},{label:`최신 차단 목록 확인`,description:`서버에서 최신 차단 목록 정보를 확인하고 있습니다.`,stages:[`resolve_api`]},{label:`차단 목록 다운로드`,description:`최신 차단 목록을 안전하게 다운로드하고 있습니다.`,stages:[`download_artifact`]},{label:`사용자 규칙 적용`,description:`허용·차단 사용자 규칙을 새 차단 목록에 반영하고 있습니다.`,stages:[`local_overrides`,`merge`]},{label:`보호 규칙 적용`,description:`새 차단 규칙을 설치하고 DNS 보호 서비스를 갱신하고 있습니다.`,stages:[`install`,`restart_dnsmasq`]},{label:`보호 상태 확인`,description:`새 차단 목록과 DNS 보호가 정상적으로 동작하는지 확인하고 있습니다.`,stages:[`runtime_check`,`blocklist_verify`]}],Cm=new Map;Sm.forEach((e,t)=>{e.stages.forEach(e=>Cm.set(e,t))});function wm(e,t){return e===`running`||t!==null&&Cm.has(t)}function Tm(e){let t=e===null?0:Cm.get(e)??0,n=Sm[t];return{description:n.description,label:n.label,number:t+1,total:Sm.length}}function Em(e){return(e??``).trim().toLowerCase()}function Dm(e,t){let n=Em(e);if(n.includes(`upgrade`)||n.includes(`version`))return`현재 SafeShield 버전으로는 갱신을 계속할 수 없습니다. 시스템 업데이트에서 SafeShield를 최신 버전으로 업데이트해 주세요.`;if(n.includes(`entitlement`)||n.includes(`license`)||n.includes(`unauthorized`)||n.includes(`forbidden`))return`차단 목록을 사용할 권한을 확인하지 못했습니다. SmartSafeHub 계정과 요금제 상태를 확인해 주세요.`;if(n.includes(`dnsmasq`)||n.includes(`restart`)||n.includes(`runtime`))return`DNS 보호 서비스를 정상적으로 갱신하지 못했습니다. 잠시 후 다시 갱신해 보고 문제가 계속되면 상태 점검 정보를 확인해 주세요.`;if(n.includes(`verify`)||n.includes(`sanity`))return`새 차단 목록을 적용한 뒤 보호 상태 확인을 완료하지 못했습니다. 다시 갱신해 주세요.`;if(n.includes(`download`)||n.includes(`fetch`)||n.includes(`timeout`)||n.includes(`network`)||n.includes(`wan`))return`차단 목록을 다운로드하지 못했습니다. 인터넷 연결을 확인한 뒤 다시 갱신해 주세요.`;if(n.includes(`resolve`)||n.includes(`manifest`)||n.includes(`artifact`)||n.includes(`api`))return`최신 차단 목록 정보를 확인하지 못했습니다. 인터넷 연결을 확인한 뒤 다시 갱신해 주세요.`;if(n.includes(`override`)||n.includes(`allowlist`)||n.includes(`merge`)||n.includes(`rule`))return`사용자 허용·차단 규칙을 새 차단 목록에 반영하지 못했습니다. 사용자 규칙을 확인한 뒤 다시 갱신해 주세요.`;if(n.includes(`install`)||n.includes(`write`)||n.includes(`space`))return`새 차단 규칙을 장치에 적용하지 못했습니다. 저장 공간과 SafeShield 상태를 확인한 뒤 다시 갱신해 주세요.`;let r=Tm(t);return r.number===2?`최신 차단 목록 정보를 확인하는 중 문제가 발생했습니다. 인터넷 연결을 확인한 뒤 다시 갱신해 주세요.`:r.number===3?`차단 목록을 다운로드하는 중 문제가 발생했습니다. 인터넷 연결을 확인한 뒤 다시 갱신해 주세요.`:r.number===4?`사용자 규칙을 적용하는 중 문제가 발생했습니다. 사용자 규칙을 확인한 뒤 다시 갱신해 주세요.`:r.number===5?`새 보호 규칙을 적용하는 중 문제가 발생했습니다. 잠시 후 다시 갱신해 주세요.`:r.number===6?`새 차단 목록의 보호 상태를 확인하지 못했습니다. 다시 갱신해 주세요.`:`차단 목록 갱신을 완료하지 못했습니다. 잠시 후 다시 갱신해 주세요.`}var Om=`https://www.smartsafehub.com/pricing/`;function km({value:e,trueLabel:t=`정상`,falseLabel:n=`확인 필요`}){return L(`span`,{class:`inline-flex items-center gap-2 text-sm font-extrabold ${e?`text-emerald-700`:`text-slate-500`}`,children:[L(`span`,{class:`size-2 rounded-full ${e?`bg-emerald-500`:`bg-slate-300`}`}),e?t:n]})}function Am(e){return!e.enabled||e.status===`disabled`?`disabled`:e.status===`running`?`refreshing`:e.status===`error`||e.health.overall===`error`||e.issueCounts.errors>0?`error`:e.status===`paused`?`paused`:e.active&&e.runtime.dnsmasqRunning&&e.runtime.dnsRuntimeOk?`protecting`:`attention`}function jm(e){return!e.enabled||e.status===`disabled`?`비활성화`:e.active&&e.runtime.dnsmasqRunning&&e.runtime.dnsRuntimeOk?`보호 중`:e.status===`paused`?`일시 중지`:`확인 필요`}function Mm(e){let t=Am(e);return t===`error`&&jm(e)===`보호 중`&&e.runtime.lastErrorCode?`갱신 실패`:{disabled:`비활성화`,refreshing:`갱신 중`,protecting:`보호 중`,paused:`일시 중지`,error:`오류`,attention:`확인 필요`}[t]}function Nm(e){return e?`자동 최적화`:`확인되지 않음`}function Pm(e){let t=Kt(e);return t===null?`확인되지 않음`:L(Ar,{timestamp:t})}function Fm(e){let t=Am(e);return t===`protecting`?e.blocklist.validLineCount>0?`${H(e.blocklist.validLineCount)}개 도메인 차단 규칙으로 DNS를 보호하고 있습니다.`:e.blocklist.installed?`차단 목록이 설치되어 있으며 DNS 보호가 정상적으로 동작하고 있습니다.`:`SafeShield 서비스와 DNS 런타임이 정상적으로 동작하고 있습니다.`:t===`refreshing`?`차단 목록을 최신 상태로 갱신하고 있습니다.`:t===`disabled`?`SafeShield DNS 보호가 비활성화되어 있습니다.`:t===`paused`?`SafeShield 작업이 일시 중지되어 있습니다.`:t===`error`?jm(e)===`보호 중`?`기존 DNS 보호는 유지되고 있지만 최근 차단 목록 갱신을 완료하지 못했습니다.`:`SafeShield 보호 상태에 문제가 발생했습니다. 아래 오류 내용을 확인해 주세요.`:e.active?e.runtime.dnsmasqRunning?e.runtime.dnsRuntimeOk?e.summary.message:`SafeShield DNS 런타임 상태를 확인해야 합니다.`:`dnsmasq가 실행 중이 아니어서 DNS 보호 상태를 확인해야 합니다.`:`SafeShield가 활성화되어 있지만 서비스가 실행 중이 아닙니다.`}function Im(e){return{ok:`정상`,warning:`경고`,error:`오류`,unavailable:`사용 불가`}[e]??e}function Lm({data:e}){let t=Am(e);return L(`span`,{class:`inline-flex items-center rounded-full px-3 py-1.5 text-xs font-extrabold ring-1 ring-inset ${t===`error`?`bg-red-50 text-red-700 ring-red-200`:t===`disabled`?`bg-slate-100 text-slate-700 ring-slate-200`:t===`refreshing`?`bg-blue-50 text-blue-700 ring-blue-200`:t===`paused`||t===`attention`?`bg-amber-50 text-amber-800 ring-amber-200`:`bg-emerald-50 text-emerald-700 ring-emerald-200`}`,children:Mm(e)})}function Rm({failed:e,step:t,total:n}){let r=2*Math.PI*17,i=r*.22,a=r-i;return L(`span`,{"aria-label":e?`갱신 실패 · ${t}/${n} 단계`:`갱신 ${t}/${n} 단계`,class:`ssh-safeshield-refresh-donut`,"data-state":e?`error`:`active`,role:`progressbar`,"aria-valuemin":0,"aria-valuemax":n,"aria-valuenow":t,children:[L(`svg`,{"aria-hidden":`true`,class:e?`ssh-safeshield-refresh-loader-svg ssh-safeshield-refresh-loader-svg-error`:`ssh-safeshield-refresh-loader-svg`,viewBox:`0 0 48 48`,children:[L(`circle`,{class:`ssh-safeshield-refresh-loader-track`,cx:`24`,cy:`24`,fill:`none`,r:17,stroke:`currentColor`,strokeWidth:2.5}),e?null:L(`circle`,{class:`ssh-safeshield-refresh-loader-arc`,cx:`24`,cy:`24`,fill:`none`,r:17,stroke:`currentColor`,strokeDasharray:`${i} ${a}`,strokeLinecap:`round`,strokeWidth:3.5})]}),L(`span`,{class:`ssh-safeshield-refresh-donut-label`,children:e?`!`:`${t}/${n}`})]})}function zm({data:e}){let t=wm(e.status,e.stage),n=Am(e)===`error`;if(!t&&!n)return null;let r=Tm(e.stage),i=n?Dm(e.runtime.lastErrorCode,e.stage):null;return L(`div`,{"aria-live":n?`assertive`:`polite`,class:`ssh-safeshield-refresh`,"data-state":n?`error`:`active`,role:n?`alert`:`status`,children:[L(Rm,{failed:n,step:r.number,total:r.total}),L(`div`,{class:`ssh-safeshield-refresh-copy`,children:[L(`p`,{class:`ssh-safeshield-refresh-kicker`,children:n?`갱신 실패 · ${r.number}/${r.total}`:`갱신 ${r.number}/${r.total}`}),L(`p`,{class:`ssh-safeshield-refresh-title`,children:n?`${r.label} 단계에서 문제가 발생했습니다.`:r.label}),L(`p`,{class:`ssh-safeshield-refresh-description`,children:i??r.description}),n&&e.runtime.lastErrorCode?L(`p`,{class:`ssh-safeshield-refresh-error-code`,children:[`오류 코드: `,L(`code`,{children:e.runtime.lastErrorCode})]}):null]})]})}function Bm({label:e,value:t}){return L(`div`,{class:`bg-white px-5 py-4 sm:px-6`,children:[L(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.14em] text-slate-400`,children:e}),L(`dd`,{class:`mt-2 mb-0 ml-0 text-sm font-extrabold text-slate-950`,title:typeof t==`string`?t:void 0,children:t})]})}function Vm(e){return e?.trim().toUpperCase()||`FREE`}function Hm(e){return e===`FREE`?`free`:e===`PRO`?`pro`:e===`ULTIMATE`?`ultimate`:`paid`}function Um({compact:e=!1,plan:t}){let n=Hm(t),r=n!==`free`;return L(`span`,{class:`ssh-safeshield-plan-badge`,"data-compact":e?`true`:`false`,"data-tier":n,title:r?`${t} 멤버십`:`무료 플랜`,children:[L(`span`,{"aria-hidden":`true`,class:`ssh-safeshield-plan-badge-mark`,children:r?`✦`:`•`}),L(`span`,{children:t})]})}function Wm({plan:e}){let t=Hm(e)===`free`;return L(`div`,{class:`bg-white px-5 py-4 sm:px-6`,children:[L(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.14em] text-slate-400`,children:`Plan`}),L(`dd`,{class:`mt-2 mb-0 ml-0 flex min-w-0 flex-wrap items-center gap-2`,children:[L(Um,{plan:e}),t?L(`span`,{class:`ssh-safeshield-plan-caption`,children:`기본 플랜`}):null]})]})}function Gm(){return L(`div`,{class:`ssh-safeshield-upgrade-card`,children:[L(`span`,{"aria-hidden":`true`,class:`ssh-safeshield-upgrade-mark`,children:`✦`}),L(`div`,{class:`ssh-safeshield-upgrade-copy`,children:[L(`div`,{class:`ssh-safeshield-upgrade-meta`,children:[L(`span`,{children:`SmartSafeHub Membership`}),L(`span`,{class:`ssh-safeshield-upgrade-soon`,children:`Coming soon`})]}),L(`strong`,{children:`더 강력한 보호 기능을 준비하고 있습니다.`}),L(`span`,{children:`프리미엄 보호와 확장된 관리 기능을 미리 확인해 보세요.`})]}),L(`a`,{class:`ssh-safeshield-upgrade-action`,href:Om,rel:`noopener noreferrer`,target:`_blank`,children:[`요금제 보기`,L(`span`,{"aria-hidden":`true`,children:`→`})]})]})}function Km({description:e,eyebrow:t,title:n}){return L(`div`,{class:`mb-4 flex flex-wrap items-end justify-between gap-3`,children:L(`div`,{children:[L(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.18em] text-teal-700`,children:t}),L(`h2`,{class:`mt-2 mb-0 text-xl font-black tracking-tight text-slate-950`,children:n}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500`,children:e})]})})}function qm({children:e,eyebrow:t,icon:n,title:r}){return L(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:[L(`div`,{class:`flex items-start justify-between gap-4`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.16em] text-slate-400`,children:t}),L(`h3`,{class:`mt-2 mb-0 text-lg font-black tracking-tight text-slate-950`,children:r})]}),L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-500`,children:n})]}),L(`dl`,{class:`mt-5 mb-0 grid gap-3`,children:e})]})}function Jm({label:e,value:t}){return L(`div`,{class:`flex min-w-0 items-center justify-between gap-4 border-b border-slate-100 pb-3 last:border-b-0 last:pb-0`,children:[L(`dt`,{class:`text-sm font-semibold text-slate-500`,children:e}),L(`dd`,{class:`m-0 min-w-0 text-right text-sm font-extrabold text-slate-950`,children:t})]})}function Ym({error:e,message:t,onDismiss:n}){if(!e&&!t)return null;let r=e!==null;return L(`div`,{"aria-live":r?`assertive`:`polite`,class:`mt-4 flex flex-col items-start gap-3 rounded-2xl border px-4 py-3 text-sm sm:flex-row sm:justify-between ${r?`border-red-200 bg-red-50 text-red-900`:`border-emerald-200 bg-emerald-50 text-emerald-900`}`,role:r?`alert`:`status`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3`,children:[L(r?V:B,{class:`mt-0.5 size-5 shrink-0`}),L(`p`,{class:`m-0 font-semibold leading-6`,children:e??t})]}),L(`button`,{"aria-label":`알림 닫기`,class:`shrink-0 rounded-lg border-0 bg-transparent px-2 py-1 font-black text-current opacity-60 transition hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-current`,onClick:n,type:`button`,children:`×`})]})}function Xm({action:e,actionError:t,actionMessage:n,data:r,error:i,loading:a,statistics:o,statisticsError:s,statisticsLoading:c,statisticsRefreshing:l,onDismissFeedback:u,onRefreshBlocklist:d,onRetry:f,onRetryStatistics:p,onSetEnabled:m,onSetStatisticsEnabled:h}){let[g,_]=F(null);if(I(()=>{r?.available&&r.blocklist.validLineCount>0&&!wm(r.status,r.stage)&&_(r.blocklist.validLineCount)},[r?.available,r?.blocklist.validLineCount,r?.stage,r?.status]),a)return L(Rn,{});if(i)return L(zn,{message:i,onRetry:f});if(!r)return L(Rn,{});if(!r.available)return L(`section`,{class:`rounded-2xl border border-amber-200 bg-amber-50 p-5 sm:p-6`,children:[L(`span`,{class:`grid size-11 place-items-center rounded-xl bg-amber-100 text-amber-800`,children:L(V,{class:`size-6`})}),L(`h2`,{class:`mt-5 mb-0 text-xl font-extrabold text-amber-950`,children:`SafeShield 상태 API를 찾을 수 없습니다`}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-amber-900`,children:`safeshield 패키지와 rpcd ucode 플러그인이 설치되어 있는지 확인해 주세요.`})]});let y=r.enabled,b=Vm(r.entitlement.plan),x=r.status===`running`,S=e!==null,C=(wm(r.status,r.stage)||Am(r)===`error`)&&r.blocklist.validLineCount===0&&g!==null?g:r.blocklist.validLineCount,w=y?e===`disable`?`끄는 중…`:`보호 끄기`:e===`enable`?`켜는 중…`:`보호 켜기`;function T(){(!y||window.confirm(`보호를 끄면, 현재 차단 목록이 제거되어 위험에 노출될 수 있습니다. 그래도 계속하시겠습니까?`))&&m(!y)}return L(v,{children:[L(`section`,{class:`overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm shadow-slate-900/5`,children:[L(`div`,{class:`flex flex-col gap-5 p-5 sm:p-6 lg:flex-row lg:items-start lg:justify-between`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3 sm:gap-4`,children:[L(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-teal-50 text-teal-700`,children:L(Ue,{class:`size-7`})}),L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.18em] text-teal-700`,children:`Protection`}),L(`div`,{class:`mt-2 flex flex-wrap items-center gap-3`,children:[L(`h2`,{class:`m-0 break-words text-xl font-black tracking-tight text-slate-950 sm:text-2xl`,children:`SafeShield 보호 상태`}),L(Lm,{data:r})]}),L(`p`,{class:`mt-3 mb-0 max-w-3xl text-sm leading-6 text-slate-600`,children:Fm(r)}),L(zm,{data:r})]})]}),L(`div`,{class:`flex w-full flex-col gap-2 sm:w-auto sm:flex-row`,children:[L(`button`,{class:`inline-flex min-h-11 items-center justify-center gap-2 rounded-xl border px-4 py-2.5 text-sm font-extrabold transition focus:outline-none focus-visible:ring-4 disabled:cursor-not-allowed disabled:opacity-50 ${r.enabled?`border-red-200 bg-white text-red-700 hover:border-red-300 hover:bg-red-50 focus-visible:ring-red-100`:`border-teal-700 bg-teal-700 text-white hover:border-teal-800 hover:bg-teal-800 focus-visible:ring-teal-100`}`,disabled:S||x,onClick:T,type:`button`,children:[L(rt,{class:`size-4 ${e===`enable`||e===`disable`?`animate-pulse`:``}`}),w]}),L(`button`,{class:`inline-flex min-h-11 items-center justify-center gap-2 rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-extrabold text-slate-700 transition hover:border-teal-300 hover:bg-teal-50 hover:text-teal-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-not-allowed disabled:opacity-50`,disabled:!r.enabled||S||x,onClick:d,type:`button`,children:[L(it,{class:`size-4 ${e===`refresh`||x?`animate-bounce`:``}`}),e===`refresh`?`시작 중…`:x?`갱신 중…`:r.status===`error`?`다시 갱신`:`지금 갱신`]})]})]}),L(`div`,{class:`px-5 pb-5 sm:px-6 sm:pb-6`,children:[L(`dl`,{class:`grid overflow-hidden rounded-xl border border-slate-200 bg-slate-200 sm:grid-cols-2 xl:grid-cols-4`,children:[L(Bm,{label:`SafeShield`,value:r.version??`확인되지 않음`}),L(Bm,{label:`Blocklist`,value:`${H(C)}개 도메인`}),L(Bm,{label:`Last refresh`,value:L(Ar,{timestamp:r.timestamps.lastSuccess})}),L(Wm,{plan:b})]}),b===`FREE`?L(Gm,{}):null]})]}),L(Ym,{error:t,message:n,onDismiss:u}),L(xm,{action:e,data:o,error:s,loading:c,onRetry:p,onSetEnabled:h,refreshing:l}),L(`section`,{class:`mt-7`,children:[L(Km,{description:`DNS 보호 런타임, 차단 목록, 갱신 일정과 상태 점검 결과를 한곳에서 확인합니다.`,eyebrow:`Protection details`,title:`보호 구성`}),L(`div`,{class:`grid gap-4 lg:grid-cols-2`,children:[L(qm,{eyebrow:`Runtime`,icon:L(B,{class:`size-5`}),title:`DNS 보호 런타임`,children:[L(Jm,{label:`SafeShield 서비스`,value:L(km,{falseLabel:`중지됨`,trueLabel:`동작 중`,value:r.active})}),L(Jm,{label:`DNS 서비스`,value:L(km,{falseLabel:`중지됨`,value:r.runtime.dnsmasqRunning})}),L(Jm,{label:`SafeShield DNS 연동`,value:L(km,{value:r.runtime.dnsRuntimeOk})}),L(Jm,{label:`차단 목록 갱신`,value:L(km,{falseLabel:`중지됨`,value:r.runtime.refreshdRunning})})]}),L(qm,{eyebrow:`Blocklist`,icon:L(at,{class:`size-5`}),title:`차단 목록`,children:[L(Jm,{label:`적용 규칙`,value:`${H(r.blocklist.validLineCount)}개 도메인`}),L(Jm,{label:`파일 크기`,value:Bt(r.blocklist.fileSizeKb*1024)}),L(Jm,{label:`설치 상태`,value:L(km,{falseLabel:`미설치`,trueLabel:`설치됨`,value:r.blocklist.installed})}),L(Jm,{label:`검증 상태`,value:L(km,{falseLabel:`확인 필요`,value:r.blocklist.verificationOk})})]}),L(qm,{eyebrow:`Refresh schedule`,icon:L(lt,{class:`size-5`}),title:`차단 목록 갱신`,children:[L(Jm,{label:`마지막 성공`,value:L(Ar,{timestamp:r.timestamps.lastSuccess})}),L(Jm,{label:`다음 갱신`,value:Ut(r.timestamps.nextRefreshAt)}),L(Jm,{label:`갱신 주기`,value:qt(r.timestamps.refreshIntervalS)}),L(Jm,{label:`최근 결과`,value:r.runtime.lastResult||(x?`갱신 중`:`확인되지 않음`)})]}),L(qm,{eyebrow:`Health`,icon:L(Ue,{class:`size-5`}),title:`상태 점검`,children:[L(Jm,{label:`전체 상태`,value:Im(r.health.overall)}),L(Jm,{label:`경고`,value:L(`span`,{class:`text-amber-700`,children:H(r.issueCounts.warnings)})}),L(Jm,{label:`오류`,value:L(`span`,{class:`text-red-700`,children:H(r.issueCounts.errors)})}),L(Jm,{label:`최근 오류 코드`,value:r.runtime.lastErrorCode||`없음`})]})]})]}),L(`section`,{class:`mt-7`,children:[L(Km,{description:`현재 적용 중인 보호 데이터와 사용자 규칙을 관리합니다.`,eyebrow:`Settings`,title:`SafeShield 설정`}),L(`div`,{class:`grid gap-4 lg:grid-cols-2`,children:[L(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[L(`div`,{class:`flex items-start justify-between gap-4`,children:[L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Protection data`}),L(`div`,{class:`mt-2 flex flex-wrap items-center gap-2`,children:[L(`h3`,{class:`m-0 text-lg font-black tracking-tight text-slate-950`,children:`보호 데이터`}),L(`span`,{"aria-label":`보호 데이터 상태: ${r.artifact.resolved&&r.blocklist.installed&&r.blocklist.verificationOk?`최신`:`확인 필요`}`,class:`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-extrabold ${r.artifact.resolved&&r.blocklist.installed&&r.blocklist.verificationOk?`bg-emerald-50 text-emerald-700`:`bg-amber-50 text-amber-700`}`,children:[r.artifact.resolved&&r.blocklist.installed&&r.blocklist.verificationOk?L(B,{class:`size-3.5 shrink-0`}):L(V,{class:`size-3.5 shrink-0`}),r.artifact.resolved&&r.blocklist.installed&&r.blocklist.verificationOk?`최신`:`확인 필요`]})]}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500`,children:`SafeShield가 기기 성능에 맞춰 사용하는 보호 목록의 구성과 최신 상태를 확인합니다.`})]}),L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-500`,children:L(at,{class:`size-5`})})]}),L(`dl`,{class:`mt-5 mb-0 grid grid-cols-2 gap-3`,children:[L(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 px-3 py-3`,children:[L(`dt`,{class:`text-xs font-bold text-slate-500`,children:`기기 최적화`}),L(`dd`,{class:`mt-1 mb-0 text-sm font-extrabold text-slate-950`,children:Nm(r.artifact.tier)})]}),L(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 px-3 py-3`,children:[L(`dt`,{class:`text-xs font-bold text-slate-500`,children:`마지막 업데이트`}),L(`dd`,{class:`mt-1 mb-0 text-sm font-extrabold leading-5 text-slate-950`,children:Pm(r.artifact.version)})]})]}),L(`details`,{class:`mt-4 rounded-xl border border-slate-200 bg-slate-50/70 px-4 py-3`,children:[L(`summary`,{class:`cursor-pointer text-sm font-extrabold text-slate-700`,children:`세부 정보`}),L(`dl`,{class:`mt-3 mb-0 grid gap-3 border-t border-slate-200 pt-3`,children:[L(Jm,{label:`데이터 프로필`,value:r.artifact.tier||`확인되지 않음`}),L(Jm,{label:`데이터 버전`,value:r.artifact.version||`확인되지 않음`})]})]})]}),L(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[L(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Custom rules`}),L(`h3`,{class:`mt-2 mb-0 text-lg font-black tracking-tight text-slate-950`,children:`사용자 규칙`}),L(`div`,{class:`mt-4`,children:L(km,{falseLabel:`비활성화`,trueLabel:`활성화`,value:r.localOverrides.enabled})}),L(`p`,{class:`mt-4 mb-0 text-sm font-semibold leading-6 text-slate-500`,children:`허용하거나 차단할 도메인을 직접 관리해 SafeShield 보호 정책에 반영합니다.`}),L(`a`,{class:`mt-5 inline-flex min-h-11 items-center justify-center gap-2 rounded-xl border border-teal-700 bg-teal-700 px-4 py-2 text-sm font-extrabold text-white no-underline shadow-sm transition hover:border-teal-800 hover:bg-teal-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100`,href:`#rules`,children:[L(pt,{class:`size-4`}),`사용자 규칙 관리`]})]})]})]})]})}var Zm=Math.floor(Date.now()/1e3),Qm=3600,$m={generatedAt:Zm,devices:[{id:`demo-phone`,hostname:`거실 스마트폰`,mac:`02:00:00:00:00:01`,ipv4Address:`192.0.2.101`,connection:`wifi`,online:!0,leaseActive:!0,leaseExpiresAt:Zm+6*Qm,interface:`wlan0`,ssid:`SmartSafeHub`,radio:`radio0`,band:`5g`,bandLabel:`5 GHz`,signalDbm:-43,inactiveMs:250,connectedSeconds:8300},{id:`demo-laptop`,hostname:`작업용 노트북`,mac:`02:00:00:00:00:02`,ipv4Address:`192.0.2.102`,connection:`wifi`,online:!0,leaseActive:!0,leaseExpiresAt:Zm+8*Qm,interface:`wlan0`,ssid:`SmartSafeHub`,radio:`radio0`,band:`5g`,bandLabel:`5 GHz`,signalDbm:-57,inactiveMs:720,connectedSeconds:29200},{id:`demo-tv`,hostname:`거실 TV`,mac:`02:00:00:00:00:03`,ipv4Address:`192.0.2.103`,connection:`ethernet`,online:!0,leaseActive:!0,leaseExpiresAt:Zm+12*Qm,interface:`lan1`,ssid:null,radio:null,band:null,bandLabel:null,signalDbm:null,inactiveMs:null,connectedSeconds:14e4},{id:`demo-tablet`,hostname:`태블릿`,mac:`02:00:00:00:00:04`,ipv4Address:`192.0.2.104`,connection:`wifi`,online:!1,leaseActive:!1,leaseExpiresAt:null,interface:null,ssid:`SmartSafeHub`,radio:`radio1`,band:`2g`,bandLabel:`2.4 GHz`,signalDbm:null,inactiveMs:null,connectedSeconds:null}],totals:{known:4,online:3,wireless:2,ethernet:1,offline:1}},eh={schema:1,scope:`current_boot`,volatile:!0,maxEvents:100,cloud:{enabled:!1,phase:`disabled`,eligible:!1,plan:`free`,retentionDays:0,pendingEvents:0,lastAttemptAt:0,lastSuccessAt:0,lastUploadedCount:0,lastErrorCode:null,nextSyncAt:0},events:[{schema:1,eventId:`demo-event-1`,eventType:`safeshield.blocklist.updated`,severity:`success`,occurredAt:Zm-7200,deviceUuid:null,source:`safeshield`,metadata:{}},{schema:1,eventId:`demo-event-2`,eventType:`system.boot`,severity:`info`,occurredAt:Zm-86400,deviceUuid:null,source:`system`,metadata:{}}]},th={device:{hostname:`SmartRouter`,model:`ipTIME AX3000SM`,boardName:`iptime,ax3000sm`},software:{distribution:`OpenWrt`,version:`25.12`,revision:`demo`,kernel:`6.12`},runtime:{uptime:86400,localtime:Zm,load:[.08,.12,.05],memory:{total:268435456,free:83886080,shared:4194304,buffered:41943040,available:171966464,cached:41943040}},network:{available:!0,up:!0,protocol:`dhcp`,ipv4Address:`198.51.100.20`},activityHistory:eh},nh={lan:{address:`192.168.1.1`,prefixLength:24,netmask:`255.255.255.0`,subnet:`192.168.1.0/24`},dhcp:{enabled:!0,start:`192.168.1.100`,end:`192.168.1.249`,leaseTime:`12h`},wan:{connected:!0,protocol:`dhcp`,address:`198.51.100.20`,prefixLength:24,subnet:`198.51.100.0/24`},conflict:{detected:!1,wanSubnet:null},recommendation:null},rh={available:!0,version:`0.3.25`,enabled:!0,active:!0,status:`idle`,stage:null,summary:{label:`보호 중`,message:`DNS 보호가 정상적으로 동작하고 있습니다.`,severity:`info`},runtime:{refreshdRunning:!0,dnsmasqRunning:!0,dnsRuntimeOk:!0,lastResult:`success`,lastErrorCode:null},entitlement:{plan:`free`,status:`active`},artifact:{resolved:!0,tier:`light`,version:`20261010T010000Z`,uniqueDomains:48e3,rules:48e3},localOverrides:{enabled:!0,allowlistPath:`/etc/safeshield/allowlist`,blocklistPath:`/etc/safeshield/blocklist`},blocklist:{installed:!0,validLineCount:48e3,fileSizeKb:4700,verificationOk:!0},health:{overall:`ok`},issueCounts:{warnings:0,errors:0},timestamps:{lastAttempt:Zm-2*Qm,lastSuccess:Zm-2*Qm,lastFailure:0,lastLocalApply:Zm-2*Qm,lastLocalApplyFailure:0,nextRefreshAt:Zm+21*Qm,refreshIntervalS:82800}},ih=Math.floor(Zm/Qm)*Qm,ah=[{queries:351,blocked:18},{queries:297,blocked:15},{queries:243,blocked:11},{queries:216,blocked:9},{queries:203,blocked:8},{queries:230,blocked:10},{queries:324,blocked:17},{queries:567,blocked:35},{queries:783,blocked:54},{queries:864,blocked:61},{queries:756,blocked:50},{queries:702,blocked:43},{queries:635,blocked:36},{queries:580,blocked:30},{queries:608,blocked:33},{queries:689,blocked:40},{queries:837,blocked:54},{queries:1026,blocked:73},{queries:1256,blocked:99},{queries:1458,blocked:125},{queries:1512,blocked:135},{queries:1323,blocked:111},{queries:1026,blocked:75},{queries:648,blocked:39}],oh=Array.from({length:24},(e,t)=>{let n=ih-(23-t)*Qm,r=ah[new Date(n*1e3).getHours()];return{bucketStart:n,queries:r.queries,blocked:r.blocked}}),sh={enabled:!0,collectorRunning:!0,available:!0,schemaVersion:1,volatile:!0,startedAt:Zm-604800,updatedAt:Zm-30,retentionHours:168,totals:{queries:oh.reduce((e,t)=>e+t.queries,0),blocked:oh.reduce((e,t)=>e+t.blocked,0)},hourly:oh,deviceLimit:128,devicesTruncated:!1,devices:[{id:`demo-phone`,mac:`02:00:00:00:00:01`,ip:`192.0.2.101`,hostname:`거실 스마트폰`,identified:!0,queries:3400,blocked:470},{id:`demo-laptop`,mac:`02:00:00:00:00:02`,ip:`192.0.2.102`,hostname:`작업용 노트북`,identified:!0,queries:2900,blocked:345},{id:`demo-tv`,mac:`02:00:00:00:00:03`,ip:`192.0.2.103`,hostname:`거실 TV`,identified:!0,queries:1200,blocked:185}]},ch={schema:1,generatedAt:Zm,overall:`ok`,summary:{message:`모든 주요 항목이 정상입니다.`,total:3,ok:3,warning:0,critical:0,unknown:0},metrics:{memoryAvailablePercent:64,load1m:.08,overlayAvailablePercent:80},checks:[],reporter:{enabled:!1,eligible:!1,plan:`free`,entitlementStatus:`active`,lastReportAt:0,nextReportAt:0,lastResult:``,lastErrorCode:null}},lh={phase:`idle`,updateCount:0,packages:[],lastCheckAt:Zm-Qm,lastInstallAt:null,lastError:null,releaseNotes:[],releaseNotesComplete:!0,settings:{channel:`stable`,checkEnabled:!0,checkIntervalSeconds:21600,autoInstall:!0,autoInstallTime:`03:00`,repositoryHost:`example.invalid`,updatePackage:`luci-app-smartsafehub`}},uh={phase:`idle`,source:null,lastCheckAt:Zm-Qm,lastPrepareAt:null,lastError:null,prepared:null,current:{deviceCode:`iptime-ax3000sm`,boardName:`iptime,ax3000sm`,buildId:`demo`,releaseVersion:`1.0.2`,openwrtVersion:`25.12`,metadataAvailable:!0},updateAvailable:!1,release:null,settings:{channel:`stable`,apiBaseUrl:`https://www.smartsafehub.com`,checkIntervalSeconds:21600,autoInstall:!1}},dh={configuration:{protocol:`dhcp`,supported:!0,pppoe:{username:``,passwordConfigured:!1},static:{address:null,prefixLength:null,netmask:null,gateway:null,dns:[]}},status:{connected:!0,pending:!1,protocol:`dhcp`,device:`wan`,address:`198.51.100.20`,prefixLength:24,gateway:`198.51.100.1`,dns:[`1.1.1.1`,`8.8.8.8`],uptimeSeconds:86400}},fh={networks:[{section:`demo_5g`,device:`radio0`,band:`5g`,bandLabel:`5 GHz`,ssid:`SmartSafeHub`,enabled:!0,runtimeUp:!0,security:`sae-mixed`,securityRaw:`sae-mixed`,passwordConfigured:!0,channel:`36`,networks:[`lan`],clientCount:2},{section:`demo_2g`,device:`radio1`,band:`2g`,bandLabel:`2.4 GHz`,ssid:`SmartSafeHub-2G`,enabled:!0,runtimeUp:!0,security:`sae-mixed`,securityRaw:`sae-mixed`,passwordConfigured:!0,channel:`6`,networks:[`lan`],clientCount:0}],totalClients:2,guest:{supported:!0,configured:!0,enabled:!0,ssid:`SmartSafeHub-Guest`,passwordConfigured:!0,subnet:`192.0.2.0/24`,radio:`radio1`}},ph={allow:[`trusted.example`,`updates.example`],block:[`ads.example`,`tracker.example`],counts:{allow:2,block:2,total:4},safeshieldEnabled:!0,applyLocalOverrides:!0},mh=async e=>({ssid:fh.networks.find(t=>t.section===e)?.ssid??`SmartSafeHub`,security:`sae-mixed`,password:`demo-only-2026`}),hh=async()=>({ssid:`SmartSafeHub-Guest`,security:`sae-mixed`,password:`guest-demo-only-2026`}),gh=new Set(Le);function _h(){let e=ii(),t=ci(),n=gh.has(e)?e:`home`,[r,i]=F(``);I(()=>{gh.has(e)||(window.history.replaceState(null,``,`${window.location.pathname}${window.location.search}#home`),i(`해당 기능은 현재 데모에서 제공하지 않습니다.`))},[e]);let a=()=>i(`데모에서는 실제 공유기 설정을 변경할 수 없습니다.`),o=()=>i(``),s=()=>i(`예시 데이터가 표시되고 있습니다. 실제 공유기에는 연결되지 않습니다.`),c=async()=>(a(),!1),l={action:null,actionError:null,data:uh,error:null,loading:!1,message:null,reconnecting:!1,uploadProgress:null,check:a,discard:c,dismissFeedback:o,install:c,prepare:a,refresh:s,upload:c},u;switch(n){case`activity`:u=L(mn,{actionError:null,actionMessage:null,data:eh,error:null,loading:!1,onDismissActionFeedback:o,onRetry:s,onSetCloudSync:a,savingCloudSync:!1});break;case`network`:u=L(`div`,{class:`min-w-0`,children:[L(li,{active:t.network,label:`네트워크 설정 메뉴`,tabs:[{id:`internet`,label:`인터넷 연결`,href:`#network`},{id:`lan`,label:`LAN 및 DHCP`,href:`#network/lan`}]}),L(`div`,{"aria-label":`인터넷 연결 설정`,"aria-labelledby":`internet-tab`,hidden:t.network!==`internet`,id:`internet-panel`,role:`tabpanel`,children:L($n,{action:null,data:dh,error:null,feedback:null,loading:!1,onDismissFeedback:o,onReconnect:a,onRetry:s,onSave:c})}),L(`div`,{"aria-label":`내부 네트워크 설정`,"aria-labelledby":`lan-tab`,hidden:t.network!==`lan`,id:`lan-panel`,role:`tabpanel`,children:L(ur,{action:null,data:nh,error:null,feedback:null,loading:!1,onApplyRecommendation:c,onDismissFeedback:o,onRetry:s,onSave:c})})]});break;case`wifi`:u=L(Tr,{data:fh,error:null,feedback:null,loading:!1,updatingSection:null,onDismissFeedback:o,onRetry:s,onUpdate:c,onUpdateGuest:c,onLoadQr:mh,onLoadGuestQr:hh});break;case`rules`:u=L(kr,{action:null,data:ph,error:null,feedback:null,loading:!1,onAddRule:c,onDeleteRule:c,onDismissFeedback:o,onRetry:s});break;case`system`:u=L(ti,{action:null,actionError:null,data:lh,error:null,firmware:l,loading:!1,message:null,onCheck:a,onDismissFeedback:o,onInstall:a,onRetry:s,onSaveSettings:c});break;case`devices`:u=L(xi,{data:$m,error:null,loading:!1,onRetry:s});break;case`safeshield`:u=L(Xm,{action:null,actionError:null,actionMessage:null,data:rh,error:null,loading:!1,statistics:sh,statisticsError:null,statisticsLoading:!1,statisticsRefreshing:!1,onDismissFeedback:o,onRefreshBlocklist:a,onRetry:s,onRetryStatistics:s,onSetEnabled:a,onSetStatisticsEnabled:a});break;default:u=L(cm,{accountRegistered:!1,activity:eh,activityError:null,activityLoading:!1,data:th,devices:$m,devicesError:null,devicesLoading:!1,error:null,firmware:uh,firmwareError:null,firmwareLoading:!1,health:ch,healthError:null,healthLoading:!1,lan:nh,lanError:null,lanLoading:!1,loading:!1,onRetry:s,safeshield:rh,safeshieldError:null,safeshieldLoading:!1,statistics:sh,statisticsError:null,statisticsLoading:!1,statisticsRefreshing:!1,updates:lh,updatesError:null,updatesLoading:!1})}return L(It,{accountRegistered:!1,demo:!0,loading:!1,onRefresh:s,refreshing:!1,route:n,updateCount:0,children:[L(`div`,{role:`status`,class:`mb-5 flex flex-wrap items-center justify-between gap-3 rounded-xl border border-teal-200 bg-teal-50 px-4 py-3 text-sm font-semibold text-teal-800`,children:[L(`span`,{children:`체험 모드 · 표시되는 기기와 보호 통계는 모두 예시입니다.`}),L(`a`,{class:`font-extrabold underline`,href:`/devices/`,children:`지원 기기 살펴보기 →`})]}),r?L(`div`,{role:`alert`,class:`mb-5 flex items-center justify-between gap-3 rounded-xl border border-amber-300 bg-amber-50 p-4 text-sm font-semibold text-amber-900`,children:[r,L(`button`,{"aria-label":`안내 닫기`,class:`font-black`,onClick:o,type:`button`,children:`✕`})]}):null,u]})}var vh=document.getElementById(`smartsafehub-demo-root`);if(vh){let e=vh.attachShadow({mode:`open`}),t=document.createElement(`link`);t.rel=`stylesheet`,t.href=vh.dataset.demoCss||`/static/router-demo/demo.css`,e.appendChild(t);let n=document.createElement(`div`);n.className=`smartsafehub-shadow-root`,e.appendChild(n),oe(L(_h,{}),n)} \ No newline at end of file diff --git a/luci-app-smartsafehub/frontend/package-lock.json b/luci-app-smartsafehub/frontend/package-lock.json index 372c8b865..481b06b29 100644 --- a/luci-app-smartsafehub/frontend/package-lock.json +++ b/luci-app-smartsafehub/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "luci-app-smartsafehub-frontend", - "version": "0.2.28", + "version": "0.2.29", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "luci-app-smartsafehub-frontend", - "version": "0.2.28", + "version": "0.2.29", "dependencies": { "chart.js": "4.5.1", "preact": "11.0.1" diff --git a/luci-app-smartsafehub/frontend/package.json b/luci-app-smartsafehub/frontend/package.json index 019509424..29f3a53e2 100644 --- a/luci-app-smartsafehub/frontend/package.json +++ b/luci-app-smartsafehub/frontend/package.json @@ -1,7 +1,7 @@ { "name": "luci-app-smartsafehub-frontend", "private": true, - "version": "0.2.28", + "version": "0.2.29", "type": "module", "engines": { "node": ">=24.0" @@ -10,6 +10,7 @@ "dev": "vite --host 0.0.0.0", "typecheck": "tsc --noEmit", "build": "npm run typecheck && vite build", + "build:demo": "npm run typecheck && vite build --config vite.demo.config.ts", "preview": "vite preview --host 0.0.0.0" }, "dependencies": { diff --git a/luci-app-smartsafehub/frontend/src/api/smartsafehub.ts b/luci-app-smartsafehub/frontend/src/api/smartsafehub.ts index a6b4e352e..ee12a1cd6 100644 --- a/luci-app-smartsafehub/frontend/src/api/smartsafehub.ts +++ b/luci-app-smartsafehub/frontend/src/api/smartsafehub.ts @@ -28,6 +28,7 @@ import type { SoftwareUpdateStatus, } from '../types/updates'; import type { + WifiQrCredentials, GuestWifiSummary, GuestWifiUpdateInput, GuestWifiUpdateResult, @@ -87,6 +88,15 @@ export interface DeviceRegistrationStatus { lastSuccessAt: number; } +export interface RecommendedRulesStatus { + revision: string | null; + domains: string[]; +} + +export function fetchRecommendedRulesStatus(): Promise { + return callApi(API_OBJECT, 'device_recommended_rules_status', {}, { timeoutMs: 5000 }); +} + export function fetchDeviceRegistrationStatus(): Promise { return callApi(API_OBJECT, 'device_registration_status', {}, { timeoutMs: 5000 }); } @@ -309,11 +319,7 @@ export function updateIptvSettings( ); } -export interface WifiQrCredentials { - ssid: string; - security: 'none' | 'psk2' | 'sae' | 'sae-mixed'; - password: string; -} +export type { WifiQrCredentials } from '../types/wifi'; export function fetchWifiQr(section: string): Promise { return callApi(API_OBJECT, 'wifi_qr', { section }, { timeoutMs: 8000 }); diff --git a/luci-app-smartsafehub/frontend/src/app/App.tsx b/luci-app-smartsafehub/frontend/src/app/App.tsx index 96e1495a3..b97a214e7 100644 --- a/luci-app-smartsafehub/frontend/src/app/App.tsx +++ b/luci-app-smartsafehub/frontend/src/app/App.tsx @@ -1,14 +1,16 @@ import type { ComponentChildren } from 'preact'; import { useEffect } from 'preact/hooks'; -import { initializeSystemTimezone } from '../api/smartsafehub'; +import { fetchGuestWifiQr, fetchRecommendedRulesStatus, fetchWifiQr, initializeSystemTimezone } from '../api/smartsafehub'; import { AppShell } from '../components/AppShell'; +import { PageTabs } from '../components/PageTabs'; import { useActivityHistory } from '../hooks/useActivityHistory'; import { useConfigurationBackup } from '../hooks/useConfigurationBackup'; import { useConnectedDevices } from '../hooks/useConnectedDevices'; import { useFirmwareUpdates } from '../hooks/useFirmwareUpdates'; import { useHashRoute } from '../hooks/useHashRoute'; +import { usePageTabs } from '../hooks/usePageTabs'; import { useHealth } from '../hooks/useHealth'; import { useIptv } from '../hooks/useIptv'; import { useLan } from '../hooks/useLan'; @@ -45,6 +47,7 @@ interface AppProps { export function App({ onAdministratorPasswordChanged }: AppProps) { const route = useHashRoute(); + const tabs = usePageTabs(); const configurationBackup = useConfigurationBackup(); const activity = useActivityHistory(route === 'activity'); const status = useStatus(route === 'home' || route === 'settings'); @@ -123,7 +126,22 @@ export function App({ onAdministratorPasswordChanged }: AppProps) { case 'network': content = (
-
+ + + {bundledVersion ? ( +
+
+ 포함된 관리 소프트웨어 +
+ {bundledVersion} + {bundledComparison === 'same' ? ( + + 현재 버전과 동일 + + ) : null} +
+
+

+ {bundledComparison === 'same' + ? '관리 소프트웨어는 이미 같은 버전입니다. 펌웨어 업데이트는 시스템 구성 요소를 갱신합니다.' + : bundledComparison === 'older' + ? `현재 설치된 관리 소프트웨어(${installedVersion})보다 이전 버전이 포함되어 있습니다. 펌웨어 설치 전 버전을 확인해 주세요.` + : bundledComparison === 'newer' + ? `현재 설치된 관리 소프트웨어(${installedVersion})보다 새 버전이 포함되어 있습니다.` + : installedVersion + ? `현재 설치 버전 ${installedVersion} · 펌웨어 포함 버전 ${bundledVersion}` + : '이 펌웨어에 포함된 관리 소프트웨어 버전입니다.'} +

+
+ ) : null} {data.release.releaseNotes.length ? (
    {data.release.releaseNotes.map((note, index) => ( diff --git a/luci-app-smartsafehub/frontend/src/components/GuestWifiCard.tsx b/luci-app-smartsafehub/frontend/src/components/GuestWifiCard.tsx index 40a6b3c64..7b2464698 100644 --- a/luci-app-smartsafehub/frontend/src/components/GuestWifiCard.tsx +++ b/luci-app-smartsafehub/frontend/src/components/GuestWifiCard.tsx @@ -3,18 +3,20 @@ import { useEffect, useState } from 'preact/hooks'; import { WifiQrDialog } from './WifiQrDialog'; import { AlertIcon, CheckCircleIcon, ChevronDownIcon, QrCodeIcon, RouterIcon } from './Icons'; -import type { GuestWifiSummary, GuestWifiUpdateInput } from '../types/wifi'; +import type { GuestWifiSummary, GuestWifiUpdateInput, WifiQrCredentials } from '../types/wifi'; export function GuestWifiCard({ guest, busy, saving, onUpdate, + onLoadQr, }: { guest: GuestWifiSummary; busy: boolean; saving: boolean; onUpdate: (input: GuestWifiUpdateInput) => Promise; + onLoadQr: () => Promise; }) { // Keep guest settings out of the way until the administrator needs them. const [expanded, setExpanded] = useState(false); @@ -169,7 +171,7 @@ export function GuestWifiCard({ {validation &&

    {validation}

    }
- {showQr && setShowQr(false)} />} + {showQr && setShowQr(false)} />} ); } diff --git a/luci-app-smartsafehub/frontend/src/components/PageTabs.tsx b/luci-app-smartsafehub/frontend/src/components/PageTabs.tsx new file mode 100644 index 000000000..8b9ecdf23 --- /dev/null +++ b/luci-app-smartsafehub/frontend/src/components/PageTabs.tsx @@ -0,0 +1,56 @@ +interface PageTab { + id: string; + label: string; + href: `#${string}`; +} + +interface PageTabsProps { + active: string; + label: string; + tabs: readonly PageTab[]; +} + +/** Hash links support bookmarks, back/forward and keyboard navigation. */ +export function PageTabs({ active, label, tabs }: PageTabsProps) { + return ( + + ); +} diff --git a/luci-app-smartsafehub/frontend/src/components/ProductNavigation.tsx b/luci-app-smartsafehub/frontend/src/components/ProductNavigation.tsx index f06ed9533..db38670d3 100644 --- a/luci-app-smartsafehub/frontend/src/components/ProductNavigation.tsx +++ b/luci-app-smartsafehub/frontend/src/components/ProductNavigation.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'preact/hooks'; -import { ROUTE_BY_NAME } from '../app/routes'; +import { DEMO_ROUTES, ROUTE_BY_NAME } from '../app/routes'; import type { AppRoute } from '../app/routes'; import { luciAdminUrl } from '../utils/luci'; import { @@ -37,6 +37,7 @@ interface ProductNavigationProps { theme: 'dark' | 'light'; title: string; updateCount: number; + demo?: boolean; } const NAVIGATION_GROUPS: readonly { @@ -99,7 +100,9 @@ function NavigationItems({ collapsed = false, route, updateCount, + demo = false, }: { + demo?: boolean; accountRegistered: boolean | null; collapsed?: boolean; route: AppRoute; @@ -107,7 +110,7 @@ function NavigationItems({ }) { return (
- {NAVIGATION_GROUPS.map((group, groupIndex) => ( + {NAVIGATION_GROUPS.filter((group) => !demo || group.routes.some((name) => DEMO_ROUTES.includes(name))).map((group, groupIndex) => (
)}
- {group.routes.map((routeName) => { + {group.routes.filter((name) => !demo || DEMO_ROUTES.includes(name)).map((routeName) => { const item = ROUTE_BY_NAME[routeName]; const active = route === routeName; return ( @@ -207,6 +210,7 @@ export function ProductNavigation({ theme, title, updateCount, + demo = false, }: ProductNavigationProps) { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const themeLabel = themeActionLabel(theme); @@ -288,8 +292,8 @@ export function ProductNavigation({ {mobileMenuOpen ? (
- -
+ + {!demo ? +
: null}
) : null} @@ -357,9 +361,9 @@ export function ProductNavigation({ class={`flex-1 overflow-y-auto py-5 ${collapsed ? 'px-2' : 'px-3'}`} id="smartsafehub-desktop-navigation" > - + - : null}
diff --git a/luci-app-smartsafehub/frontend/src/components/RelativeTimestamp.tsx b/luci-app-smartsafehub/frontend/src/components/RelativeTimestamp.tsx new file mode 100644 index 000000000..65de3e93f --- /dev/null +++ b/luci-app-smartsafehub/frontend/src/components/RelativeTimestamp.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'preact/hooks'; + +import { formatRecentTimestamp, formatTimestamp } from '../app/format'; + +interface RelativeTimestampProps { + timestamp: number | null | undefined; + fallback?: string; +} + +/** Consistent relative timestamps with an accessible, tappable absolute-time disclosure. */ +export function RelativeTimestamp({ timestamp, fallback = '기록 없음' }: RelativeTimestampProps) { + const [nowTimestamp, setNowTimestamp] = useState(() => Date.now() / 1000); + const validTimestamp = typeof timestamp === 'number' && Number.isFinite(timestamp) && timestamp > 0 && timestamp <= 8.64e12; + + useEffect(() => { + if (!validTimestamp) { + return; + } + + setNowTimestamp(Date.now() / 1000); + const timer = window.setInterval(() => setNowTimestamp(Date.now() / 1000), 60_000); + return () => window.clearInterval(timer); + }, [timestamp, validTimestamp]); + + if (!validTimestamp || timestamp === null || timestamp === undefined) { + return {fallback}; + } + + const exactTime = formatTimestamp(timestamp); + return ( + + +
+ + + + +
+
+ ); +} diff --git a/luci-app-smartsafehub/frontend/src/components/SoftwareUpdatesCard.tsx b/luci-app-smartsafehub/frontend/src/components/SoftwareUpdatesCard.tsx index 3fb97da27..896cd8f8a 100644 --- a/luci-app-smartsafehub/frontend/src/components/SoftwareUpdatesCard.tsx +++ b/luci-app-smartsafehub/frontend/src/components/SoftwareUpdatesCard.tsx @@ -1,8 +1,10 @@ import { useEffect, useState } from 'preact/hooks'; import { CustomSelect } from './CustomSelect'; +import { RelativeTimestamp } from './RelativeTimestamp'; import { TimeSelect } from './TimeSelect'; import type { SoftwareUpdateAction } from '../hooks/useSoftwareUpdates'; +import type { FirmwareStatus } from '../types/firmware'; import type { SoftwareUpdateChannel, SoftwareUpdateError, @@ -10,6 +12,7 @@ import type { SoftwareUpdateStatus, } from '../types/updates'; import { isSoftwareUpdateCheckStale } from '../utils/softwareUpdates'; +import { compareBundledVersion } from '../utils/firmwareBundling'; import { AlertIcon, CheckCircleIcon, @@ -24,6 +27,7 @@ interface SoftwareUpdatesCardProps { action: SoftwareUpdateAction; actionError: string | null; data: SoftwareUpdateStatus | null; + firmware: FirmwareStatus | null; error: string | null; loading: boolean; message: string | null; @@ -188,6 +192,7 @@ export function SoftwareUpdatesCard({ action, actionError, data, + firmware, error, loading, message, @@ -226,6 +231,12 @@ export function SoftwareUpdatesCard({ const currentPackage = data?.packages.find((item) => item.name === UPDATE_PACKAGE) ?? data?.packages[0] ?? null; + const firmwareBundledVersion = firmware?.updateAvailable + ? firmware.release?.bundledSoftware?.smartsafehub + : null; + const softwareIncludedInFirmware = + Boolean(data?.lastCheckAt && data.updateCount === 0 && !updateCheckStale && currentPackage?.name === UPDATE_PACKAGE) && + compareBundledVersion(firmwareBundledVersion, currentPackage?.installedVersion) === 'same'; const hasSettingsChanges = (next: Partial = {}) => { if (!data) { @@ -495,7 +506,7 @@ export function SoftwareUpdatesCard({ Last check
- {formatTimestamp(data.lastCheckAt)} +
@@ -522,7 +533,7 @@ export function SoftwareUpdatesCard({

업데이트 확인이 지연되고 있습니다.

- 마지막 확인은 {formatTimestamp(data.lastCheckAt)}에 완료되었습니다. 자동 확인 상태를 점검하거나 업데이트 확인을 실행해 주세요. + 마지막 업데이트 확인: . 자동 확인 상태를 점검하거나 업데이트 확인을 실행해 주세요.

@@ -539,8 +550,13 @@ export function SoftwareUpdatesCard({

최신 버전을 사용 중입니다.

- 마지막 확인은 {formatTimestamp(data.lastCheckAt)}에 완료되었습니다. + 마지막 업데이트 확인: .

+ {softwareIncludedInFirmware ? ( +

+ 새 펌웨어에도 동일한 관리 소프트웨어({firmwareBundledVersion})가 포함되어 있어 별도로 업데이트할 필요가 없습니다. +

+ ) : null}
diff --git a/luci-app-smartsafehub/frontend/src/components/StatePanels.tsx b/luci-app-smartsafehub/frontend/src/components/StatePanels.tsx index af62a733c..5fa189202 100644 --- a/luci-app-smartsafehub/frontend/src/components/StatePanels.tsx +++ b/luci-app-smartsafehub/frontend/src/components/StatePanels.tsx @@ -13,9 +13,6 @@ export function LoadingPanel() {

장치 상태를 확인하고 있습니다

-

- OpenWrt의 rpcd API에서 정보를 불러오는 중입니다. -

diff --git a/luci-app-smartsafehub/frontend/src/components/WifiQrDialog.tsx b/luci-app-smartsafehub/frontend/src/components/WifiQrDialog.tsx index 9c8747a3c..dc857c441 100644 --- a/luci-app-smartsafehub/frontend/src/components/WifiQrDialog.tsx +++ b/luci-app-smartsafehub/frontend/src/components/WifiQrDialog.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'preact/hooks'; -import { fetchGuestWifiQr, fetchWifiQr } from '../api/smartsafehub'; +import type { WifiQrCredentials } from '../types/wifi'; import { DownloadIcon } from './Icons'; import { createQrMatrix } from '../utils/qrMatrix'; @@ -31,19 +31,19 @@ export function drawWifiQrPng( })); } -export function WifiQrDialog({ section, guest = false, onClose }: { section: string; guest?: boolean; onClose: () => void }) { +export function WifiQrDialog({ load, onClose }: { load: () => Promise; onClose: () => void }) { const [qr, setQr] = useState<{ ssid: string; matrix: boolean[][] } | null>(null); const [error, setError] = useState(null); const [downloadError, setDownloadError] = useState(null); useEffect(() => { let active = true; - void (guest ? fetchGuestWifiQr() : fetchWifiQr(section)).then((data) => { + void load().then((data) => { if (!active) return; try { setQr({ ssid: data.ssid, matrix: createQrMatrix(wifiQrPayload(data.ssid, data.security, data.password)) }); } catch { setError('QR 코드를 생성할 수 없습니다.'); } }).catch(() => { if (active) setError('Wi-Fi 정보를 불러오지 못했습니다.'); }); return () => { active = false; setQr(null); }; - }, [section, guest]); + }, [load]); const count = qr?.matrix.length ?? 0; const downloadQr = () => { if (!qr) return; diff --git a/luci-app-smartsafehub/frontend/src/demo/data.ts b/luci-app-smartsafehub/frontend/src/demo/data.ts new file mode 100644 index 000000000..8c8d54150 --- /dev/null +++ b/luci-app-smartsafehub/frontend/src/demo/data.ts @@ -0,0 +1,182 @@ +// Entirely fictional values. Never use a real router address, MAC, credential or token. +import type { ActivityHistory } from '../types/activity'; +import type { ConnectedDevicesSummary } from '../types/devices'; +import type { FirmwareStatus } from '../types/firmware'; +import type { WanSettings } from '../types/wan'; +import type { WifiSummary, WifiQrCredentials } from '../types/wifi'; +import type { SafeShieldRules } from '../types/rules'; +import type { HealthStatus } from '../types/health'; +import type { LanSettings } from '../types/lan'; +import type { SafeShieldStatistics, SafeShieldStatus } from '../types/safeshield'; +import type { SmartSafeHubStatus } from '../types/status'; +import type { SoftwareUpdateStatus } from '../types/updates'; + +const now = Math.floor(Date.now() / 1000); +const hour = 3600; + +export const demoDevices: ConnectedDevicesSummary = { + generatedAt: now, + devices: [ + { id: 'demo-phone', hostname: '거실 스마트폰', mac: '02:00:00:00:00:01', ipv4Address: '192.0.2.101', connection: 'wifi', online: true, leaseActive: true, leaseExpiresAt: now + 6 * hour, interface: 'wlan0', ssid: 'SmartSafeHub', radio: 'radio0', band: '5g', bandLabel: '5 GHz', signalDbm: -43, inactiveMs: 250, connectedSeconds: 8300 }, + { id: 'demo-laptop', hostname: '작업용 노트북', mac: '02:00:00:00:00:02', ipv4Address: '192.0.2.102', connection: 'wifi', online: true, leaseActive: true, leaseExpiresAt: now + 8 * hour, interface: 'wlan0', ssid: 'SmartSafeHub', radio: 'radio0', band: '5g', bandLabel: '5 GHz', signalDbm: -57, inactiveMs: 720, connectedSeconds: 29200 }, + { id: 'demo-tv', hostname: '거실 TV', mac: '02:00:00:00:00:03', ipv4Address: '192.0.2.103', connection: 'ethernet', online: true, leaseActive: true, leaseExpiresAt: now + 12 * hour, interface: 'lan1', ssid: null, radio: null, band: null, bandLabel: null, signalDbm: null, inactiveMs: null, connectedSeconds: 140000 }, + { id: 'demo-tablet', hostname: '태블릿', mac: '02:00:00:00:00:04', ipv4Address: '192.0.2.104', connection: 'wifi', online: false, leaseActive: false, leaseExpiresAt: null, interface: null, ssid: 'SmartSafeHub', radio: 'radio1', band: '2g', bandLabel: '2.4 GHz', signalDbm: null, inactiveMs: null, connectedSeconds: null }, + ], + totals: { known: 4, online: 3, wireless: 2, ethernet: 1, offline: 1 }, +}; + +export const demoActivity: ActivityHistory = { + schema: 1, scope: 'current_boot', volatile: true, maxEvents: 100, + cloud: { enabled: false, phase: 'disabled', eligible: false, plan: 'free', retentionDays: 0, pendingEvents: 0, lastAttemptAt: 0, lastSuccessAt: 0, lastUploadedCount: 0, lastErrorCode: null, nextSyncAt: 0 }, + events: [ + { schema: 1, eventId: 'demo-event-1', eventType: 'safeshield.blocklist.updated', severity: 'success', occurredAt: now - 7200, deviceUuid: null, source: 'safeshield', metadata: {} }, + { schema: 1, eventId: 'demo-event-2', eventType: 'system.boot', severity: 'info', occurredAt: now - 86400, deviceUuid: null, source: 'system', metadata: {} }, + ], +}; + +export const demoStatus: SmartSafeHubStatus = { + device: { hostname: 'SmartRouter', model: 'ipTIME AX3000SM', boardName: 'iptime,ax3000sm' }, + software: { distribution: 'OpenWrt', version: '25.12', revision: 'demo', kernel: '6.12' }, + runtime: { uptime: 86400, localtime: now, load: [0.08, 0.12, 0.05], memory: { total: 268435456, free: 83886080, shared: 4194304, buffered: 41943040, available: 171966464, cached: 41943040 } }, + network: { available: true, up: true, protocol: 'dhcp', ipv4Address: '198.51.100.20' }, + activityHistory: demoActivity, +}; + +export const demoLan: LanSettings = { + lan: { address: '192.168.1.1', prefixLength: 24, netmask: '255.255.255.0', subnet: '192.168.1.0/24' }, + dhcp: { enabled: true, start: '192.168.1.100', end: '192.168.1.249', leaseTime: '12h' }, + wan: { connected: true, protocol: 'dhcp', address: '198.51.100.20', prefixLength: 24, subnet: '198.51.100.0/24' }, + conflict: { detected: false, wanSubnet: null }, recommendation: null, +}; + +export const demoSafeShield: SafeShieldStatus = { + available: true, version: '0.3.25', enabled: true, active: true, status: 'idle', stage: null, + summary: { label: '보호 중', message: 'DNS 보호가 정상적으로 동작하고 있습니다.', severity: 'info' }, + runtime: { refreshdRunning: true, dnsmasqRunning: true, dnsRuntimeOk: true, lastResult: 'success', lastErrorCode: null }, + entitlement: { plan: 'free', status: 'active' }, + artifact: { resolved: true, tier: 'light', version: '20261010T010000Z', uniqueDomains: 48000, rules: 48000 }, + localOverrides: { enabled: true, allowlistPath: '/etc/safeshield/allowlist', blocklistPath: '/etc/safeshield/blocklist' }, + blocklist: { installed: true, validLineCount: 48000, fileSizeKb: 4700, verificationOk: true }, + health: { overall: 'ok' }, issueCounts: { warnings: 0, errors: 0 }, + timestamps: { lastAttempt: now - 2 * hour, lastSuccess: now - 2 * hour, lastFailure: 0, lastLocalApply: now - 2 * hour, lastLocalApplyFailure: 0, nextRefreshAt: now + 21 * hour, refreshIntervalS: 82800 }, +}; + +const latestHourBucket = Math.floor(now / hour) * hour; + +const hourlyProfiles = [ + { queries: 351, blocked: 18 }, + { queries: 297, blocked: 15 }, + { queries: 243, blocked: 11 }, + { queries: 216, blocked: 9 }, + { queries: 203, blocked: 8 }, + { queries: 230, blocked: 10 }, + { queries: 324, blocked: 17 }, + { queries: 567, blocked: 35 }, + { queries: 783, blocked: 54 }, + { queries: 864, blocked: 61 }, + { queries: 756, blocked: 50 }, + { queries: 702, blocked: 43 }, + { queries: 635, blocked: 36 }, + { queries: 580, blocked: 30 }, + { queries: 608, blocked: 33 }, + { queries: 689, blocked: 40 }, + { queries: 837, blocked: 54 }, + { queries: 1026, blocked: 73 }, + { queries: 1256, blocked: 99 }, + { queries: 1458, blocked: 125 }, + { queries: 1512, blocked: 135 }, + { queries: 1323, blocked: 111 }, + { queries: 1026, blocked: 75 }, + { queries: 648, blocked: 39 }, +] as const; + +const hourly = Array.from({ length: 24 }, (_, i) => { + const bucketStart = latestHourBucket - (23 - i) * hour; + const hourOfDay = new Date(bucketStart * 1_000).getHours(); + const profile = hourlyProfiles[hourOfDay]!; + + return { + bucketStart, + queries: profile.queries, + blocked: profile.blocked, + }; +}); +export const demoStatistics: SafeShieldStatistics = { + enabled: true, collectorRunning: true, available: true, schemaVersion: 1, volatile: true, + startedAt: now - 7 * 86400, updatedAt: now - 30, retentionHours: 168, + totals: { queries: hourly.reduce((sum, item) => sum + item.queries, 0), blocked: hourly.reduce((sum, item) => sum + item.blocked, 0) }, + hourly, deviceLimit: 128, devicesTruncated: false, + devices: [ + { id: 'demo-phone', mac: '02:00:00:00:00:01', ip: '192.0.2.101', hostname: '거실 스마트폰', identified: true, queries: 3400, blocked: 470 }, + { id: 'demo-laptop', mac: '02:00:00:00:00:02', ip: '192.0.2.102', hostname: '작업용 노트북', identified: true, queries: 2900, blocked: 345 }, + { id: 'demo-tv', mac: '02:00:00:00:00:03', ip: '192.0.2.103', hostname: '거실 TV', identified: true, queries: 1200, blocked: 185 }, + ], +}; + +export const demoHealth: HealthStatus = { + schema: 1, generatedAt: now, overall: 'ok', + summary: { message: '모든 주요 항목이 정상입니다.', total: 3, ok: 3, warning: 0, critical: 0, unknown: 0 }, + metrics: { memoryAvailablePercent: 64, load1m: 0.08, overlayAvailablePercent: 80 }, + checks: [], + reporter: { enabled: false, eligible: false, plan: 'free', entitlementStatus: 'active', lastReportAt: 0, nextReportAt: 0, lastResult: '', lastErrorCode: null }, +}; + +export const demoUpdates: SoftwareUpdateStatus = { + phase: 'idle', updateCount: 0, packages: [], lastCheckAt: now - hour, lastInstallAt: null, lastError: null, + releaseNotes: [], releaseNotesComplete: true, + settings: { channel: 'stable', checkEnabled: true, checkIntervalSeconds: 21600, autoInstall: true, autoInstallTime: '03:00', repositoryHost: 'example.invalid', updatePackage: 'luci-app-smartsafehub' }, +}; + +export const demoFirmware: FirmwareStatus = { + phase: 'idle', source: null, lastCheckAt: now - hour, lastPrepareAt: null, lastError: null, prepared: null, + current: { deviceCode: 'iptime-ax3000sm', boardName: 'iptime,ax3000sm', buildId: 'demo', releaseVersion: '1.0.2', openwrtVersion: '25.12', metadataAvailable: true }, + updateAvailable: false, release: null, + settings: { channel: 'stable', apiBaseUrl: 'https://www.smartsafehub.com', checkIntervalSeconds: 21600, autoInstall: false }, +}; + +// The interactive demo deliberately uses only invented, RFC 5737 addresses. +export const demoWan: WanSettings = { + configuration: { + protocol: 'dhcp', supported: true, + pppoe: { username: '', passwordConfigured: false }, + static: { address: null, prefixLength: null, netmask: null, gateway: null, dns: [] }, + }, + status: { + connected: true, pending: false, protocol: 'dhcp', device: 'wan', + address: '198.51.100.20', prefixLength: 24, gateway: '198.51.100.1', + dns: ['1.1.1.1', '8.8.8.8'], uptimeSeconds: 86400, + }, +}; + +export const demoWifi: WifiSummary = { + networks: [ + { section: 'demo_5g', device: 'radio0', band: '5g', bandLabel: '5 GHz', + ssid: 'SmartSafeHub', enabled: true, runtimeUp: true, security: 'sae-mixed', + securityRaw: 'sae-mixed', passwordConfigured: true, channel: '36', + networks: ['lan'], clientCount: 2 }, + { section: 'demo_2g', device: 'radio1', band: '2g', bandLabel: '2.4 GHz', + ssid: 'SmartSafeHub-2G', enabled: true, runtimeUp: true, security: 'sae-mixed', + securityRaw: 'sae-mixed', passwordConfigured: true, channel: '6', + networks: ['lan'], clientCount: 0 }, + ], + totalClients: 2, + guest: { supported: true, configured: true, enabled: true, + ssid: 'SmartSafeHub-Guest', passwordConfigured: true, + subnet: '192.0.2.0/24', radio: 'radio1' }, +}; + +export const demoRules: SafeShieldRules = { + allow: ['trusted.example', 'updates.example'], + block: ['ads.example', 'tracker.example'], + counts: { allow: 2, block: 2, total: 4 }, + safeshieldEnabled: true, applyLocalOverrides: true, +}; + +// The QR dialog uses an injected demo-only data provider, never the router RPC. +export const demoWifiQr = async (section: string): Promise => ({ + ssid: demoWifi.networks.find((network) => network.section === section)?.ssid ?? 'SmartSafeHub', + security: 'sae-mixed', password: 'demo-only-2026', +}); +export const demoGuestWifiQr = async (): Promise => ({ + ssid: 'SmartSafeHub-Guest', security: 'sae-mixed', password: 'guest-demo-only-2026', +}); diff --git a/luci-app-smartsafehub/frontend/src/demo/main.tsx b/luci-app-smartsafehub/frontend/src/demo/main.tsx new file mode 100644 index 000000000..ab024d8bf --- /dev/null +++ b/luci-app-smartsafehub/frontend/src/demo/main.tsx @@ -0,0 +1,154 @@ +import { render } from 'preact'; +import { useEffect, useState } from 'preact/hooks'; + +import { AppShell } from '../components/AppShell'; +import { DEMO_ROUTES } from '../app/routes'; +import { ActivityPage } from '../pages/ActivityPage'; +import { WanPage } from '../pages/WanPage'; +import { LanPage } from '../pages/LanPage'; +import { WifiPage } from '../pages/WifiPage'; +import { SafeShieldRulesPage } from '../pages/SafeShieldRulesPage'; +import { UpdatePage } from '../pages/UpdatePage'; +import { useHashRoute } from '../hooks/useHashRoute'; +import { usePageTabs } from '../hooks/usePageTabs'; +import { PageTabs } from '../components/PageTabs'; +import { ConnectedDevicesPage } from '../pages/ConnectedDevicesPage'; +import { HomePage } from '../pages/HomePage'; +import { SafeShieldPage } from '../pages/SafeShieldPage'; +import { + demoActivity, demoDevices, demoFirmware, demoHealth, demoLan, + demoSafeShield, demoStatistics, demoStatus, demoUpdates, + demoWan, demoWifi, demoRules, demoWifiQr, demoGuestWifiQr, +} from './data'; +import '../styles/app.css'; + +// This entry MUST NOT import App, auth/session, or api/*. No backend is involved. +const ALLOWED_ROUTES = new Set(DEMO_ROUTES); + +function DemoApp() { + const currentRoute = useHashRoute(); + const tabs = usePageTabs(); + const route = ALLOWED_ROUTES.has(currentRoute) ? currentRoute : 'home'; + const [notice, setNotice] = useState(''); + useEffect(() => { + if (!ALLOWED_ROUTES.has(currentRoute)) { + window.history.replaceState(null, '', `${window.location.pathname}${window.location.search}#home`); + setNotice('해당 기능은 현재 데모에서 제공하지 않습니다.'); + } + }, [currentRoute]); + const readonlyNotice = () => setNotice('데모에서는 실제 공유기 설정을 변경할 수 없습니다.'); + const dismiss = () => setNotice(''); + const reset = () => setNotice('예시 데이터가 표시되고 있습니다. 실제 공유기에는 연결되지 않습니다.'); + + // All callbacks are local-only. Never load router hooks or the authenticated App. + const disabledAction = async (): Promise => { + readonlyNotice(); + return false; + }; + const firmware = { + action: null, actionError: null, data: demoFirmware, error: null, + loading: false, message: null, reconnecting: false, uploadProgress: null, + check: readonlyNotice, discard: disabledAction, dismissFeedback: dismiss, + install: disabledAction, prepare: readonlyNotice, refresh: reset, upload: disabledAction, + }; + + let content; + switch (route) { + case 'activity': + content = ; + break; + case 'network': + content =
+ + + +
; + break; + case 'wifi': + content = ; + break; + case 'rules': + content = ; + break; + case 'system': + content = ; + break; + case 'devices': + content = ; + break; + case 'safeshield': + content = ( + + ); + break; + default: + content = ( + + ); + } + + return ( + +
+ 체험 모드 · 표시되는 기기와 보호 통계는 모두 예시입니다. + 지원 기기 살펴보기 → +
+ {notice ? ( + + ) : null} + {content} +
+ ); +} + +// The real router app mounts into a shadow root too. Isolate Tailwind Preflight +// from Django's global CSS and keep the same actual router components/styling. +const host = document.getElementById('smartsafehub-demo-root'); +if (host) { + const shadow = host.attachShadow({ mode: 'open' }); + const stylesheet = document.createElement('link'); + stylesheet.rel = 'stylesheet'; + stylesheet.href = host.dataset.demoCss || '/static/router-demo/demo.css'; + shadow.appendChild(stylesheet); + const mount = document.createElement('div'); + mount.className = 'smartsafehub-shadow-root'; + shadow.appendChild(mount); + render(, mount); +} diff --git a/luci-app-smartsafehub/frontend/src/hooks/useHashRoute.ts b/luci-app-smartsafehub/frontend/src/hooks/useHashRoute.ts index a3acfe993..1b17eba54 100644 --- a/luci-app-smartsafehub/frontend/src/hooks/useHashRoute.ts +++ b/luci-app-smartsafehub/frontend/src/hooks/useHashRoute.ts @@ -6,6 +6,8 @@ const HASH_ROUTES: Readonly> = { '#home': 'home', '#activity': 'activity', '#network': 'network', + '#network/internet': 'network', + '#network/lan': 'network', '#wan': 'network', '#lan': 'network', '#wifi': 'wifi', @@ -16,6 +18,9 @@ const HASH_ROUTES: Readonly> = { '#account': 'account', '#system': 'system', '#settings': 'settings', + '#settings/diagnostics': 'settings', + '#settings/time': 'settings', + '#settings/management': 'settings', }; function routeFromHash(): AppRoute { diff --git a/luci-app-smartsafehub/frontend/src/hooks/usePageTabs.ts b/luci-app-smartsafehub/frontend/src/hooks/usePageTabs.ts new file mode 100644 index 000000000..20be17a19 --- /dev/null +++ b/luci-app-smartsafehub/frontend/src/hooks/usePageTabs.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from 'preact/hooks'; + +export type NetworkTab = 'internet' | 'lan'; +export type SettingsTab = 'diagnostics' | 'time' | 'management'; + +// The parent sidebar still owns #network / #settings. The hashes below select +// a panel without introducing new sidebar routes, and preserve old WAN/LAN URLs. +export function networkTabFromHash(hash: string): NetworkTab { + return hash === '#lan' || hash === '#network/lan' ? 'lan' : 'internet'; +} + +export function settingsTabFromHash(hash: string): SettingsTab { + if (hash === '#settings/time') return 'time'; + if (hash === '#settings/management') return 'management'; + return 'diagnostics'; +} + +function readTabs() { + const hash = window.location.hash; + return { + network: networkTabFromHash(hash), + settings: settingsTabFromHash(hash), + }; +} + +export function usePageTabs() { + const [tabs, setTabs] = useState(readTabs); + + useEffect(() => { + const onHashChange = () => setTabs(readTabs()); + window.addEventListener('hashchange', onHashChange); + return () => window.removeEventListener('hashchange', onHashChange); + }, []); + + return tabs; +} diff --git a/luci-app-smartsafehub/frontend/src/hooks/useSafeShieldRules.ts b/luci-app-smartsafehub/frontend/src/hooks/useSafeShieldRules.ts index bf77f2562..719e51660 100644 --- a/luci-app-smartsafehub/frontend/src/hooks/useSafeShieldRules.ts +++ b/luci-app-smartsafehub/frontend/src/hooks/useSafeShieldRules.ts @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'preact/hooks'; +import { useCallback, useRef, useState } from 'preact/hooks'; import { addSafeShieldRule, @@ -129,14 +129,25 @@ export function useSafeShieldRules(active: boolean) { error: null, feedback: null, }); + const latestMutationId = useRef(0); const mutate = useCallback( async ( operation: 'add' | 'delete', action: SafeShieldRuleAction, domain: string, + onPersisted?: () => void, ): Promise => { - setMutation({ + const mutationId = ++latestMutationId.current; + // DNS verification can outlive persistence, while a newer rule is saved. + // Never overwrite the newer request's feedback with an older result. + const report = (next: SafeShieldRulesMutationState) => { + if (latestMutationId.current === mutationId) { + setMutation(next); + } + }; + + report({ action: { operation, action, domain }, error: null, feedback: null, @@ -153,6 +164,9 @@ export function useSafeShieldRules(active: boolean) { : await deleteSafeShieldRule(action, domain, true); resource.replaceData(result.rules); + // A successful rules API response means the rule is saved. Notify the + // input immediately, without waiting for DNS apply verification. + onPersisted?.(); const shouldApply = result.changed && @@ -160,7 +174,7 @@ export function useSafeShieldRules(active: boolean) { result.rules.applyLocalOverrides; if (!shouldApply) { - setMutation({ + report({ action: null, error: null, feedback: savedOnlyMessage(operation, result), @@ -169,7 +183,7 @@ export function useSafeShieldRules(active: boolean) { } if (!result.refresh.accepted) { - setMutation({ + report({ action: null, error: `규칙은 저장되었지만 로컬 DNS 적용 요청을 시작하지 못했습니다${result.refresh.reason ? ` (${result.refresh.reason})` : ''}.`, feedback: null, @@ -177,16 +191,28 @@ export function useSafeShieldRules(active: boolean) { return true; } - setMutation({ + report({ action: null, error: null, feedback: applyingMessage(operation, result), }); - const outcome = await waitForLocalApply(baseline); + let outcome: LocalApplyOutcome; + try { + outcome = await waitForLocalApply(baseline); + } catch { + // The rule is already saved; a status-query failure must not + // incorrectly report the whole mutation as an unsuccessful save. + report({ + action: null, + error: '규칙은 저장되었지만 DNS 적용 상태를 확인하지 못했습니다. SafeShield 상태를 확인해 주세요.', + feedback: null, + }); + return true; + } if (outcome.kind === 'success') { - setMutation({ + report({ action: null, error: null, feedback: appliedMessage(operation, result), @@ -195,7 +221,7 @@ export function useSafeShieldRules(active: boolean) { } if (outcome.kind === 'disabled') { - setMutation({ + report({ action: null, error: null, feedback: '규칙은 저장되었지만 SafeShield가 꺼져 있어 DNS 적용을 대기하고 있습니다.', @@ -204,7 +230,7 @@ export function useSafeShieldRules(active: boolean) { } if (outcome.kind === 'failure') { - setMutation({ + report({ action: null, error: '규칙은 저장되었지만 로컬 DNS 적용이 실패했습니다. SafeShield 상태를 확인해 주세요.', feedback: null, @@ -212,14 +238,14 @@ export function useSafeShieldRules(active: boolean) { return true; } - setMutation({ + report({ action: null, error: '규칙은 저장되었지만 제한 시간 안에 로컬 DNS 적용 완료를 확인하지 못했습니다.', feedback: null, }); return true; } catch (error) { - setMutation({ + report({ action: null, error: errorMessage(error, '사용자 규칙을 변경하지 못했습니다.'), feedback: null, @@ -231,8 +257,8 @@ export function useSafeShieldRules(active: boolean) { ); const addRule = useCallback( - (action: SafeShieldRuleAction, domain: string) => - mutate('add', action, domain), + (action: SafeShieldRuleAction, domain: string, onPersisted?: () => void) => + mutate('add', action, domain, onPersisted), [mutate], ); diff --git a/luci-app-smartsafehub/frontend/src/main.tsx b/luci-app-smartsafehub/frontend/src/main.tsx index 75a6f607c..f28352227 100644 --- a/luci-app-smartsafehub/frontend/src/main.tsx +++ b/luci-app-smartsafehub/frontend/src/main.tsx @@ -44,7 +44,7 @@ function installBootstrap(sessionId: string, host: HTMLElement): void { sessionId, rpcUrl: luciUrl('/admin/ubus'), assetBase: host.dataset.assetBase ?? '/luci-static/smartsafehub/', - assetVersion: host.dataset.assetVersion ?? '0.2.28-r1', + assetVersion: host.dataset.assetVersion ?? '0.2.29-r1', locale: document.documentElement.lang || 'ko', }); } diff --git a/luci-app-smartsafehub/frontend/src/pages/SafeShieldPage.tsx b/luci-app-smartsafehub/frontend/src/pages/SafeShieldPage.tsx index 45c6326ae..57406b8d5 100644 --- a/luci-app-smartsafehub/frontend/src/pages/SafeShieldPage.tsx +++ b/luci-app-smartsafehub/frontend/src/pages/SafeShieldPage.tsx @@ -12,6 +12,7 @@ import { ShieldIcon, } from '../components/Icons'; import { SafeShieldStatisticsPanel } from '../components/SafeShieldStatisticsPanel'; +import { RelativeTimestamp } from '../components/RelativeTimestamp'; import { ErrorPanel, LoadingPanel } from '../components/StatePanels'; import type { SafeShieldAction } from '../hooks/useSafeShieldActions'; import type { SafeShieldStatistics, SafeShieldStatus } from '../types/safeshield'; @@ -25,6 +26,7 @@ import { formatInterval, formatNumber, formatTimestamp, + parseArtifactVersionTimestamp, } from '../app/format'; const SMARTSAFEHUB_PRICING_URL = 'https://www.smartsafehub.com/pricing/'; @@ -152,28 +154,11 @@ function getArtifactOptimizationLabel(tier: string | null): string { return '자동 최적화'; } -function formatArtifactUpdatedAt(version: string | null): string { - if (!version) { - return '확인되지 않음'; - } - - const match = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(version); - - if (!match) { - return '확인되지 않음'; - } - - const [, year, month, day, hour, minute, second] = match; - const timestamp = Date.UTC( - Number(year), - Number(month) - 1, - Number(day), - Number(hour), - Number(minute), - Number(second), - ) / 1000; - - return formatTimestamp(timestamp); +function formatArtifactUpdatedAt(version: string | null): ComponentChildren { + const timestamp = parseArtifactVersionTimestamp(version); + return timestamp === null + ? '확인되지 않음' + : ; } function getSummaryMessage(data: SafeShieldStatus): string { @@ -369,13 +354,13 @@ function RefreshProgress({ data }: { data: SafeShieldStatus }) { ); } -function SummaryFact({ label, value }: { label: string; value: string }) { +function SummaryFact({ label, value }: { label: string; value: ComponentChildren }) { return (
{label}
-
+
{value}
@@ -739,7 +724,7 @@ export function SafeShieldPage({ label="Blocklist" value={`${formatNumber(displayedBlocklistCount)}개 도메인`} /> - + } /> {planName === 'FREE' ? : null} @@ -815,7 +800,7 @@ export function SafeShieldPage({ icon={} title="차단 목록 갱신" > - + } /> void, ) => Promise; onDeleteRule: ( action: SafeShieldRuleAction, @@ -30,6 +31,8 @@ interface SafeShieldRulesPageProps { ) => Promise; onDismissFeedback: () => void; onRetry: () => void; + // Injected only by the authenticated router app; the public demo has no RPC. + onLoadRecommendedRulesStatus?: () => Promise<{ domains: string[] }>; } interface RuleListCardProps { @@ -38,12 +41,15 @@ interface RuleListCardProps { onAddRule: ( action: SafeShieldRuleAction, domain: string, + onPersisted?: () => void, ) => Promise; onDeleteRule: ( action: SafeShieldRuleAction, domain: string, ) => Promise; rules: string[]; + recommendedDomains: string[]; + recommendedKnown: boolean; } function normalizeDomain(value: string): string { @@ -73,6 +79,8 @@ function RuleListCard({ onAddRule, onDeleteRule, rules, + recommendedDomains, + recommendedKnown, }: RuleListCardProps) { const [input, setInput] = useState(''); const [search, setSearch] = useState(''); @@ -100,11 +108,11 @@ function RuleListCard({ } setValidationError(null); - const saved = await onAddRule(kind, domain); - - if (saved) { - setInput(''); - } + await onAddRule(kind, domain, () => { + // Clear as soon as the server has saved the rule, not after DNS apply. + // Do not erase a newer draft if the input was edited in the meantime. + setInput((current) => normalizeDomain(current) === domain ? '' : current); + }); } const theme = isAllow @@ -217,6 +225,13 @@ function RuleListCard({ {filteredRules.length > 0 ? (
    {filteredRules.map((domain) => { + // Only domains created by the local recommender are managed. + // A previously manual allow entry stays manual even when the + // same domain appears in an assigned recommendation. + const cloudManaged = isAllow && recommendedDomains.includes(domain); + const ruleOrigin = !isAllow || recommendedKnown + ? (cloudManaged ? '추천 규칙' : '직접 추가') + : null; const deleting = action?.operation === 'delete' && action.action === kind && @@ -224,15 +239,27 @@ function RuleListCard({ return (
  • - - {domain} - +
    + + {domain} + + {ruleOrigin ? ( + + {ruleOrigin} + + ) : null} +
    - - ); - } - - const memory = data?.runtime.memory; - const totalMemory = memory?.total ?? 0; - const memoryUsage = memory ? getMemoryUsage(memory) : null; - const usedMemory = memoryUsage?.used ?? 0; - const memoryPercent = Math.round(memoryUsage?.percent ?? 0); const advancedSystemUrl = luciAdminUrl('/admin/system'); const logsUrl = luciAdminUrl('/admin/status/logs'); - const customFirmwareAvailable = Boolean(firmware?.current.metadataAvailable); - const firmwareValue = !data - ? '미확인' - : firmwareLoading && !firmware - ? '확인 중' - : firmwareError && !firmware - ? '확인 필요' - : customFirmwareAvailable - ? firmware?.current.releaseVersion - ? `SmartSafeHub ${firmware.current.releaseVersion}` - : 'SmartSafeHub 펌웨어' - : `${data.software.distribution} ${data.software.version}`; - const firmwareDescription = !data - ? '버전 정보를 확인할 수 없습니다.' - : firmwareLoading && !firmware - ? '펌웨어 정보를 확인하고 있습니다.' - : firmwareError && !firmware - ? firmwareError - : customFirmwareAvailable - ? firmware?.current.buildId - ? `빌드 ID ${firmware.current.buildId}` - : '빌드 ID를 확인할 수 없습니다.' - : `리비전 ${data.software.revision}`; return (
    @@ -1699,57 +1625,63 @@ export function SettingsPage({ )} -
    -
    -

    - System -

    -

    시스템 상태

    -

    - 장치의 펌웨어, 실행 시간, 메모리와 시스템 부하를 확인합니다. -

    -
    -
    - - - - + + -
    + -
    +