diff --git a/clashoo/Makefile b/clashoo/Makefile index a5f5babe7..c55e7796d 100644 --- a/clashoo/Makefile +++ b/clashoo/Makefile @@ -6,7 +6,7 @@ PKG_UPSTREAM_TAG:=v$(PKG_UPSTREAM_VERSION) PKG_SHORT_SHA:=3f27ef3 PKG_VERSION:=2026.10.27-r5 -PKG_RELEASE:=21 +PKG_RELEASE:=22 PKG_SOURCE:=$(PKG_NAME)-stable-$(PKG_UPSTREAM_TAG)-$(PKG_SHORT_SHA).tar.gz PKG_SOURCE_SUBDIR:=$(PKG_NAME)-stable-$(PKG_UPSTREAM_TAG)-$(PKG_SHORT_SHA) diff --git a/clashoo/files/usr/share/clashoo/nftables/geoip6_cn.nft b/clashoo/files/usr/share/clashoo/nftables/geoip6_cn.nft index 5cd2612d3..daf983914 100644 --- a/clashoo/files/usr/share/clashoo/nftables/geoip6_cn.nft +++ b/clashoo/files/usr/share/clashoo/nftables/geoip6_cn.nft @@ -1091,7 +1091,6 @@ set clashoo_china6 { 2402:e2c0::/32, 2402:e3c0::/32, 2402:e480::/32, - 2402:e540::/32, 2402:e680::/32, 2402:e740::/32, 2402:e780::/32, @@ -2657,7 +2656,6 @@ set clashoo_china6 { 2406:840:fee8::/46, 2406:840:feec::/48, 2406:840:feee::/47, - 2406:840:feff::/48, 2406:840:ff00::/40, 2406:880::/32, 2406:8c0::/32, @@ -3387,7 +3385,16 @@ set clashoo_china6 { 2a0a:d685:220::/43, 2a0a:d685:240::/42, 2a0a:d685:280::/41, - 2a0a:d685:400::/38, + 2a0a:d685:400::/40, + 2a0a:d685:500::/45, + 2a0a:d685:508::/48, + 2a0a:d685:50a::/47, + 2a0a:d685:50c::/46, + 2a0a:d685:510::/44, + 2a0a:d685:520::/43, + 2a0a:d685:540::/42, + 2a0a:d685:580::/41, + 2a0a:d685:600::/39, 2a0a:d685:800::/37, 2a0a:d685:1000::/36, 2a0a:d685:2000::/35, diff --git a/clashoo/files/usr/share/clashoo/nftables/geoip_cn.nft b/clashoo/files/usr/share/clashoo/nftables/geoip_cn.nft index 26ac0cf79..37b399112 100644 --- a/clashoo/files/usr/share/clashoo/nftables/geoip_cn.nft +++ b/clashoo/files/usr/share/clashoo/nftables/geoip_cn.nft @@ -394,13 +394,16 @@ set clashoo_china { 43.92.0.0/15, 43.94.0.0/18, 43.96.0.0/23, + 43.96.2.130/32, 43.96.3.0/24, 43.96.4.0/22, 43.96.8.0/22, + 43.96.16.130/32, 43.96.18.0/24, 43.96.20.0/22, 43.96.24.0/22, 43.96.32.0/22, + 43.96.36.130/32, 43.96.40.0/24, 43.96.42.0/24, 43.96.44.0/23, @@ -411,6 +414,7 @@ set clashoo_china { 43.96.62.0/23, 43.96.64.0/21, 43.96.72.0/22, + 43.96.77.131/32, 43.96.78.0/24, 43.96.80.0/23, 43.96.85.0/24, @@ -1007,21 +1011,12 @@ set clashoo_china { 47.77.128.0/17, 47.78.0.0/15, 47.80.0.0/14, - 47.84.0.0/16, - 47.85.0.0/17, - 47.85.128.0/23, - 47.85.131.0/24, - 47.85.132.0/22, - 47.85.136.0/21, - 47.85.144.0/20, - 47.85.160.0/19, - 47.85.192.0/18, + 47.84.0.0/15, 47.86.0.0/16, 47.87.0.0/20, 47.87.16.0/21, 47.87.32.0/19, 47.87.64.0/21, - 47.87.72.0/24, 47.87.80.0/20, 47.87.96.0/19, 47.87.128.0/20, @@ -1528,7 +1523,8 @@ set clashoo_china { 103.19.40.0/21, 103.19.64.0/21, 103.19.72.0/22, - 103.19.232.0/22, + 103.19.233.0/24, + 103.19.234.0/23, 103.20.12.0/22, 103.20.32.0/24, 103.20.34.0/23, @@ -2214,7 +2210,7 @@ set clashoo_china { 103.98.232.0/22, 103.98.240.0/20, 103.99.53.0/24, - 103.99.54.0/23, + 103.99.55.0/24, 103.99.56.0/22, 103.99.62.0/24, 103.99.104.0/22, @@ -2267,7 +2263,6 @@ set clashoo_china { 103.104.104.0/22, 103.104.152.0/22, 103.104.188.0/22, - 103.104.198.0/23, 103.104.252.0/22, 103.105.0.0/21, 103.105.12.0/22, @@ -2678,7 +2673,6 @@ set clashoo_china { 103.186.228.0/23, 103.189.92.0/23, 103.189.152.0/22, - 103.189.197.0/24, 103.190.20.0/23, 103.190.71.0/24, 103.190.104.0/23, @@ -3639,7 +3633,7 @@ set clashoo_china { 117.134.128.0/18, 117.134.208.0/23, 117.134.212.0/23, - 117.134.216.0/22, + 117.134.218.0/23, 117.134.222.0/23, 117.134.232.0/21, 117.134.240.0/20, @@ -3729,7 +3723,9 @@ set clashoo_china { 119.19.0.0/16, 119.20.0.0/14, 119.27.64.0/18, - 119.27.128.0/17, + 119.27.128.0/18, + 119.27.208.0/20, + 119.27.224.0/20, 119.28.28.0/24, 119.28.240.0/20, 119.29.0.0/16, @@ -4240,7 +4236,7 @@ set clashoo_china { 142.86.224.0/20, 142.86.240.0/21, 142.86.248.0/22, - 142.86.254.0/23, + 142.86.255.0/24, 143.64.0.0/16, 144.0.0.0/16, 144.7.0.0/16, @@ -4514,7 +4510,6 @@ set clashoo_china { 163.204.0.0/16, 163.223.28.0/23, 163.223.32.0/23, - 163.223.72.0/23, 163.223.124.0/23, 163.223.222.0/23, 163.227.34.0/23, @@ -6299,8 +6294,7 @@ set clashoo_china { 211.144.0.0/14, 211.148.0.0/16, 211.149.0.0/19, - 211.149.33.0/24, - 211.149.34.0/23, + 211.149.32.0/22, 211.149.48.0/21, 211.149.64.0/21, 211.149.80.0/21, diff --git a/clashoo/files/usr/share/clashoo/ruleset/cn-ip.srs b/clashoo/files/usr/share/clashoo/ruleset/cn-ip.srs index ef9118faf..03e9ee7ff 100644 Binary files a/clashoo/files/usr/share/clashoo/ruleset/cn-ip.srs and b/clashoo/files/usr/share/clashoo/ruleset/cn-ip.srs differ diff --git a/clashoo/files/usr/share/clashoo/ruleset/cn.mrs b/clashoo/files/usr/share/clashoo/ruleset/cn.mrs index 610f2d2dd..f57532f0f 100644 Binary files a/clashoo/files/usr/share/clashoo/ruleset/cn.mrs and b/clashoo/files/usr/share/clashoo/ruleset/cn.mrs differ diff --git a/clashoo/files/usr/share/clashoo/ruleset/geolocation-!cn.srs b/clashoo/files/usr/share/clashoo/ruleset/geolocation-!cn.srs index 6f26b8989..b1101499d 100644 Binary files a/clashoo/files/usr/share/clashoo/ruleset/geolocation-!cn.srs and b/clashoo/files/usr/share/clashoo/ruleset/geolocation-!cn.srs differ diff --git a/clashoo/files/usr/share/clashoo/ruleset/geosite-cn.srs b/clashoo/files/usr/share/clashoo/ruleset/geosite-cn.srs index 216609fe4..9e4d515d8 100644 Binary files a/clashoo/files/usr/share/clashoo/ruleset/geosite-cn.srs and b/clashoo/files/usr/share/clashoo/ruleset/geosite-cn.srs differ diff --git a/gately/Makefile b/gately/Makefile index 28640f901..0dc3da05a 100644 --- a/gately/Makefile +++ b/gately/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=gately -PKG_VERSION:=0.6.2.3 -PKG_RELEASE:=22 +PKG_VERSION:=0.6.3 +PKG_RELEASE:=23 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/wangwanjie/gately/tar.gz/v$(PKG_VERSION)? diff --git a/gost/Makefile b/gost/Makefile index 0e0f4ccde..bcf1e54c9 100644 --- a/gost/Makefile +++ b/gost/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=gost -PKG_VERSION:=3.3.2 -PKG_RELEASE:=7 +PKG_VERSION:=3.3.3 +PKG_RELEASE:=8 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/go-gost/gost/tar.gz/v$(PKG_VERSION)? diff --git a/isp-split/Makefile b/isp-split/Makefile index 67b94f467..c888f5870 100644 --- a/isp-split/Makefile +++ b/isp-split/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=isp-split -PKG_VERSION:=0.3.2 -PKG_RELEASE:=5 +PKG_VERSION:=0.4.0 +PKG_RELEASE:=6 PKG_LICENSE:=MIT PKG_LICENSE_FILES:=LICENSE PKG_MAINTAINER:=jihebube @@ -155,6 +155,9 @@ define Package/isp-split/install # scripts/install.sh --uninstall 都调它;postrm 在二进制先被删掉时 # 用内联等价清单兜底(锚点由 selftest 第 21 节双向核对)。 $(INSTALL_BIN) $(PKG_BUILD_DIR)/files/usr/lib/isp-split/teardown.sh $(1)/usr/lib/isp-split/teardown.sh + # 环境体检的判据实现(doctor 子命令与 tools/router-preflight.sh 共用一份)。 + # 与 teardown.sh 同理:漏装 = 设备上 `isp-split doctor` 直接拒绝运行。 + $(INSTALL_DATA) $(PKG_BUILD_DIR)/files/usr/lib/isp-split/doctor.sh $(1)/usr/lib/isp-split/doctor.sh $(INSTALL_DATA) $(PKG_BUILD_DIR)/files/usr/lib/isp-split/cidr2range.awk $(1)/usr/lib/isp-split/cidr2range.awk $(INSTALL_DATA) $(PKG_BUILD_DIR)/files/usr/lib/isp-split/merge.awk $(1)/usr/lib/isp-split/merge.awk diff --git a/isp-split/files/usr/lib/isp-split/doctor.sh b/isp-split/files/usr/lib/isp-split/doctor.sh new file mode 100644 index 000000000..aaaaed820 --- /dev/null +++ b/isp-split/files/usr/lib/isp-split/doctor.sh @@ -0,0 +1,2039 @@ +#!/bin/sh +# doctor.sh — 环境体检判据(唯一实现,两套入口共用) +# +# 消费者: +# 1) isp-split doctor [--json] (files/usr/sbin/isp-split 里 source 本文件) +# 2) tools/router-preflight.sh (部署前预检,source 同一份函数) +# 为什么共用:preflight 与 doctor 回答的是同一件事——"这台机器上的前提还在不在"。 +# 两套判据各写一份的历史(F7)已经证明会立刻漂移,所以判据函数只在这里。 +# +# 设计红线(AUDIT-SECURITY-v2 第三部分 C0): +# 1. 不产生业务流量:不做 DNS 查询、不连外网、不 STUN。功能验证归其它子命令。 +# 2. 不写系统配置:本文件唯一的写操作是 /tmp 下的临时文件(当前实现为零临时文件)。 +# 3. 不假设工具存在:ss/lsof/pgrep/jq 一律按"可能没有"写;主判据落在 +# /proc、/sys、uci、nft、ip 五样上。 +# 4. 永不 die:任何探针失败降级为 unknown + probe_errors(与 need_nft 的 die +# 风格相反,是有意的:体检要在半坏的机器上仍然说得上话)。 +# +# 探针原语 P1–P11 一次定义、六项检查复用:`nft -a list ruleset` 全量只跑一次 +# (DOC_P4),检查三/五共享,这是结构性保证而不是纪律要求。 +# +# busybox 兼容:无 strtonum(十六进制统一走 printf/$(( )))、无 and()/or() +# (位运算在 shell 里做)、ERE 里不写 \{ \}(busybox awk 会报 Invalid contents)。 +set -u + +DOC_SCHEMA=1 +# 探针登记表默认先建好:本文件允许被单独 source(preflight 就只取主机判据), +# 此时 doc_main 还没跑,任何 _doc_probe 调用都不能因为 set -u 直接退出。 +DOC_PROBES="" +DOC_PROBE_ERR="" +# mwan3 标记位段的默认值。真值永远取 mwan3.globals.mmx_mask(P11), +# 这里只是"读不到时"的兜底:硬编码 0x3f00 会在改过 mmx_mask 的机器上全盘误判。 +DOC_MASK_DEFAULT=0x3F00 +# mwan3 的 ip rule 起始 pref 兜底值(实测 2005)。真值优先从 P5 现算。 +DOC_MINPREF_FALLBACK=2005 + +# ---------------------------------------------------------------- 小工具 + +_doc_have() { command -v "$1" >/dev/null 2>&1; } + +# JSON 转义:优先复用主脚本的 _lc_json_esc(同一份语义,避免两套转义规则)。 +_doc_esc() { + # 快路径:不含引号/反斜杠/控制字符时直接输出(printf 是内建,零 fork)。 + case "${1:-}" in + *'"'* | *'\\'* | *[[:cntrl:]]*) ;; + *) printf '%s' "${1:-}" && return 0 ;; + esac + if _doc_have _lc_json_esc; then + _lc_json_esc "${1:-}" + return 0 + fi + printf '%s' "${1:-}" | + sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ / /g' | + tr -d '\r\n' | + sed -e 's/[[:cntrl:]]//g' +} + +# 带引号的 JSON 字符串;空值/未知一律输出 null(UI 才不会把 "" 当"有值") +_doc_jstr() { + case "${1:-}" in + "") printf 'null' ;; + *) printf '"%s"' "$(_doc_esc "$1")" ;; + esac +} + +_doc_jbool() { [ "${1:-0}" = "1" ] && printf 'true' || printf 'false'; } + +# 0x.. -> 十进制。空串/非法 -> 0(调用方不因此 die)。 +_doc_hex() { + case "${1:-}" in + 0x* | 0X*) ;; + *) + printf '0' + return 0 + ;; + esac + printf '%d' "$(( $1 ))" 2>/dev/null || printf '0' +} + +# 同上,但结果放全局 HEX:循环里用它可以完全避免 fork(x86 VM 上 148 个写者 +# 从 4.2s 降到 <1s)。用法:_doc_hx "$v"; _owned=$(( HEX & _M ))。 +_doc_hx() { + case "${1:-}" in + 0x* | 0X* | [0-9]*) HEX=$(( $1 )) 2>/dev/null || HEX=0 ;; + *) HEX=0 ;; + esac +} + +# 十进制 -> 0x 小写字符串(用于 JSON 里的掩码/值展示) +_doc_hxstr() { + printf '0x%x' "$(( ${1:-0} & 0xffffffff ))" 2>/dev/null || printf '0x0' +} + +# grep -c 在"0 匹配"时已经打印了 0、却返回 1;写成 `|| printf 0` 会多打一行, +# 拼进 JSON 就变成 "0\n0"(本文件第一版踩过)。所有计数统一走这两个函数。 +_doc_cnt() { # stdin -> 非空行数 + _dc=$(grep -c . 2>/dev/null) || true + case "${_dc:-}" in '' | *[!0-9]*) _dc=0 ;; esac + printf '%s' "$_dc" +} + +_doc_cnt_pat() { # stdin -> 匹配行数 + _dcp=$(grep -c -- "$1" 2>/dev/null) || true + case "${_dcp:-}" in '' | *[!0-9]*) _dcp=0 ;; esac + printf '%s' "$_dcp" +} + +_doc_bool_uci() { + case "${1:-}" in + 1 | true | TRUE | yes | on) printf '1' ;; + *) printf '0' ;; + esac +} + +# 毫秒时间戳(busybox 的 date 没有 %N,用 /proc/uptime 的百分秒) +_doc_now_ms() { + awk '{ printf "%d", $1 * 1000 }' /proc/uptime 2>/dev/null || printf '0' +} + +# 时间文本:主脚本的 _ts_text 需要一个 epoch 参数(不是无参调用), +# sed/mawk 时代的实现差异在这里吃掉,命令行文本才不会变成 "-"。 +_doc_ts_text() { + if _doc_have _ts_text; then + _ts_text "${DOC_GEN:-$(date +%s 2>/dev/null)}" + else + date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || printf '-' + fi +} + +# 探针登记:JSON 里必须始终有 probes / probe_errors 两个数组—— +# "缺探针"不等于"没问题",这是 unknown 从哪来的溯源。 +_doc_probe() { # + _dp_id=$1 + _dp_rc=$2 + shift 2 + _dp_extra="" + for _dp_kv in "$@"; do + case "$_dp_kv" in + *=*) + _dp_k=${_dp_kv%%=*} + _dp_v=${_dp_kv#*=} + case "$_dp_v" in + '' | *[!0-9]*) continue ;; + esac + _dp_extra="${_dp_extra}${_dp_extra:+,}\"$(_doc_esc "$_dp_k")\":$_dp_v" + ;; + esac + done + DOC_PROBES="${DOC_PROBES}${DOC_PROBES:+,}{\"id\":\"$(_doc_esc "$_dp_id")\",\"rc\":$_dp_rc${_dp_extra:+,$_dp_extra}}" +} + +_doc_probe_err() { # + DOC_PROBE_ERR="${DOC_PROBE_ERR}${DOC_PROBE_ERR:+,}{\"probe\":$(_doc_jstr "$1"),\"cmd\":$(_doc_jstr "$2"),\"note\":$(_doc_jstr "$3")}" +} + +# ---------------------------------------------------------------- 主机前提(preflight 共用判据) +# +# 任务书要求 doctor 收敛 tools/router-preflight.sh 的判据:固件 / 内存 / nft 版本 / +# mwan3-nft 存在性 / 依赖二进制。这五项在这里各有一个函数,preflight 直接调用, +# doctor 也直接用它们的返回值填 host 块——一份判据,两套出口。 + +doc_host_firmware() { + DOC_FW_ID="" + DOC_FW_RELEASE="" + DOC_FW_TARGET="" + DOC_FW_REV="" + DOC_FW_DESC="" + DOC_ARCH=$(uname -m 2>/dev/null) || DOC_ARCH="" + [ -f /etc/openwrt_release ] || return 1 + # 用 sed 取值而不是 `. /etc/openwrt_release`:source 会覆盖调用方的同名变量 + # (preflight 以前就是这么干的),共享函数里不能有这种副作用。 + DOC_FW_ID=$(sed -n "s/^DISTRIB_ID='\(.*\)'$/\1/p" /etc/openwrt_release 2>/dev/null | head -1) + DOC_FW_RELEASE=$(sed -n "s/^DISTRIB_RELEASE='\(.*\)'$/\1/p" /etc/openwrt_release 2>/dev/null | head -1) + DOC_FW_TARGET=$(sed -n "s/^DISTRIB_TARGET='\(.*\)'$/\1/p" /etc/openwrt_release 2>/dev/null | head -1) + DOC_FW_REV=$(sed -n "s/^DISTRIB_REVISION='\(.*\)'$/\1/p" /etc/openwrt_release 2>/dev/null | head -1) + DOC_FW_DESC=$(sed -n "s/^DISTRIB_DESCRIPTION='\(.*\)'$/\1/p" /etc/openwrt_release 2>/dev/null | head -1) + [ -n "$DOC_FW_ID" ] || DOC_FW_ID="OpenWrt" + return 0 +} + +# 内存:数据管线峰值约为几个 MB(awk 流式处理),这里只挡"明显不够"的机器。 +# total < 32MiB -> warn(可能跑不动 1.3 万条 CIDR 的互斥化) +# avail < 8MiB -> warn(同步时其它进程可能 OOM) +doc_host_mem() { + DOC_MEM_TOTAL_KB=0 + DOC_MEM_AVAIL_KB=0 + DOC_MEM_VERDICT=unknown + _doc_mem=$(sed -n 's/^MemTotal:[[:space:]]*\([0-9]*\).*/\1/p' /proc/meminfo 2>/dev/null | head -1) + _doc_avail=$(sed -n 's/^MemAvailable:[[:space:]]*\([0-9]*\).*/\1/p' /proc/meminfo 2>/dev/null | head -1) + # 老内核(4.14 之前)没有 MemAvailable:用 MemFree 兜底 + if [ -z "${_doc_avail:-}" ]; then + _doc_avail=$(sed -n 's/^MemFree:[[:space:]]*\([0-9]*\).*/\1/p' /proc/meminfo 2>/dev/null | head -1) + fi + case "${_doc_mem:-}" in '' | *[!0-9]*) _doc_mem=0 ;; esac + case "${_doc_avail:-}" in '' | *[!0-9]*) _doc_avail=0 ;; esac + DOC_MEM_TOTAL_KB=$_doc_mem + DOC_MEM_AVAIL_KB=$_doc_avail + if [ "$_doc_mem" -eq 0 ]; then + DOC_MEM_VERDICT=unknown + elif [ "$_doc_mem" -lt 32768 ] || { [ "$_doc_avail" -gt 0 ] && [ "$_doc_avail" -lt 8192 ]; }; then + DOC_MEM_VERDICT=warn + else + DOC_MEM_VERDICT=ok + fi + printf '%s\n' "$DOC_MEM_VERDICT" +} + +# nft 版本:>= 1.0 才具备本仓库依赖的 `-a` / `list counters` 行为。 +doc_nft_version() { + DOC_NFT_VERSION="" + DOC_NFT_FULL="" + DOC_NFT_OK=0 + DOC_NFT_MAJOR=0 + if ! _doc_have nft; then + return 1 + fi + DOC_NFT_FULL=$(nft --version 2>/dev/null | head -1) + DOC_NFT_VERSION=$(nft --version 2>/dev/null | head -1 | + sed -n 's/.*v\([0-9][0-9.]*\).*/\1/p') + [ -n "$DOC_NFT_VERSION" ] || DOC_NFT_VERSION=$(nft --version 2>/dev/null | awk '{print $2}' | head -1) + DOC_NFT_MAJOR=$(printf '%s' "$DOC_NFT_VERSION" | cut -d. -f1) + case "$DOC_NFT_MAJOR" in '' | *[!0-9]*) DOC_NFT_MAJOR=0 ;; esac + [ "$DOC_NFT_MAJOR" -ge 1 ] 2>/dev/null && DOC_NFT_OK=1 + return 0 +} + +doc_mwan3_installed() { [ -f /lib/mwan3/mwan3.sh ]; } + +# mwan3 是不是 nftables 版:判据沿用 install.sh / preflight,只此一处 +# (grep render_config_ipsets)。iptables 版装上去不会报错,只会静默不生效。 +doc_mwan3_is_nft() { + doc_mwan3_installed || return 1 + grep -q render_config_ipsets /lib/mwan3/mwan3.sh 2>/dev/null +} + +doc_mwan3_table() { + _doc_have nft || return 1 + nft list table inet mwan3 >/dev/null 2>&1 +} + +# 版本(P10):opkg 与 apk 双分支;两者都没有 -> 空(不判"漂移",判 unknown) +doc_pkg_version() { + _dv_pkg=$1 + _dv_out="" + if _doc_have opkg; then + _dv_out=$(opkg list-installed 2>/dev/null | + awk -v p="$_dv_pkg" '$1 == p { print $3; exit }') + fi + if [ -z "$_dv_out" ] && _doc_have apk; then + # apk 的列形如 "mwan3-3.6.12-r5 x86_64 {...} [installed]": + # 只剥掉 "<包名>-" 前缀,剩下的整段(3.6.12-r5)才是版本。 + _dv_out=$(apk list -I 2>/dev/null | + awk -v p="$_dv_pkg" 'index($1, p "-") == 1 { + v = $1; sub("^" p "-", "", v); print v; exit }') + fi + printf '%s' "$_dv_out" +} + +# 依赖二进制:必需项(缺了核心功能就不成立)与可选项分开,供 gate 使用。 +DOC_BIN_REQUIRED="nft uci curl awk sed grep ip" +DOC_BIN_OPTIONAL="socat conntrack ubus jsonfilter dnsmasq ss" +doc_bins() { + DOC_BINS_JSON="" + DOC_BINS_HAVE="" + DOC_BINS_MISSING="" + _bn_all=0 + for _bn in $DOC_BIN_REQUIRED $DOC_BIN_OPTIONAL; do + _bn_have=0 + _doc_have "$_bn" && _bn_have=1 + DOC_BINS_JSON="${DOC_BINS_JSON}${DOC_BINS_JSON:+,}\"$(_doc_esc "$_bn")\":$(_doc_jbool "$_bn_have")" + [ "$_bn_have" = "1" ] && DOC_BINS_HAVE="${DOC_BINS_HAVE:+$DOC_BINS_HAVE }$_bn" + case " $DOC_BIN_REQUIRED " in + *" $_bn "*) + [ "$_bn_have" = "0" ] && DOC_BINS_MISSING="${DOC_BINS_MISSING:+$DOC_BINS_MISSING }$_bn" + ;; + esac + done + return 0 +} + +# 某个命令在不在(doc_bins 的结论;preflight 用它替代自己写 command -v) +doc_bin_have() { + case " ${DOC_BINS_HAVE:-} " in + *" $1 "*) return 0 ;; + esac + return 1 +} + +# ---------------------------------------------------------------- 探针 P1–P11 + +# P11:位段真值。空 -> 默认 0x3F00,并记录来源(默认值 vs 用户配置)。 +doc_p11_mask() { + [ "${DOC_P11_DONE:-0}" = "1" ] && return 0 + DOC_P11_DONE=1 + DOC_MMX_MASK="" + DOC_MMX_SOURCE="default" + _doc_mask=$(uci -q get mwan3.globals.mmx_mask 2>/dev/null) || _doc_mask="" + case "${_doc_mask:-}" in + 0x* | 0X* | 0[0-9]* | [1-9]*) + DOC_MMX_MASK="$_doc_mask" + DOC_MMX_SOURCE="uci mwan3.globals.mmx_mask" + ;; + esac + [ -n "$DOC_MMX_MASK" ] || DOC_MMX_MASK=$DOC_MASK_DEFAULT + DOC_MMX_MASK_DEC=$(_doc_hex "$DOC_MMX_MASK") + _doc_probe P11 0 +} + +# P1:谁在听 :53。/proc/net 是硬证据,ss/netstat 只作加速器(可能没编进固件)。 +doc_p1_listen() { + [ "${DOC_P1_DONE:-0}" = "1" ] && return 0 + DOC_P1_DONE=1 + DOC_P1_TSV="" + DOC_P1_RC=0 + if [ -r /proc/net/udp ]; then + DOC_P1_TSV=$(awk 'FNR > 1 && $2 ~ /:0035$/ { print "udp\t" FILENAME "\t" $2 "\t" $4 "\t" $10 }' \ + /proc/net/udp /proc/net/udp6 2>/dev/null) + else + DOC_P1_RC=1 + fi + if [ -r /proc/net/tcp ]; then + DOC_P1_TCP=$(awk 'FNR > 1 && $2 ~ /:0035$/ && $4 == "0A" { print "tcp\t" FILENAME "\t" $2 "\t" $4 "\t" $10 }' \ + /proc/net/tcp /proc/net/tcp6 2>/dev/null) + DOC_P1_TSV="${DOC_P1_TSV}${DOC_P1_TSV:+ +}${DOC_P1_TCP}" + fi + DOC_P1_ROWS=$(printf '%s\n' "$DOC_P1_TSV" | _doc_cnt) + _doc_probe P1 "$DOC_P1_RC" "rows=$DOC_P1_ROWS" + # P1b 加速器:只用于交叉验证,不存在不报错 + if _doc_have ss; then + DOC_P1B=$(ss -lunp 2>/dev/null | _doc_cnt_pat ':53 ') + _doc_probe P1b 0 "rows=$DOC_P1B" + else + _doc_probe_err P1b "ss -lunp" "ss 不存在,已跳过(不影响结论)" + fi +} + +# 十六进制地址 -> 点分十进制 / "::" / "v6"。只做展示与"是否回环"两类判断, +# 不追求完整的 v6 格式化(没有消费方需要)。 +_doc_addr_of() { + _da_h=$(printf '%s' "$1" | awk -F: '{ print $1 }') + _da_v6=0 + case "$_da_h" in + ????????????????????????????????) _da_v6=1 ;; + esac + if [ "$_da_v6" = "1" ]; then + case "$_da_h" in + 00000000000000000000000000000000) printf '::' ;; + *0000000000000000000000000100007F) printf '127.0.0.1' ;; + *) printf 'v6:%s' "$(printf '%s' "$_da_h" | cut -c1-8)" ;; + esac + return 0 + fi + printf '%s' "$_da_h" | awk '{ + h = toupper($0) + d = "0123456789ABCDEF" + n = 0 + for (i = 1; i <= 8; i += 2) + b[++n] = (index(d, substr(h, i, 1)) - 1) * 16 + index(d, substr(h, i + 1, 1)) - 1 + printf "%d.%d.%d.%d", b[4], b[3], b[2], b[1] + }' +} + +_doc_is_loopback() { + case "$1" in + 127.* | ::1 | "::") return 0 ;; + esac + return 1 +} + +# P2:inode -> pid -> comm。一次性把 /proc/*/fd 的符号链接表读进变量, +# 之后逐 inode 匹配,避免每个监听者都把整个 /proc 扫一遍。 +doc_p2_map() { + [ "${DOC_P2_DONE:-0}" = "1" ] && return 0 + DOC_P2_DONE=1 + DOC_P2_MAP="" + DOC_P2_RC=0 + # ls 在非 root 下会因为读不到别人的 fd 目录而返回非 0;只认"表是不是空的" + DOC_P2_MAP=$(ls -l /proc/[0-9]*/fd 2>/dev/null) + [ -n "$DOC_P2_MAP" ] || DOC_P2_RC=1 + _doc_probe P2 "$DOC_P2_RC" +} + +# inode -> "pid comm",找不到输出空 +_doc_pid_of_inode() { + # busybox 的 `ls -l /proc/*/fd` 每个目录打一行 "/proc//fd:" 头, + # 条目只有裸文件名("7 -> socket:[1234]");GNU ls 会把完整路径写进条目。 + # 两种都得认,否则 pid 永远是空 -> "谁在听 :53" 全线失效(VM 实测过)。 + _dpi_pid=$(printf '%s\n' "${DOC_P2_MAP:-}" | awk -v ino="$1" ' + /\/fd:$/ { + p = $0 + sub(/^.*\/proc\//, "", p) + sub(/\/fd:$/, "", p) + pid = p + next + } + { + if (index($0, "socket:[" ino "]") == 0) next + if (match($0, /\/proc\/[0-9]+\/fd\//)) { + t = substr($0, RSTART + 6, RLENGTH - 6) + sub(/\/fd\/$/, "", t) + pid = t + } + print pid + exit + }') + [ -n "${_dpi_pid:-}" ] || return 1 + _dpi_comm=$(cat "/proc/$_dpi_pid/comm" 2>/dev/null | tr -d '\n') + printf '%s %s\n' "$_dpi_pid" "${_dpi_comm:-?}" +} + +# P3:dnsmasq confdir 真值。四级优先级(runtime -> ps -> uci -> default)的实现 +# 只有主脚本里的 _dns_confdir_detect 一份;doctor 直接复用它(同一 shell 里 +# source 进来时就是同一个函数),独立运行时才用下面的精简兜底。 +doc_p3_confdir() { + [ "${DOC_P3_DONE:-0}" = "1" ] && return 0 + DOC_P3_DONE=1 + DOC_P3_SOURCE="" + DOC_P3_DIRS="" + DOC_P3_DETAIL="" + DOC_P3_RC=0 + if _doc_have _dns_confdir_detect; then + _dns_confdir_detect >/dev/null 2>&1 + DOC_P3_SOURCE=${DNS_CONFDIR_SOURCE:-} + DOC_P3_DIRS=${DNS_CONFDIR_DIRS:-} + DOC_P3_DETAIL=${DNS_CONFDIR_DETAIL:-} + [ -n "$DOC_P3_SOURCE" ] || DOC_P3_SOURCE="unknown" + else + # 兜底:uci 值 -> 运行配置里的 conf-dir= -> 默认目录 + _doc_cd=$(uci -q get dhcp.@dnsmasq[0].confdir 2>/dev/null) || _doc_cd="" + if [ -n "${_doc_cd:-}" ]; then + DOC_P3_SOURCE="uci" + DOC_P3_DETAIL="dhcp.@dnsmasq[0].confdir" + DOC_P3_DIRS=$(printf '%s\n' "$_doc_cd" | tr ' ' '\n' | sed 's/,$//' | grep -v '^$' | tr '\n' ' ') + else + _doc_cd=$(sed -n 's/^[[:space:]]*conf-dir=//p' ${DNSMASQ_CONF_GLOB:-/var/etc/dnsmasq.conf.*} 2>/dev/null | + head -1 | sed 's/,.*$//; s/[[:space:]]*$//') + if [ -n "${_doc_cd:-}" ]; then + DOC_P3_SOURCE="runtime" + DOC_P3_DETAIL="${DNSMASQ_CONF_GLOB:-/var/etc/dnsmasq.conf.*}" + DOC_P3_DIRS=$_doc_cd + else + DOC_P3_SOURCE="default" + DOC_P3_DETAIL="dnsmasq 默认目录" + DOC_P3_DIRS=${DNSMASQ_DEFAULT_DIR:-/tmp/dnsmasq.d} + fi + fi + fi + _doc_probe P3 "$DOC_P3_RC" "rows=$(printf '%s\n' "$DOC_P3_DIRS" | wc -w | tr -d ' ')" +} + +# 运行中的 dnsmasq 实例:pid 列表(过滤 --test 之类的瞬时进程) +_doc_dnsmasq_pids() { + _ddp_out="" + for _ddp_p in $(pidof dnsmasq 2>/dev/null); do + # OpenWrt 25.12 的 dnsmasq 跑在 ujail 里:pidof 会把 ujail 包装进程也 + # 列出来,而 ujail 会把 comm 改成 "dnsmasq"(VM 实测),所以 comm 不能 + # 用来区分。只认 argv[0] 是 dnsmasq 的那个进程。 + _ddp_exe=$(tr '\0' '\n' <"/proc/$_ddp_p/cmdline" 2>/dev/null | head -1) + case "$_ddp_exe" in + */dnsmasq) ;; + *) continue ;; + esac + _ddp_cmd=$(tr '\0' ' ' <"/proc/$_ddp_p/cmdline" 2>/dev/null) || continue + case " $_ddp_cmd " in + *" --test "*) continue ;; + esac + _ddp_out="${_ddp_out:+$_ddp_out }$_ddp_p" + done + printf '%s' "$_ddp_out" +} + +# 某个 pid 是否把 作为 conf-dir 加载: +# ① cmdline 里直接有 --conf-dir=(OpenWrt 一般不这么传) +# ② cmdline 里的 -C/--conf-file 指向的运行配置里有 conf-dir= +# ②是 24.10 的真实形态:conf-dir 写在 /var/etc/dnsmasq.conf.* 里, +# 只 grep cmdline 会得到"所有机器都是假阴性"(判据设计里的 1.6 必须这么落)。 +_doc_pid_loads_dir() { + _dpl_pid=$1 + _dpl_dir=$2 + _dpl_cmd=$(tr '\0' ' ' <"/proc/$_dpl_pid/cmdline" 2>/dev/null) + for _dpl_tok in $_dpl_cmd; do + case "$_dpl_tok" in + --conf-dir=*) + _dpl_v=${_dpl_tok#--conf-dir=} + _dpl_v=${_dpl_v%%,*} + _dpl_v=${_dpl_v%/} + [ "$_dpl_v" = "${_dpl_dir%/}" ] && return 0 + ;; + --conf-file=*) _dpl_f=${_dpl_tok#--conf-file=} ;; + esac + done + # -C 两 token 形式(OpenWrt 实际用法) + _dpl_f=${_dpl_f:-} + if [ -z "$_dpl_f" ]; then + _dpl_prev="" + for _dpl_tok in $_dpl_cmd; do + case "$_dpl_prev" in + -C) _dpl_f=$_dpl_tok ;; + esac + _dpl_prev=$_dpl_tok + done + fi + [ -n "$_dpl_f" ] || return 1 + [ -r "$_dpl_f" ] || return 1 + sed -n 's/^[[:space:]]*conf-dir=//p' "$_dpl_f" 2>/dev/null | + while IFS= read -r _dpl_line; do + for _dpl_d in $(printf '%s' "$_dpl_line" | tr ',' ' '); do + [ "${_dpl_d%/}" = "${_dpl_dir%/}" ] && { + printf x + break + } + done + done | grep -q x +} + +# P4:一次全量 dump。DOC_P4_RAW 供 hijack 扫描(要看 tproxy/redirect), +# DOC_P4 是剥掉 comment 与 elements 段之后的解析面。 +doc_p4_ruleset() { + [ "${DOC_P4_DONE:-0}" = "1" ] && return 0 + DOC_P4_DONE=1 + DOC_P4_RC=0 + DOC_P4_RAW="" + DOC_P4="" + if ! _doc_have nft; then + DOC_P4_RC=127 + _doc_probe_err P4 "nft -a list ruleset" "nft 不存在,检查三/五判 unknown" + _doc_probe P4 127 + return 0 + fi + DOC_P4_RAW=$(nft -a list ruleset 2>/dev/null) || DOC_P4_RC=$? + if [ "$DOC_P4_RC" -ne 0 ]; then + _doc_probe_err P4 "nft -a list ruleset" "nft 执行失败(rc=$DOC_P4_RC)" + _doc_probe P4 "$DOC_P4_RC" + return 0 + fi + # 先剥 elements = { ... }(状态机,与 count_set_elements 同款思路), + # 再剥 comment "...":两者都会让 hex 解析误判/变慢。 + DOC_P4=$(printf '%s\n' "$DOC_P4_RAW" | awk ' + { + line = $0 + if (inb) { + if (index(line, "}")) inb = 0 + next + } + p = index(line, "elements = {") + if (p) { + pre = substr(line, 1, p - 1) + rest = substr(line, p + 12) + printf "%s}\n", pre + if (!index(rest, "}")) inb = 1 + next + } + while (match(line, /comment "[^"]*"/)) + line = substr(line, 1, RSTART - 1) substr(line, RSTART + RLENGTH) + print line + }') + _doc_probe P4 0 "bytes=$(printf '%s\n' "$DOC_P4_RAW" | wc -c | tr -d ' ')" +} + +doc_p4b_table() { + [ "${DOC_P4B_DONE:-0}" = "1" ] && return 0 + DOC_P4B_DONE=1 + DOC_P4B=0 + if doc_mwan3_table; then + DOC_P4B=1 + fi + _doc_probe P4b $([ "$DOC_P4B" = "1" ] && printf 0 || printf 1) +} + +# 写者枚举(检查三/五共用):一次 awk 出 TSV,位运算留给 shell(busybox awk +# 没有 and()/or())。字段:fam tab chain handle field form mask value comment +doc_p4_writers() { + DOC_W="" + [ -n "${DOC_P4:-}" ] || { + DOC_W_ROWS=0 + return 0 + } + DOC_W=$(printf '%s\n' "$DOC_P4" | awk ' + function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s } + BEGIN { fam = ""; tab = ""; chain = "" } + { + tl = trim($0) + if (index(tl, "table ") == 1) { + n = split(tl, A, " ") + fam = A[2]; tab = A[3]; chain = "" + next + } + if (index(tl, "chain ") == 1) { + n = split(tl, A, " ") + chain = A[2] + next + } + line = $0 + cm = "" + p = index(line, "mark set") + if (p == 0) next + field = "mark" + if (index(line, "ct mark set") > 0) { + field = "ctmark"; p = index(line, "ct mark set") + 13 + } else if (index(line, "meta mark set") > 0) { + p = index(line, "meta mark set") + 14 + } else { + p = p + 8 + } + rest = trim(substr(line, p)) + h = "" + q = index(rest, "# handle ") + if (q > 0) { h = substr(rest, q + 9); rest = trim(substr(rest, 1, q - 1)) } + sub(/[^0-9].*$/, "", h) + form = "other"; mask = "-"; value = "-" + if (index(rest, "&") > 0) { + q = index(rest, "&") + r2 = trim(substr(rest, q + 1)) + split(r2, B, " ") + mask = B[1] + if (index(rest, "|") > 0) { + q2 = index(rest, "|") + split(trim(substr(rest, q2 + 1)), C, " ") + value = C[1] + form = "mask_set" + } else if (index(rest, "or ") > 0) { + q2 = index(rest, "or ") + split(trim(substr(rest, q2 + 3)), C, " ") + value = C[1] + form = "mask_or" + } else { + form = "mask_clear" + } + } else { + split(rest, C, " ") + value = C[1] + form = "set" + } + if (value != "-" && value != "" && substr(value, 1, 2) != "0x") { + form = "other"; value = "-"; mask = "-" + } + if (fam == "" || tab == "") next + printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", fam, tab, chain, (h == "" ? "-" : h), field, form, mask, value, (cm == "" ? "-" : cm) + }') + DOC_W_ROWS=$(printf '%s\n' "$DOC_W" | _doc_cnt) +} + +# 归属(检查五 5.2 的顺序,写死):① inet mwan3 + mwan3* 链 -> mwan3; +# ② 同一张表里 comment 含 isp-split -> isp-split;③ 非 inet 表 -> legacy; +# ④ 其余 -> third_party。顺序即优先级。 +_doc_owner_of() { # fam tab chain comment + case "$1" in + inet) + if [ "$2" = "mwan3" ]; then + case "$3" in + mwan3* | "") printf 'mwan3' ;; + *) printf 'isp-split' ;; + esac + return 0 + fi + case "${4:-}" in + *isp-split*) printf 'isp-split' ;; + *) printf 'third_party' ;; + esac + ;; + *) printf 'legacy' ;; + esac +} + +# 产品指纹(只用于文案,不作判据) +_doc_guess_product() { # fam tab chain + case "$2|$3" in + *clash* | *Clash*) printf 'openclash' ;; + *PSW* | *passwall* | *PassWall*) printf 'passwall' ;; + *tailscale* | *Tailscale*) printf 'tailscale' ;; + *oaf* | *OAF*) printf 'oaf' ;; + *mihomo* | *Mihomo* | *Meta*) printf 'mihomo' ;; + *sing* | *SING*) printf 'sing-box' ;; + *) printf '' ;; + esac +} + +# P5:ip rule。无掩码规则等价掩码 0xffffffff——不补全会让位段判据全错。 +doc_p5_iprule() { + [ "${DOC_P5_DONE:-0}" = "1" ] && return 0 + DOC_P5_DONE=1 + DOC_P5="" + DOC_P5_RC=0 + if ! _doc_have ip; then + DOC_P5_RC=127 + _doc_probe_err P5 "ip -4 rule show" "ip 不存在" + _doc_probe P5 127 + return 0 + fi + DOC_P5=$(ip -4 rule show 2>/dev/null | awk ' + { + # 空行不是规则:不 skip 会解析出一条 pref="-"、value=0xffffffff 的 + # 假"精确 fwmark 规则",直接触发 MARK_READER_STARVED 误报(实测踩过)。 + if ($0 == "") next + line = $0 + pref = $1; sub(/:$/, "", pref) + val = ""; mask = "0xffffffff"; lk = ""; iif = ""; sup = ""; notv = "" + if (index(line, "not fwmark ") > 0) { notv = "1"; p = index(line, "not fwmark ") + 11 } + else if (index(line, "fwmark ") > 0) p = index(line, "fwmark ") + 7 + else p = 0 + if (p > 0) { + t = substr(line, p) + sub(/[ \t].*$/, "", t) + if (index(t, "/") > 0) { + val = substr(t, 1, index(t, "/") - 1) + mask = substr(t, index(t, "/") + 1) + } else { + val = t + } + } + if (index(line, "lookup ") > 0) { + t = substr(line, index(line, "lookup ") + 7) + sub(/[ \t].*$/, "", t) + lk = t + } + if (index(line, "iif ") > 0) { + t = substr(line, index(line, "iif ") + 4) + sub(/[ \t].*$/, "", t) + iif = t + } + if (index(line, "suppress_prefixlength ") > 0) { + t = substr(line, index(line, "suppress_prefixlength ") + 23) + sub(/[ \t].*$/, "", t) + sup = t + } + printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", pref, (val == "" ? "-" : val), mask, (lk == "" ? "-" : lk), (iif == "" ? "-" : iif), (sup == "" ? "-" : sup), (notv == "" ? "0" : notv) + }') + DOC_P5_ROWS=$(printf '%s\n' "$DOC_P5" | _doc_cnt) + _doc_probe P5 0 "rows=$DOC_P5_ROWS" + # mwan3 的最小 pref:mask 等于 mmx_mask 且值非零的那些规则(mwan3 自己写的)。 + DOC_MINPREF=$DOC_MINPREF_FALLBACK + DOC_MINPREF_FOUND=0 + while IFS=" " read -r _mp_pref _mp_val _mp_mask _mp_lk _mp_iif _mp_sup _mp_not; do + [ -n "${_mp_pref:-}" ] || continue + [ "$_mp_val" != "-" ] || continue + _doc_hx "$_mp_mask" && [ "$HEX" = "$DOC_MMX_MASK_DEC" ] || continue + _doc_hx "$_mp_val" && [ "$HEX" = "0" ] && continue + case "$_mp_pref" in '' | *[!0-9]*) continue ;; esac + if [ "$DOC_MINPREF_FOUND" = "0" ] || [ "$_mp_pref" -lt "$DOC_MINPREF" ]; then + DOC_MINPREF=$_mp_pref + DOC_MINPREF_FOUND=1 + fi + done </dev/null) || DOC_RG_RC=$? + DOC_RG_DEV=$(printf '%s\n' "$_rg" | sed -n 's/.*[[:space:]]dev[[:space:]]\([^[:space:]]*\).*/\1/p' | head -1) + DOC_RG_VIA=$(printf '%s\n' "$_rg" | sed -n 's/.*[[:space:]]via[[:space:]]\([^[:space:]]*\).*/\1/p' | head -1) + DOC_RG_TABLE=$(printf '%s\n' "$_rg" | sed -n 's/.*[[:space:]]table[[:space:]]\([^[:space:]]*\).*/\1/p' | head -1) + DOC_DEF_V4=$(ip -4 route show default 2>/dev/null | head -1) + DOC_DEF_V6=$(ip -6 route show default 2>/dev/null | head -1) + _doc_probe P6 "$DOC_RG_RC" "rows=1" +} + +# P7:TUN 设备。/sys/class/net/*/tun_flags 是设备性质的硬证据,ip 侧只作加速器。 +doc_p7_tun() { + [ "${DOC_P7_DONE:-0}" = "1" ] && return 0 + DOC_P7_DONE=1 + DOC_TUN="" + DOC_TUN_RC=0 + DOC_TUN=$(ls -d /sys/class/net/*/tun_flags 2>/dev/null | + sed 's#^/sys/class/net/##; s#/tun_flags$##') + [ -n "$DOC_TUN" ] || DOC_TUN_RC=0 + DOC_TUN_ROWS=$(printf '%s\n' "$DOC_TUN" | _doc_cnt) + _doc_probe P7 0 "rows=$DOC_TUN_ROWS" + if _doc_have ip && ip -o link show type tun >/dev/null 2>&1; then + _doc_probe P7b 0 + else + _doc_probe_err P7b "ip -o link show type tun" "ip-tiny 不支持 type 过滤(只作加速器,不影响结论)" + fi +} + +# 谁持有 /dev/net/tun(设备归属) +_doc_tun_owner() { + _dto_pids="" + _dto_out="" + # 与 _doc_pid_of_inode 同一套目录头解析:谁持有 /dev/net/tun + for _dto_line in $(printf '%s\n' "${DOC_P2_MAP:-}" | awk ' + /\/fd:$/ { + p = $0 + sub(/^.*\/proc\//, "", p) + sub(/\/fd:$/, "", p) + pid = p + next + } + { + if (index($0, "/dev/net/tun") == 0) next + if (match($0, /\/proc\/[0-9]+\/fd\//)) { + t = substr($0, RSTART + 6, RLENGTH - 6) + sub(/\/fd\/$/, "", t) + pid = t + } + if (pid != "" && !seen[pid]++) print pid + }'); do + case " $_dto_pids " in + *" $_dto_line "*) continue ;; + esac + _dto_pids="$_dto_pids $_dto_line" + _dto_c=$(cat "/proc/$_dto_line/comm" 2>/dev/null | tr -d '\n') + _dto_out="${_dto_out}${_dto_out:+/}${_dto_c:-?}:$_dto_line" + done + printf '%s' "$_dto_out" +} + +# fake-ip 判据:优先认假 IP 段(198.18.0.0/16)而不是产品名 +_doc_fakeip() { + DOC_FIP_DETECTED=0 + DOC_FIP_PRODUCT="" + DOC_FIP_RANGE="" + DOC_FIP_EVIDENCE="" + if _doc_have _openclash_fakeip && _openclash_fakeip; then + DOC_FIP_DETECTED=1 + DOC_FIP_PRODUCT="openclash" + DOC_FIP_RANGE="198.18.0.0/16" + DOC_FIP_EVIDENCE="uci openclash.config.en_mode=fake-ip" + return 0 + fi + for _fip_f in /etc/config/openclash /etc/mihomo/config.yaml /etc/sing-box/config.json \ + /etc/config/mihomo /etc/config/sing-box; do + [ -f "$_fip_f" ] || continue + grep -q '198\.18\.' "$_fip_f" 2>/dev/null || continue + DOC_FIP_DETECTED=1 + DOC_FIP_RANGE="198.18.0.0/16" + case "$_fip_f" in + *openclash*) DOC_FIP_PRODUCT="openclash" ;; + *mihomo*) DOC_FIP_PRODUCT="mihomo" ;; + *sing-box*) DOC_FIP_PRODUCT="sing-box" ;; + *) DOC_FIP_PRODUCT="" ;; + esac + DOC_FIP_EVIDENCE="grep 198.18. $_fip_f" + return 0 + done + return 0 +} + +# P8:offload。uci 与 ruleset 双证——只读 uci 会漏掉"改了没 reload"。 +doc_p8_offload() { + [ "${DOC_P8_DONE:-0}" = "1" ] && return 0 + DOC_P8_DONE=1 + DOC_OFF_SW=$(_doc_bool_uci "$(uci -q get firewall.@defaults[0].flow_offloading 2>/dev/null)") + DOC_OFF_HW=$(_doc_bool_uci "$(uci -q get firewall.@defaults[0].flow_offloading_hw 2>/dev/null)") + DOC_OFF_FLOWTABLE=0 + DOC_OFF_FLOWADD=0 + if [ -n "${DOC_P4_RAW:-}" ]; then + DOC_OFF_FLOWTABLE=$(printf '%s\n' "$DOC_P4_RAW" | grep -cE '^[[:space:]]*flowtable ' 2>/dev/null) || true + DOC_OFF_FLOWADD=$(printf '%s\n' "$DOC_P4_RAW" | grep -c 'flow add @' 2>/dev/null) || true + case "${DOC_OFF_FLOWTABLE:-}" in '' | *[!0-9]*) DOC_OFF_FLOWTABLE=0 ;; esac + case "${DOC_OFF_FLOWADD:-}" in '' | *[!0-9]*) DOC_OFF_FLOWADD=0 ;; esac + fi + DOC_OFF_TURBOACC=0 + [ -f /etc/config/turboacc ] && DOC_OFF_TURBOACC=1 + DOC_OFF_VENDOR="" + for _of_m in fast_classifier shortcut_fe ecm; do + grep -q "^$_of_m" /proc/modules 2>/dev/null && DOC_OFF_VENDOR="${DOC_OFF_VENDOR:+$DOC_OFF_VENDOR }$_of_m" + done + [ -d /sys/kernel/debug/hnat ] && DOC_OFF_VENDOR="${DOC_OFF_VENDOR:+$DOC_OFF_VENDOR }hnat-sysfs" + _doc_probe P8 0 "rows=$((DOC_OFF_FLOWTABLE + DOC_OFF_FLOWADD))" +} + +# P9:conntrack 参数(弱证据:只说明"有人在调",不说明是谁调的) +doc_p9_conntrack() { + [ "${DOC_P9_DONE:-0}" = "1" ] && return 0 + DOC_P9_DONE=1 + DOC_CT_COUNT=$(cat /proc/sys/net/netfilter/nf_conntrack_count 2>/dev/null || printf '') + DOC_CT_MAX=$(cat /proc/sys/net/netfilter/nf_conntrack_max 2>/dev/null || printf '') + DOC_CT_ACCT=$(cat /proc/sys/net/netfilter/nf_conntrack_acct 2>/dev/null || printf '') + DOC_CT_EST=$(cat /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established 2>/dev/null || printf '') + DOC_CT_TUNED="" + for _ct_f in /etc/sysctl.conf /etc/sysctl.d/turboacc.conf; do + [ -f "$_ct_f" ] || continue + grep -q nf_conntrack "$_ct_f" 2>/dev/null && DOC_CT_TUNED="${DOC_CT_TUNED:+$DOC_CT_TUNED }$_ct_f" + done + _doc_probe P9 0 +} + +# 接口计数(唯一"正确口径"的吞吐来源) +_doc_dev_bytes() { # -> "rx tx",读不到输出 "- -" + _ddb_dev=$1 + _ddb_rx=$(cat "/sys/class/net/$_ddb_dev/statistics/rx_bytes" 2>/dev/null || printf '') + _ddb_tx=$(cat "/sys/class/net/$_ddb_dev/statistics/tx_bytes" 2>/dev/null || printf '') + printf '%s %s\n' "${_ddb_rx:--}" "${_ddb_tx:--}" +} + +# ---------------------------------------------------------------- 检查一:53 端口归属 +# +# 只在"没人听也不会静默失效"的地方给 ok:owner 空且无劫持 -> unknown,因为 +# 可能是没配 DNS,也可能是探针被裁掉了。这条与 preflight 的"缺就 ✗"不同: +# doctor 的 unknown 是"不知道",不是"有问题"。 +doc_check_dns53() { + DOC_C1_ID="dns53_owner" + DOC_C1_TITLE="53 端口归属与 dnsmasq 片段可达性" + DOC_C1_VERDICT=unknown + DOC_C1_CONF=high + DOC_C1_SERVER="" + + doc_p1_listen + doc_p2_map + doc_p3_confdir + + _l_listeners="" + _l_third="" + _l_dnsq=0 + _l_lan_dnsq=0 + _l_count=0 + while IFS=" " read -r _l_proto _l_file _l_addr _l_st _l_inode; do + [ -n "${_l_proto:-}" ] || continue + _l_count=$((_l_count + 1)) + _l_ip=$(_doc_addr_of "$_l_addr") + _l_owner=$(_doc_pid_of_inode "$_l_inode") + _l_pid=${_l_owner%% *} + _l_comm=${_l_owner#* } + [ "$_l_owner" = "$_l_pid" ] && _l_comm="" + # 0A=TCP LISTEN,07=UDP 未连接;其它状态(如 01 ESTABLISHED)不算"在听" + _l_listen=0 + case "$_l_st" in + 0A | 07) _l_listen=1 ;; + esac + _l_listeners="${_l_listeners}${_l_listeners:+,}{\"pid\":${_l_pid:-0},\"comm\":$(_doc_jstr "$_l_comm"),\"proto\":$(_doc_jstr "$_l_proto"),\"addr\":$(_doc_jstr "$_l_ip"),\"port\":53,\"listen\":$(_doc_jbool "$_l_listen"),\"inode\":$(_doc_jstr "$_l_inode")}" + if [ "$_l_comm" = "dnsmasq" ]; then + _l_dnsq=1 + _doc_is_loopback "$_l_ip" || _l_lan_dnsq=1 + elif [ -n "$_l_comm" ]; then + _l_third="${_l_third}${_l_third:+,}$(_doc_jstr "$_l_comm")" + _doc_is_loopback "$_l_ip" || _l_lan_dnsq=1 + fi + done </dev/null | + head -6) + [ -n "$_l_hijack" ] && _l_hijack_n=$(printf '%s\n' "$_l_hijack" | _doc_cnt) + fi + _l_hijack_json="" + if [ "$_l_hijack_n" -gt 0 ]; then + while IFS= read -r _l_h; do + [ -n "$_l_h" ] || continue + _l_hijack_json="${_l_hijack_json}${_l_hijack_json:+,}$(_doc_jstr "$(printf '%s' "$_l_h" | sed 's/^[[:space:]]*//')")" + done </dev/null) + [ -n "${_l_target:-}" ] || _l_target=$(printf '%s\n' "${DOC_P3_DIRS:-}" | awk '{ print $1 }') + [ -n "${_l_target:-}" ] || _l_target=${DNSMASQ_DEFAULT_DIR:-/tmp/dnsmasq.d} + _l_frag="$_l_target/isp-split.conf" + _l_loaded="" + _l_loaded_n=0 + for _l_p in $_l_pids; do + if _doc_pid_loads_dir "$_l_p" "${DOC_P3_DIRS:-$_l_target}"; then + _l_loaded="${_l_loaded}${_l_loaded:+,}$_l_p" + _l_loaded_n=$((_l_loaded_n + 1)) + fi + done + # 域名分流是否配置过:DOMAIN_SPLIT 非空,或 $DOMAINDIR/*.txt 非空。 + # 只做 IP 分流时 dnsmasq 片段本来就不生成(isp-split dns 会直接返回), + # 把"没有片段"判成 conflict 是假阳性——VM 实测踩到过。 + _l_domcfg=0 + [ -n "${DOMAIN_SPLIT:-}" ] && _l_domcfg=1 + _l_domdir=${DOMAINDIR:-${DOMAIN_LIST_DIR:-${BASE:-/etc/isp-split}/domains}} + for _l_df in "$_l_domdir"/*.txt; do + [ -f "$_l_df" ] && { + _l_domcfg=1 + break + } + done + _l_fexists=0 + _l_fmtime="" + _l_fserver=0 + if [ -f "$_l_frag" ]; then + _l_fexists=1 + _l_fmtime=$(stat -c %Y "$_l_frag" 2>/dev/null || printf '') + _l_fserver=$(grep -c '^server=' "$_l_frag" 2>/dev/null) || true + case "${_l_fserver:-}" in '' | *[!0-9]*) _l_fserver=0 ;; esac + fi + _l_stale=0 + if [ -f "$_l_frag" ] && [ -f "${DATA:-/nonexistent}/.stamp" ]; then + _l_stamp=$(stat -c %Y "${DATA}/.stamp" 2>/dev/null || printf '0') + [ "${_l_fmtime:-0}" -lt "${_l_stamp:-0}" ] 2>/dev/null && _l_stale=1 + fi + _l_dnsrules=$(_doc_have _dns_rule_count && _dns_rule_count 2>/dev/null) + [ -n "${_l_dnsrules:-}" ] || _l_dnsrules=$(nft -a list chain inet mwan3 mwan3_output 2>/dev/null | + grep -c 'comment "isp-split-dns"' 2>/dev/null) || true + case "${_l_dnsrules:-}" in '' | *[!0-9]*) _l_dnsrules=0 ;; esac + + DOC_C1_FACTS="\"mode\":$(_doc_jstr "${DNS_MODE:-unknown}"),\"listeners\":[$_l_listeners],\"hijack_rules\":[$_l_hijack_json],\"dnsmasq_instances\":$_l_inst,\"confdir\":{\"source\":$(_doc_jstr "${DOC_P3_SOURCE:-unknown}"),\"detail\":$(_doc_jstr "${DOC_P3_DETAIL:-}"),\"dirs\":$(_doc_jstr "${DOC_P3_DIRS:-}"),\"loaded_by_running_pid\":[$_l_loaded]},\"domain_split_configured\":$(_doc_jbool "$_l_domcfg"),\"fragment\":{\"path\":$(_doc_jstr "$_l_frag"),\"exists\":$(_doc_jbool "$_l_fexists"),\"mtime\":$(_doc_jstr "$_l_fmtime"),\"server_lines\":$_l_fserver,\"stale\":$(_doc_jbool "$_l_stale")},\"nft_dns_rules\":{\"chain\":\"inet mwan3 mwan3_output\",\"comment\":\"isp-split-dns\",\"count\":${_l_dnsrules:-0}}" + + _l_find="" + _doc_find() { # + _l_find="${_l_find}${_l_find:+,}{\"code\":$(_doc_jstr "$1"),\"verdict\":$(_doc_jstr "$2"),\"detail\":$(_doc_jstr "$3"),\"evidence\":$(_doc_jstr "$4")}" + } + + if [ "$_l_count" -eq 0 ] && [ "$_l_hijack_n" -eq 0 ]; then + DOC_C1_VERDICT=unknown + _doc_find "" unknown "53 端口上没有任何监听者,也没有透明劫持规则" "P1 + P4" + DOC_C1_ADVICE="可能是还没配 DNS,也可能是探针在裁剪过的固件上读不到;装好 dnsmasq 后重跑 doctor。" + elif [ "$_l_dnsq" = "0" ] && [ "$_l_hijack_n" -gt 0 ]; then + DOC_C1_VERDICT=unknown + _doc_find "DNS_TRANSPARENT_HIJACK" unknown "只有透明劫持规则、没有程序 bind :53(eBPF/redirect 型),无法静态判定归属" "P4 redirect/tproxy 扫描" + DOC_C1_ADVICE="这类接管常来自 eBPF 型 DNS 插件;把 DNS 模式改成 mark 后功能不再依赖 dnsmasq 片段。" + elif [ "$_l_dnsq" = "1" ] && [ "$_l_fexists" = "1" ] && [ "$_l_loaded_n" -gt 0 ]; then + if [ "$_l_inst" -gt 1 ]; then + DOC_C1_VERDICT=warn + _doc_find "DNS_MULTI_INSTANCE" warn "有 $_l_inst 个 dnsmasq 实例,片段可能只进了其中一个的加载目录" "P1 + /proc//cmdline" + else + DOC_C1_VERDICT=ok + _doc_find "" ok "dnsmasq 持有 :53,分线片段已被运行中的实例加载" "P1 + P3 + cmdline" + fi + [ "$_l_stale" = "1" ] && { + DOC_C1_VERDICT=warn + _doc_find "DNS_FRAGMENT_STALE" warn "片段比最新数据旧(sh -x:数据已更新但片段没重写)" "mtime(${_l_fmtime:-0}) < stamp" + } + elif [ "$_l_dnsq" = "1" ] && [ "$_l_fexists" = "0" ] && [ "$_l_domcfg" = "0" ]; then + # 没有域名分流配置:片段本来就不该存在,DNS_TABLE 的 nft 旁路与它无关 + DOC_C1_VERDICT=ok + _doc_find "DNS_FRAGMENT_NOT_NEEDED" ok "没有配置域名分流,dnsmasq 片段本来就不需要(DNS_TABLE 的 nft 旁路不受影响)" "DOMAIN_SPLIT 为空且 ${_l_domdir} 无清单" + elif [ "$_l_dnsq" = "1" ] && { [ "$_l_fexists" = "0" ] || [ "$_l_loaded_n" -eq 0 ]; }; then + DOC_C1_VERDICT=conflict + _doc_find "DNS_FRAGMENT_ORPHAN" conflict "dnsmasq 在跑,但分线片段不在它加载的目录里(写了没人读)" "P3 dirs=${DOC_P3_DIRS:-} fragment=$_l_frag" + elif [ "$_l_dnsq" = "0" ] && [ -n "$_l_third" ]; then + if [ "${DNS_MODE:-bind}" = "mark" ]; then + DOC_C1_VERDICT=warn + _doc_find "DNS_BYPASS_PARTIAL" warn "53 端口由第三方程序持有;mark 模式下 nft 侧仍生效,dnsmasq 片段作废" "P1 owner=$_l_third" + else + DOC_C1_VERDICT=conflict + _doc_find "DNS_BYPASS_DEAD" conflict "53 端口由第三方程序持有,bind 模式下 dnsmasq 片段与 nft 旁路都不生效" "P1 owner=$_l_third" + fi + else + DOC_C1_VERDICT=warn + _doc_find "" warn "没能把端口归属与片段加载状态对上(有第三方监听,dnsmasq 也在)" "P1 + P3" + fi + + [ "$_l_lan_dnsq" = "0" ] && [ "$_l_count" -gt 0 ] && [ "$_l_dnsq" = "1" ] && + _doc_find "DNS_LOOPBACK_ONLY" warn "dnsmasq 只监听回环地址,LAN 客户端的查询可能根本不经过它" "P1 addr 字段" + + # 建议按"根因 code"给:还没生成片段 ≠ 被别人接管,两者的下一步完全不同。 + _l_code="" + case "$_l_find" in + *'"code":"DNS_FRAGMENT_NOT_NEEDED"'*) _l_code=DNS_FRAGMENT_NOT_NEEDED ;; + *'"code":"DNS_FRAGMENT_ORPHAN"'*) _l_code=DNS_FRAGMENT_ORPHAN ;; + *'"code":"DNS_BYPASS_DEAD"'*) _l_code=DNS_BYPASS_DEAD ;; + *'"code":"DNS_BYPASS_PARTIAL"'*) _l_code=DNS_BYPASS_PARTIAL ;; + *'"code":"DNS_TRANSPARENT_HIJACK"'*) _l_code=DNS_TRANSPARENT_HIJACK ;; + *'"code":"DNS_MULTI_INSTANCE"'*) _l_code=DNS_MULTI_INSTANCE ;; + *'"code":"DNS_FRAGMENT_STALE"'*) _l_code=DNS_FRAGMENT_STALE ;; + esac + case "$_l_code" in + DNS_FRAGMENT_NOT_NEEDED) + DOC_C1_ADVICE="只用 IP 分流,不需要 dnsmasq 片段;DNS_TABLE 的按目标 IP 旁路仍在 nft 里生效。" ;; + DNS_FRAGMENT_ORPHAN) + DOC_C1_ADVICE="dnsmasq 已在跑但还没有分线片段:先 isp-split dns 生成片段;若解析器不是 dnsmasq,就把 DNS_MODE 改成 mark。" ;; + DNS_BYPASS_DEAD) + DOC_C1_ADVICE="把 DNS 模式改成 mark(isp-split conf set DNS_MODE mark)再 isp-split dns 重放规则;dnsmasq 片段可保留但不再起作用。" ;; + DNS_BYPASS_PARTIAL) + DOC_C1_ADVICE="nft 侧(mark 模式)不受影响;dnsmasq 片段对第三方解析器的查询无效,可以忽略。" ;; + DNS_TRANSPARENT_HIJACK) + DOC_C1_ADVICE="这类接管常来自 eBPF 型 DNS 插件;把 DNS 模式改成 mark 后功能不再依赖 dnsmasq 片段。" ;; + DNS_MULTI_INSTANCE) + DOC_C1_ADVICE="多个解析器实例时片段可能只进了其中一个的目录;确认 LAN 面那个实例也加载同一目录(isp-split dns 会写进探测到的第一个目录)。" ;; + DNS_FRAGMENT_STALE) + DOC_C1_ADVICE="数据更新后片段没重写:跑一次 isp-split dns 即可。" ;; + *) + case "$DOC_C1_VERDICT" in + ok) DOC_C1_ADVICE="DNS 由 dnsmasq 接管,分线片段已在它的加载目录里,域名分线正常生效。" ;; + unknown) DOC_C1_ADVICE="没能确认谁在应答 DNS;先确认 dnsmasq 在跑,或把 DNS 模式改成 mark 以摆脱对解析器类型的依赖。" ;; + *) DOC_C1_ADVICE="53 端口上有别的程序;建议把 DNS 模式改成 mark(isp-split conf set DNS_MODE mark),让 nft 侧承担分线。" ;; + esac + ;; + esac + DOC_C1_FIND="$_l_find" + DOC_C1_CONF=high + [ "${DOC_P2_RC:-0}" -ne 0 ] && DOC_C1_CONF=low + [ "${DOC_P1_RC:-0}" -ne 0 ] && DOC_C1_CONF=low +} + +# ---------------------------------------------------------------- 检查二:TUN 劫持 +doc_check_tun() { + DOC_C2_ID="tun_hijack" + DOC_C2_TITLE="TUN 设备与默认路由劫持" + DOC_C2_VERDICT=ok + DOC_C2_CONF=high + + doc_p7_tun + doc_p6_route + _doc_fakeip + + _t_tuns="" + _t_n=0 + _t_hit=0 + _t_hijack_dev="" + if [ "$DOC_RG_DEV" != "" ]; then + case " $(printf '%s\n' "$DOC_TUN" | tr '\n' ' ') " in + *" $DOC_RG_DEV "*) _t_hit=1 ;; + esac + fi + # 也认 "名字像 tun" 的设备(tun_flags 缺失但路由指向它) + case "$DOC_RG_DEV" in + Meta | tun[0-9]* | utun* | singbox* | clash* | mihomo*) _t_hit=1 ;; + esac + while IFS= read -r _t_dev; do + [ -n "$_t_dev" ] || continue + _t_n=$((_t_n + 1)) + _t_owner=$(_doc_tun_owner) + _t_ifidx=$(cat "/sys/class/net/$_t_dev/ifindex" 2>/dev/null || printf '') + set -- $(_doc_dev_bytes "$_t_dev") + _t_tuns="${_t_tuns}${_t_tuns:+,}{\"dev\":$(_doc_jstr "$_t_dev"),\"ifindex\":$(_doc_jstr "$_t_ifidx"),\"owner\":$(_doc_jstr "$_t_owner"),\"rx_bytes\":$(_doc_jstr "${1:--}"),\"tx_bytes\":$(_doc_jstr "${2:--}")}" + done < lookup N`(实测 pref 1005/1006)。 + # 这些不是第三方抢占;iif 在 mwan3 里有同名 interface 段就跳过。 + if [ "$_t_iif" != "-" ]; then + [ "$(uci -q get "mwan3.$_t_iif" 2>/dev/null)" = "interface" ] && continue + fi + _t_pre="${_t_pre}${_t_pre:+,}{\"pref\":$_t_pref,\"match\":$(_doc_jstr "$( [ "$_t_val" = "-" ] && printf 'from all' || printf 'fwmark %s/%s' "$_t_val" "$_t_mask")"),\"lookup\":$(_doc_jstr "$_t_lk"),\"iif\":$(_doc_jstr "$_t_iif"),\"suppress_prefixlength\":$(_doc_jstr "$_t_sup")}" + _t_pre_n=$((_t_pre_n + 1)) + done < mwan3 清位后这条规则失配 + if [ "$_r_m" -ne "$_M" ] && [ "$(( _r_v & _M ))" -ne 0 ]; then + _c3_starved="${_c3_starved}${_c3_starved:+,}{\"where\":\"ip rule\",\"pref\":$(_doc_jstr "$_r_pref"),\"value\":$(_doc_jstr "$_r_val"),\"lookup\":$(_doc_jstr "$_r_lk")}" + _c3_starved_n=$((_c3_starved_n + 1)) + fi + done </dev/null | head -1) + _c3_dev=$(printf '%s' "$_c3_rg" | sed -n 's/.*[[:space:]]dev[[:space:]]\([^[:space:]]*\).*/\1/p') + _c3_tab=$(printf '%s' "$_c3_rg" | sed -n 's/.*[[:space:]]table[[:space:]]\([^[:space:]]*\).*/\1/p') + _c3_route="{\"mark\":$(_doc_jstr "$_c3_mark_ex"),\"dev\":$(_doc_jstr "$_c3_dev"),\"table\":$(_doc_jstr "$_c3_tab")}" + else + _c3_route="null" + fi + + DOC_C3_FACTS="\"mmx_mask\":$(_doc_jstr "$DOC_MMX_MASK"),\"mmx_mask_source\":$(_doc_jstr "$DOC_MMX_SOURCE"),\"writers\":[$_c3_writers],\"readers\":[$_c3_readers],\"mwan3_min_pref\":$DOC_MINPREF,\"third_party_outside_mask\":[$_c3_outside],\"reader_starved\":[$_c3_starved],\"route_get_mark\":$_c3_route" + + _c3_find="" + _doc_find3() { + _c3_find="${_c3_find}${_c3_find:+,}{\"code\":$(_doc_jstr "$1"),\"verdict\":$(_doc_jstr "$2"),\"detail\":$(_doc_jstr "$3"),\"evidence\":$(_doc_jstr "$4")}" + } + + if [ "$DOC_P4B" != "1" ]; then + DOC_C3_VERDICT=unknown + _doc_find3 "" unknown "没有 table inet mwan3(mwan3-nft 未运行),无法判定位段占用" "P4b" + DOC_C3_ADVICE="先确认 mwan3(nft 版)在跑;只看 /etc/config/mwan3 无法判断运行态。" + elif [ "$_c3_wn" -eq 0 ]; then + DOC_C3_VERDICT=ok + _doc_find3 "" ok "规则表里没有任何 mark 写者" "P4" + elif [ "$_c3_ct" -gt 0 ]; then + DOC_C3_VERDICT=conflict + _doc_find3 "CTMARK_COLLISION" conflict "有程序在写 ct mark 的位段(OAF 类 DPI),改道语义会串" "P4 ct mark set" + elif [ "$_c3_overlap_unordered" -gt 0 ]; then + DOC_C3_VERDICT=conflict + _doc_find3 "MARK_OVERLAP_UNORDERED" conflict "有 $_c3_overlap_unordered 个第三方写者落在 mmx_mask 位段内,且没有排在 mwan3 之前的 rule" "P4 + P5" + elif [ "$_c3_overlap_ordered" -gt 0 ] || [ "$_c3_overlap" -gt 0 ]; then + DOC_C3_VERDICT=warn + _doc_find3 "MARK_OVERLAP_ORDERED" warn "有 $_c3_overlap 个第三方写者与位段有交集,但其 ip rule 排在 mwan3($DOC_MINPREF) 之前(先手),实际按它的规则走" "P4 + P5" + elif [ "$_c3_outside_n" -gt 0 ]; then + DOC_C3_VERDICT=ok + _doc_find3 "MARK_OUTSIDE" ok "第三方 mark 写者共 $_c3_outside_n 个,全部落在 mmx_mask 之外" "P4" + else + DOC_C3_VERDICT=ok + _doc_find3 "" ok "标记位段只有 mwan3 自己在用" "P4" + fi + if [ "$_c3_starved_n" -gt 0 ]; then + [ "$DOC_C3_VERDICT" = "ok" ] && DOC_C3_VERDICT=warn + _doc_find3 "MARK_READER_STARVED" warn "有 $_c3_starved_n 条精确 fwmark 的 ip rule 落在位段内:mwan3 无条件清位后它们不再匹配" "P4 + P5(COMPAT B1④)" + fi + [ "$DOC_MMX_MASK" != "$DOC_MASK_DEFAULT" ] && + _doc_find3 "MMX_MASK_CUSTOM" ok "mmx_mask=$(printf '%s' "$DOC_MMX_MASK") 不是默认 0x3F00,本报告的阈值已按它现算" "P11" + + case "$DOC_C3_VERDICT" in + ok) DOC_C3_ADVICE="路由标记位段只有 mwan3 在用(或第三方全部在位段之外),分流选路正常。" ;; + warn) DOC_C3_ADVICE="无需改动即可用;若要彻底消除歧义,把代理的 fwmark 移到 mmx_mask 之外(如 0x80000 段)。" ;; + conflict) DOC_C3_ADVICE="检测到有程序直接改写与 mwan3 相同的标记位,分流可能选错线;建议把它的标记值改到位段之外,或把它排到 mwan3 之后。" ;; + unknown) DOC_C3_ADVICE="没能读取当前 nft 规则,无法判断标记位是否被占用。" ;; + esac + DOC_C3_FIND="$_c3_find" +} + +# ---------------------------------------------------------------- 检查四:统计口径 +doc_check_offload() { + DOC_C4_ID="offload_accounting" + DOC_C4_TITLE="flow offload / turboacc 对统计口径的影响" + DOC_C4_VERDICT=ok + DOC_C4_CONF=high + + doc_p8_offload + doc_p9_conntrack + + _c4_devs="" + _c4_truth="" + # 只有默认路由设备与 lan 设备两个取样点:足够交叉显示,不引入额外开销 + _c4_rg=$DOC_RG_DEV + [ -n "${_c4_rg:-}" ] || _c4_rg=$(_doc_have ip && ip -4 route show default 2>/dev/null | + sed -n 's/.*[[:space:]]dev[[:space:]]\([^[:space:]]*\).*/\1/p' | head -1) + for _c4_d in $_c4_rg $(printf '%s\n' "$DOC_DEF_V4" | sed -n 's/.*[[:space:]]dev[[:space:]]\([^[:space:]]*\).*/\1/p'); do + [ -n "${_c4_d:-}" ] || continue + case " $_c4_devs " in + *" $_c4_d "*) continue ;; + esac + set -- $(_doc_dev_bytes "$_c4_d") + [ "$1" = "-" ] && continue + _c4_devs="$_c4_devs $_c4_d" + _c4_truth="${_c4_truth}${_c4_truth:+,}{\"dev\":$(_doc_jstr "$_c4_d"),\"rx_bytes\":$(_doc_jstr "$1"),\"tx_bytes\":$(_doc_jstr "$2")}" + done + + DOC_C4_FACTS="\"flow_offloading\":$(_doc_jstr "$DOC_OFF_SW"),\"flow_offloading_hw\":$(_doc_jstr "$DOC_OFF_HW"),\"vocabulary\":{\"flowtable_chains\":$DOC_OFF_FLOWTABLE,\"flow_add_rules\":$DOC_OFF_FLOWADD},\"owner\":{\"turboacc_uci\":$(_doc_jbool "$DOC_OFF_TURBOACC"),\"vendor_accel\":$(_doc_jstr "$DOC_OFF_VENDOR")},\"conntrack\":{\"count\":$(_doc_jstr "$DOC_CT_COUNT"),\"max\":$(_doc_jstr "$DOC_CT_MAX"),\"acct\":$(_doc_jstr "$DOC_CT_ACCT"),\"tcp_established\":$(_doc_jstr "$DOC_CT_EST"),\"tuned_by\":$(_doc_jstr "$DOC_CT_TUNED")},\"truth_counters\":[${_c4_truth}]" + + _c4_find="" + _doc_find4() { + _c4_find="${_c4_find}${_c4_find:+,}{\"code\":$(_doc_jstr "$1"),\"verdict\":$(_doc_jstr "$2"),\"detail\":$(_doc_jstr "$3"),\"evidence\":$(_doc_jstr "$4")}" + } + if [ "$DOC_OFF_HW" = "1" ] || [ -n "${DOC_OFF_VENDOR:-}" ]; then + DOC_C4_VERDICT=conflict + DOC_C4_CONF=medium + _doc_find4 "OFFLOAD_HW_ROUTING" conflict "已开启硬件流量加速:除了统计偏差,mwan3 的选路语义也可能失效" "uci flow_offloading_hw=$DOC_OFF_HW / vendor=$DOC_OFF_VENDOR" + elif [ "$DOC_OFF_SW" = "1" ]; then + DOC_C4_VERDICT=warn + _doc_find4 "OFFLOAD_SW_COUNTERS" warn "软件 flow offload 生效:已建流的后续包不走 mwan3_rules,命中包数只反映建连首包" "uci flow_offloading=1;flowtable=$DOC_OFF_FLOWTABLE flow_add=$DOC_OFF_FLOWADD" + elif [ "$DOC_OFF_FLOWADD" -gt 0 ]; then + DOC_C4_VERDICT=warn + _doc_find4 "OFFLOAD_STALE" warn "uci 里 offload 已关,但规则表里仍有 flow add 规则(改了没 reload)" "P4 计数=$DOC_OFF_FLOWADD" + else + DOC_C4_VERDICT=ok + _doc_find4 "" ok "没有开启流量加速,页面上的命中包数与实际流量口径一致" "P8" + fi + + case "$DOC_C4_VERDICT" in + ok) DOC_C4_ADVICE="无需处理;命中包数可以直接当参考。" ;; + warn) DOC_C4_ADVICE="统计口径问题,不影响分流选路:要看真实速率请读 /sys/class/net//statistics/。不要为了数字关掉 offload。" ;; + conflict) DOC_C4_ADVICE="建议先关掉硬件加速(firewall.@defaults[0].flow_offloading_hw=0)并 fw4 reload,观察分流是否恢复;这台固件也可能默认就开着,不是你的操作。" ;; + esac + DOC_C4_FIND="$_c4_find" +} + +# ---------------------------------------------------------------- 检查五:写者清单 +doc_check_writers() { + DOC_C5_ID="mark_writers" + DOC_C5_TITLE="第三方 mark 写者清单与 mwan3 归属" + DOC_C5_VERDICT=ok + DOC_C5_CONF=high + + if [ -z "${DOC_W:-}" ] && [ "${DOC_P4_RC:-0}" -ne 0 ]; then + DOC_C5_VERDICT=unknown + DOC_C5_CONF=low + DOC_C5_FACTS="\"counts\":{},\"writers\":[],\"foreign_chains_in_mwan3\":[],\"legacy_writers\":[]" + DOC_C5_FIND="" + DOC_C5_ADVICE="没能读取 nft 规则表,无法列出标记写者。" + return 0 + fi + + _c5_total=0 + _c5_mw=0 + _c5_own=0 + _c5_third=0 + _c5_inmask=0 + _c5_out=0 + _c5_legacy=0 + _c5_third_inmask=0 + _c5_list="" + while IFS=" " read -r _w_fam _w_tab _w_chn _w_hnd _w_fld _w_form _w_msk _w_val _w_cm; do + [ -n "${_w_fam:-}" ] || continue + _c5_total=$((_c5_total + 1)) + _w_owner=$(_doc_owner_of "$_w_fam" "$_w_tab" "$_w_chn" "$_w_cm") + _w_owned=0 + _w_cleared=0 + case "$_w_form" in + set) + _doc_hx "$_w_val" + _w_owned=$(( HEX & DOC_MMX_MASK_DEC )) + ;; + mask_set | mask_or) + _doc_hx "$_w_val" + _w_owned=$(( HEX & DOC_MMX_MASK_DEC )) + _doc_hx "$_w_msk" + _w_cleared=$(( (~HEX) & DOC_MMX_MASK_DEC )) + ;; + mask_clear) + _doc_hx "$_w_msk" + _w_cleared=$(( (~HEX) & DOC_MMX_MASK_DEC )) + ;; + esac + _w_touched=$(( _w_cleared | _w_owned )) + _w_prod=$(_doc_guess_product "$_w_fam" "$_w_tab" "$_w_chn") + case "$_w_owner" in + mwan3) _c5_mw=$((_c5_mw + 1)) ;; + isp-split) _c5_own=$((_c5_own + 1)) ;; + legacy) _c5_legacy=$((_c5_legacy + 1)) ;; + *) _c5_third=$((_c5_third + 1)) ;; + esac + if [ "$(( _w_touched & DOC_MMX_MASK_DEC ))" -ne 0 ]; then + _c5_inmask=$((_c5_inmask + 1)) + case "$_w_owner" in + third_party | legacy) _c5_third_inmask=$((_c5_third_inmask + 1)) ;; + esac + else + _c5_out=$((_c5_out + 1)) + fi + # JSON 体积上限:清单只列前 40 条(真实机器上写者通常 < 10) + if [ "$_c5_total" -le 40 ]; then + _c5_list="${_c5_list}${_c5_list:+,}{\"owner\":$(_doc_jstr "$_w_owner"),\"product_guess\":$(_doc_jstr "$_w_prod"),\"fam\":$(_doc_jstr "$_w_fam"),\"table\":$(_doc_jstr "$_w_tab"),\"chain\":$(_doc_jstr "$_w_chn"),\"handle\":$(_doc_jstr "$_w_hnd"),\"field\":$(_doc_jstr "$_w_fld"),\"form\":$(_doc_jstr "$_w_form"),\"mask\":$(_doc_jstr "$([ "$_w_msk" = "-" ] && printf '' || printf '%s' "$_w_msk")"),\"value\":$(_doc_jstr "$([ "$_w_val" = "-" ] && printf '' || printf '%s' "$_w_val")"),\"touched\":$(_doc_jstr "$(_doc_hxstr "$_w_touched")")}" + fi + done < green_with_version_drift(展示为绿但带角标)。 +_doc_wl_baseline() { + case "$1" in + tailscale) printf '1.78' ;; + ddns-go) printf '5.7' ;; + *) printf '' ;; + esac +} + +# 产品是否装了:二进制 / uci / 配置文件,任一命中即算 +_doc_wl_installed() { + case "$1" in + tailscale) + _doc_have tailscale || _doc_have tailscaled || [ -f /etc/config/tailscale ] || [ -e /sys/class/net/tailscale0 ] + ;; + ntopng) + _doc_have ntopng || [ -f /etc/config/ntopng ] || [ -d /etc/ntopng ] + ;; + ddns-go) + _doc_have ddns-go || [ -f /etc/config/ddns-go ] || [ -f /etc/ddns-go/config.yaml ] + ;; + *) return 1 ;; + esac +} + +_doc_wl_running() { + case "$1" in + tailscale) pidof tailscaled >/dev/null 2>&1 ;; + ntopng) pidof ntopng >/dev/null 2>&1 ;; + ddns-go) pidof ddns-go >/dev/null 2>&1 ;; + *) return 1 ;; + esac +} + +_doc_wl_comm() { + case "$1" in + tailscale) printf 'tailscaled' ;; + ntopng) printf 'ntopng' ;; + ddns-go) printf 'ddns-go' ;; + *) printf '' ;; + esac +} + +doc_check_whitelist() { + DOC_C6_ID="coexist_whitelist" + DOC_C6_TITLE="已实测共存白名单(tailscale / ntopng / ddns-go)" + DOC_C6_VERDICT=ok + DOC_C6_CONF=medium + + _c6_products="" + _c6_find="" + _doc_find6() { + _c6_find="${_c6_find}${_c6_find:+,}{\"code\":$(_doc_jstr "$1"),\"verdict\":$(_doc_jstr "$2"),\"detail\":$(_doc_jstr "$3"),\"evidence\":$(_doc_jstr "$4")}" + } + _c6_warn="" + _c6_worst=ok + + for _p in tailscale ntopng ddns-go; do + _inst=0 + _doc_wl_installed "$_p" && _inst=1 + _run=0 + _doc_wl_running "$_p" && _run=1 + _ver="" + [ "$_inst" = "1" ] && _ver=$(doc_pkg_version "$_p") + _base=$(_doc_wl_baseline "$_p") + _vib=null + if [ -n "$_base" ] && [ -n "${_ver:-}" ]; then + case "$_ver" in + "$_base"*) _vib=1 ;; + *) _vib=0 ;; + esac + fi + # G1:不占 LAN 面 :53 + _g1=1 + if [ "$_run" = "1" ]; then + case "${DOC_C1_FACTS:-}" in + *"\"comm\":\"$(_doc_wl_comm "$_p")\""*) _g1=0 ;; + esac + fi + # G2:没有覆盖位段的写者 + _g2=1 + case " ${DOC_C3_OVERLAP_NAMES:-} " in + *" $_p "*) _g2=0 ;; + esac + # G3:没有 pref 更小且会改默认路径的 rule(tailscale 的精确 fwmark 例外) + _g3=1 + case "$_p" in + tailscale) + # fwmark 0x80000 与 mmx 位段无交集:命中也不例外 + _g3=1 + ;; + *) + case " ${DOC_C3_OVERLAP_NAMES:-} " in + *" $_p "*) _g3=0 ;; + esac + ;; + esac + # G4:不改 offload / 默认路由 + _g4=1 + [ "${DOC_OFF_TURBOACC:-0}" = "1" ] && [ "$_p" = "turboacc" ] && _g4=0 + # G5:不往 inet mwan3 塞链 + _g5=1 + if [ "${DOC_C5_FACTS:-}" != "" ]; then + case "$DOC_C5_FACTS" in + *"foreign_chains_in_mwan3\":[\"$_p"*) _g5=0 ;; + esac + fi + # G6:版本在基线内 + _g6=null + case "$_vib" in + 1) _g6=1 ;; + 0) _g6=0 ;; + esac + + _state=not_running + if [ "$_inst" = "0" ]; then + _state=not_installed + elif [ "$_run" = "0" ]; then + _state=not_running + elif [ "$_g1" = "0" ] || [ "$_g2" = "0" ] || [ "$_g3" = "0" ] || [ "$_g4" = "0" ] || [ "$_g5" = "0" ]; then + _state=conflict + elif [ "$_g6" = "0" ]; then + _state=green_with_version_drift + else + _state=green + fi + + # 即使 GREEN 也要说的事(不升 verdict);没装就不提 + case "$_inst" in + 0) _p_skip_warn=1 ;; + *) _p_skip_warn=0 ;; + esac + [ "$_p_skip_warn" = "1" ] || case "$_p" in + tailscale) + _en=$(uci -q get tailscale.settings.exit_node 2>/dev/null) || _en="" + if [ -n "${_en:-}" ] && [ "$_en" != "0" ]; then + _c6_warn="${_c6_warn}${_c6_warn:+,}{\"product\":\"tailscale\",\"code\":\"EXIT_NODE_HIJACK\",\"detail\":\"uci tailscale.settings.exit_node=$_en:全部上网流量走 tailnet,分流彻底不生效\",\"verdict\":\"warn\"}" + [ "$_c6_worst" = "ok" ] && _c6_worst=warn + fi + ;; + ntopng) + if [ "${DOC_CT_ACCT:-}" = "1" ]; then + _c6_warn="${_c6_warn}${_c6_warn:+,}{\"product\":\"ntopng\",\"code\":\"CT_ACCT_BYTES\",\"detail\":\"nf_conntrack_acct=1:ntopng 的连接级字节与 nft 规则计数不同源\",\"verdict\":\"warn\"}" + [ "$_c6_worst" = "ok" ] && _c6_worst=warn + fi + ;; + ddns-go) + _c6_warn="${_c6_warn}${_c6_warn:+,}{\"product\":\"ddns-go\",\"code\":\"NO_IFACE_BIND\",\"detail\":\"ddns-go 不支持绑定出接口:无法按线路取出口 IP(需要按线测出口 IP 请用本工具的探测)\",\"verdict\":\"warn\"}" + [ "$_c6_worst" = "ok" ] && _c6_worst=warn + ;; + esac + + case "$_state" in + conflict) [ "$_c6_worst" != "conflict" ] && _c6_worst=conflict ;; + green_with_version_drift) [ "$_c6_worst" = "ok" ] && _c6_worst=warn ;; + esac + + _c6_products="${_c6_products}${_c6_products:+,}{\"name\":$(_doc_jstr "$_p"),\"installed\":$(_doc_jbool "$_inst"),\"running\":$(_doc_jbool "$_run"),\"version\":$(_doc_jstr "$_ver"),\"version_in_baseline\":$_vib,\"gates\":{\"G1\":$(_doc_jbool "$_g1"),\"G2\":$(_doc_jbool "$_g2"),\"G3\":$(_doc_jbool "$_g3"),\"G4\":$(_doc_jbool "$_g4"),\"G5\":$(_doc_jbool "$_g5"),\"G6\":$_g6},\"state\":$(_doc_jstr "$_state")}" + done + + DOC_C6_FACTS="\"products\":[$_c6_products],\"warnings_kept\":[$_c6_warn]" + if [ "$_c6_worst" = "ok" ]; then + _doc_find6 "WHITELIST_GREEN" ok "白名单产品与实测基线一致,五条机制门槛全过" "P7 + P5 + P10" + else + _doc_find6 "WHITELIST_REVIEW" "$_c6_worst" "白名单里有产品需要复核(见 products[].gates 与 warnings_kept)" "P7 + P5 + P10" + fi + DOC_C6_VERDICT=$_c6_worst + DOC_C6_ADVICE="可以共存;若发现分流不生效,先确认没有开 tailscale 的 exit node、也没有别的代理在改默认路由。" + DOC_C6_FIND="$_c6_find" +} + +# ---------------------------------------------------------------- 汇总 / 输出 + +doc_summary() { + DOC_SUM_OK=0 + DOC_SUM_WARN=0 + DOC_SUM_CONFLICT=0 + DOC_SUM_UNKNOWN=0 + # 只统计本次真的跑了的检查:--profile=page 只跑 1/4/6,把没跑的 2/3/5 + # 算成 unknown 会让页面快照永远显示"三项不知道"(实测踩过)。 + for _s in ${DOC_RUN_SET:-1 2 3 4 5 6}; do + case "$_s" in + 1) _v=${DOC_C1_VERDICT:-unknown} ;; + 2) _v=${DOC_C2_VERDICT:-unknown} ;; + 3) _v=${DOC_C3_VERDICT:-unknown} ;; + 4) _v=${DOC_C4_VERDICT:-unknown} ;; + 5) _v=${DOC_C5_VERDICT:-unknown} ;; + 6) _v=${DOC_C6_VERDICT:-unknown} ;; + esac + case "$_v" in + ok) DOC_SUM_OK=$((DOC_SUM_OK + 1)) ;; + warn) DOC_SUM_WARN=$((DOC_SUM_WARN + 1)) ;; + conflict) DOC_SUM_CONFLICT=$((DOC_SUM_CONFLICT + 1)) ;; + *) DOC_SUM_UNKNOWN=$((DOC_SUM_UNKNOWN + 1)) ;; + esac + done + # 偏序:conflict > warn > unknown > ok + if [ "$DOC_SUM_CONFLICT" -gt 0 ]; then + DOC_SUM_WORST=conflict + elif [ "$DOC_SUM_WARN" -gt 0 ]; then + DOC_SUM_WORST=warn + elif [ "$DOC_SUM_UNKNOWN" -gt 0 ]; then + DOC_SUM_WORST=unknown + else + DOC_SUM_WORST=ok + fi + + # gate:能不能继续 wire/apply。主机前提与兼容矩阵都算进来。 + DOC_GATE_CAN_WIRE=1 + DOC_GATE_REASONS="" + DOC_GATE_REASONS_TEXT="" + _gate_add() { + DOC_GATE_CAN_WIRE=0 + DOC_GATE_REASONS="${DOC_GATE_REASONS}${DOC_GATE_REASONS:+,}$(_doc_jstr "$1")" + DOC_GATE_REASONS_TEXT="${DOC_GATE_REASONS_TEXT}${DOC_GATE_REASONS_TEXT:+, }$1" + } + doc_mwan3_installed || _gate_add "MWAN3_NOT_INSTALLED" + if doc_mwan3_installed && ! doc_mwan3_is_nft; then + _gate_add "MWAN3_NOT_NFT" + fi + [ "${DOC_NFT_OK:-0}" = "1" ] || _gate_add "NFT_MISSING_OR_OLD" + [ "${DOC_P4B:-0}" = "1" ] || _gate_add "MWAN3_TABLE_MISSING" + [ -n "${DOC_BINS_MISSING:-}" ] && _gate_add "BIN_MISSING:${DOC_BINS_MISSING}" + [ "${DOC_MEM_VERDICT:-ok}" = "warn" ] && _gate_add "MEM_LOW" + [ "$DOC_SUM_CONFLICT" -gt 0 ] && _gate_add "CHECK_CONFLICT" + + # headline:第一条 conflict 的 detail,否则 worst 的第一条 warn + DOC_SUM_HEADLINE="" + if [ "$DOC_SUM_CONFLICT" -gt 0 ]; then + DOC_SUM_HEADLINE=$(_doc_first_detail conflict) + elif [ "$DOC_SUM_WARN" -gt 0 ]; then + DOC_SUM_HEADLINE=$(_doc_first_detail warn) + fi + [ -n "${DOC_SUM_HEADLINE:-}" ] || DOC_SUM_HEADLINE="没有发现会让分流静默失效的冲突" +} + +_doc_first_detail() { # -> 第一条匹配的 finding detail + _dfv_want=$1 + for _dfv_i in 1 2 3 4 5 6; do + case "$_dfv_i" in + 1) _dfv_f=${DOC_C1_FIND:-} ;; + 2) _dfv_f=${DOC_C2_FIND:-} ;; + 3) _dfv_f=${DOC_C3_FIND:-} ;; + 4) _dfv_f=${DOC_C4_FIND:-} ;; + 5) _dfv_f=${DOC_C5_FIND:-} ;; + 6) _dfv_f=${DOC_C6_FIND:-} ;; + esac + _dfv_d=$(printf '%s\n' "$_dfv_f" | + sed -n "s/.*\"verdict\":\"$_dfv_want\",\"detail\":\"\([^\"]*\)\".*/\1/p" | head -1) + [ -n "${_dfv_d:-}" ] && { + printf '%s' "$_dfv_d" + return 0 + } + done + return 0 +} + +_doc_check_json() { # + case "$1" in + 1) _cj_id=${DOC_C1_ID} _cj_title=${DOC_C1_TITLE} _cj_v=${DOC_C1_VERDICT} _cj_c=${DOC_C1_CONF} _cj_facts=${DOC_C1_FACTS} _cj_find=${DOC_C1_FIND} _cj_adv=${DOC_C1_ADVICE} ;; + 2) _cj_id=${DOC_C2_ID} _cj_title=${DOC_C2_TITLE} _cj_v=${DOC_C2_VERDICT} _cj_c=${DOC_C2_CONF} _cj_facts=${DOC_C2_FACTS} _cj_find=${DOC_C2_FIND} _cj_adv=${DOC_C2_ADVICE} ;; + 3) _cj_id=${DOC_C3_ID} _cj_title=${DOC_C3_TITLE} _cj_v=${DOC_C3_VERDICT} _cj_c=${DOC_C3_CONF} _cj_facts=${DOC_C3_FACTS} _cj_find=${DOC_C3_FIND} _cj_adv=${DOC_C3_ADVICE} ;; + 4) _cj_id=${DOC_C4_ID} _cj_title=${DOC_C4_TITLE} _cj_v=${DOC_C4_VERDICT} _cj_c=${DOC_C4_CONF} _cj_facts=${DOC_C4_FACTS} _cj_find=${DOC_C4_FIND} _cj_adv=${DOC_C4_ADVICE} ;; + 5) _cj_id=${DOC_C5_ID} _cj_title=${DOC_C5_TITLE} _cj_v=${DOC_C5_VERDICT} _cj_c=${DOC_C5_CONF} _cj_facts=${DOC_C5_FACTS} _cj_find=${DOC_C5_FIND} _cj_adv=${DOC_C5_ADVICE} ;; + 6) _cj_id=${DOC_C6_ID} _cj_title=${DOC_C6_TITLE} _cj_v=${DOC_C6_VERDICT} _cj_c=${DOC_C6_CONF} _cj_facts=${DOC_C6_FACTS} _cj_find=${DOC_C6_FIND} _cj_adv=${DOC_C6_ADVICE} ;; + esac + printf '{"id":"%s","title":"%s","verdict":"%s","status":"%s","confidence":"%s","facts":{%s},"findings":[%s],"advice":"%s"}' \ + "$(_doc_esc "$_cj_id")" "$(_doc_esc "$_cj_title")" "$_cj_v" \ + "$([ "$_cj_v" = "conflict" ] && printf fail || printf '%s' "$_cj_v")" \ + "$_cj_c" "${_cj_facts:-}" "${_cj_find:-}" "$(_doc_esc "${_cj_adv:-}")" +} + +doc_json() { + _dj_checks="" + for _dj_i in $DOC_RUN_SET; do + _dj_checks="${_dj_checks}${_dj_checks:+,}$(_doc_check_json "$_dj_i")" + done + printf '{"schema":%s,"tool":{"name":"isp-split","version":"%s"},"generated":%s,"generated_text":"%s","profile":"%s","host":{%s},"checks":[%s],"summary":{"ok":%s,"warn":%s,"conflict":%s,"unknown":%s,"worst":"%s","blocking":%s,"headline":"%s"},"gate":{"can_wire":%s,"reasons":[%s]},"probe_errors":[%s],"ms":%s}\n' \ + "$DOC_SCHEMA" "$(_doc_esc "${ISP_VERSION:-0.0.0}")" "$DOC_GEN" "$(_doc_esc "$DOC_GEN_TEXT")" \ + "$DOC_PROFILE" "$DOC_HOST_JSON" "$_dj_checks" \ + "$DOC_SUM_OK" "$DOC_SUM_WARN" "$DOC_SUM_CONFLICT" "$DOC_SUM_UNKNOWN" "$DOC_SUM_WORST" \ + "$(_doc_jbool "$([ "$DOC_SUM_WORST" = "conflict" ] && printf 1 || printf 0)")" \ + "$(_doc_esc "$DOC_SUM_HEADLINE")" "$(_doc_jbool "$DOC_GATE_CAN_WIRE")" "${DOC_GATE_REASONS:-}" \ + "${DOC_PROBE_ERR:-}" "$(( $(_doc_now_ms) - ${DOC_T0:-0} ))" +} + +DOC_HOST_JSON="" +doc_host_json() { + DOC_HOST_JSON="\"openwrt\":{\"id\":$(_doc_jstr "${DOC_FW_ID:-}"),\"release\":$(_doc_jstr "${DOC_FW_RELEASE:-}"),\"target\":$(_doc_jstr "${DOC_FW_TARGET:-}"),\"rev\":$(_doc_jstr "${DOC_FW_REV:-}")},\"kernel\":$(_doc_jstr "$(uname -r 2>/dev/null)"),\"arch\":$(_doc_jstr "${DOC_ARCH:-}"),\"nft\":$(_doc_jstr "${DOC_NFT_VERSION:-}"),\"pkgmgr\":$(_doc_jstr "$(_doc_have apk && printf apk || { _doc_have opkg && printf opkg || printf none; })"),\"mwan3\":{\"installed\":$(_doc_jbool "$(doc_mwan3_installed && printf 1 || printf 0)"),\"version\":$(_doc_jstr "$(doc_pkg_version mwan3)"),\"nft\":$(_doc_jbool "$(doc_mwan3_is_nft && printf 1 || printf 0)"),\"table\":$(_doc_jbool "${DOC_P4B:-0}"),\"mmx_mask\":$(_doc_jstr "${DOC_MMX_MASK:-}"),\"mmx_mask_source\":$(_doc_jstr "${DOC_MMX_SOURCE:-}")},\"mem\":{\"total_kb\":${DOC_MEM_TOTAL_KB:-0},\"avail_kb\":${DOC_MEM_AVAIL_KB:-0},\"verdict\":$(_doc_jstr "${DOC_MEM_VERDICT:-unknown}")},\"bins\":{${DOC_BINS_JSON:-}},\"root\":$(_doc_jbool "$([ "$(id -u 2>/dev/null)" = "0" ] && printf 1 || printf 0)")" +} + +_doc_text_check() { # + _dt_n=$1 + _dt_i=$2 + _dt_tot=$3 + case "$_dt_n" in + 1) _dt_v=${DOC_C1_VERDICT} _dt_t=${DOC_C1_TITLE} _dt_c=${DOC_C1_CONF} _dt_a=${DOC_C1_ADVICE} _dt_f=${DOC_C1_FIND} _dt_fa=${DOC_C1_FACTS} ;; + 2) _dt_v=${DOC_C2_VERDICT} _dt_t=${DOC_C2_TITLE} _dt_c=${DOC_C2_CONF} _dt_a=${DOC_C2_ADVICE} _dt_f=${DOC_C2_FIND} _dt_fa=${DOC_C2_FACTS} ;; + 3) _dt_v=${DOC_C3_VERDICT} _dt_t=${DOC_C3_TITLE} _dt_c=${DOC_C3_CONF} _dt_a=${DOC_C3_ADVICE} _dt_f=${DOC_C3_FIND} _dt_fa=${DOC_C3_FACTS} ;; + 4) _dt_v=${DOC_C4_VERDICT} _dt_t=${DOC_C4_TITLE} _dt_c=${DOC_C4_CONF} _dt_a=${DOC_C4_ADVICE} _dt_f=${DOC_C4_FIND} _dt_fa=${DOC_C4_FACTS} ;; + 5) _dt_v=${DOC_C5_VERDICT} _dt_t=${DOC_C5_TITLE} _dt_c=${DOC_C5_CONF} _dt_a=${DOC_C5_ADVICE} _dt_f=${DOC_C5_FIND} _dt_fa=${DOC_C5_FACTS} ;; + 6) _dt_v=${DOC_C6_VERDICT} _dt_t=${DOC_C6_TITLE} _dt_c=${DOC_C6_CONF} _dt_a=${DOC_C6_ADVICE} _dt_f=${DOC_C6_FIND} _dt_fa=${DOC_C6_FACTS} ;; + esac + printf '\n[%s/%s] %s\n' "$_dt_i" "$_dt_tot" "$_dt_t" + printf ' 状态: %s 置信度: %s\n' "$_dt_v" "$_dt_c" + # detail 逐条(同一 check 的多条 finding 各一行) + printf '%s\n' "$_dt_f" | sed 's/},{"code"/}\n{"code"/g' | while IFS= read -r _dt_line; do + _dt_code=$(printf '%s' "$_dt_line" | sed -n 's/.*"code":"\([^"]*\)".*/\1/p') + _dt_det=$(printf '%s' "$_dt_line" | sed -n 's/.*"detail":"\([^"]*\)".*/\1/p') + [ -n "$_dt_det" ] || continue + if [ -n "$_dt_code" ]; then + printf ' · [%s] %s\n' "$_dt_code" "$_dt_det" + else + printf ' · %s\n' "$_dt_det" + fi + done + printf ' 建议: %s\n' "$_dt_a" +} + +doc_text() { + printf '== 环境体检(doctor)%s %s\n' "$(_doc_esc "${ISP_VERSION:-0.0.0}")" "$DOC_GEN_TEXT" + _dt_mv=$(doc_pkg_version mwan3) + [ -n "${_dt_mv:-}" ] || _dt_mv=- + printf '主机: %s %s %s 内核 %s nft %s mwan3 %s(%s) mmx_mask %s(%s)\n' \ + "${DOC_FW_ID:-?}" "${DOC_FW_RELEASE:-?}" "${DOC_FW_TARGET:-?}" \ + "$(uname -r 2>/dev/null)" "${DOC_NFT_VERSION:-?}" \ + "$_dt_mv" \ + "$(doc_mwan3_is_nft && printf 'nft' || printf 'iptables/未知')" \ + "${DOC_MMX_MASK:-?}" "${DOC_MMX_SOURCE:-?}" + printf '内存: 共 %s kB / 可用 %s kB(%s)\n' "${DOC_MEM_TOTAL_KB:-0}" "${DOC_MEM_AVAIL_KB:-0}" "${DOC_MEM_VERDICT:-unknown}" + [ -n "${DOC_BINS_MISSING:-}" ] && printf '缺少必需命令: %s\n' "$DOC_BINS_MISSING" + _dt_i=0 + for _dt_n in $DOC_RUN_SET; do + _dt_i=$((_dt_i + 1)) + _doc_text_check "$_dt_n" "$_dt_i" "$DOC_RUN_COUNT" + done + printf '\n== 汇总: ok %s warn %s conflict %s unknown %s(最差: %s)\n' \ + "$DOC_SUM_OK" "$DOC_SUM_WARN" "$DOC_SUM_CONFLICT" "$DOC_SUM_UNKNOWN" "$DOC_SUM_WORST" + printf ' 结论: %s\n' "$DOC_SUM_HEADLINE" + if [ "$DOC_GATE_CAN_WIRE" = "1" ]; then + printf ' wire/apply 门: 可以继续\n' + else + printf ' wire/apply 门: 阻断(%s)\n' "${DOC_GATE_REASONS_TEXT:-}" + fi + [ -n "${DOC_PROBE_ERR:-}" ] && printf ' 探针降级: %s\n' "$DOC_PROBE_ERR" +} + +# ---------------------------------------------------------------- 入口 + +doc_main() { + DOC_T0=$(_doc_now_ms) + DOC_GEN=$(date +%s 2>/dev/null || printf '0') + DOC_GEN_TEXT=$(_doc_ts_text) + DOC_PROBES="" + DOC_PROBE_ERR="" + DOC_PROFILE=full + DOC_JSON_MODE=0 + DOC_STRICT=0 + while [ $# -gt 0 ]; do + case "$1" in + --json) DOC_JSON_MODE=1 ;; + --strict) DOC_STRICT=1 ;; + --deep) DOC_PROFILE=deep ;; + --profile=*) + case "${1#--profile=}" in + page | full | deep) DOC_PROFILE=${1#--profile=} ;; + *) printf '未知 profile: %s(可用 page|full|deep)\n' "${1#--profile=}" >&2 ;; + esac + ;; + -h | --help) + cat <<'DOCHELP' +用法: isp-split doctor [--json] [--profile=page|full|deep] [--strict] + 只读环境体检:六项兼容矩阵 + 主机前提。不产生业务流量、不改任何配置。 + --json stdout 只有一份 JSON(人类可读的提示走 stderr) + --profile=page 只跑检查 1/4/6,汇总也只统计这 3 项(供页面快照; + C1/C4 仍需读一次规则表,不是零成本) + --profile=deep 预留:满载时降级为 full。1s 采样(计数冻结 / TUN 流量) + 尚未实现,当前与 full 等价 + --strict unknown 也算"不能继续用"(退出码 1) +退出码: 0 可以继续 / 1 有保留(warn) / 2 有东西会静默失效(conflict) +DOCHELP + return 0 + ;; + *) + printf '未知参数: %s(用法: isp-split doctor [--json] [--profile=...] [--strict])\n' "$1" >&2 + # 用法错误必须非 0:以前返回 0 会让 `doctor --typo` 静默"成功"且一项没跑。 + return 2 + ;; + esac + shift + done + + case "$DOC_PROFILE" in + page) DOC_RUN_SET="1 4 6" ;; + *) DOC_RUN_SET="1 2 3 4 5 6" ;; + esac + DOC_RUN_COUNT=0 + for _x in $DOC_RUN_SET; do DOC_RUN_COUNT=$((DOC_RUN_COUNT + 1)); done + + # deep 目前与 full 等价(1s 采样未实现);满载时直接降级,避免体检本身把路由器压垮。 + if [ "$DOC_PROFILE" = "deep" ]; then + _load=$(awk '{ print $1 }' /proc/loadavg 2>/dev/null || printf '0') + _ncpu=$(grep -c '^processor' /proc/cpuinfo 2>/dev/null) || true + case "${_load:-}" in '' | *[!0-9.]*) _load=0 ;; esac + case "${_ncpu:-}" in '' | *[!0-9]*) _ncpu=1 ;; esac + _over=$(awk -v l="$_load" -v n="$_ncpu" 'BEGIN { print (l > n) ? 1 : 0 }') + if [ "$_over" = "1" ]; then + printf '负载 %.2f 高于 CPU 数 %s,deep 采样已跳过(降级为 full)\n' "$_load" "$_ncpu" >&2 + DOC_PROFILE=full + fi + fi + + # 主机前提(探针共享的缓存都在这之后才有值) + doc_host_firmware || { + DOC_FW_ID="" + } + doc_host_mem >/dev/null + doc_nft_version || true + doc_bins + doc_p11_mask + doc_p4b_table + doc_p4_ruleset + doc_p4_writers + doc_p5_iprule + doc_p6_route + doc_p7_tun + doc_p8_offload + doc_p9_conntrack + + doc_check_dns53 + for _x in $DOC_RUN_SET; do + case "$_x" in + 2) doc_check_tun ;; + 3) doc_check_mark ;; + 4) doc_check_offload ;; + 5) doc_check_writers ;; + 6) doc_check_whitelist ;; + esac + done + # 检查三/五的依赖顺序:3 必须在 5 之前(5 继承 3 的结论),6 在最后。 + doc_host_json + doc_summary + + if [ "$DOC_JSON_MODE" = "1" ]; then + doc_json + else + doc_text + fi + + # 退出码(C7 7.2):0 可以继续 / 1 有保留 / 2 会静默失效 + if [ "$DOC_SUM_CONFLICT" -gt 0 ]; then + return 2 + fi + if [ "$DOC_SUM_WARN" -gt 0 ]; then + return 1 + fi + if [ "$DOC_STRICT" = "1" ] && [ "$DOC_SUM_UNKNOWN" -gt 0 ]; then + return 1 + fi + return 0 +} diff --git a/isp-split/files/usr/lib/isp-split/teardown.sh b/isp-split/files/usr/lib/isp-split/teardown.sh index 118e3ec63..496b9f3ac 100644 --- a/isp-split/files/usr/lib/isp-split/teardown.sh +++ b/isp-split/files/usr/lib/isp-split/teardown.sh @@ -18,7 +18,7 @@ # # 用法: # sh teardown.sh all # mwan3 段 + runtime 残留 + 文件 -# sh teardown.sh mwan3 # 只删 mwan3 里 isp_split=1 的 ipset/policy/member +# sh teardown.sh mwan3 # 只删 mwan3 里 isp_split=1 的 ipset/rule/policy/member/interface # sh teardown.sh runtime # cron 行 / isp_fallback 计数器 / dnsmasq confdir 与片段 / LuCI 缓存 # sh teardown.sh files # 本工具安装的文件(含 /etc/isp-split 数据目录) # sh teardown.sh --list # 打印清单(selftest 与 postrm 漂移断言用),不动作 @@ -181,9 +181,11 @@ print_list() { echo "== 卸载清单(唯一实现:teardown.sh)==" echo "-- mwan3(UCI 段 + 服务重载)--" echo " 带 isp_split=1 标记的 ipset 段(含改名/删除的孤儿段)" + echo " 带 isp_split=1 标记的 rule 段(wire 生成的 isp_ 规则;不依赖 rules-splice 也能清)" echo " 带 isp_split=1 标记的 policy 段" echo " 带 isp_split=1 标记的 member 段(仍被其它策略引用的保留)" - echo " nft 规则块由 isp-split uninstall 的 rules-splice 摘除" + echo " 带 isp_split=1 标记的 mwan3 interface 段(bootstrap 建的;仍被成员引用的保留)" + echo " nft 规则块由 isp-split uninstall 的 rules-splice 摘除(teardown 自身只删 UCI 段)" echo "-- runtime 残留 --" echo " crontab 里的 isp-split 行" echo " nft 命名计数器 isp_fallback" @@ -220,6 +222,17 @@ cmd_mwan3() { uci -q delete "mwan3.$_mw_sec" 2>/dev/null && _mw_n=$((_mw_n + 1)) done + # rule 段:wire 生成的 isp_ 规则也带 isp_split=1。uninstall 的正常路径会先用 + # rules-splice 摘掉它们,但 `bootstrap --rollback` 与 install.sh 的 teardown + # 兜底路径不经过 splice——不在这里删,"只删带标记的段"这句承诺就是假的。 + # 顺序:先规则,再策略(规则引用策略,先删引用方永远不会让 mwan3 渲染失败)。 + _mw_nr=0 + for _mw_sec in $(uci -q show mwan3 2>/dev/null | + sed -n 's/^mwan3\.\([^=]*\)=rule$/\1/p'); do + [ "$(uci -q get "mwan3.$_mw_sec.isp_split" 2>/dev/null)" = "1" ] || continue + uci -q delete "mwan3.$_mw_sec" 2>/dev/null && _mw_nr=$((_mw_nr + 1)) + done + _mw_np=0 for _mw_sec in $(uci -q show mwan3 2>/dev/null | sed -n 's/^mwan3\.\([^=]*\)=policy$/\1/p'); do @@ -247,14 +260,40 @@ cmd_mwan3() { esac uci -q delete "mwan3.$_mw_sec" 2>/dev/null && _mw_nm=$((_mw_nm + 1)) done + # interface 段:bootstrap 建的那些(isp_split=1)。判据只有标记,绝不按 + # "名字看起来像 wan" 猜——用户手配的 wan/wanb 没有标记,必须原样保留。 + # 仍被任何成员引用的接口保留:删了 mwan3 会渲染失败(比"残留"更糟)。 + _mw_ifrefs=" " + for _mw_isec in $(uci -q show mwan3 2>/dev/null | + sed -n 's/^mwan3\.\([^=]*\)=member$/\1/p'); do + for _mw_y in $(uci -q get "mwan3.$_mw_isec.interface" 2>/dev/null); do + _mw_ifrefs="$_mw_ifrefs$_mw_y " + done + done + _mw_ni=0 + for _mw_sec in $(uci -q show mwan3 2>/dev/null | + sed -n 's/^mwan3\.\([^=]*\)=interface$/\1/p'); do + [ "$(uci -q get "mwan3.$_mw_sec.isp_split" 2>/dev/null)" = "1" ] || continue + case "$_mw_ifrefs" in + *" $_mw_sec "*) + say "警告:接口段 $_mw_sec 仍被成员引用,保留" + continue + ;; + esac + uci -q delete "mwan3.$_mw_sec" 2>/dev/null && _mw_ni=$((_mw_ni + 1)) + done uci -q commit mwan3 2>/dev/null || true [ "$_mw_n" -gt 0 ] && say "已删除 $_mw_n 个带 isp_split 标记的 ipset 段(含改名/删除的孤儿段)" + [ "$_mw_nr" -gt 0 ] && + say "已删除 $_mw_nr 个带 isp_split 标记的 mwan3 rule 段" [ "$_mw_np" -gt 0 ] || [ "$_mw_nm" -gt 0 ] && say "已删除 $_mw_np 个带 isp_split 标记的策略段、$_mw_nm 个成员段" + [ "$_mw_ni" -gt 0 ] && + say "已删除 $_mw_ni 个带 isp_split 标记的 mwan3 interface 段(bootstrap 建的骨架)" - # 规则已由 rules-splice 摘掉,这里 reload 让集合/策略随之消失。 + # 规则块在 uninstall 路径已被 rules-splice 摘掉;这里 reload 让集合/策略随之消失。 if [ -x "$MWAN3_SERVICE" ]; then "$MWAN3_SERVICE" reload >/dev/null 2>&1 || true fi diff --git a/isp-split/files/usr/sbin/isp-split b/isp-split/files/usr/sbin/isp-split index 8293222c8..da3d952b7 100644 --- a/isp-split/files/usr/sbin/isp-split +++ b/isp-split/files/usr/sbin/isp-split @@ -15,7 +15,7 @@ # install 注册开机自启与每日更新 # uninstall 卸载所有改动 set -u -ISP_VERSION="0.3.2" +ISP_VERSION="0.4.0" # 配置表里的注释行(# 开头,允许前导空白)必须在按空白展开之前整行删掉 # (B-1)。否则 `for row in $ISP_TABLE` 会把 "#" 与后面的文字拆成两个 @@ -2438,6 +2438,536 @@ _prune_orphan_policies() { } # F-14:卸载时的 policy/member 清理同样在 teardown.sh,见上。 +# ---------------------------------------------------------------- bootstrap +# +# 净机一键骨架:只保证「mwan3 里有 interface / member / policy 骨架」, +# 让 `init --bind` 能跑、`wire` 的预检能过。 +# +# 职责边界(与 init / wire 不打架): +# bootstrap 只补骨架(interface + member + policy),不碰 network/firewall +# init 只决定「哪个策略的成员指向哪个接口」 +# wire 只写集合/规则;预检策略缺失仍然硬失败 +# +# 红线(PLAN §1.4,全部"跳过并报告",不做"自动修正"): +# 1. 已存在的 mwan3. 段(任何类型/任何 enabled 值)一律不动; +# 2. 已存在的 globals / network.* / firewall.* 只读; +# 3. 同名段没有 isp_split=1 标记 → 认为是用户的,跳过; +# 4. mwan3 不是 nft 版 / 未安装 → 直接拒绝,一个字节都不写。 +# +# 事务(A-02 教训):**不做 uci 全局 revert**。所有写入都发生在 +# $WORK/boot-uci. 这个私有 confdir 里(uci -c),校验通过后用 +# _mwan3_atomic_install 做 rename(2) 原子替换。任何一步失败,正式配置 +# 一个字节都没动过,因此也就不存在"回滚掉别人的段"的可能。 +# +# 灰度(用户 2026-10-30 拍板):--apply 之后不自动 init、不 wire、不 +# reload mwan3,只打印下一步。是否生效由用户显式决定。 + +# track_ip 默认两个跨 AS 的 anycast。不要用运营商 DNS:那会把"DNS 挂了" +# 误判成"线路 down"(PLAN §1.3)。 +BOOT_TRACK_DEFAULT="1.0.0.1 1.1.1.1" + +_bt_proto_ok() { + case "${1:-}" in + pppoe | dhcp | static) return 0 ;; + esac + return 1 +} + +# network 里所有 interface 段的段名 +_bt_net_sections() { + uci -q show network 2>/dev/null | + sed -n 's/^network\.\([^=]*\)=interface$/\1/p' +} + +# lan 的 device(br-lan 之类):落在它上面的段一律不是 WAN +_bt_lan_dev() { + _bt_ld="" + _bt_ld=$(uci -q get network.lan.device 2>/dev/null) || _bt_ld="" + [ -n "$_bt_ld" ] || _bt_ld=$(uci -q get network.lan.device 2>/dev/null) || _bt_ld="" + printf '%s' "$_bt_ld" +} + +# ubus 里的 IPv4 地址(拿不到就输出空:只用于展示,不参与判据) +_bt_iface_ipv4() { + _bt_i4_raw="" + command -v ubus >/dev/null 2>&1 || return 0 + _bt_i4_raw=$(ubus call "network.interface.$1" status 2>/dev/null) || return 0 + if command -v jsonfilter >/dev/null 2>&1; then + printf '%s' "$_bt_i4_raw" | jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null + return 0 + fi + printf '%s' "$_bt_i4_raw" | + sed -n 's/.*"ipv4-address"[^]]*"address"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | + head -1 +} + +# 发现候选 WAN(全程只读)。结果: +# BT_CAND 空格分隔的候选段名(按 network 配置顺序) +# BT_SKIP_DISCOVER 形如 "name=原因;name=原因" 的跳过清单 +_bt_discover() { + BT_CAND="" + BT_SKIP_DISCOVER="" + _bt_lan=$(_bt_lan_dev) + for _bt_sec in $(_bt_net_sections); do + _bt_proto=$(uci -q get "network.$_bt_sec.proto" 2>/dev/null) || _bt_proto="" + case "$_bt_sec" in + lan | loopback) + BT_SKIP_DISCOVER="${BT_SKIP_DISCOVER}${BT_SKIP_DISCOVER:+;}$_bt_sec=管理口/回环" + continue + ;; + esac + if ! _bt_proto_ok "$_bt_proto"; then + BT_SKIP_DISCOVER="${BT_SKIP_DISCOVER}${BT_SKIP_DISCOVER:+;}$_bt_sec=proto=${_bt_proto:-空}(只认 pppoe/dhcp/static)" + continue + fi + _bt_dev=$(uci -q get "network.$_bt_sec.device" 2>/dev/null) || _bt_dev="" + [ -n "$_bt_dev" ] || _bt_dev=$(uci -q get "network.$_bt_sec.device" 2>/dev/null) || _bt_dev="" + if [ -n "${_bt_lan:-}" ] && [ "${_bt_dev:-}" = "$_bt_lan" ]; then + BT_SKIP_DISCOVER="${BT_SKIP_DISCOVER}${BT_SKIP_DISCOVER:+;}$_bt_sec=设备是 LAN 桥 $_bt_lan" + continue + fi + # static 的纯 v6 段(只有 ip6addr、没有 ipaddr)不是 IPv4 WAN + if [ "$_bt_proto" = "static" ]; then + _bt_v4=$(uci -q get "network.$_bt_sec.ipaddr" 2>/dev/null) || _bt_v4="" + _bt_v6=$(uci -q get "network.$_bt_sec.ip6addr" 2>/dev/null) || _bt_v6="" + if [ -z "${_bt_v4:-}" ] && [ -n "${_bt_v6:-}" ]; then + BT_SKIP_DISCOVER="${BT_SKIP_DISCOVER}${BT_SKIP_DISCOVER:+;}$_bt_sec=只有 IPv6 地址" + continue + fi + fi + BT_CAND="${BT_CAND:+$BT_CAND }$_bt_sec" + done +} + +# mwan3 里是否已有同名段(任何类型都算"已存在") +_bt_mwan3_exists() { + _bt_me=$(uci -q get "mwan3.$1" 2>/dev/null) || _bt_me="" + [ -n "${_bt_me:-}" ] +} + +# 已启用的 IPv4 WAN 段数量(判断"mwan3 已配置"的门槛) +_bt_enabled_wan_count() { + _bt_ew=0 + for _bt_ews in $(_mwan3_iface_sections); do + _bt_ew_en=$(uci -q get "mwan3.$_bt_ews.enabled" 2>/dev/null) || _bt_ew_en=1 + [ -n "${_bt_ew_en:-}" ] || _bt_ew_en=1 + _bt_ew_fam=$(uci -q get "mwan3.$_bt_ews.family" 2>/dev/null) || _bt_ew_fam=ipv4 + [ -n "${_bt_ew_fam:-}" ] || _bt_ew_fam=ipv4 + [ "$_bt_ew_en" = "1" ] || continue + [ "$_bt_ew_fam" = "ipv4" ] || continue + _bt_ew=$((_bt_ew + 1)) + done + printf '%s' "$_bt_ew" +} + +_bt_member_name() { + printf '%s_m1_w1\n' "$(printf '%s' "$1" | sed 's/[^A-Za-z0-9_]/_/g')" +} + +# 计划:BT_CREATE 每行 "type|段名|附属参数",BT_SKIP / BT_REUSE 为展示清单。 +# 只做计算,不写任何东西(dry-run 与 apply 共用同一份计划)。 +_bt_build_plan() { + BT_CREATE="" + BT_SKIP="" + BT_REUSE="" + BT_MEMBERS="" + BT_NEW_IFACES="" + _bt_n=0 + for _bt_if in $BT_CAND; do + _bt_n=$((_bt_n + 1)) + if _bt_mwan3_exists "$_bt_if"; then + BT_SKIP="${BT_SKIP}${BT_SKIP:+ +} interface $_bt_if (已存在,跳过)" + # 已存在的接口段仍可作为成员/策略的绑定目标 + _bt_type=$(uci -q get "mwan3.$_bt_if" 2>/dev/null) + if [ "$_bt_type" = "interface" ]; then + BT_MEMBERS="${BT_MEMBERS:+$BT_MEMBERS }$_bt_if" + fi + continue + fi + BT_CREATE="${BT_CREATE}${BT_CREATE:+ +}interface|$_bt_if|$_bt_if" + BT_NEW_IFACES="${BT_NEW_IFACES:+$BT_NEW_IFACES }$_bt_if" + BT_MEMBERS="${BT_MEMBERS:+$BT_MEMBERS }$_bt_if" + done + + # 每条新接口一个成员(_m1_w1,与 MultiWAN 管理器命名习惯一致) + for _bt_if in $BT_NEW_IFACES; do + _bt_m=$(_bt_member_name "$_bt_if") + if _bt_mwan3_exists "$_bt_m"; then + BT_SKIP="${BT_SKIP}${BT_SKIP:+ +} member $_bt_m (已存在,跳过)" + continue + fi + BT_CREATE="${BT_CREATE}${BT_CREATE:+ +}member|$_bt_m|$_bt_if" + done + + # ISP_TABLE 引用的策略:缺失才补。balanced 用全部成员,其它按顺序一一对应。 + _bt_j=0 + _bt_mem_n=0 + for _bt_x in $BT_MEMBERS; do + _bt_mem_n=$((_bt_mem_n + 1)) + done + for _bt_pol in $(_isp_policies); do + _bt_j=$((_bt_j + 1)) + _bt_ptype=$(uci -q get "mwan3.$_bt_pol" 2>/dev/null) || _bt_ptype="" + if [ -n "${_bt_ptype:-}" ]; then + BT_SKIP="${BT_SKIP}${BT_SKIP:+ +} policy $_bt_pol (已存在,跳过)" + BT_REUSE="${BT_REUSE}${BT_REUSE:+ +} policy $_bt_pol -> $(_policy_desc "$_bt_pol")" + continue + fi + [ "$_bt_mem_n" -gt 0 ] || { + BT_SKIP="${BT_SKIP}${BT_SKIP:+ +} policy $_bt_pol (没有可用成员,跳过)" + continue + } + if [ "$_bt_pol" = "balanced" ]; then + _bt_use="" + for _bt_x in $BT_MEMBERS; do + _bt_use="${_bt_use:+$_bt_use }$(_bt_member_name "$_bt_x")" + done + else + _bt_want=$(((_bt_j - 1) % _bt_mem_n + 1)) + _bt_i=0 + _bt_use="" + for _bt_x in $BT_MEMBERS; do + _bt_i=$((_bt_i + 1)) + [ "$_bt_i" -eq "$_bt_want" ] && _bt_use=$(_bt_member_name "$_bt_x") + done + fi + BT_CREATE="${BT_CREATE}${BT_CREATE:+ +}policy|$_bt_pol|$_bt_use" + done +} + +# 打印计划(dry-run 与 apply 共用;apply 之后再打一次"已写入") +_bt_print_plan() { + _bt_title=$1 + echo + echo "== 发现(只读)" + printf ' %-10s %-8s %-6s %-12s %s\n' "接口" "类型" "状态" "运行设备" "IPv4" + for _bt_if in $BT_CAND; do + _bt_p=$(uci -q get "network.$_bt_if.proto" 2>/dev/null) || _bt_p="-" + _bt_st=$(_iface_runtime_state "$_bt_if") + _bt_l3=$(_iface_l3_device "$_bt_if") + _bt_ip=$(_bt_iface_ipv4 "$_bt_if") + printf ' %-10s %-8s %-6s %-12s %s\n' \ + "$_bt_if" "${_bt_p:--}" "$_bt_st" "${_bt_l3:--}" "${_bt_ip:--}" + done + [ -n "$BT_CAND" ] || echo " (没有候选 WAN)" + if [ -n "${BT_SKIP_DISCOVER:-}" ]; then + echo + echo "== 未纳入的 network 段" + printf '%s\n' "$BT_SKIP_DISCOVER" | tr ';' '\n' | sed 's/^/ · /' + fi + + echo + echo "== $_bt_title" + if [ -n "$BT_CREATE" ]; then + printf '%s\n' "$BT_CREATE" | while IFS='|' read -r _bt_t _bt_nm _bt_arg; do + [ -n "${_bt_t:-}" ] || continue + case "$_bt_t" in + interface) + printf ' + interface %-12s enabled=1 family=ipv4 track_ip="%s" track_method=ping initial_state=online isp_split=1\n' \ + "$_bt_nm" "$BT_TRACK" + ;; + member) + printf ' + member %-12s interface=%s metric=1 weight=1 isp_split=1\n' "$_bt_nm" "$_bt_arg" + ;; + policy) + printf ' + policy %-12s use_member="%s" isp_split=1\n' "$_bt_nm" "$_bt_arg" + ;; + esac + done + else + echo " (没有需要新建的段)" + fi + [ -n "$BT_SKIP" ] && { + echo " = 跳过(已存在/不可用,绝不覆盖)" + printf '%s\n' "$BT_SKIP" + } + [ -n "$BT_REUSE" ] && { + echo " = 复用(已存在,未改动)" + printf '%s\n' "$BT_REUSE" + } + echo " · network.* / firewall.* / nft / mwan3.globals 一律只读,不产生任何改动" +} + +# 事务写入:私有 confdir 里改 -> commit -> 解析校验 -> rename 原子替换 +_bt_apply() { + _bt_tmp=$WORK/boot-uci.$$ + rm -rf "$_bt_tmp" + mkdir -p "$_bt_tmp" || die "无法创建事务目录 $_bt_tmp(配置未改动)" + cp "$CFG_MWAN3" "$_bt_tmp/mwan3" || { + rm -rf "$_bt_tmp" + die "无法复制 $CFG_MWAN3 到事务目录(配置未改动)" + } + _bt_cleanup() { rm -rf "$_bt_tmp"; } + # 备份(只作留档;回滚走 isp_split 标记,不靠备份覆盖用户改动) + mkdir -p "$STATE_DIR" 2>/dev/null || true + _bt_ts=$(date +%s) + cp "$CFG_MWAN3" "$STATE_DIR/mwan3.bootstrap.$_bt_ts.bak" 2>/dev/null || + log "警告:无法写入备份 $STATE_DIR/mwan3.bootstrap.$_bt_ts.bak(继续)" + + # 1) 写入(只在私有 confdir 里) + _bt_wr() { + _bt_t=$1 + _bt_nm=$2 + _bt_arg=$3 + uci -c "$_bt_tmp" set "mwan3.$_bt_nm=$_bt_t" || + return 1 + case "$_bt_t" in + interface) + uci -c "$_bt_tmp" set "mwan3.$_bt_nm.enabled=1" || return 1 + uci -c "$_bt_tmp" set "mwan3.$_bt_nm.family=ipv4" || return 1 + uci -c "$_bt_tmp" set "mwan3.$_bt_nm.track_method=ping" || return 1 + uci -c "$_bt_tmp" set "mwan3.$_bt_nm.initial_state=online" || return 1 + uci -c "$_bt_tmp" -q delete "mwan3.$_bt_nm.track_ip" 2>/dev/null || true + for _bt_tip in $BT_TRACK; do + uci -c "$_bt_tmp" add_list "mwan3.$_bt_nm.track_ip=$_bt_tip" || return 1 + done + ;; + member) + uci -c "$_bt_tmp" set "mwan3.$_bt_nm.interface=$_bt_arg" || return 1 + uci -c "$_bt_tmp" set "mwan3.$_bt_nm.metric=1" || return 1 + uci -c "$_bt_tmp" set "mwan3.$_bt_nm.weight=1" || return 1 + ;; + policy) + uci -c "$_bt_tmp" -q delete "mwan3.$_bt_nm.use_member" 2>/dev/null || true + for _bt_mm in $_bt_arg; do + uci -c "$_bt_tmp" add_list "mwan3.$_bt_nm.use_member=$_bt_mm" || return 1 + done + ;; + esac + uci -c "$_bt_tmp" set "mwan3.$_bt_nm.isp_split=1" || return 1 + return 0 + } + + _bt_lines="" + _bt_ok=1 + while IFS='|' read -r _bt_t _bt_nm _bt_arg; do + [ -n "${_bt_t:-}" ] || continue + _bt_wr "$_bt_t" "$_bt_nm" "$_bt_arg" || _bt_ok=0 + [ "$_bt_ok" = "1" ] || break + _bt_lines="${_bt_lines}${_bt_lines:+ +}$_bt_t $_bt_nm" + done </dev/null 2>&1; then + _bt_cleanup + die "生成的 mwan3 配置无法解析:正式配置未改动" + fi + + # 3) 复核:每个计划段都在、类型对、带标记(防止 uci 把同名段合并掉) + while IFS='|' read -r _bt_t _bt_nm _bt_arg; do + [ -n "${_bt_t:-}" ] || continue + _bt_got=$(uci -c "$_bt_tmp" get "mwan3.$_bt_nm" 2>/dev/null) || _bt_got="" + _bt_mark=$(uci -c "$_bt_tmp" get "mwan3.$_bt_nm.isp_split" 2>/dev/null) || _bt_mark="" + if [ "$_bt_got" != "$_bt_t" ] || [ "$_bt_mark" != "1" ]; then + _bt_cleanup + die "校验失败:$_bt_t '$_bt_nm' 未按预期落盘(type=${_bt_got:-空} mark=${_bt_mark:-空}):正式配置未改动" + fi + done <>"$STATE_DIR/bootstrap.created" 2>/dev/null || true + _bt_cleanup + log "已写入 mwan3 骨架($(printf '%s\n' "$_bt_lines" | grep -c . ) 个段,全部带 isp_split=1 标记)" + log "备份: $STATE_DIR/mwan3.bootstrap.$_bt_ts.bak;本次未 reload mwan3(灰度)" +} + +# 列出全部 isp_split=1 的 mwan3 段(rollback 预览与"删干净了没有"的复核共用) +_bt_marked_segments() { + for _bt_ms in ipset interface member policy rule; do + for _bt_sec in $(uci -q show mwan3 2>/dev/null | + sed -n "s/^mwan3\.\([^=]*\)=$_bt_ms\$/\1/p"); do + [ "$(uci -q get "mwan3.$_bt_sec.isp_split" 2>/dev/null)" = "1" ] || continue + printf '%s %s\n' "$_bt_ms" "$_bt_sec" + done + done +} + +cmd_bootstrap() { + _op_lock_acquire || die "另一个 isp-split 操作正在运行(锁: $OP_LOCK),本次未做任何改动;稍后重试" + _cmd_bootstrap_body "$@" + _rc=$? + _op_lock_release + return $_rc +} + +_cmd_bootstrap_body() { + BT_APPLY=0 + BT_ROLLBACK=0 + BT_FORCE=0 + BT_YES=0 + BT_TRACK=$BOOT_TRACK_DEFAULT + while [ $# -gt 0 ]; do + case "$1" in + --apply) BT_APPLY=1 ;; + --dry-run) BT_APPLY=0 ;; + --rollback) BT_ROLLBACK=1 ;; + --force) BT_FORCE=1 ;; + --yes | -y) BT_YES=1 ;; + --track-ip) + [ $# -ge 2 ] || die "--track-ip 需要参数 \" [...]\"" + BT_TRACK=$2 + shift + ;; + --track-ip=*) BT_TRACK=${1#--track-ip=} ;; + -h | --help) + cat <<'BOOTHELP' +用法: isp-split bootstrap [--apply] [--force] [--track-ip " []"] + isp-split bootstrap --rollback [--yes] + + 净机一键骨架:从 network 发现 WAN,补出 mwan3 的 interface/member/policy。 + · 默认 dry-run,只打印将创建/跳过/复用的段,不写任何东西 + · --apply 才写;写入走私有 confdir + 校验 + rename 原子替换 + · 已存在的 mwan3 段(无论 enabled)一律不覆盖、不重命名 + · 已配置的 mwan3(>=2 个启用的 IPv4 接口)默认拒绝补段,需要 --force + · 不自动 init / wire / reload mwan3(灰度);结束后打印下一步 + · --rollback 删除全部 isp_split=1 的段(= uninstall 的 bootstrap 部分) +BOOTHELP + return 0 + ;; + *) + die "未知参数: '$1'(用法: isp-split bootstrap [--apply] [--force] [--track-ip ...] | --rollback)" + ;; + esac + shift + done + + command -v uci >/dev/null 2>&1 || die "未找到 uci 命令" + [ -f "$CFG_MWAN3" ] || die "找不到 $CFG_MWAN3:请先安装 nft 版 mwan3(>= 3.6)" + + # ---------------- rollback ---------------- + if [ "$BT_ROLLBACK" = "1" ]; then + _bt_list=$(_bt_marked_segments) + if [ -z "$_bt_list" ]; then + log "没有 isp_split=1 的段,无需回滚" + return 0 + fi + echo "== 将删除以下带 isp_split=1 标记的 mwan3 段(手配段一个都不动)" + printf '%s\n' "$_bt_list" | sed 's/^/ - /' + echo " · 仍被其它策略引用的成员段会保留(删了 mwan3 会渲染失败)" + if [ "$BT_YES" != "1" ]; then + if [ ! -t 0 ]; then + die "非交互环境:确认要删除请加 --yes(本次未做任何改动)" + fi + printf '确认删除?[y/N] ' + IFS= read -r _bt_ans || _bt_ans="" + case "$_bt_ans" in + y | Y | yes | YES) ;; + *) log "已取消,未做任何改动"; return 1 ;; + esac + fi + [ -f "$LIB/teardown.sh" ] || + die "缺少 $LIB/teardown.sh,无法回滚(版本不匹配?)" + # --i-know-what-im-doing:BASE 可能被 ISP_SPLIT_BASE 覆盖(自检/沙箱), + # 而 mwan3 作用域只删 UCI 段、从不删 BASE 下的文件,所以这里的显式确认 + # 不比 uninstall(会 rm -rf BASE)的风险高。 + ISP_SPLIT_BASE=$BASE ISP_SPLIT_CONF=$CONF ISP_SPLIT_CRONTAB=$CRONTAB \ + ISP_SPLIT_LIB=$LIB MWAN3_SERVICE=$MWAN3_SERVICE \ + DNSDIR=$DNSDIR DNSMASQ_DEFAULT_DIR=$DNSMASQ_DEFAULT_DIR \ + DNSMASQ_CONF_GLOB=$DNSMASQ_CONF_GLOB \ + sh "$LIB/teardown.sh" mwan3 --i-know-what-im-doing || + die "回滚失败(见上方输出)" + _bt_left=$(_bt_marked_segments) + if [ -n "$_bt_left" ]; then + log "回滚后仍残留(被其它策略引用,已保留):" + printf '%s\n' "$_bt_left" | sed 's/^/ · /' + else + log "回滚完成:isp_split=1 的段已全部删除" + fi + return 0 + fi + + # ---------------- 发现 + 预检 ---------------- + if ! grep -q render_config_ipsets /lib/mwan3/mwan3.sh 2>/dev/null; then + die "mwan3 不是 nftables 版(/lib/mwan3/mwan3.sh 里没有 render_config_ipsets):本项目不支持,未做任何改动。请换用 nft 版 mwan3 (>= 3.6.0)" + fi + + _bt_discover + _bt_n=0 + for _bt_x in $BT_CAND; do + _bt_n=$((_bt_n + 1)) + done + if [ "$_bt_n" -eq 0 ]; then + echo "错误: 没有发现可用的 WAN 接口段(network 里没有 proto=pppoe/dhcp/static 的接口)" >&2 + [ -n "${BT_SKIP_DISCOVER:-}" ] && { + echo "已排除的段:" >&2 + printf '%s\n' "$BT_SKIP_DISCOVER" | tr ';' '\n' | sed 's/^/ · /' >&2 + } + echo "下一步:" >&2 + echo " 1) LuCI → 网络 → 接口:添加/启用 WAN(PPPoE 或 DHCP),确认它不是 lan" >&2 + echo " 2) 确认 mwan3(nft 版)已安装并至少启动过一次" >&2 + echo " 3) 重跑 isp-split bootstrap(先看不带 --apply 的预演)" >&2 + exit 1 + fi + + _bt_ew=$(_bt_enabled_wan_count) + if [ "$_bt_ew" -ge 2 ] && [ "$BT_FORCE" != "1" ]; then + die "mwan3 已配置($_bt_ew 个启用的 IPv4 接口):bootstrap 只补缺失骨架,不覆盖既有配置。 +若确认要补建缺失段,请加 --force: + isp-split bootstrap --force # 预演 + isp-split bootstrap --force --apply # 写入 +(已存在的 interface/member/policy 仍然一律跳过,不会被改名或覆盖)" + fi + + _bt_build_plan + _bt_print_plan "将创建($([ "$BT_APPLY" = "1" ] && printf 'apply' || printf 'dry-run,加 --apply 才写入'))" + + if [ "$BT_APPLY" != "1" ]; then + echo + echo "== 下一步(dry-run 未写入任何东西)" + echo " isp-split bootstrap --apply # 真正写入骨架" + echo " isp-split init --bind <策略>=<接口> ..." + echo " isp-split wire && isp-split sync" + return 0 + fi + + if [ -z "$BT_CREATE" ]; then + echo + log "没有需要新建的段(骨架已完整),配置一个字节都没改" + else + _work_writable || die "work 目录不可写($WORK):本次未做任何改动" + _bt_apply + fi + + echo + echo "== 下一步(灰度:本次没有 init / wire / reload mwan3)" + echo " 1) 绑定线路(写 member 的 interface 指向):" + echo " isp-split init --bind <策略>=<接口> [--bind ...] --assume-yes" + echo " 当前策略: $(uci -q show mwan3 2>/dev/null | sed -n 's/^mwan3\.\([^=]*\)=policy$/\1/p' | tr '\n' ' ')" + echo " 当前接口: $(_mwan3_iface_sections | tr '\n' ' ')" + echo " 2) 写规则:isp-split wire" + echo " 3) 生效: isp-split sync(会 reload mwan3,短暂断流 1-3 秒)" + echo " 撤销本步: isp-split bootstrap --rollback(只删带 isp_split=1 的段)" +} # init:发现接口 -> 为 ISP_TABLE 的每个策略绑定成员 -> 跑一次 plan 摘要 # A-02:与 fetch/wire/apply/sync 共用同一把操作锁,避免与 cmd_wire 的 @@ -2595,6 +3125,21 @@ _init_bound_iface() { return 1 } +# ---------------------------------------------------------------- doctor +# +# 环境体检。判据(探针 P1–P11 + 兼容矩阵六项 + 主机前提五项)只有一份实现: +# usr/lib/isp-split/doctor.sh,tools/router-preflight.sh 也 source 它。 +# 这里只负责装配与退出码,绝不重复写判据——两套判据必然漂移(F7 的教训)。 +# +# 只读、零副作用、不进 op.lock(不改任何东西,不需要和 wire/fetch 互斥)。 +cmd_doctor() { + [ -f "$LIB/doctor.sh" ] || + die "缺少 $LIB/doctor.sh:环境体检的判据实现不在设备上(版本不匹配?) +重装同版本包,或从同版本源码 cp files/usr/lib/isp-split/doctor.sh /usr/lib/isp-split/" + . "$LIB/doctor.sh" + doc_main "$@" +} + # 机器可读的策略状态(LuCI 首装提示条用) cmd_policy_check() { _pk_json=0 @@ -8029,6 +8574,8 @@ diff) shift; cmd_diff "$@" ;; dnstest) shift; cmd_dnstest "$@" ;; apply) cmd_apply ;; init) shift; cmd_init "$@" ;; +bootstrap) shift; cmd_bootstrap "$@" ;; +doctor) shift; cmd_doctor "$@" ;; wire) cmd_wire ;; sync) cmd_sync ;; plan) cmd_plan ;; @@ -8073,6 +8620,10 @@ line*check) rollback 回滚到上一份数据 —— 首次安装 / 上线(建议分两步)—— + bootstrap 净机一键骨架:从 network 发现 WAN,补出 mwan3 的 + interface/member/policy(默认 dry-run,--apply 才写; + 只补缺失、绝不覆盖已有段;不自动 wire/reload) + doctor 环境体检:六项兼容矩阵 + 主机前提(--json 给 LuCI 用) init 发现 mwan3 接口,为 ISP_TABLE 的每个策略创建 policy/member 交互选择接口;或 --bind <策略>=<接口>(可重复)--assume-yes stage 注入规则但保持 enabled=0,验证 mwan3 未被搞坏 diff --git a/luci-app-fchomo/Makefile b/luci-app-fchomo/Makefile index 703a27a02..9fff1243e 100644 --- a/luci-app-fchomo/Makefile +++ b/luci-app-fchomo/Makefile @@ -11,9 +11,9 @@ LUCI_DEPENDS:= \ +ca-bundle \ +firewall4 \ +kmod-inet-diag \ - +kmod-nft-tproxy \ + +kmod-nft-tproxy +ip-full \ +yq +wget-ssl +coreutils-timeout \ -# +ip-full +kmod-tun +dnsmasq-full \ +# +kmod-tun +dnsmasq-full \ PKG_NAME:=luci-app-fchomo diff --git a/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js b/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js index 60838d05c..83af3c6e7 100644 --- a/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js +++ b/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js @@ -498,7 +498,7 @@ return view.extend({ so.value('redir', _('Redirect TCP')); if (features.has_tproxy) so.value('redir_tproxy', _('Redirect TCP + TProxy UDP')); - if (features.has_ip_full && features.has_tun) { + if (features.has_tun) { so.value('redir_tun', _('Redirect TCP + Tun UDP')); so.value('tun', _('Tun TCP/UDP')); } else diff --git a/luci-app-fchomo/root/usr/share/rpcd/ucode/luci.fchomo b/luci-app-fchomo/root/usr/share/rpcd/ucode/luci.fchomo index 401fcb576..2beedf3b9 100644 --- a/luci-app-fchomo/root/usr/share/rpcd/ucode/luci.fchomo +++ b/luci-app-fchomo/root/usr/share/rpcd/ucode/luci.fchomo @@ -199,7 +199,6 @@ const methods = { features.has_luci_app_tinyfilemanager = system(`[ -n "$(${use_apk ? 'apk list -qI' : 'opkg list-installed'} luci-app-tinyfilemanager)" ]`) == 0 || null; // @less_25_12 features.has_dnsmasq_full = system(`[ -n "$(${use_apk ? 'apk list -qI' : 'opkg list-installed'} dnsmasq-full)" ]`) == 0 || null; // @less_25_12 - features.has_ip_full = access('/usr/libexec/ip-full'); features.has_stunclient = access('/usr/bin/stunclient'); features.has_tcp_brutal = hasKernelModule('brutal.ko'); features.has_tproxy = hasKernelModule('nft_tproxy.ko') || access('/etc/modules.d/nft-tproxy'); diff --git a/luci-app-isp-split/Makefile b/luci-app-isp-split/Makefile index e04f83740..f73aa910d 100644 --- a/luci-app-isp-split/Makefile +++ b/luci-app-isp-split/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-isp-split -PKG_VERSION:=0.3.2 -PKG_RELEASE:=5 +PKG_VERSION:=0.4.0 +PKG_RELEASE:=6 PKG_LICENSE:=MIT PKG_LICENSE_FILES:=LICENSE @@ -55,6 +55,8 @@ define Package/luci-app-isp-split/install $(1)/www/luci-static/resources/view/isp-split/overview.js $(INSTALL_DATA) $(PKG_BUILD_DIR)/htdocs/luci-static/resources/view/isp-split/config.js \ $(1)/www/luci-static/resources/view/isp-split/config.js + $(INSTALL_DATA) $(PKG_BUILD_DIR)/htdocs/luci-static/resources/view/isp-split/wizard.js \ + $(1)/www/luci-static/resources/view/isp-split/wizard.js if [ -f $(PKG_BUILD_DIR)/htdocs/luci-static/resources/view/isp-split/linecheck.js ]; then \ $(INSTALL_DATA) $(PKG_BUILD_DIR)/htdocs/luci-static/resources/view/isp-split/linecheck.js \ $(1)/www/luci-static/resources/view/isp-split/linecheck.js; \ diff --git a/luci-app-isp-split/htdocs/luci-static/resources/isp-split/isp-split.css b/luci-app-isp-split/htdocs/luci-static/resources/isp-split/isp-split.css index a14c6a1e2..2eb8c1519 100644 --- a/luci-app-isp-split/htdocs/luci-static/resources/isp-split/isp-split.css +++ b/luci-app-isp-split/htdocs/luci-static/resources/isp-split/isp-split.css @@ -1298,3 +1298,406 @@ } /* lc:end */ + +/* ---------------------------------------------------------------- 环境横幅(doctor,0.3.3) */ + +.isp-doctor { + margin: 0; +} + +/* fail:红条。与 .is-warn 同一套骨架,只换左边框与标题色——不新增视觉语言。 + 这里没有关闭按钮,也没有折叠:有 fail 项时横幅必须一直在。 */ +.isp-banner.is-bad { + border-left-color: var(--isp-bad); +} + +.isp-banner.is-bad .isp-banner-msg strong { + color: var(--isp-bad); +} + +.isp-banner-actions { + display: flex; + flex: 0 0 auto; + flex-direction: column; + gap: 8px; + align-items: stretch; +} + +.isp-env-list { + margin-top: 8px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.isp-env-list[hidden] { + display: none; +} + +.isp-env-item { + border-left: 3px solid var(--isp-border-strong); + padding-left: 8px; +} + +.isp-env-title { + display: block; + font-weight: 600; + color: var(--isp-text); +} + +.isp-env-adv { + display: block; + margin-top: 2px; + color: var(--isp-muted); + font-size: 0.86rem; + line-height: 1.45; +} + +/* 全绿:一行,尽量不占版面 */ +.isp-env-ok { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px 12px; + margin: 0 0 16px 0; + padding: 8px 14px; + border: 1px solid var(--isp-border); + border-left: 4px solid var(--isp-ok); + border-radius: var(--isp-radius); + background: var(--isp-card-bg); + color: var(--isp-text); + font-size: 0.9rem; +} + +.isp-env-ok .isp-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--isp-ok); +} + +.isp-env-sub { + color: var(--isp-muted); + font-size: 0.84rem; +} + +.isp-env-wait { + margin: 0 0 16px 0; + color: var(--isp-muted); + font-size: 0.86rem; +} + +/* ---------------------------------------------------------------- 首启向导(0.3.3) + + 沿用 isp-* 那套变量与卡片骨架:向导只新增"步骤条 / 自检行 / 完成态"三块, + 不复用体检页那套样式类——它可被构建期剥离,向导不能跟着消失。 */ + +.wz-steps { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; + margin: 0 0 16px 0; +} + +.wz-step { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; + padding: 10px 12px; + text-align: left; + background: var(--isp-card-bg); + border: 1px solid var(--isp-border); + border-radius: var(--isp-radius); + color: var(--isp-muted); + cursor: pointer; +} + +.wz-step.is-current { + border-color: var(--isp-primary); + box-shadow: inset 0 0 0 1px var(--isp-primary); + color: var(--isp-text); +} + +.wz-step.is-done { + border-left: 4px solid var(--isp-ok); + color: var(--isp-text); +} + +.wz-step[disabled] { + opacity: 0.55; + cursor: not-allowed; +} + +.wz-step-no { + font-size: 0.72rem; + letter-spacing: 0.08em; +} + +.wz-step-title { + font-size: 0.95rem; + font-weight: 600; +} + +.wz-step-hint { + font-size: 0.76rem; +} + +.wz-body { + display: flex; + flex-direction: column; + gap: 14px; + margin-bottom: 14px; +} + +.wz-card { + padding: 14px 16px; +} + +.wz-card h3 { + margin: 0 0 10px 0; +} + +.wz-summary { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 14px 0 10px 0; +} + +.wz-summary-head { + color: var(--isp-muted); + font-size: 0.88rem; +} + +.wz-doctor { + display: flex; + flex-direction: column; + gap: 12px; +} + +.wz-checks { + display: flex; + flex-direction: column; + gap: 10px; +} + +.wz-check { + padding: 12px 14px; + border: 1px solid var(--isp-border); + border-left: 4px solid var(--isp-border-strong); + border-radius: var(--isp-radius); + background: var(--isp-card-bg); +} + +.wz-check.is-ok { + border-left-color: var(--isp-ok); +} + +.wz-check.is-warn { + border-left-color: var(--isp-warn); +} + +.wz-check.is-bad { + border-left-color: var(--isp-bad); +} + +.wz-check-head { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.wz-check-title { + font-weight: 600; + color: var(--isp-text); +} + +.wz-check-conf { + margin-left: auto; + color: var(--isp-muted); + font-size: 0.8rem; +} + +.wz-find { + margin-top: 6px; +} + +.wz-find-line { + color: var(--isp-muted); + font-size: 0.86rem; + line-height: 1.5; +} + +.wz-advice { + margin-top: 6px; + color: var(--isp-text); + font-size: 0.88rem; + line-height: 1.5; +} + +.wz-kv { + display: flex; + gap: 10px; + padding: 3px 0; + font-size: 0.88rem; +} + +.wz-kv-key { + flex: 0 0 96px; + color: var(--isp-muted); +} + +.wz-kv-val { + flex: 1 1 auto; + color: var(--isp-text); + word-break: break-all; +} + +.wz-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + margin-top: 12px; +} + +.wz-hint { + margin: 8px 0 0 0; + color: var(--isp-muted); + font-size: 0.84rem; + line-height: 1.5; +} + +.wz-meta { + margin-top: 8px; + color: var(--isp-muted); + font-size: 0.84rem; +} + +.wz-note { + min-height: 1.2em; + margin: 10px 0 6px 0; + color: var(--isp-primary); + font-size: 0.88rem; +} + +.wz-note.is-bad { + color: var(--isp-bad); +} + +.wz-note.is-ok { + color: var(--isp-ok); +} + +.wz-loading { + color: var(--isp-muted); + font-size: 0.88rem; +} + +.wz-block { + margin-top: 10px; + padding: 10px 12px; + border-radius: var(--isp-radius); + background: var(--isp-card-bg-2); + color: var(--isp-text); + font-size: 0.88rem; + line-height: 1.5; +} + +.wz-preview { + margin-top: 12px; +} + +.wz-preview-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + margin-bottom: 8px; + color: var(--isp-muted); + font-size: 0.88rem; +} + +.wz-dry { + max-height: 320px; +} + +.isp-out.is-bad { + border-color: var(--isp-bad); +} + +.wz-cards { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + margin-top: 12px; +} + +.wz-ack { + display: flex; + align-items: center; + gap: 8px; + color: var(--isp-text); + font-size: 0.88rem; +} + +.wz-list { + margin: 8px 0 0 0; + padding-left: 20px; + color: var(--isp-text); + font-size: 0.9rem; + line-height: 1.55; +} + +.wz-footer { + display: flex; + align-items: center; + gap: 10px; + margin: 14px 0 6px 0; +} + +.wz-spacer { + flex: 1 1 auto; +} + +.wz-done { + display: flex; + gap: 14px; + align-items: flex-start; +} + +.wz-done-mark { + flex: 0 0 auto; + width: 44px; + height: 44px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background: var(--isp-ok); + color: #fff; + font-size: 1.4rem; + font-weight: 700; +} + +.wz-done-text strong { + display: block; + font-size: 1.05rem; + color: var(--isp-text); +} + +.wz-done-banner strong { + color: var(--isp-ok); +} + +@media (max-width: 820px) { + .wz-steps { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .wz-cards { + grid-template-columns: 1fr; + } +} diff --git a/luci-app-isp-split/htdocs/luci-static/resources/view/isp-split/overview.js b/luci-app-isp-split/htdocs/luci-static/resources/view/isp-split/overview.js index 1d0659c89..8a5382998 100644 --- a/luci-app-isp-split/htdocs/luci-static/resources/view/isp-split/overview.js +++ b/luci-app-isp-split/htdocs/luci-static/resources/view/isp-split/overview.js @@ -22,6 +22,20 @@ var CMD = '/usr/sbin/isp-split'; var REFRESH_SEC = 10; +/* + * 环境横幅(0.3.3):页面顶部一条 doctor 结论。 + * + * 为什么带缓存:`doctor` 不是零成本——它要读 nft 规则表、ubus、/proc,在 + * 小闪存机器上要几百毫秒到几秒。而状态页每 10 秒刷新一次,跟着刷等于拿 + * 路由器当体检机用。所以结果按 10 分钟缓存:内存一份(页面内切来切去不重跑) + * + localStorage 一份(刷新页面也不重跑)。用户点「重新体检」才强制重跑。 + * + * 颜色规矩(W2):fail 红且不能关(TUN 劫持 / DNS 53 这类问题会静默吃掉分流, + * 关掉就等于藏了个定时炸弹);warn 橙、可折叠;全绿只留一行,不占地方。 + */ +var DOCTOR_TTL_MS = 600 * 1000; +var DOCTOR_CACHE_KEY = 'isp-split.doctor.v1'; + /* ---------------------------------------------------------------- 图标(内联 SVG,无外链) */ var ICONS = { @@ -246,6 +260,187 @@ return view.extend({ }); }, + /* ---------------- 环境横幅(doctor) ---------------- + doctor 的退出码有语义(0 全绿 / 1 有保留 / 2 会静默失效),所以这里不看 + 退出码,只看 stdout 里有没有一份能解析的 JSON。 */ + + doctorCacheGet: function() { + try { + var raw = window.localStorage.getItem(DOCTOR_CACHE_KEY); + var o = raw ? JSON.parse(raw) : null; + if (o && o.t && o.d && (Date.now() - o.t) < DOCTOR_TTL_MS) + return { data: o.d, at: +o.t }; + } catch (e) { /* 隐私模式 / 配额满:退化成只在内存里缓存 */ } + return null; + }, + + doctorCachePut: function(data) { + try { + window.localStorage.setItem(DOCTOR_CACHE_KEY, + JSON.stringify({ t: Date.now(), d: data })); + } catch (e) { /* 同上:缓存写不进去不影响功能 */ } + }, + + refreshDoctor: function(force) { + var self = this; + + if (this._doctorInflight) + return this._doctorInflight; + + if (!force) { + var c = this.doctorCacheGet(); + if (c) { + this.doctor = c.data; + this.doctorAt = c.at; + this.paintDoctor(); + return Promise.resolve(); + } + } + + this.doctorErr = null; + this._doctorInflight = fs.exec(CMD, ['doctor', '--json']).then(function(res) { + var d = parseJson(res); + if (!d) + throw new Error((((res && res.stdout) || '') + ((res && res.stderr) || '')).trim() || + ('退出码 ' + ((res && res.code) != null ? res.code : '?'))); + self.doctor = d; + self.doctorAt = Date.now(); + self.doctorCachePut(d); + }).catch(function(err) { + self.doctorErr = String(err); + }).then(function() { + self._doctorInflight = null; + self.paintDoctor(); + }); + + return this._doctorInflight; + }, + + paintDoctor: function() { + var box = document.getElementById('isp-doctor'); + if (!box) + return; + + while (box.firstChild) + box.removeChild(box.firstChild); + + var self = this; + + if (this.doctorErr) { + box.appendChild(E('div', { 'class': 'isp-banner is-warn' }, [ + E('div', { 'class': 'isp-banner-msg' }, [ + E('strong', {}, [ _('环境体检不可用') ]), + E('div', {}, [ this.doctorErr ]), + E('div', { 'class': 'isp-banner-hint' }, [ + _('不影响下方的运行状态;多半是 rpcd 权限或 isp-split 版本不匹配。') + ]) + ]), + E('button', { + 'class': 'cbi-button isp-banner-btn', + 'click': function() { self.refreshDoctor(true); } + }, [ _('重试') ]) + ])); + return; + } + + if (!this.doctor) { + box.appendChild(E('div', { 'class': 'isp-env-wait' }, [ _('正在做环境体检(doctor)…') ])); + return; + } + + var d = this.doctor; + var checks = d.checks || []; + var sum = d.summary || {}; + var fails = checks.filter(function(c) { return c.status === 'fail'; }); + var warns = checks.filter(function(c) { return c.status === 'warn'; }); + var ageMin = this.doctorAt ? Math.max(0, Math.round((Date.now() - this.doctorAt) / 60000)) : 0; + var cacheHint = _('(结果缓存 ') + ageMin + _(' 分钟,10 分钟内不重跑探针)'); + + var retryBtn = E('button', { + 'class': 'cbi-button isp-banner-btn', + 'title': _('立即重跑 doctor(会跳过 10 分钟缓存)'), + 'click': function() { + retryBtn.disabled = true; + self.refreshDoctor(true); + } + }, [ _('重新体检') ]); + + /* fail:红条,不能关,建议直接摊开 */ + if (fails.length) { + var list = E('div', { 'class': 'isp-env-list' }); + fails.forEach(function(c) { + list.appendChild(E('div', { 'class': 'isp-env-item' }, [ + E('span', { 'class': 'isp-env-title' }, [ c.title || c.id ]), + E('span', { 'class': 'isp-env-adv' }, [ c.advice || '' ]) + ])); + }); + box.appendChild(E('div', { 'class': 'isp-banner is-bad' }, [ + E('div', { 'class': 'isp-banner-msg' }, [ + E('strong', {}, [ + _('环境体检:') + fails.length + _(' 项会让分流静默失效') + ]), + E('div', {}, [ sum.headline || _('按下面的建议处理后再看状态指标。') ]), + list, + E('div', { 'class': 'isp-banner-hint' }, [ + _('这条横幅在有 fail 项时不能关闭。') + cacheHint + ]) + ]), + retryBtn + ])); + return; + } + + /* warn:橙条,可折叠 */ + if (warns.length) { + var detail = E('div', { 'class': 'isp-env-list', 'hidden': '' }); + warns.forEach(function(c) { + detail.appendChild(E('div', { 'class': 'isp-env-item' }, [ + E('span', { 'class': 'isp-env-title' }, [ c.title || c.id ]), + E('span', { 'class': 'isp-env-adv' }, [ c.advice || '' ]) + ])); + }); + + var toggle = E('button', { + 'class': 'cbi-button isp-banner-btn', + 'click': function() { + if (detail.hasAttribute('hidden')) { + detail.removeAttribute('hidden'); + toggle.textContent = _('收起建议'); + } else { + detail.setAttribute('hidden', ''); + toggle.textContent = _('查看建议'); + } + } + }, [ _('查看建议') ]); + + box.appendChild(E('div', { 'class': 'isp-banner is-warn' }, [ + E('div', { 'class': 'isp-banner-msg' }, [ + E('strong', {}, [ _('环境体检:') + warns.length + _(' 项有保留') ]), + E('div', {}, [ sum.headline || '' ]), + detail, + E('div', { 'class': 'isp-banner-hint' }, [ cacheHint ]) + ]), + E('div', { 'class': 'isp-banner-actions' }, [ toggle, retryBtn ]) + ])); + return; + } + + /* 全绿:一行,不抢版面 */ + box.appendChild(E('div', { 'class': 'isp-env-ok' }, [ + E('span', { 'class': 'isp-dot' }), + E('span', {}, [ _('环境体检正常(') + checks.length + _(' 项)') ]), + E('span', { 'class': 'isp-env-sub' }, [ + sum.worst && sum.worst !== 'ok' + ? _('最差项:') + sum.worst + _(' · ') + (sum.headline || '') + : cacheHint + ]), + E('button', { + 'class': 'cbi-button isp-banner-btn', + 'click': function() { self.refreshDoctor(true); } + }, [ _('重新体检') ]) + ])); + }, + setOut: function(text) { var out = document.getElementById('isp-out'); if (out) { @@ -691,6 +886,10 @@ return view.extend({ this.paused = false; this.busy = false; this._inflight = null; + this.doctor = null; + this.doctorAt = 0; + this.doctorErr = null; + this._doctorInflight = null; var pauseLabel = E('span', {}, [_('暂停刷新')]); var pauseBtn = E('button', { @@ -754,6 +953,7 @@ return view.extend({ E('div', { 'class': 'isp-toolbar' }, [ statePill, pauseBtn ]) ]), + E('div', { 'id': 'isp-doctor', 'class': 'isp-doctor' }), E('div', { 'id': 'isp-load-error', 'class': 'isp-err', 'style': 'display:none' }), E('div', { 'id': 'isp-cards', 'class': 'isp-cards' }), @@ -782,6 +982,8 @@ return view.extend({ ]); this.refresh(); + /* 环境体检单独走自己的 10 分钟缓存:失败/成功都不影响下面的状态刷新 */ + this.refreshDoctor(false); /* 自带定时器替代 LuCI 的 poll(见文件头注释):暂停即停,页面被换走即收摊。 */ this.timer = window.setInterval(function() { diff --git a/luci-app-isp-split/htdocs/luci-static/resources/view/isp-split/wizard.js b/luci-app-isp-split/htdocs/luci-static/resources/view/isp-split/wizard.js new file mode 100644 index 000000000..c667cb539 --- /dev/null +++ b/luci-app-isp-split/htdocs/luci-static/resources/view/isp-split/wizard.js @@ -0,0 +1,1230 @@ +'use strict'; +'require view'; +'require fs'; +'require ui'; + +/* + * LuCI 首启向导(0.3.3):自检 → 体检 → 绑定 → 生效 + * + * 三条硬约束(本文件的骨架就是按它们搭的): + * + * 1) 白名单。页面上每个按钮都只映射到 WIZ 表里的一条**字面量** argv,参数 + * 从不拼接用户输入;不在表里的 key 直接拒绝,不执行任何命令。这样即使 + * 有人往页面里塞了个假按钮,效果也只有"提示不在白名单里"。 + * rpcd 的 file.exec 是按**路径**授权的,没法按参数收紧(见 docs/SECURITY.md), + * 所以白名单这一层只能在前端 + 静态断言里做,向导不做任何 shell 拼接。 + * + * 2) 状态机落盘。四步的进度写 /etc/isp-split/state/wizard.json(ACL 只给这 + * 一个文件写权限),完成后 done=1 + done_at 时间戳。刷新页面能从原步继续; + * done=1 时菜单入口(原文「引导」)在页面里显示为「重新引导」——菜单标题 + * 是静态 menu.d,改不了,只能在这里改名(FIX35-SUMMARY 偏离清单 D-1)。 + * + * 3) 危险动作二次确认。写 mwan3(bootstrap --apply)、重写规则并 reload + * mwan3(wire/apply 灰度)、整链同步(sync)都要弹框,文案里逐条列出 + * "将被写入 / 将被重载"的东西;sync 还要先勾选"我知道会短暂断流"。 + * 撤销引导(bootstrap --rollback)先跑一次不带 --yes 的预演,把 CLI 自己 + * 算出来的"将删除的段"原样贴进确认框,再带 --yes 执行。 + * + * 失败 UX:任何一步失败都留在当前步,命令与输出展开在同一个 isp-out 区块里 + * (与状态页同一个类);「回到上一步」只改界面、**不撤销**已生效的改动,文案 + * 写明;要撤销必须显式点「撤销本次引导」。 + * + * Step 1 复用 0.3.0 那套健康检查(读 state//last.json + 后台探测轮询)。 + * 那个功能可以在构建期被剥离(FEATURE_LINECHECK=0),剥离时这一整块会被 + * strip 脚本按成对标记整块删掉,只留一个"本版本未包含"的桩——所以 + * 下面用 var f = function(){} 的赋值写法,而不是同名 function 声明 + * (严格模式下重复声明在部分引擎里直接语法错)。 + */ + +var CMD = '/usr/sbin/isp-split'; +var STATE_FILE = '/etc/isp-split/state/wizard.json'; +var STATE_SCHEMA = 1; +var OVERVIEW_URL = 'admin/network/isp-split/overview'; +var CONFIG_URL = 'admin/network/isp-split/config'; +var DONE_REDIRECT_MS = 4000; + +/* 四步:key 只用于导航,title 是用户看到的名字 */ +var STEPS = [ + { key: 'check', title: '环境自检', hint: '只读,硬门' }, + { key: 'line', title: '体检', hint: '可选,只读探测' }, + { key: 'bind', title: '绑定骨架', hint: '写 mwan3' }, + { key: 'live', title: '生效', hint: '写规则并 reload' } +]; + +/* + * W3 白名单:向导按钮可用的命令行,全部写死在这里。 + * 值的形态必须是 [] 字面量(selftest 逐条比对,并断言全文件只有一处 fs.exec)。 + */ +var WIZ = { + doctor: [ 'doctor', '--json' ], + bootstrap_dry: [ 'bootstrap' ], + bootstrap_apply: [ 'bootstrap', '--apply' ], + rollback_list: [ 'bootstrap', '--rollback' ], + rollback_yes: [ 'bootstrap', '--rollback', '--yes' ], + plan: [ 'plan' ], + wire: [ 'wire' ], + apply: [ 'apply' ], + sync: [ 'sync' ] +}; + +/* ---------------------------------------------------------------- 体检步(可剥离) */ + +/* 剥离版(FEATURE_LINECHECK=0):这三个赋值留在文件里,整块体检实现被删掉 */ +var lcAvailable = function() { return false; }; +var loadLc = function() { return Promise.resolve(null); }; +var paintLc = function(box) { + box.appendChild(E('div', { 'class': 'isp-empty' }, [ + _('本版本未包含这条可选的健康检查(构建期已剥离)。直接点「下一步」即可。') + ])); +}; + +/* lc:begin */ +WIZ.linecheck_bg = [ 'linecheck', '--background' ]; +var LC_CACHE = '/etc/isp-split/state/linecheck/last.json'; +var LC_POLL_MS = 2500; +var LC_POLL_MAX_MS = 90000; +var POLL_LOCK_STALE_S = 300; + +lcAvailable = function() { return true; }; + +loadLc = function() { + return fs.read(LC_CACHE).then(function(s) { + return parseJsonStr(s); + }).catch(function() { + /* 没体检过 / 权限不足:都按"还没有结果"处理,不跑探测 */ + return null; + }); +}; + +/* 一份精简卡:一行一条线,评分圆环退化成彩色标签,明细留给体检页 */ +paintLc = function(box, snap) { + var self = this; + + if (!snap || !snap.wans || !snap.wans.length) { + box.appendChild(E('div', { 'class': 'isp-empty' }, [ + _('还没有体检快照。跑一次只读探针(20-60 秒,后台执行),结果会缓存到设备上。') + ])); + } + else { + var list = E('div', { 'class': 'isp-list' }); + snap.wans.forEach(function(w) { + var sc = w.score || {}; + var grade = String(sc.grade || 'D').toUpperCase(); + var tag = 'isp-tag' + (grade === 'A' || grade === 'B' ? ' is-ok' + : (grade === 'C' ? ' is-warn' : ' is-bad')); + list.appendChild(E('div', { 'class': 'isp-line' + (w.up ? ' is-online' : ' is-offline') }, [ + E('span', { 'class': 'isp-dot' }), + E('span', { 'class': 'isp-line-name' }, [ w.iface || '?' ]), + E('span', { 'class': 'isp-line-track' }, [ + (w.isp_label || _('未知')) + ' · ' + (w.wan_ip || '—') + ]), + E('span', { 'class': 'isp-line-pct' }, [ + E('span', { 'class': tag }, [ grade + ' ' + (sc.total != null ? sc.total : '—') ]) + ]) + ])); + if (sc.advice) + list.appendChild(E('div', { 'class': 'wz-advice' }, [ sc.advice ])); + }); + box.appendChild(list); + + var gen = +snap.generated || 0; + box.appendChild(E('div', { 'class': 'wz-meta' }, [ + gen + ? _('快照时间:') + new Date(gen * 1000).toLocaleString() + : _('快照没有时间戳'), + ' · ', + snap.geo_enabled ? _('归属地查询开') : _('归属地查询关') + ])); + } + + var runBtn = E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { lcRun(self, runBtn); } + }, [ _('跑一次体检') ]); + + box.appendChild(E('div', { 'class': 'wz-actions' }, [ + runBtn, + E('span', { 'class': 'wz-hint' }, [ _('只读探测,不改 mwan3 / nft 规则;后台跑,页面不会卡住。') ]) + ])); +}; + +/* 后台探测:起进程 → 每 2.5s 读快照比对 generated,最多 90s */ +function lcPoll(view, baseGen) { + var start = Date.now(); + if (view._lcTimer) + window.clearInterval(view._lcTimer); + + view._lcTimer = window.setInterval(function() { + if (!view.body || !document.body.contains(view.body)) { + window.clearInterval(view._lcTimer); + view._lcTimer = null; + return; + } + var waited = Date.now() - start; + if (waited > LC_POLL_MAX_MS) { + window.clearInterval(view._lcTimer); + view._lcTimer = null; + view.setOut(_('后台体检超过 90 秒还没出结果:稍后重开本页即可看到结果。'), null, 'bad'); + return; + } + loadLc().then(function(d) { + if (!d || (+d.generated || 0) <= baseGen) { + view.setNote(_('后台体检中(已等待 ') + Math.round(waited / 1000) + _(' 秒)…')); + return; + } + window.clearInterval(view._lcTimer); + view._lcTimer = null; + view.lc = d; + view.setNote(''); + view.paintStep(); + view.setOut(_('体检完成。'), null, 'ok'); + }).catch(function() { /* 文件还没写出来,下一轮再看 */ }); + }, LC_POLL_MS); +} +/* 起一次后台探测:只有完整版会渲染「跑一次体检」按钮(剥离版走 paintLc 的桩) */ +function lcRun(view, btn) { + var self = view; + if (self.busy) + return; + self.busy = true; + if (btn) + btn.disabled = true; + self.setNote(_('正在启动后台体检…')); + + return self.runWizard('linecheck_bg').then(function(res) { + var out = ((res && res.stdout) || '') + ((res && res.stderr) || ''); + var d = parseJsonStr((res && res.stdout) || ''); + self.setOut(_('命令:isp-split linecheck --background') + '\n\n' + (out.trim() || _('(无输出)')), + res, (res && res.code) ? 'bad' : ''); + + if (!d) { + self.busy = false; + if (btn) + btn.disabled = false; + self.setNote(''); + throw new Error(out.trim() || _('linecheck --background 没有返回可解析的状态')); + } + + var base = self.lc ? (+self.lc.generated || 0) : 0; + if (d.started && d.finished) { + self.busy = false; + return loadLc().then(function(snap) { + self.lc = snap; + self.setNote(''); + self.paintStep(); + }); + } + if (d.started || d.running) { + self.setNote(_('后台体检中(pid ') + (d.pid || 0) + _('),真实探测 20-60 秒…')); + lcPoll(self, base); + return; + } + self.busy = false; + if (btn) + btn.disabled = false; + if (d.cached) { + return loadLc().then(function(snap) { + self.lc = snap; + self.paintStep(); + self.setOut(_('60 秒内刚测过,直接用缓存结果。'), null, 'ok'); + }); + } + throw new Error(d.error || _('未知状态')); + }).catch(function(err) { + self.busy = false; + if (btn) + btn.disabled = false; + self.setNote(''); + ui.addNotification(null, E('p', {}, [ _('体检失败:') + String(err) ]), 'danger'); + }); +} + +/* lc:end */ + +/* ---------------------------------------------------------------- 工具 */ + +function parseJsonStr(s) { + try { + var v = JSON.parse(String(s == null ? '' : s).trim() || 'null'); + return (v && typeof v === 'object') ? v : null; + } catch (e) { + return null; + } +} + +function nowSec() { + return Math.floor(Date.now() / 1000); +} + +function fmtTime(sec) { + if (!sec) + return '—'; + return new Date(sec * 1000).toLocaleString(); +} + +function intIn(v, min, max, dflt) { + v = parseInt(v, 10); + if (isNaN(v)) + return dflt; + if (v < min) + return min; + if (v > max) + return max; + return v; +} + +/* 状态文件归一化:坏文件(截断 / 手改错)不能让页面白屏,按"从第 0 步开始"处理 */ +function normalizeState(raw) { + var st = { + schema: STATE_SCHEMA, + step: 0, + done: 0, + started: 0, + updated: 0, + done_at: 0 + }; + if (!raw || typeof raw !== 'object') + return st; + + st.step = intIn(raw.step, 0, STEPS.length - 1, 0); + st.done = raw.done ? 1 : 0; + st.started = parseInt(raw.started, 10) || 0; + st.updated = parseInt(raw.updated, 10) || 0; + st.done_at = parseInt(raw.done_at, 10) || 0; + /* done=1 但没有时间戳 = 半截写入,退回未完成,避免"已完成"横幅显示 — */ + if (st.done && !st.done_at) { + st.done = 0; + } + return st; +} + +function statusText(s) { + if (s === 'fail') + return _('会失效'); + if (s === 'warn') + return _('有保留'); + if (s === 'ok') + return _('正常'); + return _('未知'); +} + +function statusCls(s) { + if (s === 'fail') + return 'is-bad'; + if (s === 'warn') + return 'is-warn'; + if (s === 'ok') + return 'is-ok'; + return ''; +} + +function kv(label, value) { + return E('div', { 'class': 'wz-kv' }, [ + E('span', { 'class': 'wz-kv-key' }, [ label ]), + E('span', { 'class': 'wz-kv-val' }, [ value ]) + ]); +} + +/* ---------------------------------------------------------------- 视图 */ + +return view.extend({ + /* ---------------- 载入:零 exec ---------------- */ + load: function() { + return fs.read(STATE_FILE).then(function(s) { + return parseJsonStr(s); + }).catch(function() { + /* 首次进入 / 没有权限 / 文件不存在:都按"还没开始"处理 */ + return null; + }).then(function(st) { + return loadLc().then(function(lc) { + return { state: st, lc: lc }; + }); + }); + }, + + render: function(loaded) { + var self = this; + + this.state = normalizeState(loaded && loaded.state); + this.lc = (loaded && loaded.lc) || null; + this.doctor = null; + this.doctorErr = null; + this.doctorBusy = false; + this.busy = false; + this._lcTimer = null; + this._redirectTimer = null; + this._dryRun = null; + + this.heroTitle = E('h2', {}, [ _('引导') ]); + this.heroSub = E('div', { 'class': 'isp-sub' }, [ + _('四步把分流跑起来:环境自检 → 体检(可选)→ 绑定骨架 → 生效。每一步都会把真实执行的命令和输出摊开给你看。') + ]); + this.pill = E('span', { 'class': 'isp-pill' }); + this.bannerBox = E('div', { 'class': 'isp-banner-wrap' }); + this.navBox = E('div', { 'class': 'wz-steps' }); + this.body = E('div', { 'class': 'wz-body' }); + this.out = E('pre', { 'id': 'wz-out', 'class': 'isp-out' }, [ _('命令输出会显示在这里') ]); + this.note = E('div', { 'class': 'wz-note' }); + + this.prevBtn = E('button', { + 'class': 'cbi-button', + 'click': function() { self.goStep(self.state.step - 1); } + }, [ _('上一步') ]); + + this.nextBtn = E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { self.next(); } + }, [ _('下一步') ]); + + this.rollbackBtn = E('button', { + 'class': 'cbi-button cbi-button-negative', + 'click': function() { self.rollbackFlow(); } + }, [ _('撤销本次引导') ]); + + var container = E('div', { 'class': 'isp-app' }, [ + E('link', { + 'rel': 'stylesheet', + 'href': L.resource('isp-split/isp-split.css') + }), + + E('div', { 'class': 'isp-hero' }, [ + this.heroTitle, + this.heroSub, + E('div', { 'class': 'isp-toolbar' }, [ this.pill ]) + ]), + + this.bannerBox, + this.navBox, + this.body, + this.note, + this.out, + + E('div', { 'class': 'wz-footer' }, [ + this.prevBtn, + this.nextBtn, + E('span', { 'class': 'wz-hint' }, [ + _('「上一步」只改界面:已经生效的改动不会被撤销,要撤销请用右侧的「撤销本次引导」。') + ]), + E('span', { 'class': 'wz-spacer' }), + this.rollbackBtn + ]) + ]); + + this.paintNav(); + this.paintBanner(); + + if (this.state.done) + this.paintDone(); + else + this.paintStep(); + + return container; + }, + + /* ---------------- 状态读写 ---------------- */ + + saveState: function() { + var self = this; + var st = { + schema: STATE_SCHEMA, + step: this.state.step, + done: this.state.done ? 1 : 0, + started: this.state.started || nowSec(), + updated: nowSec(), + done_at: this.state.done_at || 0 + }; + this.state = st; + + return fs.write(STATE_FILE, JSON.stringify(st) + '\n').catch(function(err) { + /* 写不进去(ACL 被改 / 磁盘满)不能假装成功:提示 + 页面仍可用 */ + ui.addNotification(null, E('p', {}, [ + _('保存向导进度失败:') + String(err) + + _('(进度不会记住,但本页的操作不受影响)') + ]), 'warning'); + }); + }, + + startOver: function() { + var self = this; + if (this._redirectTimer) { + window.clearTimeout(this._redirectTimer); + this._redirectTimer = null; + } + this.state = normalizeState({ + step: 0, + done: 0, + started: nowSec(), + /* done_at 保留:历史完成时间仍是事实,只是本次要重新走一遍 */ + done_at: this.state.done_at + }); + this.doctor = null; + this.doctorErr = null; + this._dryRun = null; + return this.saveState().then(function() { + self.heroTitle.textContent = _('引导'); + self.paintNav(); + self.paintBanner(); + self.paintStep(); + }); + }, + + /* ---------------- 导航 ---------------- */ + + doctorFailed: function() { + var checks = (this.doctor && this.doctor.checks) || []; + for (var i = 0; i < checks.length; i++) + if (checks[i].status === 'fail') + return true; + return false; + }, + + /* 第 0 步是硬门:doctor 有 fail 就不许往下走(W1 的要求)。 + 读不到 doctor 结果不是 fail,是"不知道"——同样不放行,文案说清楚。 */ + canProceed: function() { + if (this.state.step !== 0) + return true; + if (!this.doctor || this.doctorFailed()) + return false; + return true; + }, + + next: function() { + if (this.state.step >= STEPS.length - 1) + return; + + if (!this.canProceed()) { + ui.addNotification(null, E('p', {}, [ + this.doctorErr + ? _('环境自检没跑成功,先修好再继续:') + this.doctorErr + : _('环境自检里有会静默失效的项(fail)。按上面的建议处理后点「重新自检」再继续。') + ]), 'danger'); + return; + } + + this.goStep(this.state.step + 1); + }, + + goStep: function(n) { + if (this._redirectTimer) { + window.clearTimeout(this._redirectTimer); + this._redirectTimer = null; + } + this.state.step = intIn(n, 0, STEPS.length - 1, 0); + this.saveState(); + this.paintNav(); + this.paintStep(); + }, + + paintNav: function() { + var self = this; + while (this.navBox.firstChild) + this.navBox.removeChild(this.navBox.firstChild); + + STEPS.forEach(function(s, i) { + var cls = 'wz-step'; + if (self.state.done || i < self.state.step) + cls += ' is-done'; + if (!self.state.done && i === self.state.step) + cls += ' is-current'; + + self.navBox.appendChild(E('button', { + 'class': cls, + 'disabled': (i > self.state.step && !self.state.done) ? '' : null, + 'title': s.hint, + 'click': function() { + /* 往回走永远允许;往前只能在已到达过的步之间点(下一步会再校验硬门) */ + if (i <= self.state.step || self.state.done) + self.goStep(i); + } + }, [ + E('span', { 'class': 'wz-step-no' }, [ String(i) ]), + E('span', { 'class': 'wz-step-title' }, [ s.title ]), + E('span', { 'class': 'wz-step-hint' }, [ s.hint ]) + ])); + }); + + if (this.state.done) { + this.pill.className = 'isp-pill'; + this.pill.textContent = _('已完成 ') + fmtTime(this.state.done_at); + } + else { + this.pill.className = 'isp-pill'; + this.pill.textContent = _('第 ') + this.state.step + _(' / 3 步'); + } + + this.prevBtn.style.display = (this.state.step > 0 && !this.state.done) ? '' : 'none'; + this.nextBtn.style.display = (this.state.step < STEPS.length - 1 && !this.state.done) ? '' : 'none'; + this.nextBtn.disabled = !this.canProceed(); + this.rollbackBtn.style.display = (this.state.step >= 2 || this.state.done) ? '' : 'none'; + }, + + /* ---------------- 横幅:完成态 / 硬门提示 ---------------- */ + + paintBanner: function() { + while (this.bannerBox.firstChild) + this.bannerBox.removeChild(this.bannerBox.firstChild); + + if (this.state.done) { + this.bannerBox.appendChild(E('div', { 'class': 'isp-banner wz-done-banner' }, [ + E('div', { 'class': 'isp-banner-msg' }, [ + E('strong', {}, [ _('引导已完成') ]), + E('div', {}, [ + _('完成时间:') + fmtTime(this.state.done_at) + + _('。再次进入本页是「重新引导」:四步会重新走一遍,但下面的任何按钮都不会自动撤销已经生效的配置。') + ]) + ]) + ])); + return; + } + + if (this.state.step === 0 && this.doctorErr) { + this.bannerBox.appendChild(E('div', { 'class': 'isp-banner is-bad' }, [ + E('div', { 'class': 'isp-banner-msg' }, [ + E('strong', {}, [ _('环境自检没跑成功') ]), + E('div', {}, [ String(this.doctorErr) ]), + E('div', { 'class': 'isp-banner-hint' }, [ + _('自检是只读的。修好上面的错误(多半是 rpcd 权限或 isp-split 版本不匹配),再点「重新自检」。') + ]) + ]) + ])); + return; + } + + if (this.state.step === 0 && this.doctor && this.doctorFailed()) { + var n = ((this.doctor.summary || {}).conflict) || 0; + this.bannerBox.appendChild(E('div', { 'class': 'isp-banner is-bad' }, [ + E('div', { 'class': 'isp-banner-msg' }, [ + E('strong', {}, [ _('有 ') + n + _(' 项会让分流静默失效(fail)') ]), + E('div', {}, [ + (this.doctor.summary || {}).headline || _('先按下面的建议处理,再点「下一步」。') + ]) + ]) + ])); + } + }, + + /* ---------------- 每步的渲染入口 ---------------- */ + + paintStep: function() { + var self = this; + while (this.body.firstChild) + this.body.removeChild(this.body.firstChild); + + if (this.state.done) + return this.paintDone(); + + this.paintNav(); + this.paintBanner(); + + if (this.state.step === 0) + this.paintCheck(); + else if (this.state.step === 1) + this.paintLine(); + else if (this.state.step === 2) + this.paintBind(); + else + this.paintLive(); + + this.paintNav(); + }, + + /* ---------------- Step 0:环境自检 ---------------- */ + + paintCheck: function() { + var self = this; + var box = E('section', { 'class': 'isp-section' }, [ + E('h3', {}, [ _('环境自检(doctor)') ]), + E('p', { 'class': 'isp-desc' }, [ + _('只读体检:六项兼容矩阵 + 主机前提。不会产生业务流量、不改任何配置;有 fail 项时本页不允许继续。') + ]), + this.checkBox = E('div', { 'class': 'wz-doctor' }) + ]); + this.body.appendChild(box); + + this.body.appendChild(E('div', { 'class': 'wz-actions' }, [ + E('button', { + 'class': 'cbi-button', + 'click': function() { self.loadDoctor(true); } + }, [ _('重新自检') ]) + ])); + + if (!this.doctor && !this.doctorErr) + this.loadDoctor(false); + else + this.paintCheckResult(); + }, + + loadDoctor: function(force) { + var self = this; + if (this.doctorBusy) + return; + if (this.doctor && !force) { + this.paintCheckResult(); + return; + } + + this.doctorBusy = true; + this.doctor = null; + this.doctorErr = null; + if (this.checkBox) { + while (this.checkBox.firstChild) + this.checkBox.removeChild(this.checkBox.firstChild); + this.checkBox.appendChild(E('div', { 'class': 'wz-loading' }, [ _('正在跑 doctor --json …') ])); + } + this.nextBtn.disabled = true; + + return this.runWizard('doctor').then(function(res) { + self.doctorBusy = false; + var data = parseJsonStr(res && res.stdout); + if (!data) { + var raw = (((res && res.stdout) || '') + ((res && res.stderr) || '')).trim(); + self.doctorErr = raw || (_('doctor --json 没有返回可解析的 JSON(退出码 ') + + ((res && res.code) != null ? res.code : '?') + ')'); + self.setOut(_('命令:isp-split doctor --json') + '\n\n' + self.doctorErr, res, 'bad'); + self.paintCheckResult(); + return; + } + self.doctor = data; + self.setOut(_('命令:isp-split doctor --json') + _('(退出码 ') + + ((res && res.code) != null ? res.code : 0) + _(',0=全绿 / 1=有保留 / 2=会失效)'), + res, self.doctorFailed() ? 'bad' : ''); + self.paintCheckResult(); + }); + }, + + paintCheckResult: function() { + if (!this.checkBox) + return; + + while (this.checkBox.firstChild) + this.checkBox.removeChild(this.checkBox.firstChild); + + if (this.doctorErr) { + this.checkBox.appendChild(E('div', { 'class': 'isp-empty' }, [ + _('自检没跑成功:') + this.doctorErr + ])); + /* 横幅依赖 doctor 结果,异步回来必须重画一次(否则红条永远不出现) */ + this.paintBanner(); + this.paintNav(); + return; + } + if (!this.doctor) { + this.checkBox.appendChild(E('div', { 'class': 'wz-loading' }, [ _('正在跑 doctor --json …') ])); + return; + } + + var d = this.doctor; + var host = d.host || {}; + var ow = host.openwrt || {}; + var mw = host.mwan3 || {}; + var mem = host.mem || {}; + var bins = host.bins || {}; + var missing = []; + Object.keys(bins).forEach(function(k) { + if (!bins[k]) + missing.push(k); + }); + + var hostBox = E('div', { 'class': 'isp-card wz-card' }, [ + E('h3', {}, [ _('主机前提') ]), + kv(_('系统'), (ow.id || '?') + ' ' + (ow.release || '?') + ' · ' + (ow.target || '?')), + kv(_('内核'), host.kernel || '?'), + kv('nftables', host.nft || '?'), + kv('mwan3', (mw.installed ? (mw.version || '?') : _('未安装')) + + (mw.installed ? (mw.nft ? _('(nftables 版)') : _('(非 nftables 版,本项目不支持)')) : '')), + kv('mmx_mask', (mw.mmx_mask || '?') + _('(来源:') + (mw.mmx_mask_source || '?') + ')'), + kv(_('可用内存'), (Math.round((+mem.avail_kb || 0) / 1024)) + ' / ' + + (Math.round((+mem.total_kb || 0) / 1024)) + ' MiB'), + kv(_('缺必需命令'), missing.length ? missing.join(' ') : _('无')), + kv(_('root 权限'), host.root ? _('是') : _('否(写操作会被拒)')) + ]); + + var sum = d.summary || {}; + var sumBox = E('div', { 'class': 'wz-summary' }, [ + E('span', { 'class': 'isp-tag is-ok' }, [ _('正常 ') + (sum.ok || 0) ]), + E('span', { 'class': 'isp-tag is-warn' }, [ _('有保留 ') + (sum.warn || 0) ]), + E('span', { 'class': 'isp-tag is-bad' }, [ _('会失效 ') + (sum.conflict || 0) ]), + E('span', { 'class': 'isp-tag' }, [ _('未知 ') + (sum.unknown || 0) ]), + E('span', { 'class': 'wz-summary-head' }, [ sum.headline || '' ]) + ]); + + var list = E('div', { 'class': 'wz-checks' }); + (d.checks || []).forEach(function(c) { + var st = c.status || 'unknown'; + var findings = c.findings || []; + var fBox = E('div', { 'class': 'wz-find' }); + findings.forEach(function(f) { + fBox.appendChild(E('div', { + 'class': 'wz-find-line', + 'title': f.code || '' + }, [ '· ', f.detail || f.code || '' ])); + }); + + list.appendChild(E('div', { 'class': 'wz-check ' + statusCls(st) }, [ + E('div', { 'class': 'wz-check-head' }, [ + E('span', { 'class': 'isp-tag ' + statusCls(st) }, [ statusText(st) ]), + E('span', { 'class': 'wz-check-title' }, [ c.title || c.id || '?' ]), + E('span', { 'class': 'wz-check-conf' }, [ _('置信度 ') + (c.confidence || '?') ]) + ]), + fBox, + E('div', { 'class': 'wz-advice' }, [ _('建议:') + (c.advice || _('无')) ]) + ])); + }); + + this.checkBox.appendChild(hostBox); + this.checkBox.appendChild(sumBox); + this.checkBox.appendChild(list); + + if (this.doctorFailed()) + this.checkBox.appendChild(E('div', { 'class': 'wz-block' }, [ + _('上面有 fail 项:这是硬门,处理完点「重新自检」。在自检通过之前「下一步」是灰的——带着会静默失效的环境往下走,后面每一步的结果都不可信。') + ])); + + var gate = d.gate || {}; + if (gate.can_wire === false) { + this.checkBox.appendChild(E('div', { 'class': 'wz-block' }, [ + _('wire/apply 门:阻断(') + ((gate.reasons || []).join(', ') || '?') + ')' + ])); + } + + this.paintBanner(); + this.paintNav(); + }, + + /* ---------------- Step 1:体检(可选) ---------------- */ + + paintLine: function() { + var box = E('section', { 'class': 'isp-section' }, [ + E('h3', {}, [ _('体检(可选)') ]), + E('p', { 'class': 'isp-desc' }, [ + _('逐条 WAN 看出口类型、四段延迟、NAT 与归属地,用来判断哪条线更适合当主力。只读探测,不改任何配置。') + ]) + ]); + paintLc.call(this, box, this.lc); + this.body.appendChild(box); + }, + + /* ---------------- Step 2:绑定骨架 ---------------- */ + + paintBind: function() { + var self = this; + + var box = E('section', { 'class': 'isp-section' }, [ + E('h3', {}, [ _('绑定骨架(mwan3)') ]), + E('p', { 'class': 'isp-desc' }, [ + _('净机上 mwan3 里没有 interface/member/policy,先让 bootstrap 只补骨架:只新增,不覆盖已有段,不碰 network/firewall,也不 reload mwan3。') + ]), + E('div', { 'class': 'wz-preview' }, [ + E('div', { 'class': 'wz-preview-head' }, [ + E('span', {}, [ _('预演(bootstrap 默认 dry-run,不写任何东西)') ]), + E('button', { + 'class': 'cbi-button', + 'click': function() { self.runDryRun(true); } + }, [ _('重新预演') ]) + ]), + this.dryBox = E('pre', { 'class': 'isp-out wz-dry' }, [ _('正在预演…') ]) + ]), + E('div', { 'class': 'wz-actions' }, [ + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'click': function() { self.applyBootstrap(); } + }, [ _('应用骨架') ]), + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { + window.location.href = L.url(CONFIG_URL); + } + }, [ _('完整编辑(ISP 表绑定)') ]) + ]), + E('p', { 'class': 'wz-hint' }, [ + _('骨架只解决「mwan3 里有没有段」。哪个运营商走哪条线属于 ISP 表,在「配置 → 策略映射」里改(上面右侧按钮直接跳过去,回来时向导会停在这一步)。') + ]) + ]); + + this.body.appendChild(box); + + if (!this._dryRun) + this.runDryRun(false); + else + this.paintDryRun(); + }, + + runDryRun: function(force) { + var self = this; + if (this._dryRunBusy) + return; + if (this._dryRun && !force) { + this.paintDryRun(); + return; + } + + this._dryRunBusy = true; + if (this.dryBox) { + this.dryBox.textContent = _('正在预演(bootstrap,不写入)…'); + this.dryBox.className = 'isp-out wz-dry'; + } + + return this.runWizard('bootstrap_dry').then(function(res) { + self._dryRunBusy = false; + self._dryRun = res || { code: 0, stdout: '', stderr: '' }; + self.paintDryRun(); + self.setOut(_('命令:isp-split bootstrap(dry-run)') + '\n\n' + + (((res.stdout || '') + (res.stderr || '')).trim() || _('(无输出)')), res, + (res && res.code) ? 'bad' : ''); + }); + }, + + paintDryRun: function() { + if (!this.dryBox || !this._dryRun) + return; + var r = this._dryRun; + var text = (((r.stdout || '') + (r.stderr || '')).trim()) || _('(无输出)'); + this.dryBox.textContent = text; + this.dryBox.className = 'isp-out wz-dry' + ((r.code) ? ' is-bad' : ''); + }, + + applyBootstrap: function() { + var self = this; + + return this.confirm(_('确认写入 mwan3 骨架?'), [ + E('p', {}, [ + _('将写入 mwan3 配置:为发现的 WAN 新增 interface / member / policy 段(段名带 isp_split=1 标记)。') + ]), + E('ul', { 'class': 'wz-list' }, [ + E('li', {}, [ _('只新增:已存在的 interface/member/policy 段一律跳过,不改名、不覆盖。') ]), + E('li', {}, [ _('不碰 network / firewall / dhcp,不 reload mwan3(灰度)。') ]), + E('li', {}, [ _('写入走 uci 暂存区 + 校验 + 原子替换;失败自动回滚,并留一份 mwan3 备份。') ]), + E('li', {}, [ _('若预演提示「mwan3 已配置」,CLI 会拒绝并且一个字节都不改。') ]) + ]), + E('p', { 'class': 'wz-hint' }, [ _('命令:isp-split bootstrap --apply') ]) + ], function() { + return self.runBusy('bootstrap_apply', _('正在写入骨架…'), function(res) { + if (res && res.code) { + self.setNote(_('写入失败:留在本步。看下面的命令输出,处理后可以重试。')); + return; + } + self.setNote(_('骨架已写入。下一步:绑定/核对 ISP 表,然后生效。')); + ui.addNotification(null, E('p', {}, [ _('骨架已写入 mwan3') ]), 'info'); + self.runDryRun(true); + }); + }); + }, + + /* ---------------- Step 3:生效 ---------------- */ + + paintLive: function() { + var self = this; + + var syncBox = E('input', { + 'type': 'checkbox', + 'id': 'wz-sync-ack', + 'click': function(ev) { syncBtn.disabled = !ev.target.checked; } + }); + + var syncBtn = E('button', { + 'class': 'cbi-button cbi-button-negative', + 'disabled': '', + 'click': function() { + if (!syncBox.checked) + return; + self.syncFlow(); + } + }, [ _('直接同步(sync)') ]); + + var box = E('section', { 'class': 'isp-section' }, [ + E('h3', {}, [ _('生效') ]), + E('p', { 'class': 'isp-desc' }, [ + _('默认走灰度:先预演看映射对不对,再「启用」——它重写 mwan3 规则并把集合刷进 nftables。要一步到位就用「直接同步」。') + ]), + + E('div', { 'class': 'wz-cards' }, [ + E('div', { 'class': 'isp-card wz-card' }, [ + E('h3', {}, [ _('灰度(推荐)') ]), + E('div', { 'class': 'wz-actions' }, [ + E('button', { + 'class': 'cbi-button', + 'click': function() { self.runPlan(); } + }, [ _('预演生效(plan)') ]), + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { self.enableFlow(); } + }, [ _('启用(wire + apply)') ]) + ]), + E('p', { 'class': 'wz-hint' }, [ + _('plan 只读:列出每个运营商集合的条目数与策略,不做任何改动。') + ]) + ]), + E('div', { 'class': 'isp-card wz-card' }, [ + E('h3', {}, [ _('一步到位') ]), + E('label', { 'class': 'wz-ack' }, [ + syncBox, + E('span', {}, [ _('我知道会短暂断流 1-3 秒') ]) + ]), + E('div', { 'class': 'wz-actions' }, [ syncBtn ]), + E('p', { 'class': 'wz-hint' }, [ + _('sync = 下载数据 + 重写规则 + 刷入 nftables + reload mwan3。') + ]) + ]) + ]) + ]); + + this.body.appendChild(box); + }, + + runPlan: function() { + var self = this; + return this.runBusy('plan', _('正在预演(plan,只读)…'), function(res) { + self.setNote((res && res.code) + ? _('plan 失败:看下面的输出。常见原因是策略缺失或成员为空(第 2 步的 ISP 表绑定)。') + : _('预演完成:以上是每个集合的条目数与策略。没问题就点「启用」。')); + }); + }, + + enableFlow: function() { + var self = this; + return this.confirm(_('确认重写规则并生效?'), [ + E('p', {}, [ _('将对 mwan3 与 nftables 做以下改动:') ]), + E('ul', { 'class': 'wz-list' }, [ + E('li', {}, [ _('wire:重写 mwan3 的 ipset 段与规则块(只动带 isp_split=1 的部分)。') ]), + E('li', {}, [ _('apply:把运营商集合刷进 nftables(inet mwan3 的 isp_* 集合)。') ]), + E('li', {}, [ _('会 reload mwan3:分流流量短暂中断 1-3 秒。') ]) + ]), + E('p', { 'class': 'wz-hint' }, [ _('命令:isp-split wire && isp-split apply') ]) + ], function() { + return self.runBusy('wire', _('正在重写规则(wire)…'), function(r1) { + if (r1 && r1.code) { + self.setNote(_('wire 失败:留在本步,已生效的配置**没有**被撤销。')); + return; + } + return self.runBusy('apply', _('正在刷入 nftables(apply)…'), function(r2) { + if (r2 && r2.code) { + self.setNote(_('apply 失败:规则已重写但集合没刷进去。看输出,重试 apply 即可。')); + return; + } + return self.finish(); + }); + }); + }); + }, + + syncFlow: function() { + var self = this; + return this.confirm(_('确认整链同步?'), [ + E('p', {}, [ _('isp-split sync 会依次做四件事:') ]), + E('ul', { 'class': 'wz-list' }, [ + E('li', {}, [ _('下载最新运营商地址数据(替换 /etc/isp-split/data)。') ]), + E('li', {}, [ _('重写 mwan3 的 ipset 段与规则块(isp_split=1)。') ]), + E('li', {}, [ _('把集合刷进 nftables。') ]), + E('li', {}, [ _('reload mwan3 —— 分流流量短暂中断 1-3 秒。') ]) + ]), + E('p', { 'class': 'wz-hint' }, [ _('命令:isp-split sync') ]) + ], function() { + return self.runBusy('sync', _('正在同步(下载 + 写规则 + 刷入 + reload)…'), function(res) { + if (res && res.code) { + self.setNote(_('同步失败:看下面的输出。已写入的部分不会自动回滚,可重试或「撤销本次引导」。')); + return; + } + return self.finish(); + }); + }); + }, + + /* ---------------- 完成态 ---------------- */ + + finish: function() { + var self = this; + this.state.done = 1; + this.state.done_at = nowSec(); + this.state.step = STEPS.length - 1; + + return this.saveState().then(function() { + self.paintNav(); + self.paintBanner(); + self.paintDone(); + ui.addNotification(null, E('p', {}, [ _('引导已完成,正在跳转到状态页…') ]), 'info'); + self._redirectTimer = window.setTimeout(function() { + window.location.href = L.url(OVERVIEW_URL); + }, DONE_REDIRECT_MS); + }); + }, + + paintDone: function() { + var self = this; + while (this.body.firstChild) + this.body.removeChild(this.body.firstChild); + + this.heroTitle.textContent = _('重新引导'); + + this.body.appendChild(E('section', { 'class': 'isp-section' }, [ + E('div', { 'class': 'wz-done' }, [ + E('div', { 'class': 'wz-done-mark' }, [ '✓' ]), + E('div', { 'class': 'wz-done-text' }, [ + E('strong', {}, [ _('引导已完成') ]), + E('div', {}, [ _('完成时间:') + fmtTime(this.state.done_at) ]), + E('div', { 'class': 'wz-hint' }, [ + _('状态已写入 /etc/isp-split/state/wizard.json(done=1)。分流是否按预期工作,请看状态页的运营商卡片与「双线健康」。') + ]) + ]) + ]), + E('div', { 'class': 'wz-actions' }, [ + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { window.location.href = L.url(OVERVIEW_URL); } + }, [ _('前往状态页') ]), + E('button', { + 'class': 'cbi-button', + 'click': function() { self.startOver(); } + }, [ _('重新引导') ]), + E('a', { + 'class': 'cbi-button', + 'href': L.url(CONFIG_URL) + }, [ _('完整编辑(ISP 表绑定)') ]) + ]) + ])); + + this.paintNav(); + }, + + /* ---------------- 撤销本次引导 ---------------- */ + + rollbackFlow: function() { + var self = this; + this.setNote(_('正在列取将删除的 mwan3 段(预演,不写入)…')); + + /* 先跑不带 --yes 的预演:CLI 会把"将删除的段"打印到 stdout,然后因为 + 非交互环境拒绝执行(退出码非 0,一个字节都不改)。这份清单直接贴进 + 确认框——不靠前端猜哪些段会被删。 */ + return this.runWizard('rollback_list').then(function(res) { + var text = ((res && res.stdout) || '').trim(); + var err = ((res && res.stderr) || '').trim(); + self.setOut(_('命令:isp-split bootstrap --rollback(预演)') + '\n\n' + + (text || err || _('(无输出)')), res, ''); + + if (!text) { + self.setNote(''); + ui.addNotification(null, E('p', {}, [ + _('没有可回滚的段(或命令失败):') + (err || _('未知原因')) + ]), 'warning'); + return; + } + + self.setNote(''); + return self.confirm(_('确认撤销本次引导?'), [ + E('p', {}, [ _('将删除下面这些带 isp_split=1 标记的 mwan3 段(手配段一个都不动):') ]), + E('pre', { 'class': 'isp-out wz-dry' }, [ text ]), + E('p', { 'class': 'wz-hint' }, [ + _('仍被其它策略引用的成员段会保留(删了 mwan3 会渲染失败)。命令:isp-split bootstrap --rollback --yes') + ]) + ], function() { + return self.runBusy('rollback_yes', _('正在撤销骨架…'), function(r) { + if (r && r.code) { + self.setNote(_('撤销失败:看下面的输出。')); + return; + } + self.state.done = 0; + self.state.done_at = 0; + self.state.step = 2; + return self.saveState().then(function() { + self.heroTitle.textContent = _('引导'); + self.paintBanner(); + self.paintStep(); + ui.addNotification(null, E('p', {}, [ _('骨架已撤销') ]), 'info'); + }); + }); + }); + }); + }, + + /* ---------------- 执行与输出 ---------------- */ + + /* 唯一允许调用 CLI 的入口:key 必须在 WIZ 白名单里,argv 只从表里取 */ + runWizard: function(key) { + var args = WIZ[key]; + if (!Array.isArray(args)) + return Promise.resolve({ + code: 126, + stdout: '', + stderr: _('子命令不在向导白名单里:') + String(key) + }); + return fs.exec(CMD, args.slice()); + }, + + /* 危险动作统一走这里:打印命令 + 退出码 + 输出;退出码非 0 时自动留在本步 */ + runBusy: function(key, note, done) { + var self = this; + if (this.busy) + return Promise.resolve(null); + this.busy = true; + this.busyKey = key; + this.setNote(note); + this.setBusy(true); + + var args = WIZ[key] || [ key ]; + var cmdText = _('命令:isp-split ') + args.join(' '); + + return this.runWizard(key).then(function(res) { + var out = (((res && res.stdout) || '') + ((res && res.stderr) || '')).trim(); + var code = (res && res.code != null) ? res.code : 0; + self.busy = false; + self.setBusy(false); + self.setOut(cmdText + '\n' + _('退出码:') + code + '\n\n' + (out || _('(无输出)')), + res, code ? 'bad' : 'ok'); + if (code) + ui.addNotification(null, E('p', {}, [ _('执行失败:') + cmdText.replace(/^命令:/, '') ]), 'danger'); + return done ? done(res) : null; + }).catch(function(err) { + self.busy = false; + self.setBusy(false); + self.setNote(''); + self.setOut(cmdText + '\n' + _('执行失败:') + String(err), null, 'bad'); + ui.addNotification(null, E('p', {}, [ _('执行失败:') + String(err) ]), 'danger'); + }); + }, + + setBusy: function(on) { + var btns = this.body ? this.body.querySelectorAll('button') : []; + for (var i = 0; i < btns.length; i++) + btns[i].disabled = !!on; + if (!on && this.nextBtn) + this.nextBtn.disabled = !this.canProceed(); + }, + + setNote: function(text, kind) { + if (!this.note) + return; + this.note.textContent = text || ''; + this.note.className = 'wz-note' + (kind ? ' is-' + kind : ''); + }, + + setOut: function(text, res, kind) { + if (!this.out) + return; + this.out.textContent = text || ''; + this.out.className = 'isp-out' + (kind ? ' is-' + kind : ''); + this.out.scrollTop = 0; + if (res && res.code === 126) + this.setNote(_('这个按钮调用的子命令不在白名单里,已被拒绝。'), 'bad'); + }, + + /* 统一的二次确认弹框:只包裹"已经写好的动作",文案由调用方逐条列出 */ + confirm: function(title, kids, onOk) { + return ui.showModal(title, kids.concat([ + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn', 'click': ui.hideModal }, [ _('取消') ]), + ' ', + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'click': function() { + ui.hideModal(); + return onOk(); + } + }, [ _('确认执行') ]) + ]) + ])); + }, + + /* 只读页:不带 cbi-map,也不给 Save & Apply 底栏(同其他只读页的注释) */ + handleSave: null, + handleSaveApply: null, + handleReset: null +}); diff --git a/luci-app-isp-split/root/usr/share/luci/menu.d/luci-app-isp-split.json b/luci-app-isp-split/root/usr/share/luci/menu.d/luci-app-isp-split.json index aa84d4037..88fed3bde 100644 --- a/luci-app-isp-split/root/usr/share/luci/menu.d/luci-app-isp-split.json +++ b/luci-app-isp-split/root/usr/share/luci/menu.d/luci-app-isp-split.json @@ -10,6 +10,14 @@ "uci": { "mwan3": true } } }, + "admin/network/isp-split/wizard": { + "title": "引导", + "order": 0, + "action": { + "type": "view", + "path": "isp-split/wizard" + } + }, "admin/network/isp-split/overview": { "title": "状态", "order": 1, diff --git a/luci-app-isp-split/root/usr/share/rpcd/acl.d/luci-app-isp-split.json b/luci-app-isp-split/root/usr/share/rpcd/acl.d/luci-app-isp-split.json index a6059fc05..e95d7103d 100644 --- a/luci-app-isp-split/root/usr/share/rpcd/acl.d/luci-app-isp-split.json +++ b/luci-app-isp-split/root/usr/share/rpcd/acl.d/luci-app-isp-split.json @@ -1,23 +1,23 @@ { "luci-app-isp-split": { - "description": "Grant access to ISP split status and control. WARNING: write = execute ANY isp-split subcommand as root (wire/apply/conf set/uninstall); grant only to fully trusted admins. read = conf + whole state tree (public IP/geo/STUN).", + "description": "Grant access to ISP split status and control. WARNING: write = execute ANY isp-split subcommand as root (wire/apply/conf set/uninstall); grant only to fully trusted admins. read = conf + whole state tree (public IP/geo/STUN). wizard = one file write scope only (/etc/isp-split/state/wizard.json), no directory-wide write.", "read": { "ubus": { "file": [ "read", "stat" ] }, "file": { "/etc/isp-split.conf": [ "read" ], - "/etc/isp-split": [ "read" ], "/etc/isp-split/*": [ "read" ], "/etc/isp-split/state/*": [ "read" ] } }, "write": { "ubus": { - "file": [ "exec" ] + "file": [ "exec", "write" ] }, "file": { - "/usr/sbin/isp-split": [ "exec" ] + "/usr/sbin/isp-split": [ "exec" ], + "/etc/isp-split/state/wizard.json": [ "write" ] } } } diff --git a/luci-app-open-box/Makefile b/luci-app-open-box/Makefile index 94507d2cd..8ff288ded 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.388 -PKG_RELEASE:=57 +PKG_VERSION:=0.1.389 +PKG_RELEASE:=58 PKG_MAINTAINER:=liandu2024 PKG_LICENSE:=GPL-3.0-or-later diff --git a/luci-app-openclaw/CHANGELOG.md b/luci-app-openclaw/CHANGELOG.md index 7062ca838..19ae7059d 100644 --- a/luci-app-openclaw/CHANGELOG.md +++ b/luci-app-openclaw/CHANGELOG.md @@ -4,6 +4,91 @@ 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。 +## [2.3.0] - 2026-11-02 + +基于 v2.2.3。默认 OpenClaw 从 `2026.9.1` 升级到 `2026.9.36`,Node.js 从 22 升级到 24,模型配置菜单整体重写。 + +### 升级前请了解 + +- **Node.js 要求变了。** `2026.9.34` 起 OpenClaw 要求 `>=24.22.1 <25 || >=26.12.1`。升级核心时会先把 Node.js 升到 `24.22.2`,新运行时校验通过后才替换旧目录,失败时原安装不变。`24.22.2` 也满足 `2026.9.1` 的要求,所以只升级插件不升级核心时,现有的 Node 22 + `2026.9.1` 可以继续运行。 +- **默认不再安装未验证版本。** 想用上游最新版,需要在高级选项里明确勾选,或设置 `OC_ALLOW_UNVERIFIED=1`。 +- **核心升级后磁盘占用变大。** `2026.9.36` 解压后约 390MB,`2026.9.1` 约 175MB。 +- **微信插件要求变了。** 旧插件在新核心上能装上,`plugins list` 也显示 `loaded`,渠道却会反复崩溃。升级核心时,如果微信已启用且插件低于该版本的要求,会自动升到 `2.4.10`,结果写在升级报告的 `WECHAT:` 一行。如果这一行是 `failed`,核心升级仍算成功,请到 LuCI 微信页面重新安装插件。 +- **Control UI 改走设备配对。** 上游已退役 `gateway.controlUi.dangerouslyDisableDeviceAuth`,Doctor 会删除它,首次打开控制台需要按正常流程配对设备。 +- **工作区文件会被官方迁移改动。** `TOOLS.md` 合并进 `AGENTS.md`,未改动过的 `HEARTBEAT.md` 模板会被移除,`auth-profiles.json` 和会话文件迁入 SQLite。升级前的备份归档保留在 `data/backups`。 + +### 新增 + +- **发布通道与版本锁定**:可在 `extended-stable`(默认)和 `latest` 之间选择,也可以锁定具体版本。「上游已发布」通过 npm dist-tags 实时查询,「本插件已验证」记录在 `openclaw-compat.json`,两者分开显示。上游有未验证的新版本时只提示,不自动安装。registry 不可达时仍可安装已验证版本。 +- **模型配置菜单重写**:分为快速配置、官方 API、订阅套餐与 Coding Plan、聚合平台与自定义 API、本地模型,以及状态查看、默认模型、备用模型、连通性测试、删除凭据的管理功能。交互式菜单和 Shell 菜单共用后端 `oc-providers.js` 与目录 `provider-catalog.json`,按键和文案一致。 +- **连通性测试**:发真实请求确认账号能否调用,并区分认证失败、限流、余额不足、超时等状态。Gateway 运行时走 `models.probe`,不中断服务。能查到模型不代表账号有权调用,菜单里有明确说明。 +- **模型发现回落**:依次尝试实时列表、`--refresh`、本地缓存。精选模型和手动输入始终可用,并显示真实的失败原因。 +- **预放置安装([#113](https://github.com/10000ge10000/luci-app-openclaw/issues/113))**:部分厂商闭源内核的固件在大量快速写盘时会被看门狗复位,在线安装因此反复失败。README 新增「预放置安装」说明:在电脑上准备好 Node.js 和 OpenClaw,限速拷贝到位后执行 `openclaw-env setup`。此时 `setup` 只创建系统用户、校验运行时并初始化,不下载、不解压。这是绕过方案,不能修复内核层面的复位。 +- **备份下载、上传与恢复([#111](https://github.com/10000ge10000/luci-app-openclaw/issues/111))**:备份列表每项新增「下载」按钮,对话框新增「上传备份」。上传采用分块方式,上限 20MB,上传后可在列表中直接恢复。 + +### 修复 + +- **旧配置导致 Gateway 拒绝启动**:`2026.9.35` 起配置校验会拒绝未知字段,而插件历史上每次启动都写入已退役的 `gateway.controlUi.allowInsecureAuth`。现在不再写入,新增 `oc-config-fixups.js` 在 Doctor 和 Gateway 之前删除它。 +- **Discord Token 可能丢失**:插件一直写 `channels.discord.botToken`,上游字段是 `token`,新版 Doctor 会删除未知键。现在写入正确字段,存量配置自动迁移且保留原值。`gateway.logLevel` 同样迁移到 `logging.level`。 +- **凭据写入已退役的文件**:`auth-profiles.json` 已被上游退役,写入会触发 `AUTH_PROFILE_MIGRATION_REQUIRED`。现在只通过官方 CLI 写入 `state/openclaw.sqlite`,重置和恢复出厂也改为逐个登出。 +- **恢复出厂时 onboard 一直静默失败**:使用了不存在的 `--tools-profile` 参数,改为与初始化相同的参数。 +- **设备配对报 `sh: timeout: not found`([#110](https://github.com/10000ge10000/luci-app-openclaw/issues/110))**:部分固件的 BusyBox 没有 `timeout` applet。现在优先用系统 `timeout`,缺失时由内置看门狗兜底,不再需要手动安装 `coreutils-timeout`。 +- **Web 控制台显示「拒绝连接」([#108](https://github.com/10000ge10000/luci-app-openclaw/issues/108))**:Control UI 固定返回 `X-Frame-Options: DENY`,上游没有关闭它的配置项,2.2.0 起的 iframe 内嵌在任何浏览器里都会被拒绝。改为「在新窗口打开控制台」入口。 +- **启动日志出现 `_ensure_critical_config: not found`([#108](https://github.com/10000ge10000/luci-app-openclaw/issues/108))**:这两个函数在定义之前就被调用。移除提前调用后行为不变。 +- **升级时 npm 报 `Class extends value undefined`([#109](https://github.com/10000ge10000/luci-app-openclaw/issues/109))**:这是运行时自带的 npm 损坏的表现,旧版只比较 Node 版本号,用户只能卸载重装。现在升级前先自检 npm 并给出修复指引;在已安装的环境里点「安装运行环境」时,若 npm 自检失败,只重新下载 Node.js,OpenClaw 与数据不变。 +- **服务启动或菜单操作可能清空配置**:配置无法解析时(例如多了一个尾随逗号),`_ensure_critical_config`、Token 回写、微信渠道自愈和 Shell 菜单的写入函数都会把默认值写回磁盘,API Key 和渠道配置随之丢失。现在一律解析失败即中止,原文件不变。 +- **部分设备上「创建备份」一直失败**:备份命令缺少 `NODE_ICU_DATA`,接口却仍返回成功。现在补齐环境变量,并以备份文件实际生成为准判断成败。备份列表也能正确显示带时区后缀的文件名时间。 +- **菜单输入问题**:在输入框里键入字母 `q` 会取消输入,粘贴内容时搜索框会丢字符,现已修复。 +- **预放置环境被误拒**:`setup` 的 Node 版本检查改为以已安装包的 `engines` 为准,Node 22 + `2026.9.1` 的预放置安装不会被新的默认下限拦住。 +- **升级后微信渠道崩溃循环**:核心移除 plugin-sdk 子路径后,旧插件的渠道反复退出,升级报告却显示成功。现在按兼容矩阵自动升级插件,默认安装 `2.4.10`。 +- **显式回滚会杀死自己**:回滚用 `pgrep -f "openclaw"` 清理进程,BusyBox 上会匹配到回滚脚本本身,服务已停却没有恢复。现在只清理服务用户的进程和 Gateway。 +- **`2026.9.36` 上备份失败导致无法升级**:环境变量只绑定到了复合命令的第一条,CLI 带着 root 的 `HOME` 运行,`backup create` 报临时目录不安全。 +- **装过 npm 插件的备份恢复不了**:`node_modules/.bin` 的相对软链接被当成路径穿透;新版 `backup verify` 也拒绝旧备份里插件 peerDependency 的绝对链接。现在按解析结果判断,其它越界链接仍然拒绝。 +- **恢复备份会删除工作区**:`2026.9.x` 的备份不含 `workspace` 和 `npm`,整目录替换时它们会被一起删除。现在归档里没有的这两个目录会保留。 +- **交互式菜单退出后卡住**:打印「再见」后进程不结束,Web 终端停在那里。 +- **配置备份无限累积**:`.ipk` 每次安装都在 `/etc/config` 留一份含 Token 的 `openclaw.<时间戳>.bak`,从不清理。现在只保留最近 5 份,其它名称的备份不受影响。 +- **配置 AI 模型时终端反复断开重连**:Web 终端把 `script` 输出的原始字节块直接当文本帧发送,中文字符被切到两个块时,浏览器判定 UTF-8 非法并断开,菜单随之重启,正在填写的内容丢失。模型菜单输出多,最容易触发。现在按完整字符转发。 +- **BusyBox 上进程清理不生效**:BusyBox 的 `pgrep` 没有 `-u`,部分固件也没有 `pkill`,服务停止、卸载和微信重新登录时都清理不到残留进程,而且不报错。现在改为按 `/proc` 属主查找服务用户的进程,微信登录改用 `pgrep -f`。服务停止时先发 `SIGTERM`,最多等 3 秒再强制结束,避免打断子进程收尾。 + +### 安全加固 + +- **API Key 不进命令行参数**:通过环境变量或标准输入交给官方 CLI,不出现在进程列表里。后端输出会过滤掉 Key,上游意外回显时同样隐藏。状态页不显示上游的掩码标签。 +- **安装包完整性校验**:已验证版本的 tarball 必须与兼容矩阵记录的 sha512 一致,镜像源返回的文件被篡改或损坏时安装失败。未验证版本的生命周期脚本必须与已验证版本相同。 +- **备份恢复不再解压到根目录**:旧版会把归档里 `payload/posix/` 下的所有路径解压到 `/`。现在只解压状态目录子树。上传的归档不能包含状态目录之外的路径、符号链接、硬链接或设备文件。 +- **备份文件名白名单**:只允许字母、数字和 `._+-`,所有命令参数统一做 shell 转义。指定的备份不存在时直接报错,不再静默恢复「最新的那一份」。 +- **外部插件需确认**:需要官方 Provider 插件时先询问用户,且只允许安装 `@openclaw/*-provider`。 +- **Web 终端的 Token 不再写入系统日志**:WebSocket 连接日志原来记录完整 URL,其中含 PTY Token,现在只记录路径。 + +### 移除 + +- **「一万AI分享 粉丝专享 API」**:对应的公益站点已停止运营,菜单入口、固定地址和相关模型预设全部删除。已有配置里的 `yiwanai` provider 和 Key 不会被自动删除,需要停用时请切换默认模型,再在菜单 `9` 中删除。 +- **`model-presets.json`**:由 `provider-catalog.json` 取代。 + +### 构建与测试 + +- **发布流水线的 arm64 musl Node 改为转发 unofficial-builds**:Alpine apk 的 Node 补丁版本经常与目标不一致(查询时 v3.23 与 edge 为 `24.22.1`),原来的精确版本模式会让发布失败。现在下载上游构建,校验 `SHASUMS256.txt`,在 ARM64 容器内运行验证后原样发布。 +- **打包清单补全**:`.ipk`、`.run` 和 feeds/SDK 三条路径都安装新增的文件,`test_packaging_parity.sh` 同时覆盖 `.json` 文件。 +- **新增测试**:Provider 后端(用替身 CLI 覆盖 Key 传递、认证失败、默认模型保留、自定义 Provider 冲突、模型发现回落、连通性测试)、Provider 目录与两套菜单一致性、旧配置迁移、发布通道选择。`test_openclaw_upgrade_state.sh` 不再调用机器上真实安装的 OpenClaw(新增 `OPENCLAW_CLI=none`)。 + +### 已知限制 + +- OAuth 与设备码登录(GitHub Copilot、OpenAI Codex、MiniMax OAuth)只透传官方流程,没有用真实账号验证过。 +- 硅基流动中国站的官方文档在核实时无法访问(502),接入参数来自国际站文档与兼容接口约定。 +- [#113](https://github.com/10000ge10000/luci-app-openclaw/issues/113) 的整机复位来自固件内核,插件无法修复,只能用预放置安装绕过。新版本组合尚未在这类设备上验证。 +- [#110](https://github.com/10000ge10000/luci-app-openclaw/issues/110) 中提到的「配对二维码」建议未包含在本次更新里。 +- `2026.10.9` 只在 macOS 上验证过微信插件 `2.4.10` 能加载,没有登录账号。 +- 回滚没有在真机上端到端执行。恢复逻辑用真实备份在隔离目录验证过,完整流程只跑过替身环境。升级成功后代码备份会被清理,此时回滚只恢复状态,不恢复旧版核心。 + +### 社区贡献与致谢 + +本次更新的问题和方案来自这些反馈,感谢: + +- [@KotobaWakusei](https://github.com/KotobaWakusei):在 [#113](https://github.com/10000ge10000/luci-app-openclaw/issues/113) 提供了 vendor 内核设备上安装触发复位的完整排查数据,以及可行的预放置方案,README 的预放置说明以此为基础。 +- [@qwedsazxcce](https://github.com/qwedsazxcce):在 [#110](https://github.com/10000ge10000/luci-app-openclaw/issues/110) 报告设备配对错误,并确认缺少 `timeout` 是原因。 +- [@pbclaodentm](https://github.com/pbclaodentm):在 [#108](https://github.com/10000ge10000/luci-app-openclaw/issues/108) 持续反馈 Web 控制台和全新部署的问题,并提供了安装日志。 +- [@LittleRabbit2023](https://github.com/LittleRabbit2023):在 [#109](https://github.com/10000ge10000/luci-app-openclaw/issues/109) 报告升级时的 npm 报错。 +- [@keyboydg](https://github.com/keyboydg):在 [#111](https://github.com/10000ge10000/luci-app-openclaw/issues/111) 提出备份下载与上传恢复的需求。 + ## [2.2.3] - 2026-09-11 本次发布适配 LuCI 深色/暗黑主题,修复状态面板、控制台、高级设置与微信插件页面在深色模式下浅色硬编码导致的背景不可见、对比度不足问题。 diff --git a/luci-app-openclaw/Makefile b/luci-app-openclaw/Makefile index 391327e6c..37c3bd60e 100644 --- a/luci-app-openclaw/Makefile +++ b/luci-app-openclaw/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-openclaw PKG_VERSION:=$(strip $(shell cat $(CURDIR)/VERSION 2>/dev/null || echo "1.0.0")) -PKG_RELEASE:=29 +PKG_RELEASE:=30 PKG_MAINTAINER:=10000ge10000 <10000ge10000@users.noreply.github.com> PKG_LICENSE:=GPL-3.0 @@ -74,6 +74,7 @@ define Package/$(PKG_NAME)/install $(INSTALL_DATA) ./luasrc/openclaw/paths.lua $(1)/usr/lib/lua/openclaw/paths.lua $(INSTALL_DATA) ./luasrc/openclaw/devices.lua $(1)/usr/lib/lua/openclaw/devices.lua $(INSTALL_DATA) ./luasrc/openclaw/console.lua $(1)/usr/lib/lua/openclaw/console.lua + $(INSTALL_DATA) ./luasrc/openclaw/backup.lua $(1)/usr/lib/lua/openclaw/backup.lua $(INSTALL_DIR) $(1)/usr/lib/lua/luci/model/cbi/openclaw $(INSTALL_DATA) ./luasrc/model/cbi/openclaw/basic.lua $(1)/usr/lib/lua/luci/model/cbi/openclaw/basic.lua $(INSTALL_DIR) $(1)/usr/lib/lua/luci/view/openclaw @@ -90,7 +91,10 @@ define Package/$(PKG_NAME)/install $(INSTALL_BIN) ./root/usr/share/openclaw/oc-config-interactive.js $(1)/usr/share/openclaw/oc-config-interactive.js $(INSTALL_DATA) ./root/usr/share/openclaw/oc-menu-engine.js $(1)/usr/share/openclaw/oc-menu-engine.js $(INSTALL_BIN) ./root/usr/share/openclaw/openclaw-package-contract.js $(1)/usr/share/openclaw/openclaw-package-contract.js - $(INSTALL_DATA) ./root/usr/share/openclaw/model-presets.json $(1)/usr/share/openclaw/model-presets.json + $(INSTALL_DATA) ./root/usr/share/openclaw/oc-providers.js $(1)/usr/share/openclaw/oc-providers.js + $(INSTALL_DATA) ./root/usr/share/openclaw/oc-config-fixups.js $(1)/usr/share/openclaw/oc-config-fixups.js + $(INSTALL_DATA) ./root/usr/share/openclaw/provider-catalog.json $(1)/usr/share/openclaw/provider-catalog.json + $(INSTALL_DATA) ./root/usr/share/openclaw/openclaw-compat.json $(1)/usr/share/openclaw/openclaw-compat.json $(INSTALL_DIR) $(1)/usr/share/openclaw/ui $(CP) ./root/usr/share/openclaw/ui/* $(1)/usr/share/openclaw/ui/ endef diff --git a/luci-app-openclaw/README.md b/luci-app-openclaw/README.md index 900aefd80..7c7bbb311 100644 --- a/luci-app-openclaw/README.md +++ b/luci-app-openclaw/README.md @@ -27,14 +27,34 @@ | 组件 | 默认版本 | 说明 | |------|----------|------| -| OpenClaw | `2026.9.1` | npm `latest` 稳定标签(Phase 0 锁定已测试版本;安装时阻断未知脚本,仅运行已校验 lifecycle) | -| Node.js | `22.23.2` | x86_64 与 aarch64 均优先从 unofficial-builds 及镜像源获取,自托管资产作为兜底;严格验证 `engines.node`,失败关闭 | -| 微信插件 | `@tencent-weixin/openclaw-weixin@2.4.8` | CLI 使用 `@tencent-weixin/openclaw-weixin-cli@2.1.4` | +| OpenClaw | `2026.9.36` | 默认通道 `extended-stable`(官方长期维护)。也可选 `latest` 通道(当前已验证 `2026.10.9`)或锁定具体版本,见下方「发布通道与版本」 | +| Node.js | `24.22.2` | 满足上述版本要求的 `>=24.22.1 <25 \|\| >=26.12.1`,也兼容旧版 `2026.9.1`。升级 OpenClaw 时先升级 Node.js;x86_64 与 aarch64 优先从官方、unofficial-builds 及镜像源获取,自托管资产作为兜底;严格校验 `engines.node`,失败即中止 | +| 微信插件 | `@tencent-weixin/openclaw-weixin@2.4.10` | CLI 使用 `@tencent-weixin/openclaw-weixin-cli@2.1.4` | ### 运行时与架构说明 -- **x86_64 / aarch64 (musl)**:Node.js v22.23.2 预编译运行时优先通过上游 [unofficial-builds](https://unofficial-builds.nodejs.org/) 及 npmmirror 镜像源获取,安装时自动下载并强校验 `SHASUMS256.txt`。同时在 `.github/workflows/build-node-musl.yml` 中保留基于 Alpine LTS 的自托管构建打包流程作为兜底源。 +- **x86_64 / aarch64 (musl)**:Node.js v24.22.2 优先通过上游 [unofficial-builds](https://unofficial-builds.nodejs.org/) 及 npmmirror 镜像源获取,安装时自动下载并强校验 `SHASUMS256.txt`。依赖只有系统 musl 和 `libgcc`。Release 中的 `node-bins` 资产是同一文件的兜底镜像,由 `.github/workflows/build-node-musl.yml` 校验哈希后原样转发。 +### 发布通道与版本 + +插件把「上游已发布」和「本插件已验证」分开记录。上游版本通过 npm dist-tags 实时查询;已验证版本写在 `openclaw-compat.json`,包含 `engines`、生命周期脚本和 tarball `integrity`,安装时逐项比对。 + +| 选项 | 安装什么 | +|------|----------| +| `extended-stable`(默认) | 该通道的已验证版本,只接收修复,适合路由器长期运行 | +| `latest` | 该通道的已验证版本,功能更新快 | +| 锁定版本 | 指定版本,配置项 `openclaw.main.pin_version` | +| 允许安装未验证版本 | 上游最新版或矩阵外的锁定版本。生命周期脚本必须与已验证版本一致,否则仍会拒绝。兼容性由你自行承担 | + +上游有更新但插件还没验证时,「检测升级」只提示,不会自动安装。在 LuCI 基本设置里选择,或使用命令行: + +```bash +OC_CHANNEL=latest openclaw-env upgrade +OC_VERSION=2026.9.36 openclaw-env upgrade +OC_CHANNEL=latest OC_ALLOW_UNVERIFIED=1 openclaw-env upgrade +``` + +版本的验证范围记录在 `openclaw-compat.json` 的 `verification` 字段里,`device` 为 `null` 表示还没有真机验证记录。 ## 📦 安装 @@ -94,9 +114,12 @@ find bin/ -name "luci-app-openclaw*.ipk" 1. 打开 LuCI → 服务 → OpenClaw,点击「安装运行环境」 2. 安装完成后服务会自动启动,点击「刷新页面」查看状态 -3. 进入「Web 控制台」添加 AI 模型和 API Key +3. 进入「Web 控制台」,点击「在新窗口打开控制台」添加 AI 模型和 API Key 4. 进入「配置管理」可使用向导配置消息渠道 +> OpenClaw 官方控制台响应头固定为 `X-Frame-Options: DENY`,浏览器不允许把它嵌入 LuCI 页面,所以只能在独立窗口中打开。 +> 首次打开若提示需要设备配对,回到 LuCI「Web 控制台」页点击「📱 设备配对」批准,再刷新控制台窗口即可。 + ## 自定义安装路径 UCI 字段仍然是 `openclaw.main.install_path`,语义为基础目录。例如: @@ -111,8 +134,42 @@ openclaw-env setup 安装前会执行写入探针;如果 overlay 已满、只读或外置盘未正确挂载,安装会在下载前失败并给出明确日志。 +## 预放置安装(安装时整机重启的设备) + +部分采用厂商闭源内核的固件(例如 #113 中 IPQ60xx + Kwrt 的京东云亚瑟)在短时间内大量写盘时会被硬件看门狗复位。在线安装需要下载、解压 Node.js 并安装约 3 万个依赖文件,正好会触发这个问题,表现为安装一半路由器重启。这是内核层面的问题,插件无法从根本上修复。对这类设备,可以在电脑上准备好文件,限速拷贝到路由器,再让插件只做初始化。 + +**1. 选择存储位置**:优先选独立的 ext4 分区(如 eMMC 上的 `/opt`)。不要选 tmpfs 或 overlay。#113 的实测中,USB 盘上的 ext4 因日志和元数据写放大,反而比 eMMC 更容易触发复位。 + +**2. 在电脑上准备文件**(以 arm64 musl 设备为例,版本与插件默认版本保持一致。`2026.9.36` 解压后约 390MB,请预留足够空间): + +```bash +# Node.js 运行时:musl 设备使用 unofficial-builds 的 musl 构建 +curl -LO https://unofficial-builds.nodejs.org/download/release/v24.22.2/node-v24.22.2-linux-arm64-musl.tar.xz +tar xJf node-v24.22.2-linux-arm64-musl.tar.xz && mv node-v24.22.2-linux-arm64-musl node + +# OpenClaw 及其依赖:按目标平台解析依赖树 +npm install -g openclaw@2026.9.36 --prefix ./global \ + --os=linux --cpu=arm64 --libc=musl --ignore-scripts --omit=optional +``` + +**3. 限速拷贝到路由器**:目录结构必须是 `<安装目录>/openclaw/node/bin/node` 和 `<安装目录>/openclaw/global/lib/node_modules/openclaw/openclaw.mjs`(插件按这两个路径识别已安装的运行环境)。拷贝时请限速,例如 `scp -l 16000 -r`(约 2MB/s);路由器的 SSH 不支持 SFTP 时加 `-O`。不要先拷贝压缩包再到路由器上解压,解压本身就会触发复位。 + +**4. 在路由器上完成初始化**: + +```bash +uci set openclaw.main.install_path='/opt' +uci commit openclaw +openclaw-env setup +``` + +`setup` 识别到程序文件已就位、但还没有 `openclaw.json` 时,只会创建 `openclaw` 系统用户、校验 Node.js 版本、执行初始化,不下载、不解压任何文件。完成后在 LuCI → 服务 → OpenClaw 中启用服务,首次启动时会生成 `openclaw.json`。 + +> 限制说明:在线安装会在受控环境中执行经过校验的 OpenClaw `preinstall` / `postinstall` 脚本,预放置时用的是 `--ignore-scripts`,这两步不会执行。#113 的实测组合是 Node.js 22 + OpenClaw 2026.9.1,这个组合 `setup` 仍然接受;上面的新版本组合还没有在这类设备上验证过,请自行确认。`openclaw-env setup-offline <目录>` 是给 `.run` 离线安装器用的,它仍会在路由器上解压压缩包,不适合这类设备。 + ## 微信插件依赖 +官方插件只声明了核心版本下限,旧插件在新核心上能装上但渠道会反复崩溃,所以每个已验证核心要求的最低插件版本记录在 `openclaw-compat.json` 里。`2026.9.x` 要求 `2.4.9` 及以上,`2026.10.9` 要求 `2.4.10` 及以上。升级核心时会自动收敛,结果见升级报告的 `WECHAT:` 一行。 + 微信渠道安装前会检查: - `openclaw` 系统用户是否存在,不存在时自动创建 @@ -145,18 +202,46 @@ opkg install python3-light - 插件注册缺失:重新安装插件,启动时也会自动补齐 `openclaw-weixin` 配置。 - 目录权限错误:按页面日志提示修复 OpenClaw 数据目录权限。 -## AI 模型选择机制 +## AI 模型配置 -配置菜单里的模型清单分三层,避免硬编码的模型 ID 随上游迭代而过期: +LuCI →「配置管理」里的交互式菜单和无 TTY 时的传统 Shell 菜单功能相同,共用同一个后端(`oc-providers.js`)和同一份目录(`provider-catalog.json`)。 -1. **精选模型** —— 内置于 `model-presets.json`,每个 Provider 给推荐/最强/均衡/快速四档。 - 离线可用、首屏即显示。 -2. **从 OpenClaw 获取完整模型列表** —— 调用 `openclaw models list --provider ` - 动态发现当前实际可用的模型(带 6 秒超时,失败或超时自动回落到精选列表)。 - 未安装对应 Provider 插件、或未配置 API Key 时可能返回空,属正常情况。 -3. **手动输入模型 ID** —— 永久保留。上游发布新模型时无需等待本插件更新。 +| 按键 | 功能 | +|------|------| +| `q` | 常用 Provider 快速配置 | +| `1` | 官方 API(按量付费):OpenAI、Claude、Gemini、DeepSeek、千问、智谱等 | +| `2` | 订阅套餐、Coding Plan、账号登录:百炼、GLM、Kimi Code、Copilot 等 | +| `3` | 聚合平台与自定义 API:OpenRouter、硅基流动、OpenAI / Anthropic 兼容接口 | +| `4` | 本地与局域网模型:Ollama、vLLM、LM Studio | +| `5` | 查看模型与认证状态(已脱敏) | +| `6` / `7` | 切换默认模型 / 管理备用模型(fallback) | +| `8` | 连通性测试 | +| `9` | 删除凭据或 Provider | +| `w` | 官方完整配置向导 | -因此 GPT / Claude / Gemini 等更新后,通常不必升级本插件即可使用新模型。 +选模型时有三个来源,上游发布新模型后通常不必升级插件: + +1. **精选模型**:内置在目录里,离线可用,并注明核实时依据的 OpenClaw 版本。 +2. **从 OpenClaw 获取完整列表**:调用 `openclaw models list`,失败时依次回落到 `--refresh` 和本地缓存,并显示真实原因。 +3. **手动输入模型 ID**:始终保留。 + +使用时注意: + +- **能查到模型不等于账号有权调用。** 列表只代表上游登记过这个模型,要确认账号能否调用请用连通性测试。它会发出真实请求,可能消耗少量 token。Gateway 运行时走 `models.probe`,不会中断服务。 +- **凭据不写入 `auth-profiles.json`。** 该文件已被上游退役,Key 由官方 CLI 存入 `state/openclaw.sqlite`。Key 通过环境变量或标准输入传递,不进命令行参数,也不显示在菜单输出里。自定义 Provider 的 Key 按上游设计以明文保存在 `openclaw.json` 的 `models.providers` 下,备份文件里也有,请妥善保管。 +- **不会覆盖已有配置。** 新增 Provider 默认保留当前默认模型,已有凭据原地更新。同一 ID 的自定义 Provider 只追加模型,Base URL 不同时拒绝。配置文件损坏时直接中止,原文件不变。 +- **部分 Provider 需要官方插件。** 菜单会先询问,同意后才安装 `@openclaw/*-provider`。 +- **OAuth 和设备码登录**(Copilot、Codex、MiniMax)只透传官方流程,维护者没有用真实账号验证过,菜单里有标注。 + +## 备份与恢复 + +LuCI → 服务 → OpenClaw →「💾 备份/恢复」: + +- **创建**:「仅配置文件」只包含 `openclaw.json`(模型、渠道、插件设置);「配置 + 状态数据」还包含会话等状态数据。 +- **下载**:列表中每份备份都可以下载到电脑保存。备份里有 API Key,请妥善保管。 +- **上传与恢复**:重装或配置损坏后,上传之前下载的 `*-openclaw-backup.tar.gz`(上限 20MB),再在列表中点「恢复」即可。恢复前,当前配置会另存为 `openclaw.json.pre-restore`。 + +恢复只会写入 OpenClaw 状态目录(如 `/opt/openclaw/data/.openclaw`)。上传的备份不能包含状态目录之外的文件或链接,否则会被拒绝。 ## 已知说明 @@ -192,14 +277,18 @@ luci-app-openclaw/ │ ├── oc-config.sh # 配置管理入口(无 TTY 时的回退菜单) │ ├── oc-config-interactive.js # 交互式配置菜单(默认路径) │ ├── oc-menu-engine.js # 方向键菜单引擎 -│ ├── model-presets.json # 精选模型预设(shell 与 JS 共读) +│ ├── oc-providers.js # 模型与凭据配置后端(两套菜单共用) +│ ├── oc-config-fixups.js # 启动和升级前迁移旧版配置字段 +│ ├── provider-catalog.json # Provider 目录与精选模型(唯一数据源) +│ ├── openclaw-compat.json # 发布通道与已验证版本矩阵 +│ ├── openclaw-package-contract.js # 安装包与运行时契约校验 │ ├── web-pty.js # Web PTY 服务 │ └── ui/ # 配置终端前端资源 ├── scripts/ │ ├── build_ipk.sh # 本地 IPK 构建 │ ├── build_run.sh # .run 安装包构建 │ ├── gen-release-body.sh # Release 说明生成 -│ └── build-node-musl.sh # 编译 Node.js musl 静态链接版本 +│ └── build-node-musl.sh # 转发并校验 Node.js arm64 musl 兜底资产 ├── tests/ # 契约测试(sh tests/run_all.sh) └── .github/workflows/ ├── build.yml # 在线构建 + 发布 diff --git a/luci-app-openclaw/VERSION b/luci-app-openclaw/VERSION index 585940699..276cbf9e2 100644 --- a/luci-app-openclaw/VERSION +++ b/luci-app-openclaw/VERSION @@ -1 +1 @@ -2.2.3 +2.3.0 diff --git a/luci-app-openclaw/docs/RELEASE_CHECK_2.3.0.md b/luci-app-openclaw/docs/RELEASE_CHECK_2.3.0.md new file mode 100644 index 000000000..4a1b2a14c --- /dev/null +++ b/luci-app-openclaw/docs/RELEASE_CHECK_2.3.0.md @@ -0,0 +1,157 @@ +# luci-app-openclaw 2.3.0 发版检查报告(草稿) + +检查日期:2026-11-02。分支 `fix/v2.2.4-maintenance`,基于 v2.2.3,改动尚未提交。 + +**结论:代码和验证已基本就绪,还差你确认后才能发布。** 真机验证查出并修复了 5 个阻断问题,其中 3 个都会让回滚失败。发布前还差微信扫码后的收发确认,以及 CI 上的 ARM64 Node 验证。回滚没有在真机端到端执行,见第 3 节。 + +## 1. 版本建议 + +建议 `2.3.0`,而不是 `2.2.4`。默认 OpenClaw 和 Node.js 都换了大版本线,模型菜单整体重写,用户升级核心时的行为也变了。 + +| 组件 | v2.2.3 | 本次默认 | +|------|--------|----------| +| OpenClaw | 2026.9.1 | 2026.9.36(extended-stable),可选 2026.10.9(latest) | +| Node.js | 22.23.2 | 24.22.2 | + +上游状态(2026-11-01 查询):npm `latest` 2026.10.9,`extended-stable` 2026.9.36。 + +## 2. 已通过的验证 + +### 自动化测试(本机 macOS x86_64) + +`sh tests/run_all.sh`:36 项全部通过(32 个 Shell、4 个 Lua)。 + +- 本机没有 Lua 和 xz。Lua 5.1.5 编译在临时目录,xz 用 Python lzma 垫片代替,均未安装到系统。 +- 新增 `test_provider_backend`、`test_provider_catalog_contract`、`test_config_fixups`、`test_channel_selection`,并移除被取代的 `test_model_presets_contract`。 +- 变异验证:向后端、菜单和 Node 校验注入 13 种缺陷(Key 进 argv、不脱敏、清空已有 Key、不恢复默认模型、不回滚、菜单漂移等),测试全部能抓到。 +- 新增 `test_wechat_plugin_convergence`,再注入 9 种缺陷(版本下限回退、安装失败报成功、只看退出码不核对版本等),也全部能抓到。这个测试还抓到一个真问题:版本号校验写得太宽,带 `;` 的字符串能通过并拼进 shell 命令,已修复。 +- 新增 `test_state_restore_fidelity`,并为 `test_openclaw_upgrade_state` 增加完整回滚用例,又注入 7 种缺陷,全部能抓到。第一轮漏了一种(旧 workspace 被嵌套进恢复目录),补断言后抓到。 +- 新增 `test_config_backup_retention`,注入 5 种缺陷,全部能抓到;清理命令也在设备的 BusyBox 上实测过。 +- CI 尚未运行这一版。 + +### 真实上游 CLI 冒烟(本机隔离目录,假 Key,不发真实模型请求) + +2026.9.36 与 2026.10.9 各 20 项,共 40 项全部通过:恢复出厂的 onboard 参数;插件写入的配置键通过 schema 校验;Gateway 真实启动;上游拒绝旧配置,迁移后通过且值不丢;自定义 Provider 新增、追加、冲突;保留默认模型;原地更新 Key;输出不含 Key;不写 `auth-profiles.json`;逐个登出。 + +### 2026-11-01 运行、未在最终代码上重跑的项目 + +相关脚本已随临时目录清除。 + +| 项目 | 结果 | 范围 | +|------|------|------| +| 35 个 auth-choice 与目录核对 | 213 项通过 | 两个版本 | +| 后端真实 CLI 用例 | 30/30 | 两个版本 | +| Shell 菜单端到端 | 10/10 | 两个版本 | +| 交互式菜单 expect 流程 | 通过 | 仅 2026.10.9 | +| 连通性测试真实请求返回可用 | 通过 | 用户提供的测试 API | + +### 微信插件加载(本机隔离目录,核心 2026.10.9,未登录微信) + +| 插件版本 | 结果 | +|---------|------| +| 2.4.10 | `plugins inspect --runtime` 为 `loaded`,Gateway 正常启动 | +| 2.4.8 | `status: error`,`cannot import openclaw/plugin-sdk/infra-runtime` | + +### 打包 + +| 路径 | 结果 | +|------|------| +| `.ipk` | 本机实际构建,新增文件齐全,与源文件逐字节一致 | +| `.run` | 本机实际构建(`sed -i` 是 GNU 语法,用垫片绕过),归档内文件齐全 | +| feeds / SDK | **只做了静态清单检查,没有实际编译** | + +### 其它检查 + +Shell、JS、Lua、JSON 语法全部通过;改动文件均为 UTF-8、LF;敏感信息扫描未发现 Key、Token 或内网地址;`.gitignore` 未改动;暂存区为空。 + +## 3. 真机验证 + +环境:Raspberry Pi 4(1.9GB 内存)、Kwrt 25.12.5、aarch64 musl。这是一台在跑业务的设备,验证前已备份。按授权范围只做一次核心升级,没有跑全量测试,也没有做压力复现。 + +### 通过 + +| 项目 | 结果 | +|------|------| +| 插件 2.0.11 → 2.3.0,核心不动 | 配置只有 `allowInsecureAuth` 被移除,Gateway、PTY、微信正常 | +| 注入错误 integrity | Node 24.22.2 下载校验通过;OpenClaw 包被拒绝,核心未变,Gateway 未中断 | +| 核心 2026.6.11 → 2026.9.36,Node 22 → 24 | `RESULT: success`,备份校验、Doctor、Gateway 全部通过,用时 678 秒,无重启 | +| 迁移后的数据 | `config validate` 通过;已配对设备保留;API Key 迁入 `openclaw.sqlite`;Token 哈希不变 | +| 服务启动期间断电 | 重启后服务自动恢复,`openclaw.sqlite` quick_check 为 ok,配置可解析(用户操作电源时碰巧发生) | +| 备份创建与恢复(隔离目录) | 升级前的真实归档校验、恢复通过,907 个文件中 904 个与对照一致,其余差异来自对照副本早生成约 15 分钟;2026.9.36 生成的归档校验、恢复通过 | +| root 运行模型配置 | 自定义 Provider 新增、保留默认模型、真实请求可用;错误 Key 报认证失败;删除后 Provider 列表和默认模型还原;文件属主仍为 `openclaw`;配置与日志中没有 Key | +| 两套菜单(真实终端) | 进入模型菜单、查看状态、提供商列表、返回、退出;内容一致,默认模型和凭据显示正确,配置哈希不变 | +| LuCI 接口与页面 | `status_api`、`check_update`、`wechat_status`、`check_system`、`devices_list` 正常;`basic`、`advanced`、`wechat`、`console` 均返回 200,没有 Lua 错误 | + +升级峰值:温度 54°C,负载 4.44,磁盘余量最低约 493MB。 + +删除临时 Provider 后,配置里多了上游 `onboard` 写入的 `gateway.tailscale: {"mode":"off"}`,`wizard.*` 也更新了。这是默认值,不影响运行,没有改回去。 + +### 真机查出的问题 + +**微信渠道崩溃循环(已修复)。** 升级后 Gateway 健康,`plugins list` 显示 `loaded`,渠道却反复退出:`Package subpath './plugin-sdk/channel-runtime' is not defined by "exports"`。旧流程不检查插件版本,报告还写着成功。现在兼容矩阵按核心版本记录最低插件版本,升级时自动收敛并写入报告。 + +**静态判断曾经出错。** 修复前我根据导入分析认为 2026.10.9 与所有微信插件版本都不兼容。这是误判:2.4.10 里的 `channel-message` 是捕获异常后才用的旧路径常量。查了官方 changelog 又实测加载后才改正,同时发现 2.4.9 修过 2026.9.x 的入站回复错误,所以 2.4.8 也不够。 + +另外 4 个问题也来自真机,详见 CHANGELOG「修复」:回滚杀死自身(BusyBox 上 `exit=143`)、`2026.9.36` 上备份失败、装过 npm 插件的备份恢复不了、恢复备份删除工作区。前三个的本地和隔离验证已补,真实归档验证通过。交互式菜单退出卡住是额外发现的小问题,也已修复。 + +### 未通过或未确认 + +- **微信消息收发。** 插件升到 2.4.10 后渠道能启动,不再崩溃,但服务端返回 token 失效,重启后仍是如此,需要重新扫码才能确认收发。账号文件的修改时间还是 7 月,升级没有动过它。此前 2.4.6 运行的 15 分钟内日志没有这条错误,所以失效是约 20 分钟的渠道中断造成的,还是版本变化造成的,无法区分。这期间 `wechat_status` 仍返回 `logged_in: true`,LuCI 不会提示。 +- **真机端到端回滚没有执行。** 按「只做一次升级」的授权,没有制造第二次核心安装。升级成功后代码备份已清理,在这台设备上回滚只会得到新核心加旧状态,没有验证价值。已验证的部分:恢复逻辑用真实归档在隔离目录跑通,完整流程在替身环境跑通。没有覆盖的部分:Doctor 或 Gateway 失败后,真机上恢复代码备份并重启。 +- **两次未归类的整机复位。** 11-01(跑完整测试时)和 11-02 约 11:22(设备空闲)的寄存器值已被覆盖。11-02 14:26 的一次已确认是用户操作电源:`PM_RSTS` 为 `0x1000`(上电复位),没有看门狗标志。此后约 47 分钟的采样里 `get_throttled` 始终为 `0x0`,这段时间没有欠压,但不能说明高负载时的情况。这版固件没有 `rpi_volt` 驱动,内核日志不会记录欠压,`get_throttled` 重启后也会清零,排查时不要依赖日志。 + +### 还没做 + +1. 浏览器里的实际显示(通道选择、升级提示)。接口和 HTML 已验证,版式需要你看一眼 +2. 菜单内的写入操作。写入逻辑走同一后端,真机上验证的是后端,菜单只做了查看 +3. `--omit=optional` 在 musl 上是否影响新版功能 + +## 4. 其它未验证项 + +- **`BUILD_MODE=unofficial`**:本机没有 Docker,只验证了下载、SHA256 匹配和 ELF 依赖(只需 musl 与 libgcc),容器内运行步骤要等 CI。 +- **x86_64 Linux(glibc 与 musl)**:没有实际设备或容器验证。 +- **OAuth 与设备码登录**:没有真实账号,菜单已标注未验证。 +- **目录中大多数 Provider 的真实调用**:只核对了 auth-choice、环境变量和配置结果,没有各家的真实 Key。真实调用只验证过一个 OpenAI 兼容接口。 +- **#113 类设备**:新版本组合未验证。 +- **#108 中的「全新部署难以启动」**:报告者的具体环境没有复现,不能断言已解决,请不要据此关闭。 + +## 5. 主要风险 + +| 风险 | 影响 | 缓解 | +|------|------|------| +| 新版体积变大(9.36 解压约 390MB) | 小容量设备空间不足,低端设备升级更慢 | 安装前有磁盘检查,升级失败保留原安装;需真机确认 | +| 上游未来再改 schema | 新版本可能拒绝启动 | 默认只装已验证版本 | +| Fixups 在所有版本上运行 | 旧配置字段被改写 | 只改三个路径,解析失败不写入,已测 | +| 自定义 Provider 的 Key 明文存于配置 | 备份外泄即泄露 | 上游设计,README 已说明 | +| 升级中途断电或复位 | 事务可能停在中间状态 | 启动期断电已实测恢复;升级事务中途断电未测 | +| 用户升级后微信需要重新扫码 | 渠道收不到消息 | 尚不确定是否必现,需要重新扫码后观察 | + +## 6. 发布前待办 + +需要你确认后才执行: + +- [ ] 重新扫码后确认微信收发 +- [x] 这台设备保留新版本(用户 2026-11-02 确认)。正式发布后再升级到正式包,因为当前是 Mac 上构建的测试包 +- [ ] 决定是否找一台没有业务的设备,执行端到端回滚 +- [ ] 手动运行 `build-node-musl.yml`,把 `node-v24.22.2-linux-arm64-musl.tar.xz` 发布到 `node-bins`,否则 unofficial-builds 和 npmmirror 都不可达时没有兜底 +- [ ] 提交并推送分支,确认 CI 通过(含 `BUILD_MODE=unofficial`) +- [ ] 把 CHANGELOG 的 `[Unreleased]` 改为 `[2.3.0] - 日期`。`build.yml` 按此标题提取发布说明,未改名会显示「暂无更新日志」 +- [ ] 剩余验证完成后再更新一次 CHANGELOG「已知限制」和 `verification.device` +- [ ] 合并 main、打 tag、发布 +- [ ] 回复并关闭相关 Issue。发布说明里的 `@用户名` 会通知到对应用户 +- [ ] 删除 #109 中重复的评论(待你批准) + +## 7. 社区致谢核对 + +Issue 作者均通过 GitHub API 核实(2026-11-02)。 + +| Issue | 作者 | 状态 | +|-------|------|------| +| [#108](https://github.com/10000ge10000/luci-app-openclaw/issues/108) | pbclaodentm | open | +| [#109](https://github.com/10000ge10000/luci-app-openclaw/issues/109) | LittleRabbit2023 | open | +| [#110](https://github.com/10000ge10000/luci-app-openclaw/issues/110) | qwedsazxcce | open | +| [#111](https://github.com/10000ge10000/luci-app-openclaw/issues/111) | keyboydg | open | +| [#113](https://github.com/10000ge10000/luci-app-openclaw/issues/113) | KotobaWakusei | open | + +- v2.2.3 之后没有已合并的 PR。[#104](https://github.com/10000ge10000/luci-app-openclaw/pull/104)(gershwin97)的修复在 v2.2.3 之前已整合,#107 也已随 v2.2.3 发布,均未列入本次致谢。 +- qibaoxiaotai-beep 在 #108 的评论是使用建议,不涉及本次改动,未列入。如需致谢请告诉我。 diff --git a/luci-app-openclaw/luasrc/controller/openclaw.lua b/luci-app-openclaw/luasrc/controller/openclaw.lua index c0273b964..be4ebde44 100644 --- a/luci-app-openclaw/luasrc/controller/openclaw.lua +++ b/luci-app-openclaw/luasrc/controller/openclaw.lua @@ -3,6 +3,7 @@ module("luci.controller.openclaw", package.seeall) local paths_ok, oc_paths = pcall(require, "openclaw.paths") local devices_ok, oc_devices = pcall(require, "openclaw.devices") +local backup_ok, oc_backup = pcall(require, "openclaw.backup") local function shellquote(value) if paths_ok and oc_paths.shellquote then @@ -77,6 +78,49 @@ local function is_newer_version(latest, current) return latest ~= nil and current ~= nil and latest ~= "" and current ~= "" and compare_versions(latest, current) > 0 end +-- 解析安装/升级的版本选择。 +-- channel: extended-stable / latest (空 = UCI 中保存的通道);pin: 精确版本或空; +-- legacy_version: 旧前端的 version 参数 (stable / latest / x.y.z);allow: "1" 允许未验证版本。 +-- 返回 { channel, pin, allow } 或 nil, 错误信息。所有值都在这里白名单校验,后续才拼入 shell。 +local function parse_version_selection(channel, pin, legacy_version, allow) + channel = tostring(channel or ""):gsub("%s+", "") + pin = tostring(pin or ""):gsub("%s+", ""):gsub("^v", "") + legacy_version = tostring(legacy_version or ""):gsub("%s+", "") + if channel == "" and legacy_version ~= "" then + if legacy_version == "stable" then + channel = "" + elseif legacy_version == "latest" or legacy_version == "extended-stable" then + channel = legacy_version + else + pin = legacy_version:gsub("^v", "") + end + end + if channel == "stable" then channel = "" end + if channel == "" then + local uci = require "luci.model.uci".cursor() + channel = uci:get("openclaw", "main", "channel") or "" + end + if channel == "" then channel = "extended-stable" end + if channel ~= "extended-stable" and channel ~= "latest" then + return nil, "未知的发布通道: " .. channel + end + if pin ~= "" and not pin:match("^%d+%.%d+%.%d+$") then + return nil, "锁定版本格式无效,应为 2026.9.36 这样的版本号" + end + return { channel = channel, pin = pin, allow = (tostring(allow or "") == "1") } +end + +local function version_env_prefix(sel) + local prefix = "OC_CHANNEL=" .. shellquote(sel.channel) .. " " + if sel.pin ~= "" then + prefix = prefix .. "OC_VERSION=" .. shellquote(sel.pin) .. " " + end + if sel.allow then + prefix = prefix .. "OC_ALLOW_UNVERIFIED=1 " + end + return prefix +end + local function fix_openclaw_state_permissions(oc_data) local sys = require "luci.sys" local state_dir = tostring(oc_data or "") .. "/.openclaw" @@ -223,7 +267,7 @@ local function wechat_openclaw_plugin_install_cmd(install_path, oc_entry, log_fi "OPENCLAW_SUPERVISOR_MODE=external OPENCLAW_SERVICE_REPAIR_POLICY=external " .. "NODE_ICU_DATA=" .. install_path .. "/node/share/icu NPM_CONFIG_CACHE=$OC_WECHAT_DATA/.npm npm_config_cache=$OC_WECHAT_DATA/.npm TMPDIR=$OC_WECHAT_DATA/.tmp " .. "PATH=" .. install_path .. "/node/bin:" .. install_path .. "/global/bin:$PATH " .. - "\"$OC_WECHAT_NODE\" \"$OC_WECHAT_ENTRY\" plugins install --force --pin @tencent-weixin/openclaw-weixin@2.4.8' >> " .. shellquote(log_file) .. " 2>&1; " .. + "\"$OC_WECHAT_NODE\" \"$OC_WECHAT_ENTRY\" plugins install --force --pin @tencent-weixin/openclaw-weixin@2.4.10' >> " .. shellquote(log_file) .. " 2>&1; " .. "RC=$?; echo $RC > " .. shellquote(exit_file) .. "; " .. "if [ $RC -eq 0 ]; then echo '✅ OpenClaw 插件索引注册完成' >> " .. shellquote(log_file) .. "; else echo '❌ OpenClaw 插件安装失败 (exit: '$RC')' >> " .. shellquote(log_file) .. "; fi; " end @@ -736,25 +780,17 @@ function action_service_ctl() elseif action == "setup" then -- 先清理旧日志和状态 sys.exec("rm -f /tmp/openclaw-setup.log /tmp/openclaw-setup.pid /tmp/openclaw-setup.exit") - -- 获取用户选择的版本 (stable=指定版本, latest=最新版) - local version = http.formvalue("version") or "" - -- 获取自定义安装路径 (用户输入的是基础路径,如 /opt 或 /mnt/data) + -- 发布通道 / 锁定版本 / 是否允许未验证版本 (旧版前端只传 version=stable|latest|) local install_path = http.formvalue("install_path") or "" - local env_prefix = "" - if version == "stable" then - -- 稳定版: 读取 openclaw-env 中定义的 OC_TESTED_VERSION - local tested_ver = sys.exec("grep '^OC_TESTED_VERSION=' /usr/bin/openclaw-env 2>/dev/null | cut -d'\"' -f2"):gsub("%s+", "") - if tested_ver ~= "" then - env_prefix = "OC_VERSION=" .. shellquote(tested_ver) .. " " - end - elseif version == "latest" then - env_prefix = "OC_VERSION='latest' " - elseif version ~= "" and version ~= "latest" then - -- 校验版本号格式 (仅允许数字、点、横线、字母) - if version:match("^[%d%.%-a-zA-Z]+$") then - env_prefix = "OC_VERSION=" .. shellquote(version) .. " " - end + local sel, err = parse_version_selection(http.formvalue("channel"), http.formvalue("pin"), http.formvalue("version"), http.formvalue("allow_unverified")) + if not sel then + http.prepare_content("application/json") + http.write_json({ status = "error", message = err }) + return end + -- 安装时记住所选通道与锁定版本,后续「检测升级」与升级按同一策略执行 + sys.exec("uci set openclaw.main.channel=" .. shellquote(sel.channel) .. "; uci set openclaw.main.pin_version=" .. shellquote(sel.pin) .. "; uci commit openclaw 2>/dev/null") + local env_prefix = version_env_prefix(sel) -- 处理自定义安装路径 if install_path ~= "" then local normalized = normalize_install_base(install_path) @@ -776,15 +812,13 @@ function action_service_ctl() elseif action == "upgrade" then -- 先清理旧日志和状态 sys.exec("rm -f /tmp/openclaw-setup.log /tmp/openclaw-setup.pid /tmp/openclaw-setup.exit") - local target_ver = http.formvalue("version") or "" - local env_prefix = "" - if target_ver == "latest" then - env_prefix = "OC_VERSION='latest' " - elseif target_ver ~= "" and target_ver ~= "stable" then - if target_ver:match("^[%d%.%-a-zA-Z]+$") then - env_prefix = "OC_VERSION=" .. shellquote(target_ver) .. " " - end + local sel, err = parse_version_selection(http.formvalue("channel"), http.formvalue("pin"), http.formvalue("version"), http.formvalue("allow_unverified")) + if not sel then + http.prepare_content("application/json") + http.write_json({ status = "error", message = err }) + return end + local env_prefix = version_env_prefix(sel) -- 异步执行 openclaw-env upgrade 事务状态机 sys.exec("( " .. env_prefix .. "/usr/bin/openclaw-env upgrade > /tmp/openclaw-setup.log 2>&1; echo $? > /tmp/openclaw-setup.exit ) & echo $! > /tmp/openclaw-setup.pid") http.prepare_content("application/json") @@ -906,34 +940,32 @@ function action_check_update() end -- 2. OpenClaw 核心版本检查 - local openclaw_current = "" - local openclaw_latest = "" - local openclaw_has_update = false - - local oc_check_str = sys.exec("/usr/bin/openclaw-env check 2>/dev/null | grep -i 'OpenClaw:' | head -1"):gsub("%s+", "") - if oc_check_str ~= "" then - openclaw_current = oc_check_str:match("[vV]?([%d%.%-]+)") or "" - end - if openclaw_current == "" or openclaw_current == "未安装" then - -- 备用路径读取: 从 package.json 探测 - local candidate_pkg = "/opt/openclaw/global/lib/node_modules/openclaw/package.json" - local f_pkg = io.open(candidate_pkg, "r") - if f_pkg then - local content = f_pkg:read("*a") - f_pkg:close() - openclaw_current = content:match('"version"%s*:%s*"([^"]+)"') or "" + -- openclaw-env versions 同时给出「本插件已验证」(兼容矩阵) 与「上游已发布」(npm dist-tags); + -- 是否提示升级只看已验证版本,上游更新的版本仅作提示,不会被当作可安全升级。 + local openclaw_current, openclaw_latest, openclaw_has_update = "", "", false + local openclaw_channel, openclaw_upstream, openclaw_upstream_newer, openclaw_pinned = "", "", false, "" + local openclaw_upstream_error = false + local vjson = sys.exec("/usr/bin/openclaw-env versions 2>/dev/null") + local ok_j, jsonc = pcall(require, "luci.jsonc") + local v = (ok_j and vjson and vjson ~= "") and jsonc.parse(vjson) or nil + if type(v) == "table" then + openclaw_current = v.installed or "" + openclaw_channel = v.channel or v.defaultChannel or "" + openclaw_pinned = v.pin or "" + local ch = type(v.channels) == "table" and v.channels[openclaw_channel] or nil + if type(ch) == "table" then + openclaw_latest = ch.verified or "" + openclaw_upstream = ch.upstream or "" + openclaw_upstream_newer = ch.upstreamNewer == true end + openclaw_upstream_error = (v.upstreamOk ~= true) end - - -- 推荐/已验证的最新稳定版本 - local tested_ver = sys.exec("sed -n 's/^OC_TESTED_VERSION=\"\\(.*\\)\"/\\1/p' /usr/bin/openclaw-env 2>/dev/null"):gsub("%s+", "") - if tested_ver == "" then tested_ver = "2026.9.1" end - openclaw_latest = tested_ver - - if openclaw_current ~= "" and openclaw_current ~= "未安装" and openclaw_latest ~= "" then - if is_newer_version(openclaw_latest, openclaw_current) then - openclaw_has_update = true - end + if openclaw_pinned == "" and openclaw_current ~= "" and openclaw_latest ~= "" then + openclaw_has_update = is_newer_version(openclaw_latest, openclaw_current) + end + -- 上游比已验证版本更新,但当前安装就是上游版本时不再提示 + if openclaw_upstream_newer and openclaw_upstream == openclaw_current then + openclaw_upstream_newer = false end http.prepare_content("application/json") @@ -945,7 +977,12 @@ function action_check_update() release_notes = release_notes, openclaw_current = openclaw_current, openclaw_latest = openclaw_latest, - openclaw_has_update = openclaw_has_update + openclaw_has_update = openclaw_has_update, + openclaw_channel = openclaw_channel, + openclaw_pinned = openclaw_pinned, + openclaw_upstream = openclaw_upstream, + openclaw_upstream_newer = openclaw_upstream_newer, + openclaw_upstream_error = openclaw_upstream_error }) end @@ -985,7 +1022,8 @@ function action_uninstall() sys.exec("for p in " .. port .. " " .. pty_port .. "; do pid=$(netstat -tulnp 2>/dev/null | grep \":$p \" | awk '{print $7}' | cut -d'/' -f1); [ -n \"$pid\" ] && kill -9 \"$pid\" 2>/dev/null; done") sys.exec("ss -tulnp 2>/dev/null | awk '/:" .. port .. " |:" .. pty_port .. " /{print $NF}' | awk -F',' '{print $2}' | awk -F'=' '{print $2}' | xargs -r kill -9 2>/dev/null") sys.exec("pgrep -f 'openclaw-gateway|web-pty.js' 2>/dev/null | xargs -r kill -9 2>/dev/null") - sys.exec("pgrep -u openclaw 2>/dev/null | xargs -r kill -9 2>/dev/null") + -- BusyBox pgrep 没有 -u,按 /proc 属主枚举服务用户的进程 + sys.exec("ls -ld /proc/[0-9]* 2>/dev/null | awk '$3 == \"openclaw\" { p = $NF; sub(\".*/\", \"\", p); print p }' | xargs -r kill -9 2>/dev/null") -- 禁用开机启动 sys.exec("/etc/init.d/openclaw disable 2>/dev/null") @@ -1174,9 +1212,18 @@ end function action_backup() local http = require "luci.http" local sys = require "luci.sys" - local uci = require "luci.model.uci".cursor() local action = http.formvalue("action") or "create" + local function reply(tbl) + http.prepare_content("application/json") + http.write_json(tbl) + end + + if not backup_ok or not oc_backup then + reply({ status = "error", message = "备份校验模块不可用,请重新安装插件" }) + return + end + -- 使用统一路径 helper,兼容用户误填 /mnt/data/openclaw 的场景。 local install_path = get_install_path() local node_bin = install_path .. "/node/bin/node" @@ -1198,23 +1245,55 @@ function action_backup() end end - if oc_entry == "" then - http.prepare_content("application/json") - http.write_json({ status = "error", message = "OpenClaw 未安装,无法执行备份操作" }) + -- 下载/上传/列表/删除不依赖 OpenClaw CLI,未安装时也允许操作 (便于重装后上传恢复) + local needs_cli = (action == "create" or action == "verify") + if needs_cli and oc_entry == "" then + reply({ status = "error", message = "OpenClaw 未安装,无法执行备份操作" }) return end + -- NODE_ICU_DATA: musl 小 ICU 构建的 Node.js 缺少它时 OpenClaw 启动即崩溃 + -- (Invalid regular expression ... \p{Extended_Pictographic}),备份因此从未成功过。 local env_prefix = string.format( - "HOME=%s/data OPENCLAW_HOME=%s/data " .. - "OPENCLAW_STATE_DIR=%s/data/.openclaw " .. - "OPENCLAW_CONFIG_PATH=%s/data/.openclaw/openclaw.json " .. - "PATH=%s/node/bin:%s/global/bin:$PATH ", - install_path, install_path, install_path, install_path, install_path, install_path + "HOME=%s OPENCLAW_HOME=%s " .. + "OPENCLAW_STATE_DIR=%s OPENCLAW_CONFIG_PATH=%s " .. + "NODE_ICU_DATA=%s PATH=%s:%s:$PATH ", + shellquote(install_path .. "/data"), shellquote(install_path .. "/data"), + shellquote(install_path .. "/data/.openclaw"), shellquote(install_path .. "/data/.openclaw/openclaw.json"), + shellquote(install_path .. "/node/share/icu"), + shellquote(install_path .. "/node/bin"), shellquote(install_path .. "/global/bin") ) + local cli = shellquote(node_bin) .. " " .. shellquote(oc_entry) -- 备份目录 (openclaw backup create 输出到 CWD,需要 cd) - local backup_dir = install_path .. "/data/.openclaw/backups" - local cd_prefix = "mkdir -p " .. backup_dir .. " && cd " .. backup_dir .. " && " + local oc_data_dir = install_path .. "/data/.openclaw" + local backup_dir = oc_data_dir .. "/backups" + local cd_prefix = "mkdir -p " .. shellquote(backup_dir) .. " && cd " .. shellquote(backup_dir) .. " && " + + local function list_backup_names() + local names = {} + local iter = nixio.fs.dir(backup_dir) + if iter then + for name in iter do + if oc_backup.valid_name(name) then + local st = nixio.fs.stat(backup_dir .. "/" .. name) + if st and st.type == "reg" then names[#names + 1] = { name = name, mtime = st.mtime or 0 } end + end + end + end + table.sort(names, function(a, b) return a.mtime > b.mtime end) + return names + end + + -- 解析请求中的备份文件名: 必须通过白名单且文件存在,不做模糊回退 + local function resolve_backup(raw) + local name = oc_backup.valid_name(raw) + if not name then return nil, "无效的备份文件名" end + local path = backup_dir .. "/" .. name + local st = nixio.fs.stat(path) + if not st or st.type ~= "reg" then return nil, "备份文件不存在" end + return path, name + end -- ── 辅助: 解析单个备份文件的 manifest 信息 ── local function parse_backup_info(filepath) @@ -1222,17 +1301,11 @@ function action_backup() -- 文件大小 local st = nixio.fs.stat(filepath) local size = st and st.size or 0 - -- 从文件名提取时间戳: 2026-03-11T18-28-43.149Z-openclaw-backup.tar.gz - local ts = filename:match("^(%d%d%d%d%-%d%d%-%d%dT%d%d%-%d%d%-%d%d%.%d+Z)") - local display_time = "" - if ts then - -- 2026-03-11T18-28-43.149Z -> 2026-03-11 18:28:43 - display_time = ts:gsub("T", " "):gsub("(%d%d)%-(%d%d)%-(%d%d)%.%d+Z", "%1:%2:%3") - end + local display_time = oc_backup.display_time(filename) -- 读取 manifest.json 判断备份类型 local backup_type = "unknown" local manifest_json = sys.exec( - "tar --wildcards -xzf " .. filepath .. " '*/manifest.json' -O 2>/dev/null" + "tar --wildcards -xzf " .. shellquote(filepath) .. " '*/manifest.json' -O 2>/dev/null" ) if manifest_json and manifest_json ~= "" then -- 简单字符串匹配,避免依赖 JSON 库 @@ -1266,178 +1339,264 @@ function action_backup() size = size, size_str = size_str, time = display_time, - backup_type = backup_type + backup_type = backup_type, + uploaded = oc_backup.is_uploaded(filename) } end + local function upload_part_path(id) + return "/tmp/openclaw-backup-upload-" .. id .. ".part" + end + if action == "create" then local only_config = http.formvalue("only_config") or "1" local backup_cmd if only_config == "1" then - backup_cmd = cd_prefix .. env_prefix .. node_bin .. " " .. oc_entry .. " backup create --only-config --no-include-workspace 2>&1" + backup_cmd = cd_prefix .. env_prefix .. cli .. " backup create --only-config --no-include-workspace 2>&1" else - backup_cmd = cd_prefix .. "HOME=" .. backup_dir .. " " .. env_prefix .. node_bin .. " " .. oc_entry .. " backup create --no-include-workspace 2>&1" + backup_cmd = cd_prefix .. "HOME=" .. shellquote(backup_dir) .. " " .. env_prefix .. cli .. " backup create --no-include-workspace 2>&1" end - local output = sys.exec(backup_cmd) + local output = sys.exec(backup_cmd) or "" -- 完整备份可能输出到 HOME,移动到 backup_dir - sys.exec("mv " .. install_path .. "/data/*-openclaw-backup.tar.gz " .. backup_dir .. "/ 2>/dev/null") - -- 提取备份文件路径 - local backup_path = output:match("([%S]+%.tar%.gz)") - http.prepare_content("application/json") - http.write_json({ + sys.exec("mv " .. shellquote(install_path .. "/data") .. "/*-openclaw-backup.tar.gz " .. shellquote(backup_dir) .. "/ 2>/dev/null") + -- 以归档实际落盘为准判断成败,CLI 崩溃时不能返回 ok + local reported = output:match("([%S]+%-openclaw%-backup%.tar%.gz)") + local created = reported and oc_backup.valid_name(reported:match("([^/]+)$") or "") + if not created or not nixio.fs.stat(backup_dir .. "/" .. created, "type") then + local tail = output:gsub("\27%[[%d;]*m", ""):sub(-600) + reply({ status = "error", action = "create", message = "备份创建失败: " .. (tail ~= "" and tail or "未生成备份文件"), output = output }) + return + end + reply({ status = "ok", action = "create", output = output, - backup_path = backup_path or "" + backup_path = backup_dir .. "/" .. created }) elseif action == "verify" then -- 找到最新的备份文件 - local latest = sys.exec("ls -t " .. backup_dir .. "/*-openclaw-backup.tar.gz 2>/dev/null | head -1"):gsub("%s+", "") - if latest == "" then - http.prepare_content("application/json") - http.write_json({ status = "error", message = "未找到备份文件,请先创建备份" }) + local names = list_backup_names() + if #names == 0 then + reply({ status = "error", message = "未找到备份文件,请先创建备份" }) return end - local output = sys.exec(env_prefix .. node_bin .. " " .. oc_entry .. " backup verify " .. latest .. " 2>&1") - http.prepare_content("application/json") - http.write_json({ + local latest = backup_dir .. "/" .. names[1].name + local output = sys.exec(env_prefix .. cli .. " backup verify " .. shellquote(latest) .. " 2>&1") + reply({ status = "ok", action = "verify", output = output, backup_path = latest }) elseif action == "restore" then - -- 支持指定文件名,不指定则用最新 + -- 指定文件名时必须精确命中,不再静默回退到最新备份 (避免恢复了用户没选的备份) local target_file = http.formvalue("file") or "" - local restore_path = "" + local restore_path, restore_name if target_file ~= "" then - -- 安全: 只允许文件名,不允许路径穿越 - target_file = target_file:match("([^/]+)$") or "" - if target_file:match("%-openclaw%-backup%.tar%.gz$") then - restore_path = backup_dir .. "/" .. target_file + restore_path, restore_name = resolve_backup(target_file) + if not restore_path then + reply({ status = "error", message = restore_name }) + return end + else + local names = list_backup_names() + if #names == 0 then + reply({ status = "error", message = "未找到备份文件,请先创建备份" }) + return + end + restore_name = names[1].name + restore_path = backup_dir .. "/" .. restore_name end - if restore_path == "" or not nixio.fs.stat(restore_path, "type") then - -- fallback 到最新 - restore_path = sys.exec("ls -t " .. backup_dir .. "/*-openclaw-backup.tar.gz 2>/dev/null | head -1"):gsub("%s+", "") - end - if restore_path == "" then - http.prepare_content("application/json") - http.write_json({ status = "error", message = "未找到备份文件,请先创建备份" }) - return - end - local oc_data_dir = install_path .. "/data/.openclaw" local config_path = oc_data_dir .. "/openclaw.json" - -- 1) 先验证备份中的 openclaw.json 是否有效 - local check_cmd = "tar -xzf " .. restore_path .. " --wildcards '*/openclaw.json' -O 2>/dev/null" - local json_content = sys.exec(check_cmd) - if not json_content or json_content == "" then - http.prepare_content("application/json") - http.write_json({ status = "error", message = "备份文件中未找到 openclaw.json" }) - return - end - -- 写入临时文件并用 node 验证 - local tmpfile = "/tmp/oc-restore-check.json" - local f = io.open(tmpfile, "w") - if f then f:write(json_content); f:close() end - local check = sys.exec(node_bin .. " -e \"try{JSON.parse(require('fs').readFileSync('" .. tmpfile .. "','utf8'));console.log('OK')}catch(e){console.log('FAIL')}\" 2>/dev/null"):gsub("%s+", "") - os.remove(tmpfile) - if check ~= "OK" then - http.prepare_content("application/json") - http.write_json({ status = "error", message = "备份文件中的配置无效,恢复已取消" }) + -- 1) 校验归档结构: 上传的备份严格校验 (仅普通文件、仅状态目录子树) + local strict = oc_backup.is_uploaded(restore_name) + local names_text = sys.exec("tar -tzf " .. shellquote(restore_path) .. " 2>/dev/null") + local verbose_text = strict and sys.exec("tar -tvzf " .. shellquote(restore_path) .. " 2>/dev/null") or nil + local info, verr = oc_backup.validate_listing(names_text, verbose_text, strict) + if not info then + reply({ status = "error", message = "备份校验失败: " .. tostring(verr) }) return end - -- 2) 备份当前配置 - sys.exec("cp -f " .. config_path .. " " .. config_path .. ".pre-restore 2>/dev/null") - - -- 3) 获取备份名前缀 (如: 2026-03-11T18-21-17.209Z-openclaw-backup) - -- 备份结构: /payload/posix/<绝对路径> - local first_entry = sys.exec("tar -tzf " .. restore_path .. " 2>/dev/null | head -1"):gsub("%s+", "") - local backup_name = first_entry:match("^([^/]+)/") or "" - if backup_name == "" then - http.prepare_content("application/json") - http.write_json({ status = "error", message = "备份文件格式无法识别" }) + -- 2) 验证备份中的 openclaw.json 是否为合法 JSON 对象 + local json_content = sys.exec("tar -xzf " .. shellquote(restore_path) .. " -O " .. shellquote(info.member .. "/openclaw.json") .. " 2>/dev/null") + local jsonc = require "luci.jsonc" + local parsed = json_content and json_content ~= "" and jsonc.parse(json_content) or nil + if type(parsed) ~= "table" then + reply({ status = "error", message = "备份文件中的配置无效,恢复已取消" }) return end - local payload_prefix = backup_name .. "/payload/posix/" - -- strip 3 层: / payload / posix - local strip_count = 3 + + -- 3) 备份当前配置 + sys.exec("cp -f " .. shellquote(config_path) .. " " .. shellquote(config_path .. ".pre-restore") .. " 2>/dev/null") -- 4) 停止服务 sys.exec("/etc/init.d/openclaw stop >/dev/null 2>&1") -- 等待端口释放 sys.exec("sleep 2") - -- 5) 提取 payload 文件到根目录 (还原到原始绝对路径) - -- 注: --wildcards 与 --strip-components 组合在某些 tar 版本不兼容 - -- 使用精确路径前缀代替 wildcards - local extract_cmd = string.format( - "tar -xzf %s --strip-components=%d -C / '%s' 2>&1", - restore_path, strip_count, payload_prefix - ) - local extract_out = sys.exec(extract_cmd) + -- 5) 只解压备份中的状态目录子树,写入当前状态目录 + -- (兼容从其它安装路径迁移,且归档中的其它路径不会被写到磁盘) + sys.exec("mkdir -p " .. shellquote(oc_data_dir)) + local extract_out = sys.exec(string.format( + "tar -xzf %s --strip-components=%d -C %s %s 2>&1; echo \"__EXIT:$?__\"", + shellquote(restore_path), info.strip, shellquote(oc_data_dir), shellquote(info.member) + )) or "" + local extract_rc = tonumber(extract_out:match("__EXIT:(%d+)__") or "1") + extract_out = extract_out:gsub("%s*__EXIT:%d+__%s*$", "") -- 6) 修复权限 - fix_openclaw_state_permissions(oc_data_dir) + fix_openclaw_state_permissions(install_path .. "/data") -- 7) 重启服务 sys.exec("/etc/init.d/openclaw start >/dev/null 2>&1 &") - http.prepare_content("application/json") - http.write_json({ + if extract_rc ~= 0 then + reply({ status = "error", action = "restore", message = "解压备份失败,原配置已保存为 openclaw.json.pre-restore: " .. extract_out:sub(-400) }) + return + end + reply({ status = "ok", action = "restore", - message = "已从备份完整恢复所有配置和数据,服务正在重启。原配置已保存为 openclaw.json.pre-restore", + message = "已从备份恢复配置和数据,服务正在重启。原配置已保存为 openclaw.json.pre-restore", backup_path = restore_path, - extract_output = extract_out or "" + extract_output = extract_out }) elseif action == "list" then - -- 返回结构化的备份文件列表(含类型/大小/时间) - local files_raw = sys.exec("ls -t " .. backup_dir .. "/*-openclaw-backup.tar.gz 2>/dev/null"):gsub("%s+$", "") + -- 返回结构化的备份文件列表(含类型/大小/时间),文件名不合规的条目不展示 local backups = {} - if files_raw ~= "" then - for fpath in files_raw:gmatch("[^\n]+") do - fpath = fpath:gsub("%s+", "") - if fpath ~= "" then - backups[#backups + 1] = parse_backup_info(fpath) - end - -- 最多返回 20 条 - if #backups >= 20 then break end - end + for _, item in ipairs(list_backup_names()) do + backups[#backups + 1] = parse_backup_info(backup_dir .. "/" .. item.name) + -- 最多返回 20 条 + if #backups >= 20 then break end end - http.prepare_content("application/json") - http.write_json({ + reply({ status = "ok", action = "list", backups = backups }) elseif action == "delete" then - local target_file = http.formvalue("file") or "" - -- 安全: 只允许文件名,不允许路径穿越 - target_file = target_file:match("([^/]+)$") or "" - if target_file == "" or not target_file:match("%-openclaw%-backup%.tar%.gz$") then - http.prepare_content("application/json") - http.write_json({ status = "error", message = "无效的备份文件名" }) - return - end - local del_path = backup_dir .. "/" .. target_file - if not nixio.fs.stat(del_path, "type") then - http.prepare_content("application/json") - http.write_json({ status = "error", message = "备份文件不存在" }) + local del_path, del_name = resolve_backup(http.formvalue("file") or "") + if not del_path then + reply({ status = "error", message = del_name }) return end os.remove(del_path) - http.prepare_content("application/json") - http.write_json({ + reply({ status = "ok", action = "delete", - message = "已删除备份: " .. target_file + message = "已删除备份: " .. del_name }) + elseif action == "download" then + -- 下载备份 (#111)。备份含 API Key 等敏感信息,仅限已登录管理员经 POST + CSRF 获取。 + local path, name = resolve_backup(http.formvalue("file") or "") + if not path then + reply({ status = "error", message = name }) + return + end + local f = io.open(path, "rb") + if not f then + reply({ status = "error", message = "无法读取备份文件" }) + return + end + http.header("Content-Disposition", 'attachment; filename="' .. name .. '"') + http.header("Cache-Control", "no-store") + http.prepare_content("application/gzip") + while true do + local block = f:read(65536) + if not block then break end + http.write(block) + end + f:close() + elseif action == "upload_begin" then + -- 分块上传 (#111): 浏览器按块提交 base64,避免一次性大表单占满路由器内存, + -- 也不依赖不同 LuCI 版本的 multipart 处理差异。 + local size = tonumber(http.formvalue("size") or "") + if not size or size <= 0 or size ~= math.floor(size) then + reply({ status = "error", message = "无效的文件大小" }) + return + end + if size > oc_backup.MAX_UPLOAD_BYTES then + reply({ status = "error", message = string.format("备份文件过大 (上限 %d MB)", oc_backup.MAX_UPLOAD_BYTES / 1048576) }) + return + end + -- 清理 1 小时前遗留的未完成上传 + sys.exec("find /tmp -maxdepth 1 -name 'openclaw-backup-upload-*.part' -mmin +60 -exec rm -f {} \\; 2>/dev/null") + local id = sys.exec("head -c 8 /dev/urandom 2>/dev/null | hexdump -v -e '/1 \"%02x\"' 2>/dev/null"):gsub("%s+", "") + if not oc_backup.valid_upload_id(id) then + id = string.format("%08x%08x", os.time() % 4294967296, math.random(0, 4294967295)) + end + local f = io.open(upload_part_path(id), "wb") + if not f then + reply({ status = "error", message = "无法在 /tmp 创建临时文件" }) + return + end + f:close() + reply({ status = "ok", upload_id = id, chunk_b64 = 262144 }) + elseif action == "upload_chunk" then + local id = oc_backup.valid_upload_id(http.formvalue("upload_id")) + local offset = tonumber(http.formvalue("offset") or "") + local data = http.formvalue("data") or "" + local part = id and upload_part_path(id) + local st = part and nixio.fs.stat(part) + if not st then + reply({ status = "error", message = "上传会话不存在或已过期,请重新上传" }) + return + end + if #data > oc_backup.MAX_CHUNK_B64 then + reply({ status = "error", message = "上传分块过大" }) + return + end + if offset ~= st.size then + reply({ status = "error", message = "上传分块顺序错误,请重新上传" }) + return + end + local bytes = oc_backup.b64decode(data) + if not bytes then + reply({ status = "error", message = "上传数据编码无效" }) + return + end + if st.size + #bytes > oc_backup.MAX_UPLOAD_BYTES then + os.remove(part) + reply({ status = "error", message = "备份文件过大,已取消上传" }) + return + end + local f = io.open(part, "ab") + if not f then + reply({ status = "error", message = "写入临时文件失败" }) + return + end + f:write(bytes) + f:close() + reply({ status = "ok", received = st.size + #bytes }) + elseif action == "upload_finish" then + local id = oc_backup.valid_upload_id(http.formvalue("upload_id")) + local part = id and upload_part_path(id) + if not part or not nixio.fs.stat(part) then + reply({ status = "error", message = "上传会话不存在或已过期,请重新上传" }) + return + end + local names_text = sys.exec("tar -tzf " .. shellquote(part) .. " 2>/dev/null") + local verbose_text = sys.exec("tar -tvzf " .. shellquote(part) .. " 2>/dev/null") + local info, verr = oc_backup.validate_listing(names_text, verbose_text, true) + if not info then + os.remove(part) + reply({ status = "error", message = "不是有效的 OpenClaw 备份: " .. tostring(verr) }) + return + end + sys.exec("mkdir -p " .. shellquote(backup_dir)) + local name = oc_backup.upload_name() + local dest = backup_dir .. "/" .. name + local mv_out = sys.exec("mv -f " .. shellquote(part) .. " " .. shellquote(dest) .. " 2>&1 && chmod 600 " .. shellquote(dest) .. " && echo __OK__") + if not mv_out:find("__OK__", 1, true) then + os.remove(part) + reply({ status = "error", message = "保存上传的备份失败: " .. mv_out }) + return + end + reply({ status = "ok", action = "upload", filename = name, message = "备份已上传: " .. name }) else - http.prepare_content("application/json") - http.write_json({ status = "error", message = "未知备份操作: " .. action }) + reply({ status = "error", message = "未知备份操作: " .. action }) end end @@ -1853,7 +2012,8 @@ function action_wechat_login() -- 清理旧状态和可能的残留进程 sys.exec("kill -9 $(cat /tmp/openclaw-wechat-login.pid 2>/dev/null) 2>/dev/null") - sys.exec("pkill -f 'channels login --channel openclaw-weixin' 2>/dev/null") + -- BusyBox 固件常没有 pkill。[w] 让正则匹配不到执行这条命令的 sh 自身 + sys.exec("pgrep -f 'channels login --channel openclaw-[w]eixin' 2>/dev/null | xargs -r kill 2>/dev/null") sys.exec("rm -f /tmp/openclaw-wechat-qrcode.txt /tmp/openclaw-wechat-login.pid /tmp/openclaw-wechat-login.exit /tmp/openclaw-wechat-restarted") local wechat_plugin_dir = find_wechat_plugin_dir(install_path) @@ -2342,6 +2502,25 @@ end -- ═══════════════════════════════════════════ -- 设备配对管理 API (v2026.9.1+ Control UI 设备配对审批) -- ═══════════════════════════════════════════ + +-- 可移植的超时执行 (#110) +-- iStoreOS / Kwrt 等固件的 BusyBox 未编译 timeout applet,也未预装 coreutils-timeout, +-- 直接调用 `timeout` 会得到 "sh: timeout: not found"。优先使用系统 timeout, +-- 缺失时用后台看门狗兜底,超时返回 124 (与 coreutils 语义一致)。 +-- 看门狗的 stdio 必须重定向,否则它会持有 popen 管道,让 LuCI 请求一直挂起; +-- 看门狗按秒轮询、目标结束后自行退出,不由调用方 kill,避免 shell 把 "Terminated" 作业通知 +-- 写进 2>&1 捕获的输出,污染 devices list 的 JSON。 +local OC_PORTABLE_TIMEOUT_SH = + "_oc_timeout() { _oc_t=$1; shift; " .. + "if command -v timeout >/dev/null 2>&1; then timeout \"$_oc_t\" \"$@\"; return $?; fi; " .. + "_oc_m=/tmp/.openclaw-timeout-$$-$(date +%s 2>/dev/null); rm -f \"$_oc_m\"; " .. + "\"$@\" & _oc_p=$!; " .. + "( _oc_n=0; while kill -0 \"$_oc_p\" 2>/dev/null; do " .. + "if [ \"$_oc_n\" -ge \"$_oc_t\" ]; then : > \"$_oc_m\"; kill -TERM \"$_oc_p\" 2>/dev/null; sleep 2; kill -KILL \"$_oc_p\" 2>/dev/null; break; fi; " .. + "sleep 1; _oc_n=$((_oc_n + 1)); done ) /dev/null 2>&1 & " .. + "wait \"$_oc_p\"; _oc_r=$?; " .. + "if [ -e \"$_oc_m\" ]; then rm -f \"$_oc_m\"; return 124; fi; return $_oc_r; }; " + local function run_openclaw_devices_cli(args) local sys = require "luci.sys" local install_path = get_install_path() @@ -2368,13 +2547,14 @@ local function run_openclaw_devices_cli(args) if not ensure_openclaw_user(oc_data) then return nil, "无法创建或读取 openclaw 系统用户" end - local inner_cmd = string.format( - "cd %s && HOME=%s OPENCLAW_HOME=%s " .. + local inner_cmd = OC_PORTABLE_TIMEOUT_SH .. string.format( + -- 环境变量用 export 显式导出: 「VAR=x 函数名」前缀赋值在 ash/dash 中是否传给函数内的子进程属于未定义行为 + "cd %s && export HOME=%s OPENCLAW_HOME=%s " .. "OPENCLAW_STATE_DIR=%s/.openclaw OPENCLAW_CONFIG_PATH=%s/.openclaw/openclaw.json " .. "OPENCLAW_SUPERVISOR_MODE=external OPENCLAW_SERVICE_REPAIR_POLICY=external " .. "NODE_ICU_DATA=%s/node/share/icu NPM_CONFIG_CACHE=%s/.npm npm_config_cache=%s/.npm " .. - "TMPDIR=%s/.tmp PATH=%s/node/bin:%s/global/bin:$PATH " .. - "timeout 20 %s %s %s", + "TMPDIR=%s/.tmp PATH=%s/node/bin:%s/global/bin:$PATH && " .. + "_oc_timeout 20 %s %s %s", shellquote(oc_data), shellquote(oc_data), shellquote(oc_data), shellquote(oc_data), shellquote(oc_data), shellquote(install_path), shellquote(oc_data), shellquote(oc_data), shellquote(oc_data), diff --git a/luci-app-openclaw/luasrc/model/cbi/openclaw/basic.lua b/luci-app-openclaw/luasrc/model/cbi/openclaw/basic.lua index 37de3a5a7..5e2bb83b6 100644 --- a/luci-app-openclaw/luasrc/model/cbi/openclaw/basic.lua +++ b/luci-app-openclaw/luasrc/model/cbi/openclaw/basic.lua @@ -54,21 +54,41 @@ act.cfgvalue = function(self, section) html[#html+1] = '
' html[#html+1] = '

📦 选择安装版本

' html[#html+1] = '
' - -- 稳定版选项 - local default_tested_ver = luci.sys.exec("sed -n 's/^OC_TESTED_VERSION=\"\\(.*\\)\"/\\1/p' /usr/bin/openclaw-env 2>/dev/null"):gsub("%s+", "") - if default_tested_ver == "" then default_tested_ver = "2026.9.1" end - html[#html+1] = '' - -- 最新版选项 - html[#html+1] = '' + -- 发布通道选项: 数据来自兼容矩阵 (本插件已验证版本);上游实际发布版本在「检测升级」时联网查询 + local compat_channels = { ["extended-stable"] = { verified = "?" }, latest = { verified = "?" } } + local default_channel = "extended-stable" + do + local ok_j, jsonc = pcall(require, "luci.jsonc") + local cf = io.open("/usr/share/openclaw/openclaw-compat.json", "r") + if ok_j and cf then + local data = jsonc.parse(cf:read("*a") or "") + if type(data) == "table" and type(data.channels) == "table" then + for name, ch in pairs(data.channels) do compat_channels[name] = ch end + if type(data.defaultChannel) == "string" then default_channel = data.defaultChannel end + end + end + if cf then cf:close() end + end + local function esc(s) return (tostring(s or ""):gsub("[<>&\"]", { ["<"] = "<", [">"] = ">", ["&"] = "&", ['"'] = """ })) end + local channel_opts = { + { id = "extended-stable", icon = "✅", title = "长期稳定版 extended-stable (推荐)", desc = "官方长期维护通道,只接收修复,适合路由器长期运行。" }, + { id = "latest", icon = "🆕", title = "最新正式版 latest", desc = "官方最新正式发布,功能更新快,变化也更多。" }, + } + for _, opt in ipairs(channel_opts) do + local checked = (opt.id == default_channel) + html[#html+1] = '' + end html[#html+1] = '
' + -- 高级: 未验证版本与版本锁定 + html[#html+1] = '
高级选项 (版本锁定 / 未验证版本)' + html[#html+1] = '
' + html[#html+1] = '' + html[#html+1] = '' + html[#html+1] = '
' -- 自定义安装路径 html[#html+1] = '
' html[#html+1] = '
📂 安装路径
' @@ -98,6 +118,8 @@ act.cfgvalue = function(self, section) -- JavaScript html[#html+1] = ' + diff --git a/luci-app-smartsafehub/root/www/luci-static/smartsafehub/app.css b/luci-app-smartsafehub/root/www/luci-static/smartsafehub/app.css index 4958fa0ef..546a92e50 100644 --- a/luci-app-smartsafehub/root/www/luci-static/smartsafehub/app.css +++ b/luci-app-smartsafehub/root/www/luci-static/smartsafehub/app.css @@ -1,3 +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-outline-style:solid;--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-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-outline-style:solid;--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}}}@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-500:#fb2c36;--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-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-3xl:48rem;--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-normal:0em;--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-500:color(display-p3 .903738 .262579 .253307);--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-500:lab(55.4814% 75.0732 48.8528);--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)}.mt-auto{margin-top:auto}.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)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-0{margin-left:0}.ml-1{margin-left:var(--spacing)}.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)}.size-14{width:calc(var(--spacing) * 14);height:calc(var(--spacing) * 14)}.size-24{width:calc(var(--spacing) * 24);height:calc(var(--spacing) * 24)}.size-\[76px\]{width:76px;height:76px}.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-\[20rem\]{max-height:20rem}.max-h-\[22rem\]{max-height:22rem}.max-h-\[28rem\]{max-height:28rem}.min-h-0{min-height:0}.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-14{min-height:calc(var(--spacing) * 14)}.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-3xl{max-width:var(--container-3xl)}.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-md{max-width:var(--container-md)}.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)}.min-w-24{min-width:calc(var(--spacing) * 24)}.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}.content-start{align-content:flex-start}.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}.justify-start{justify-content:flex-start}.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-1\.5{column-gap:calc(var(--spacing) * 1.5)}.gap-x-2{column-gap:calc(var(--spacing) * 2)}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-5{column-gap:calc(var(--spacing) * 5)}.gap-y-0\.5{row-gap:calc(var(--spacing) * .5)}.gap-y-1{row-gap:var(--spacing)}.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-300{border-color:var(--color-red-300)}.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-400{border-color:var(--color-teal-400)}.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-500{background-color:var(--color-red-500)}.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)}.p-8{padding:calc(var(--spacing) * 8)}.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)}.py-12{padding-block:calc(var(--spacing) * 12)}.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)}.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-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.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-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.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-900{color:var(--color-teal-900)}.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,.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)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.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\:rounded:focus-visible{border-radius:.25rem}.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:focus-visible{outline-style:var(--tw-outline-style);outline-width:1px}.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-teal-500:focus-visible{outline-color:var(--color-teal-500)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:cursor-progress:disabled{cursor:progress}.disabled\:cursor-wait:disabled{cursor:wait}.disabled\:bg-red-700:disabled{background-color:var(--color-red-700)}.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}.disabled\:opacity-80:disabled{opacity:.8}@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-52{width:calc(var(--spacing) * 52)}.sm\:w-auto{width:auto}.sm\:max-w-\[392px\]{max-width:392px}.sm\:max-w-\[592px\]{max-width:592px}.sm\:flex-none{flex:none}.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\:justify-start{justify-content:flex-start}.sm\:gap-4{gap:calc(var(--spacing) * 4)}.sm\:self-auto{align-self:auto}.sm\:p-4{padding:calc(var(--spacing) * 4)}.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\:w-72{width:calc(var(--spacing) * 72)}.lg\:flex-1{flex:1}.lg\:shrink-0{flex-shrink:0}.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\:grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,18\.5rem\)\]{grid-template-columns:minmax(0,1fr) minmax(0,18.5rem)}.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\:size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.xl\:h-full{height:100%}.xl\:w-80{width:calc(var(--spacing) * 80)}.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3,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\.08fr\)_minmax\(0\,0\.92fr\)\]{grid-template-columns:minmax(0,1.08fr) minmax(0,.92fr)}.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\:flex-col{flex-direction:column}.xl\:items-stretch{align-items:stretch}.xl\:justify-end{justify-content:flex-end}.xl\:gap-2{gap:calc(var(--spacing) * 2)}.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\:p-4{padding:calc(var(--spacing) * 4)}.xl\:px-10{padding-inline:calc(var(--spacing) * 10)}.xl\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.xl\:whitespace-nowrap{white-space:nowrap}}@media (min-width:96rem){.\32 xl\:size-10{width:calc(var(--spacing) * 10);height:calc(var(--spacing) * 10)}.\32 xl\:flex-row{flex-direction:row}.\32 xl\:p-5{padding:calc(var(--spacing) * 5)}.\32 xl\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}}.\[\&\:\:-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-apps-nav-link:not([aria-current=page]){color:#115e59;background-color:#f0fdfa;border:1px solid #99f6e4}.ssh-apps-nav-link:not([aria-current=page]):hover{background-color:#ccfbf1;border-color:#5eead4}.ssh-apps-nav-description{color:#0f766e}.ssh-app[data-theme=dark] .ssh-apps-nav-link:not([aria-current=page]){color:#e2e8f0;background-color:#14b8a617;border-color:#134e4a}.ssh-app[data-theme=dark] .ssh-apps-nav-link:not([aria-current=page]):hover{color:#5eead4;background-color:#14b8a629;border-color:#0f766e}.ssh-app[data-theme=dark] .ssh-apps-nav-description{color:#5eead4}.ssh-apps-nav-link:focus-visible{outline-offset:2px;outline:2px solid #0d9488}.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 .ssh-rule-group-toggle:hover{background-color:#f1f5f9}.ssh-app[data-theme=dark] .ssh-rule-group-toggle:hover{background-color:#1e293b}.ssh-app[data-theme=dark] .ssh-password-input:-webkit-autofill{-webkit-text-fill-color:#f8fafc;caret-color:#f8fafc;box-shadow:inset 0 0 0 1000px #0f172a}.ssh-app[data-theme=dark] .ssh-password-input:-webkit-autofill:hover{-webkit-text-fill-color:#f8fafc;caret-color:#f8fafc;box-shadow:inset 0 0 0 1000px #0f172a}.ssh-app[data-theme=dark] .ssh-password-input:-webkit-autofill:focus{-webkit-text-fill-color:#f8fafc;caret-color:#f8fafc;box-shadow:inset 0 0 0 1000px #0f172a}.ssh-app[data-theme=light] .ssh-password-input:-webkit-autofill{-webkit-text-fill-color:#0f172a;caret-color:#0f172a;box-shadow:inset 0 0 0 1000px #fff}.ssh-app[data-theme=light] .ssh-password-input:-webkit-autofill:hover{-webkit-text-fill-color:#0f172a;caret-color:#0f172a;box-shadow:inset 0 0 0 1000px #fff}.ssh-app[data-theme=light] .ssh-password-input:-webkit-autofill:focus{-webkit-text-fill-color:#0f172a;caret-color:#0f172a;box-shadow:inset 0 0 0 1000px #fff}.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-900],.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-red-50:hover{background-color:#ef444429}.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}.ssh-apps-storage-track,.ssh-apps-storage-free{fill:none;stroke-width:9px}.ssh-apps-storage-track{stroke:#e2e8f0}.ssh-apps-storage-free{stroke:#0d9488;transform-origin:50%;transform:rotate(-90deg)}.ssh-apps-storage-ring[data-level=warning] .ssh-apps-storage-free{stroke:#d97706}.ssh-apps-storage-ring[data-level=critical] .ssh-apps-storage-free{stroke:#e11d48}.ssh-app[data-theme=dark] .ssh-apps-storage-track{stroke:#334155}.ssh-app[data-theme=dark] .ssh-apps-storage-free{stroke:#2dd4bf}.ssh-app[data-theme=dark] .ssh-apps-storage-ring[data-level=warning] .ssh-apps-storage-free{stroke:#fbbf24}.ssh-app[data-theme=dark] .ssh-apps-storage-ring[data-level=critical] .ssh-apps-storage-free{stroke:#fb7185}.ssh-apps-storage-card[data-level=warning] .ssh-apps-storage-available,.ssh-dashboard-storage-card[data-level=warning] .ssh-apps-storage-available{color:#b45309}.ssh-apps-storage-card[data-level=critical] .ssh-apps-storage-available,.ssh-dashboard-storage-card[data-level=critical] .ssh-apps-storage-available{color:#be123c}.ssh-app[data-theme=dark] .ssh-apps-storage-card[data-level=warning] .ssh-apps-storage-available,.ssh-app[data-theme=dark] .ssh-dashboard-storage-card[data-level=warning] .ssh-apps-storage-available{color:#fbbf24}.ssh-app[data-theme=dark] .ssh-apps-storage-card[data-level=critical] .ssh-apps-storage-available,.ssh-app[data-theme=dark] .ssh-dashboard-storage-card[data-level=critical] .ssh-apps-storage-available{color:#fb7185}.ssh-apps-installed-badge{color:#475569;background:#f1f5f9;border-color:#cbd5e1}.ssh-apps-update-badge{color:#92400e;background:#fffbeb;border-color:#fcd34d}.ssh-apps-update-button{color:#fff;background:#d97706;border-color:#d97706}.ssh-apps-update-button:hover:not(:disabled){background:#b45309;border-color:#b45309}.ssh-apps-update-button:focus-visible{outline-offset:2px;outline:2px solid #d97706}.ssh-app[data-theme=dark] .ssh-apps-installed-badge{color:#cbd5e1;background:#1e293b;border-color:#475569}.ssh-app[data-theme=dark] .ssh-apps-update-badge{color:#fcd34d;background:#451a03;border-color:#92400e}.ssh-app[data-theme=dark] .ssh-apps-update-button{color:#fff;background:#b45309;border-color:#b45309}.ssh-app[data-theme=dark] .ssh-apps-update-button:hover:not(:disabled){background:#d97706;border-color:#d97706}@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-outline-style{syntax:"*";inherits:false;initial-value:solid}@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-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}} +@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-outline-style:solid;--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-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-outline-style:solid;--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}}}@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-500:#fb2c36;--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-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-3xl:48rem;--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-normal:0em;--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-500:color(display-p3 .903738 .262579 .253307);--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-500:lab(55.4814% 75.0732 48.8528);--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)}.mt-auto{margin-top:auto}.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)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-0{margin-left:0}.ml-1{margin-left:var(--spacing)}.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-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.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)}.size-14{width:calc(var(--spacing) * 14);height:calc(var(--spacing) * 14)}.size-24{width:calc(var(--spacing) * 24);height:calc(var(--spacing) * 24)}.size-\[76px\]{width:76px;height:76px}.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-\[20rem\]{max-height:20rem}.max-h-\[22rem\]{max-height:22rem}.max-h-\[28rem\]{max-height:28rem}.min-h-0{min-height:0}.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-14{min-height:calc(var(--spacing) * 14)}.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-3xl{max-width:var(--container-3xl)}.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-md{max-width:var(--container-md)}.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)}.min-w-24{min-width:calc(var(--spacing) * 24)}.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-disc{list-style-type:disc}.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}.content-start{align-content:flex-start}.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}.justify-start{justify-content:flex-start}.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-1\.5{column-gap:calc(var(--spacing) * 1.5)}.gap-x-2{column-gap:calc(var(--spacing) * 2)}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-5{column-gap:calc(var(--spacing) * 5)}.gap-y-0\.5{row-gap:calc(var(--spacing) * .5)}.gap-y-1{row-gap:var(--spacing)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}.gap-y-3{row-gap:calc(var(--spacing) * 3)}: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-100>:not(:last-child)){border-color:var(--color-slate-100)}:where(.divide-slate-200>:not(:last-child)){border-color:var(--color-slate-200)}.self-start{align-self:flex-start}.justify-self-start{justify-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-300{border-color:var(--color-red-300)}.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-400{border-color:var(--color-teal-400)}.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-500{background-color:var(--color-red-500)}.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)}.p-8{padding:calc(var(--spacing) * 8)}.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)}.py-12{padding-block:calc(var(--spacing) * 12)}.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)}.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-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.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-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.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-900{color:var(--color-teal-900)}.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,.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)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.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\:underline:hover{text-decoration-line:underline}.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\:rounded:focus-visible{border-radius:.25rem}.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:focus-visible{outline-style:var(--tw-outline-style);outline-width:1px}.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-teal-500:focus-visible{outline-color:var(--color-teal-500)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:cursor-progress:disabled{cursor:progress}.disabled\:cursor-wait:disabled{cursor:wait}.disabled\:bg-red-700:disabled{background-color:var(--color-red-700)}.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}.disabled\:opacity-80:disabled{opacity:.8}@media (min-width:40rem){.sm\:mx-6{margin-inline:calc(var(--spacing) * 6)}.sm\:mt-3{margin-top:calc(var(--spacing) * 3)}.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-52{width:calc(var(--spacing) * 52)}.sm\:w-auto{width:auto}.sm\:max-w-\[392px\]{max-width:392px}.sm\:max-w-\[592px\]{max-width:592px}.sm\:flex-none{flex:none}.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\:justify-start{justify-content:flex-start}.sm\:gap-4{gap:calc(var(--spacing) * 4)}.sm\:self-auto{align-self:auto}.sm\:justify-self-end{justify-self:flex-end}.sm\:p-4{padding:calc(var(--spacing) * 4)}.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\:w-72{width:calc(var(--spacing) * 72)}.lg\:flex-1{flex:1}.lg\:shrink-0{flex-shrink:0}.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\:grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,18\.5rem\)\]{grid-template-columns:minmax(0,1fr) minmax(0,18.5rem)}.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\:size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.xl\:h-full{height:100%}.xl\:w-80{width:calc(var(--spacing) * 80)}.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3,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\.08fr\)_minmax\(0\,0\.92fr\)\]{grid-template-columns:minmax(0,1.08fr) minmax(0,.92fr)}.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\:flex-col{flex-direction:column}.xl\:items-stretch{align-items:stretch}.xl\:justify-end{justify-content:flex-end}.xl\:gap-2{gap:calc(var(--spacing) * 2)}.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\:p-4{padding:calc(var(--spacing) * 4)}.xl\:px-10{padding-inline:calc(var(--spacing) * 10)}.xl\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.xl\:whitespace-nowrap{white-space:nowrap}}@media (min-width:96rem){.\32 xl\:size-10{width:calc(var(--spacing) * 10);height:calc(var(--spacing) * 10)}.\32 xl\:flex-row{flex-direction:row}.\32 xl\:p-5{padding:calc(var(--spacing) * 5)}.\32 xl\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}}.\[\&\:\:-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-apps-nav-link:not([aria-current=page]){color:#115e59;background-color:#f0fdfa;border:1px solid #99f6e4}.ssh-apps-nav-link:not([aria-current=page]):hover{background-color:#ccfbf1;border-color:#5eead4}.ssh-apps-nav-description{color:#0f766e}.ssh-app[data-theme=dark] .ssh-apps-nav-link:not([aria-current=page]){color:#e2e8f0;background-color:#14b8a617;border-color:#134e4a}.ssh-app[data-theme=dark] .ssh-apps-nav-link:not([aria-current=page]):hover{color:#5eead4;background-color:#14b8a629;border-color:#0f766e}.ssh-app[data-theme=dark] .ssh-apps-nav-description{color:#5eead4}.ssh-apps-nav-link:focus-visible{outline-offset:2px;outline:2px solid #0d9488}.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 .ssh-rule-group-toggle:hover{background-color:#f1f5f9}.ssh-app[data-theme=dark] .ssh-rule-group-toggle:hover{background-color:#1e293b}.ssh-app[data-theme=dark] .ssh-password-input:-webkit-autofill{-webkit-text-fill-color:#f8fafc;caret-color:#f8fafc;box-shadow:inset 0 0 0 1000px #0f172a}.ssh-app[data-theme=dark] .ssh-password-input:-webkit-autofill:hover{-webkit-text-fill-color:#f8fafc;caret-color:#f8fafc;box-shadow:inset 0 0 0 1000px #0f172a}.ssh-app[data-theme=dark] .ssh-password-input:-webkit-autofill:focus{-webkit-text-fill-color:#f8fafc;caret-color:#f8fafc;box-shadow:inset 0 0 0 1000px #0f172a}.ssh-app[data-theme=light] .ssh-password-input:-webkit-autofill{-webkit-text-fill-color:#0f172a;caret-color:#0f172a;box-shadow:inset 0 0 0 1000px #fff}.ssh-app[data-theme=light] .ssh-password-input:-webkit-autofill:hover{-webkit-text-fill-color:#0f172a;caret-color:#0f172a;box-shadow:inset 0 0 0 1000px #fff}.ssh-app[data-theme=light] .ssh-password-input:-webkit-autofill:focus{-webkit-text-fill-color:#0f172a;caret-color:#0f172a;box-shadow:inset 0 0 0 1000px #fff}.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]{border-color:#334155}.ssh-app[data-theme=dark] [class~=divide-slate-100]>:not(:last-child),.ssh-app[data-theme=dark] [class~=divide-slate-200]>:not(:last-child){border-color:#1e293b}.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-900],.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-100],.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-100],.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-slate-100:hover{background-color:#334155}.ssh-app[data-theme=dark] .hover\:bg-teal-50:hover,.ssh-app[data-theme=dark] .hover\:bg-teal-100:hover{background-color:#0d94883d}.ssh-app[data-theme=dark] .hover\:bg-amber-50:hover,.ssh-app[data-theme=dark] .hover\:bg-amber-100:hover{background-color:#f59e0b38}.ssh-app[data-theme=dark] .hover\:bg-rose-100:hover{background-color:#f43f5e38}.ssh-app[data-theme=dark] .focus\:bg-white:focus{background-color:#0f172a}.ssh-app[data-theme=dark] .disabled\:bg-slate-100:disabled{background-color:#1e293b}.ssh-app[data-theme=dark] .focus\:ring-teal-100:focus{--tw-ring-color:#134e4a}.ssh-app[data-theme=dark] .focus-visible\:ring-teal-100:focus-visible{--tw-ring-color:#134e4a}.ssh-app[data-theme=dark] .focus-visible\:ring-teal-200:focus-visible{--tw-ring-color:#134e4a}.ssh-app[data-theme=dark] .focus-visible\:ring-amber-100:focus-visible{--tw-ring-color:#78350f}.ssh-app[data-theme=dark] .focus-visible\:ring-amber-200:focus-visible{--tw-ring-color:#78350f}.ssh-app[data-theme=dark] .focus-visible\:ring-emerald-100:focus-visible{--tw-ring-color:#064e3b}.ssh-app[data-theme=dark] .focus-visible\:ring-red-100:focus-visible{--tw-ring-color:#7f1d1d}.ssh-app[data-theme=dark] .focus-visible\:ring-red-200:focus-visible{--tw-ring-color:#7f1d1d}.ssh-app[data-theme=dark] .hover\:border-red-200:hover,.ssh-app[data-theme=dark] .hover\:border-red-300:hover{border-color:#991b1b}.ssh-app[data-theme=dark] .hover\:border-teal-200:hover,.ssh-app[data-theme=dark] .hover\:border-teal-300:hover{border-color:#115e59}.ssh-app[data-theme=dark] .hover\:text-teal-700:hover,.ssh-app[data-theme=dark] .hover\:text-teal-800:hover,.ssh-app[data-theme=dark] .hover\:text-teal-900:hover{color:#5eead4}.ssh-app[data-theme=dark] .hover\:text-slate-800:hover{color:#e2e8f0}.ssh-app[data-theme=dark] .hover\:text-red-700:hover{color:#fca5a5}.ssh-app[data-theme=dark] .hover\:bg-red-50:hover{background-color:#ef444429}.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}.ssh-apps-storage-track,.ssh-apps-storage-free{fill:none;stroke-width:9px}.ssh-apps-storage-track{stroke:#e2e8f0}.ssh-apps-storage-free{stroke:#0d9488;transform-origin:50%;transform:rotate(-90deg)}.ssh-apps-storage-ring[data-level=warning] .ssh-apps-storage-free{stroke:#d97706}.ssh-apps-storage-ring[data-level=critical] .ssh-apps-storage-free{stroke:#e11d48}.ssh-app[data-theme=dark] .ssh-apps-storage-track{stroke:#334155}.ssh-app[data-theme=dark] .ssh-apps-storage-free{stroke:#2dd4bf}.ssh-app[data-theme=dark] .ssh-apps-storage-ring[data-level=warning] .ssh-apps-storage-free{stroke:#fbbf24}.ssh-app[data-theme=dark] .ssh-apps-storage-ring[data-level=critical] .ssh-apps-storage-free{stroke:#fb7185}.ssh-apps-storage-card[data-level=warning] .ssh-apps-storage-available,.ssh-dashboard-storage-card[data-level=warning] .ssh-apps-storage-available{color:#b45309}.ssh-apps-storage-card[data-level=critical] .ssh-apps-storage-available,.ssh-dashboard-storage-card[data-level=critical] .ssh-apps-storage-available{color:#be123c}.ssh-app[data-theme=dark] .ssh-apps-storage-card[data-level=warning] .ssh-apps-storage-available,.ssh-app[data-theme=dark] .ssh-dashboard-storage-card[data-level=warning] .ssh-apps-storage-available{color:#fbbf24}.ssh-app[data-theme=dark] .ssh-apps-storage-card[data-level=critical] .ssh-apps-storage-available,.ssh-app[data-theme=dark] .ssh-dashboard-storage-card[data-level=critical] .ssh-apps-storage-available{color:#fb7185}.ssh-apps-official-badge{color:#0f766e;background:#f0fdfa;border-color:#99f6e4}.ssh-app[data-theme=dark] .ssh-apps-official-badge{color:#5eead4;background:#042f2e;border-color:#115e59}.ssh-apps-installed-badge{color:#475569;background:#f1f5f9;border-color:#cbd5e1}.ssh-apps-update-badge{color:#92400e;background:#fffbeb;border-color:#fcd34d}.ssh-apps-update-button{color:#fff;background:#d97706;border-color:#d97706}.ssh-apps-update-button:hover:not(:disabled){background:#b45309;border-color:#b45309}.ssh-apps-update-button:focus-visible{outline-offset:2px;outline:2px solid #d97706}.ssh-app[data-theme=dark] .ssh-apps-installed-badge{color:#cbd5e1;background:#1e293b;border-color:#475569}.ssh-app[data-theme=dark] .ssh-apps-update-badge{color:#fcd34d;background:#451a03;border-color:#92400e}.ssh-app[data-theme=dark] .ssh-apps-update-button{color:#fff;background:#b45309;border-color:#b45309}.ssh-app[data-theme=dark] .ssh-apps-update-button:hover:not(:disabled){background:#d97706;border-color:#d97706}@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-outline-style{syntax:"*";inherits:false;initial-value:solid}@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-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/root/www/luci-static/smartsafehub/app.js b/luci-app-smartsafehub/root/www/luci-static/smartsafehub/app.js index 2ba53ce00..6a637ac31 100644 --- a/luci-app-smartsafehub/root/www/luci-static/smartsafehub/app.js +++ b/luci-app-smartsafehub/root/www/luci-static/smartsafehub/app.js @@ -1,3 +1,3 @@ -var e,t,n,r,i,a,o,s,c,l,u=Symbol.for(`pv`),d={},f=[],p=/^m(i|n|o|s|text|space)$/,m=Array.isArray,h=f.slice,g=Object.assign;function _(e){e&&e.parentNode&&e.remove()}function v(e,t,n){var r,i,a,o={},s=arguments.length;for(a in t)a==`key`?r=t[a]:a==`ref`&&typeof e!=`function`?i=t[a]:o[a]=t[a];return s>2&&(o.children=s>3?h.call(arguments,2):n),y(e,o,r,i,null)}function y(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:u,__v:o||++t,__i:-1,__u:0};return!o&&e.vnode&&e.vnode(s),s}function b(e){return e.children}function x(e,t){this.props=e,this.context=t,this.__g=0}function S(e,t){if(t==null)return e.__&&S(e.__,e.__i+1);for(var n;to&&n.sort(a),t=n.shift(),o=n.length,8&t.__g&&(c=void 0,l=void 0,d=(l=(s=t).__v).__e,f=[],p=[],(m=s.__P)&&((c=g({constructor:u},l)).__v=l.__v+1,e.vnode&&e.vnode(c),P(m,c,l,s.__n,m.namespaceURI,32&l.__u?[d]:null,f,d||S(l),32&l.__u,p),c.__v=l.__v,c.__.__k[c.__i]=c,ne(f,c,p),l.__=l.__e=null,c.__e!=d&&C(c)))}finally{(i=n.length)&&(r||queueMicrotask)(T)}}function E(e,t,n,r,i,a,o,s,c,l,u){var p,m,h,g,_,v,y=r.__k||f,b=t.length;for(c=D(n,t,y,c,b),p=0;pc?v--:v++,x=1)):(~l||(i>g?v--:i>1]+!!c){for(i=n-1;i>=0;i--)if((s=t[i])&&!(2&s.__u)&&a==s.key&&o==s.type)return i;for(i=n+1;i=0?e.slice(0,t+13):he}function _e(){let e=ge();return e.endsWith(`/luci`)?e.slice(0,-5):me}function ve(e){let t=e.startsWith(`/`)?e:`/${e}`;return`${_e()}${t}`}function ye(e){let t=e.startsWith(`/`)?e:`/${e}`;return`${ge()}${t}`}function be(e){return ye(e)}function xe(e=``){return`/${e?e.startsWith(`#`)?e:`#${e}`:``}`}var Se=/^[0-9a-f]{32}$/i;function Ce(){return ye(`/smartsafehub/session`)}function we(e){return e.headers.get(`X-LuCI-Login-Required`)===`yes`}async function Te(e){if(e.status===401||e.status===403||we(e))return null;if(!e.ok)throw Error(`LuCI session endpoint returned HTTP ${e.status}`);let t=(await e.text()).trim();if(Se.test(t))return t;if(e.redirected||/^(?:access|permission)\s+denied\.?$/i.test(t)||/^)/i.test(t))return null;throw Error(`LuCI session endpoint returned an invalid session id`)}async function Ee(e={}){return Te(await fetch(Ce(),{cache:`no-store`,credentials:`same-origin`,redirect:`follow`,...e}))}function De(){return Ee()}async function Oe(){await fetch(ye(`/admin/logout`),{cache:`no-store`,credentials:`same-origin`,redirect:`manual`})}function ke(e,t){let n=new URLSearchParams;return n.set(`luci_username`,e),n.set(`luci_password`,t),Ee({method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded;charset=UTF-8`},body:n})}var Ae=`smartsafehub:session-expired`,je=`로그인 세션이 만료되었습니다. 계속하려면 다시 로그인해 주세요.`,Me=null;function Ne(){Me=null}function Pe(e){Me!==e&&(Me=e,window.dispatchEvent(new CustomEvent(Ae)))}var Fe,Ie,Le,Re,ze=Object.is,Be=0,Ve=[],He=[],Ue=e,We=Ue.__b,Ge=Ue.__r,Ke=Ue.diffed,qe=Ue.__c,Je=Ue.unmount,Ye=Ue.__;function Xe(e,t){Ue.__h&&Ue.__h(Ie,e,Be||t),Be=0;var n=Ie.__H||(Ie.__H={__:[],__h:[]});return e>=n.__.length&&n.__.push({}),n.__[e]}function F(e){return Be=1,Ze(st,e)}function Ze(e,t,n){var r=Xe(Fe++,2);if(r.t=e,!r.__c&&(r.__=[n?n(t):st(void 0,t),function(e){var t=r.__N?r.__N[0]:r.__[0],n=r.t(t,e);ze(t,n)||(r.__N=[n,r.__[1]],r.__c.setState({}))}],r.__c=Ie,!Ie.__f)){Ie.__f=!0;var i=Ie.shouldComponentUpdate;Ie.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,ze(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=Xe(Fe++,3);!Ue.__s&&ot(n.__H,t)&&(n.__P=!0,n.__=e,n.u=t,Ie.__H.__h.push(n))}function L(e){return Be=5,Qe(function(){return{current:e}},[])}function Qe(e,t){var n=Xe(Fe++,7);return ot(n.__H,t)&&(n.__=e(),n.__H=t),n.__}function R(e,t){return Be=8,Qe(function(){return e},t)}function $e(){var e=Xe(Fe++,11);if(!e.__){for(var t=Ie.__v;!t.__m&&t.__;)t=t.__;var n=t.__m||(t.__m=[0,0]);e.__=`P`+n[0]+`-`+n[1]++}return e.__}function et(){var e;do{for(;e=He.shift();)try{it(e)}catch(t){Ue.__e(t,{__:(e=e.__P)&&e.__v})}for(;e=Ve.shift();){var t=e.__H;if(e.__P&&t)try{t.__h.some(it),t.__h.some(at),t.__h=[]}catch(n){t.__h=[],Ue.__e(n,e.__v)}}}while(He.length)}Ue.__b=function(e){Ie=null,We&&We(e)},Ue.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),Ye&&Ye(e,t)},Ue.__r=function(e){Ge&&Ge(e),Fe=0;var t=(Ie=e.__c).__H;t&&(Le==Ie?Ie.__h=[]:(t.__h.some(it),t.__h.some(at),Fe=0),t.__h=[],t.__.some(function(e){e.__N&&(e.__=e.__N),e.u=e.__N=void 0})),Le=Ie},Ue.diffed=function(e){Ke&&Ke(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&rt(Ve.push(t)),t.__H.__.some(function(e){e.u&&(e.__H=e.u)})),Le=Ie=null},Ue.__c=function(e,t){t.some(function(e){try{e.__h.some(it),e.__h=e.__h.filter(function(e){return!e.__||at(e)})}catch(t){e.__h=[function(){throw t}]}}),qe&&qe(e,t)},Ue.unmount=function(e){Je&&Je(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,rt(He.push(r))}else it(r)}catch(e){t=e}}),r.__H=void 0,t&&Ue.__e(t,r.__v))};var tt=typeof requestAnimationFrame==`function`;function nt(e){var t,n=function(){clearTimeout(r),tt&&cancelAnimationFrame(t),setTimeout(e)},r=setTimeout(n,35);tt&&(t=requestAnimationFrame(n))}function rt(e){e!=1&&Re==Ue.requestAnimationFrame||((Re=Ue.requestAnimationFrame)||nt)(et)}function it(e){var t=Ie,n=e.__c;typeof n==`function`&&(e.__c=void 0,n()),Ie=t}function at(e){var t=Ie;e.__c=e.__(),Ie=t}function ot(e,t){return!e||e.length!=t.length||t.some(function(t,n){return!ze(t,e[n])})}function st(e,t){return typeof t==`function`?t(e):t}var ct=1500,lt=60,ut=64,dt=new Map,ft=[];function pt(e){return e===null?null:Math.max(0,Math.round(e))}function mt(e){let t=`${e.object}.${e.method}`,n=pt(e.elapsedMs)??0,r=n>=ct,i=e.result!==`ok`,a=dt.get(t);return!a&&dt.sizelt&&ft.shift()),r}function ht(){return{slowThresholdMs:ct,summary:[...dt.values()].map(({totalMs:e,...t})=>({...t})),recent:ft.map(e=>({...e}))}}function gt(){dt.clear(),ft.length=0}var _t={snapshot:ht,reset:gt};function vt(){var e;(e=window).__SMARTHUB_RPC_DIAGNOSTICS__??(e.__SMARTHUB_RPC_DIAGNOSTICS__=_t)}var yt=4,bt=3,xt=0,St=0,Ct=[];function wt(){for(;xte.priority!==`low`||St{--xt,t.priority===`low`&&--St,wt()}))}}function Tt(e,t){return t?.aborted?Promise.reject(new DOMException(`Cancelled`,`AbortError`)):new Promise((n,r)=>{let i={priority:e,start:e=>{t?.removeEventListener(`abort`,i.abort),n(e)},abort:()=>{let e=Ct.indexOf(i);e!==-1&&Ct.splice(e,1),t?.removeEventListener(`abort`,i.abort),r(new DOMException(`Cancelled`,`AbortError`))}};t?.addEventListener(`abort`,i.abort,{once:!0});let a=Ct.findIndex(t=>e===`high`&&t.priority!==`high`||e===`normal`&&t.priority===`low`);a===-1?Ct.push(i):Ct.splice(a,0,i),wt()})}function Et(e){"@babel/helpers - typeof";return Et=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},Et(e)}function Dt(e,t){if(Et(e)!=`object`||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||`default`);if(Et(r)!=`object`)return r;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(t===`string`?String:Number)(e)}function Ot(e){var t=Dt(e,`string`);return Et(t)==`symbol`?t:t+``}function z(e,t,n){return(t=Ot(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var kt={1:`잘못된 명령입니다.`,2:`요청 인자가 올바르지 않습니다.`,3:`요청한 메서드를 찾을 수 없습니다.`,4:`요청한 리소스를 찾을 수 없습니다.`,5:`응답 데이터가 없습니다.`,6:`접근 권한이 없습니다.`,7:`요청 시간이 초과되었습니다.`,8:`지원하지 않는 기능입니다.`,9:`장치에서 알 수 없는 오류가 발생했습니다.`,10:`장치 연결이 끊어졌습니다.`},At=2e4,jt=3e3,Mt=1e3,Nt=6e4,Pt=1,B=class extends Error{constructor(e,t){super(t),z(this,`code`,void 0),this.name=`RpcError`,this.code=e}};function Ft(){let e=window.__SMARTHUB_BOOTSTRAP__;if(!e?.sessionId||!e.rpcUrl)throw new B(`BOOTSTRAP_MISSING`,`LuCI 세션 정보를 찾을 수 없습니다. 페이지를 새로고침해 주세요.`);return e}function It(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Lt(){return new B(`INVALID_RESPONSE`,`장치 API가 올바르지 않은 응답을 반환했습니다.`)}function Rt(e){return typeof e!=`number`||!Number.isFinite(e)?At:Math.min(Nt,Math.max(Mt,Math.trunc(e)))}function zt(e){let t=e.message.trim().toLowerCase();return e.code===`UBUS_6`||e.code===`JSON_RPC_-32002`||e.code===`HTTP_ERROR`&&/HTTP (401|403)\b/.test(e.message)||t.includes(`access denied`)||t.includes(`permission denied`)||t.includes(`접근 권한`)}function Bt(e){return window.__SMARTHUB_BOOTSTRAP__?.sessionId===e}async function Vt(e){let t=Pt++,n=new AbortController,r=window.setTimeout(()=>n.abort(),jt);try{let r=await fetch(e.rpcUrl,{method:`POST`,credentials:`same-origin`,headers:{"Content-Type":`application/json`},body:JSON.stringify({jsonrpc:`2.0`,id:t,method:`call`,params:[e.sessionId,`smartsafehub`,`system_root_password_status`,{}]}),signal:n.signal});if(r.status===401||r.status===403)return`expired`;if(!r.ok)return`unknown`;let i=await r.json();if(!It(i)||i.jsonrpc!==`2.0`||i.id!==t)return`unknown`;if(`error`in i){let e=i.error;return It(e)&&typeof e.code==`number`&&e.code===-32002?`expired`:`unknown`}let a=i.result;return!Array.isArray(a)||a.length<1?`unknown`:a[0]===0?`active`:a[0]===6?`expired`:`unknown`}catch{return`unknown`}finally{window.clearTimeout(r)}}async function Ht(e,t){if(!zt(e)||!Bt(t.sessionId))return e;let n=await Vt(t);return Bt(t.sessionId)?n===`active`?new B(`RPC_PERMISSION_DENIED`,`현재 로그인 세션에 이 기능 권한이 없습니다. SmartSafeHub 업데이트 직후라면 로그아웃 후 다시 로그인해 주세요.`):n===`expired`?(Pe(t.sessionId),new B(`SESSION_EXPIRED`,je)):e:e}function Ut(e,t){if(!It(e)||e.jsonrpc!==`2.0`||e.id!==t)throw Lt();if(`error`in e){let t=e.error;throw!It(t)||typeof t.code!=`number`?Lt():new B(`JSON_RPC_${t.code}`,typeof t.message==`string`&&t.message.length>0?t.message:`JSON-RPC 요청에 실패했습니다.`)}let n=e.result;if(!Array.isArray(n)||n.length<1||n.length>2)throw Lt();let r=n[0];if(typeof r!=`number`||!Number.isInteger(r))throw Lt();if(r!==0)throw new B(`UBUS_${r}`,kt[r]??`ubus 오류 코드 ${r}가 반환되었습니다.`);if(n.length<2||n[1]===void 0)throw new B(`EMPTY_RESPONSE`,`장치 API 응답이 비어 있습니다.`);return n[1]}async function Wt(e,t,n={},r={}){let i=Ft();vt();let a=performance.now(),o=null,s=null,c=`ok`,l=Pt++,u=null,d=null,f=new AbortController,p=!1,m=()=>{p=!0,f.abort()};r.signal?.addEventListener(`abort`,m,{once:!0});try{if(u=await Tt(r.priority??`normal`,r.signal),p||r.signal?.aborted)throw new B(`RPC_CANCELLED`,`요청이 취소되었습니다.`);d=window.setTimeout(()=>f.abort(),Rt(r.timeoutMs));let c=await fetch(i.rpcUrl,{method:`POST`,credentials:`same-origin`,headers:{"Content-Type":`application/json`},body:JSON.stringify({jsonrpc:`2.0`,id:l,method:`call`,params:[i.sessionId,e,t,n]}),signal:f.signal});if(o=performance.now()-a,!c.ok)throw new B(`HTTP_ERROR`,`장치 API가 HTTP ${c.status} 오류를 반환했습니다.`);let m;try{m=await c.json(),s=performance.now()-a-(o??0)}catch{throw Lt()}return Ut(m,l)}catch(e){if(p||r.signal?.aborted)throw c=`RPC_CANCELLED`,new B(`RPC_CANCELLED`,`요청이 취소되었습니다.`);if(f.signal.aborted)throw c=`RPC_TIMEOUT`,new B(`RPC_TIMEOUT`,`장치 응답 시간이 초과되었습니다. 잠시 후 다시 시도해 주세요.`);if(e instanceof DOMException&&e.name===`AbortError`)throw c=`RPC_CANCELLED`,new B(`RPC_CANCELLED`,`요청이 취소되었습니다.`);if(e instanceof B){let t=await Ht(e,i);throw c=t.code,t}throw c=`NETWORK_ERROR`,new B(`NETWORK_ERROR`,`공유기와 통신할 수 없습니다. 네트워크 연결을 확인해 주세요.`)}finally{d!==null&&window.clearTimeout(d),r.signal?.removeEventListener(`abort`,m),u?.();let n=Math.round(performance.now()-a);(mt({object:e,method:t,elapsedMs:n,responseMs:o,decodeMs:s,result:c})||c===`RPC_TIMEOUT`)&&console.warn(`[SmartSafeHub RPC] ${e}.${t}: ${n}ms (${c})`)}}async function V(e,t,n={},r={}){let i=await Wt(e,t,n,r);if(!i.ok)throw new B(i.error.code,i.error.message);return i.data}function Gt(){return V(`smartsafehub`,`system_root_password_status`)}function Kt(e){return V(`smartsafehub`,`system_root_password_set`,{password:e})}var qt=Symbol.for(`pv`),Jt=0;Array.isArray;function H(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:qt,__v:--Jt,__i:-1,__u:0};return(a||o)&&(u.__source=a,u.__self=o),e.vnode&&e.vnode(u),u}function U({children:e,title:t,...n}){return H(`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?H(`title`,{children:t}):null,e]})}function Yt(e){return H(U,{...e,children:[H(`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`}),H(`path`,{d:`m9 12 2 2 4-4`})]})}function Xt(e){return H(U,{...e,children:[H(`rect`,{height:`7`,rx:`2`,width:`18`,x:`3`,y:`13`}),H(`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 Zt(e){return H(U,{...e,children:[H(`rect`,{height:`11`,rx:`2`,width:`16`,x:`4`,y:`3`}),H(`path`,{d:`M8 21h8M12 14v7`})]})}function Qt(e){return H(U,{...e,children:[H(`path`,{d:`m3 11 9-8 9 8`}),H(`path`,{d:`M5 10v10h14V10M9 20v-6h6v6`})]})}function $t(e){return H(U,{...e,children:[H(`circle`,{cx:`12`,cy:`8`,r:`4`}),H(`path`,{d:`M4.5 21a7.5 7.5 0 0 1 15 0`})]})}function en(e){return H(U,{...e,children:[H(`rect`,{x:`3`,y:`3`,width:`7`,height:`7`,rx:`1.5`}),H(`rect`,{x:`14`,y:`3`,width:`7`,height:`7`,rx:`1.5`}),H(`rect`,{x:`3`,y:`14`,width:`7`,height:`7`,rx:`1.5`}),H(`path`,{d:`M17.5 14v7M14 17.5h7`})]})}function tn(e){return H(U,{...e,children:[H(`rect`,{x:`2.5`,y:`3.5`,width:`7`,height:`6`,rx:`1.5`}),H(`rect`,{x:`14.5`,y:`3.5`,width:`7`,height:`6`,rx:`1.5`}),H(`rect`,{x:`8.5`,y:`15`,width:`7`,height:`6`,rx:`1.5`}),H(`path`,{d:`M6 9.5v2.5h12V9.5M12 12v3`})]})}function nn(e){return H(U,{...e,children:[H(`path`,{d:`M7.5 7h9a5 5 0 0 1 4.9 4l.8 5a2.5 2.5 0 0 1-4 2.3L15.5 16h-7l-2.7 2.3a2.5 2.5 0 0 1-4-2.3l.8-5A5 5 0 0 1 7.5 7Z`}),H(`path`,{d:`M6.5 11v4M4.5 13h4M16 11.5h.01M18.5 13.5h.01`})]})}function rn(e){return H(U,{...e,children:H(`path`,{d:`M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.287l-7.91 7.91a2.121 2.121 0 0 1-3-3l7.91-7.91a6 6 0 0 1 7.287-8.259c.438.12.54.662.218.984z`})})}function an(e){return H(U,{...e,children:[H(`circle`,{cx:`12`,cy:`12`,r:`10`}),H(`path`,{d:`M8 12h.01M12 12h.01M16 12h.01`})]})}function on(e){return H(U,{...e,children:[H(`path`,{d:`M12 16V4`}),H(`path`,{d:`m7 9 5-5 5 5`}),H(`path`,{d:`M5 14v5h14v-5`})]})}function sn(e){return H(U,{...e,children:[H(`circle`,{cx:`12`,cy:`12`,r:`3`}),H(`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 cn(e){return H(U,{...e,children:[H(`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`}),H(`path`,{d:`M12 20h.01`})]})}function ln(e){return H(U,{...e,children:[H(`path`,{d:`M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4z`}),H(`path`,{d:`M6.5 6.5h1M16.5 6.5h1M6.5 16.5h1`}),H(`path`,{d:`M14 14h2v2h-2zM18 14h2v2h-2zM16 16h2v2h-2zM18 18h2v2h-2zM14 18h1`})]})}function un(e){return H(U,{...e,children:[H(`path`,{d:`M7 3v4M17 3v4M5 7h14v4a7 7 0 0 1-14 0V7Z`}),H(`path`,{d:`M12 18v3`})]})}function dn(e){return H(U,{...e,children:[H(`circle`,{cx:`12`,cy:`12`,r:`9`}),H(`path`,{d:`M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18`})]})}function fn(e){return H(U,{...e,children:[H(`rect`,{height:`13`,rx:`2`,width:`18`,x:`3`,y:`6`}),H(`path`,{d:`m8 2 4 4 4-4M8 22h8`})]})}function pn(e){return H(U,{...e,children:[H(`circle`,{cx:`12`,cy:`12`,r:`9`}),H(`path`,{d:`M12 7v5l3 2`})]})}function mn(e){return H(U,{...e,children:[H(`rect`,{height:`14`,rx:`2`,width:`14`,x:`5`,y:`5`}),H(`path`,{d:`M9 9h6v6H9zM9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M2 15h3M19 9h3M19 15h3`})]})}function hn(e){return H(U,{...e,children:[H(`path`,{d:`M20 11a8 8 0 1 0-2.3 5.7`}),H(`path`,{d:`M20 4v7h-7`})]})}function gn(e){return H(U,{...e,children:[H(`path`,{d:`M12 2v10`}),H(`path`,{d:`M18.4 6.6a8 8 0 1 1-12.8 0`})]})}function _n(e){return H(U,{...e,children:[H(`path`,{d:`M12 3v12`}),H(`path`,{d:`m7 10 5 5 5-5`}),H(`path`,{d:`M5 21h14`})]})}function vn(e){return H(U,{...e,children:[H(`ellipse`,{cx:`12`,cy:`5`,rx:`8`,ry:`3`}),H(`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 yn(e){return H(U,{...e,children:[H(`circle`,{cx:`8`,cy:`15`,r:`4`}),H(`path`,{d:`m11 12 8-8M15 8l2 2M17 6l2 2`})]})}function bn(e){return H(U,{...e,children:[H(`rect`,{height:`13`,rx:`2`,width:`13`,x:`8`,y:`8`}),H(`path`,{d:`M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3`})]})}function xn(e){return H(U,{...e,children:[H(`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`}),H(`circle`,{cx:`12`,cy:`12`,r:`2.5`})]})}function Sn(e){return H(U,{...e,children:[H(`path`,{d:`m3 3 18 18`}),H(`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`}),H(`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`}),H(`path`,{d:`M10.3 10.3a2.5 2.5 0 0 0 3.4 3.4`})]})}function Cn(e){return H(U,{...e,children:[H(`rect`,{height:`17`,rx:`2`,width:`18`,x:`3`,y:`4`}),H(`path`,{d:`M8 2v4M16 2v4M3 9h18`})]})}function wn(e){return H(U,{...e,children:[H(`circle`,{cx:`12`,cy:`12`,r:`9`}),H(`path`,{d:`m8 12 2.5 2.5L16 9`})]})}function W(e){return H(U,{...e,children:[H(`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`}),H(`path`,{d:`M12 9v4M12 17h.01`})]})}function Tn(e){return H(U,{...e,children:H(`path`,{d:`M12 5v14M5 12h14`})})}function En(e){return H(U,{...e,children:H(`path`,{d:`M4 7h16M9 7V4h6v3M7 7l1 13h8l1-13M10 11v5M14 11v5`})})}function Dn(e){return H(U,{...e,children:[H(`circle`,{cx:`11`,cy:`11`,r:`7`}),H(`path`,{d:`m20 20-4-4`})]})}function On(e){return H(U,{...e,children:[H(`path`,{d:`M8 6h12M8 12h12M8 18h12`}),H(`path`,{d:`M4 6h.01M4 12h.01M4 18h.01`})]})}function kn(e){return H(U,{...e,children:[H(`path`,{d:`M10 5H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h4`}),H(`path`,{d:`M14 8l4 4-4 4M8 12h10`})]})}function An(e){return H(U,{...e,children:H(`path`,{d:`M4 7h16M4 12h16M4 17h16`})})}function jn(e){return H(U,{...e,children:H(`path`,{d:`m6 9 6 6 6-6`})})}function Mn(e){return H(U,{...e,children:H(`path`,{d:`m6 6 12 12M18 6 6 18`})})}function Nn(e){return H(U,{...e,children:H(`path`,{d:`m15 18-6-6 6-6`})})}function Pn(e){return H(U,{...e,children:H(`path`,{d:`m9 18 6-6-6-6`})})}function Fn(e){return H(U,{...e,children:H(`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 In(e){return H(U,{...e,children:[H(`circle`,{cx:`12`,cy:`12`,r:`4`}),H(`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`})]})}var Ln=`smartsafehub.theme`;function Rn(){if(typeof window>`u`)return`light`;try{let e=window.localStorage.getItem(Ln);if(e===`dark`||e===`light`)return e}catch{}return window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}function zn(e){try{window.localStorage.setItem(Ln,e)}catch{}}function Bn(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`}function Vn(e){return{length:e.length>=8,letter:/[A-Za-z]/.test(e),number:/[0-9]/.test(e)}}function Hn(e){return e.length&&e.letter&&e.number}function Un({theme:e}){return H(e===`dark`?In:Fn,{})}function Wn({met:e,children:t}){return H(`li`,{class:e?`ssh-password-requirement is-met`:`ssh-password-requirement`,children:[H(wn,{"aria-hidden":`true`}),H(`span`,{children:t})]})}function Gn({onCompleted:e,recovery:t}){let n=L(null),r=L(null),[i,a]=F(``),[o,s]=F(``),[c,l]=F(!1),[u,d]=F(!1),[f,p]=F(!1),[m,h]=F(null),[g,_]=F(Rn),v=Vn(i),y=Hn(v),b=o.length>0&&i===o,x=g===`dark`?`라이트 모드로 전환`:`다크 모드로 전환`;I(()=>{zn(g),Bn(g)},[g]),I(()=>{let e=window.requestAnimationFrame(()=>n.current?.focus());return()=>window.cancelAnimationFrame(e)},[]);let S=e=>{d(e.getModifierState?.(`CapsLock`)??!1)};return H(`main`,{class:`ssh-login-page`,"data-theme":g,children:[H(`section`,{class:`ssh-login-brand`,"aria-labelledby":`ssh-password-setup-brand-title`,children:[H(`div`,{class:`ssh-login-brand-orb ssh-login-brand-orb-one`,"aria-hidden":`true`}),H(`div`,{class:`ssh-login-brand-orb ssh-login-brand-orb-two`,"aria-hidden":`true`}),H(`div`,{class:`ssh-login-brand-content`,children:[H(`span`,{class:`ssh-login-brand-mark`,"aria-hidden":`true`,children:H(Yt,{})}),H(`p`,{class:`ssh-login-eyebrow`,children:t?`PASSWORD RECOVERY`:`FIRST SECURITY SETUP`}),H(`h1`,{id:`ssh-password-setup-brand-title`,children:[H(`span`,{class:`ssh-password-setup-brand-title-line`,children:`SmartSafeHub`}),H(`span`,{class:`ssh-password-setup-brand-title-line`,children:t?`비밀번호 복구`:`보호 시작`})]}),H(`p`,{class:`ssh-login-brand-description`,children:t?`물리 Reset 버튼으로 관리자 비밀번호 복구가 요청되었습니다. 네트워크, Wi-Fi, SafeShield 등 기존 설정은 그대로 유지됩니다.`:`관리자 비밀번호가 없는 상태에서는 SmartSafeHub 관리 기능을 사용할 수 없습니다. 먼저 이 공유기의 root 관리자 비밀번호를 설정해 주세요.`}),H(`div`,{class:`ssh-password-setup-summary`,children:[H(Yt,{"aria-hidden":`true`}),H(`div`,{children:[H(`strong`,{children:t?`새 관리자 비밀번호를 설정해 주세요`:`초기 보안 설정이 필요합니다`}),H(`span`,{children:t?`새 비밀번호를 설정하기 전까지 SSH 접근은 비활성화되고 다른 관리 기능도 잠겨 있습니다.`:`설정이 완료되기 전까지 다른 SmartSafeHub 기능은 잠겨 있습니다.`})]})]})]})]}),H(`section`,{class:`ssh-login-panel`,"aria-labelledby":`ssh-password-setup-title`,children:[H(`button`,{"aria-label":x,class:`ssh-login-theme-toggle`,onClick:()=>_(e=>e===`dark`?`light`:`dark`),title:x,type:`button`,children:H(Un,{theme:g})}),H(`div`,{class:`ssh-login-panel-inner`,children:[H(`div`,{class:`ssh-login-card`,children:[H(`div`,{class:`ssh-login-card-heading`,children:[H(`span`,{class:`ssh-login-mobile-mark`,"aria-hidden":`true`,children:H(yn,{})}),H(`div`,{children:[H(`p`,{class:`ssh-login-kicker`,children:`ADMIN PASSWORD`}),H(`h2`,{id:`ssh-password-setup-title`,children:t?`관리자 비밀번호 복구`:`관리자 비밀번호 설정`}),H(`p`,{class:`ssh-login-subtitle`,children:`비밀번호 설정 후 새 비밀번호로 다시 로그인해야 합니다.`})]})]}),m?H(`div`,{class:`ssh-login-alert`,role:`alert`,"aria-live":`polite`,children:m}):null,H(`form`,{class:`ssh-login-form`,onSubmit:async t=>{if(t.preventDefault(),h(null),!y)h(`비밀번호는 8자 이상이며 영문과 숫자를 각각 하나 이상 포함해야 합니다.`),window.requestAnimationFrame(()=>n.current?.focus());else if(i!==o)h(`비밀번호 확인 값이 일치하지 않습니다.`),window.requestAnimationFrame(()=>r.current?.focus());else{p(!0);try{await Kt(i),a(``),s(``);try{await Oe()}catch{}e()}catch(e){h(e instanceof B?e.message:`관리자 비밀번호를 설정하지 못했습니다. 잠시 후 다시 시도해 주세요.`)}finally{p(!1)}}},noValidate:!0,children:[H(`label`,{class:`ssh-login-field`,for:`smartsafehub-new-root-password`,children:[H(`span`,{class:`ssh-login-label`,children:`새 비밀번호`}),H(`span`,{class:`ssh-login-input-wrap`,children:[H(`span`,{class:`ssh-login-field-icon`,"aria-hidden":`true`,children:H(yn,{})}),H(`input`,{autoComplete:`new-password`,disabled:f,id:`smartsafehub-new-root-password`,name:`new-password`,onBlur:()=>d(!1),onInput:e=>a(e.currentTarget.value),onKeyDown:S,onKeyUp:S,placeholder:`새 관리자 비밀번호`,ref:n,...c?{type:`text`}:{type:`password`},value:i}),H(`button`,{"aria-label":c?`비밀번호 숨기기`:`비밀번호 표시`,"aria-pressed":c,class:`ssh-login-password-toggle`,disabled:f,onClick:()=>l(e=>!e),title:c?`비밀번호 숨기기`:`비밀번호 표시`,type:`button`,children:H(c?Sn:xn,{})})]})]}),H(`ul`,{class:`ssh-password-requirements`,"aria-label":`비밀번호 요구 사항`,children:[H(Wn,{met:v.length,children:`8자 이상`}),H(Wn,{met:v.letter,children:`영문자 1자 이상 포함`}),H(Wn,{met:v.number,children:`숫자 1자 이상 포함`})]}),H(`label`,{class:`ssh-login-field`,for:`smartsafehub-confirm-root-password`,children:[H(`span`,{class:`ssh-login-label`,children:`비밀번호 확인`}),H(`span`,{class:`ssh-login-input-wrap`,children:[H(`span`,{class:`ssh-login-field-icon`,"aria-hidden":`true`,children:H(yn,{})}),H(`input`,{autoComplete:`new-password`,disabled:f,id:`smartsafehub-confirm-root-password`,name:`confirm-password`,onBlur:()=>d(!1),onInput:e=>s(e.currentTarget.value),onKeyDown:S,onKeyUp:S,placeholder:`새 관리자 비밀번호 다시 입력`,ref:r,...c?{type:`text`}:{type:`password`},value:o})]})]}),o.length>0?H(`p`,{class:b?`ssh-password-match is-met`:`ssh-password-match`,role:`status`,children:b?`비밀번호가 일치합니다.`:`비밀번호가 일치하지 않습니다.`}):null,u?H(`p`,{class:`ssh-login-caps`,role:`status`,children:`Caps Lock이 켜져 있습니다.`}):null,H(`button`,{class:`ssh-login-submit`,disabled:f,type:`submit`,children:[f?H(hn,{class:`ssh-login-submit-spinner`,"aria-hidden":`true`}):null,H(`span`,{children:f?`설정 중…`:`비밀번호 설정`})]})]}),H(`div`,{class:`ssh-login-security-note`,children:[H(Yt,{"aria-hidden":`true`}),H(`span`,{children:`비밀번호는 현재 공유기에 직접 설정되며 외부 서버로 전송되지 않습니다.`})]})]}),H(`footer`,{class:`ssh-login-footer`,children:[H(`span`,{children:`SmartSafeHub`}),H(`span`,{"aria-hidden":`true`,children:`·`}),H(`span`,{children:window.location.host})]})]})]})]})}var G=`smartsafehub`,Kn=`smartsafehub_guest`,qn=`smartsafehub_network`;function Jn(){return V(G,`connected_devices`)}async function Yn(){let e=await V(G,`activity_history`);return{...e,cloud:{...e.cloud,enabled:e.cloud.enabled??!1}}}function Xn(e){return V(G,`activity_cloud_sync_update`,{enabled:e})}function Zn(){return V(G,`status`)}function Qn(e){return V(G,`device_recommended_rules_status`,{},{...e?{signal:e}:{},priority:`low`,timeoutMs:5e3})}function $n(){return V(G,`device_recommended_rules_refresh`,{},{priority:`high`,timeoutMs:6e3})}function er(e){return V(G,`device_recommended_rules_revoke`,{rule_id:e},{priority:`high`,timeoutMs:6e3})}function tr(e){return V(G,`device_registration_status`,{},{...e?{signal:e}:{},priority:`low`,timeoutMs:5e3})}function nr(){return V(G,`device_registration_refresh`,{},{priority:`high`,timeoutMs:6e3})}function rr(){return V(G,`device_pairing_refresh`,{},{priority:`high`,timeoutMs:6e3})}function ir(){return V(G,`health_status`)}function ar(){return V(G,`health_run`)}function or(e){return V(G,`health_reporter_update`,{enabled:e})}function sr(e){return V(G,`apps_catalog`,{},{...e?{signal:e}:{},priority:`low`,timeoutMs:6e3})}function cr(){return V(G,`apps_catalog`,{refresh:!0},{priority:`high`,timeoutMs:6e3})}function lr(e){return V(G,`apps_status`,{},{...e?{signal:e}:{},priority:`low`,timeoutMs:6e3})}function ur(){return V(G,`apps_status`,{refresh:!0},{priority:`high`,timeoutMs:6e3})}function dr(e){return V(G,`apps_metrics`,{},{...e?{signal:e}:{},priority:`low`,timeoutMs:6e3})}function fr(){return V(G,`apps_metrics`,{refresh:!0},{priority:`high`,timeoutMs:6e3})}function pr(e){return V(G,`apps_install`,{app_id:e},{timeoutMs:8e3})}function mr(e){return V(G,`apps_update`,{app_id:e},{timeoutMs:8e3})}function hr(e){return V(G,`apps_remove`,{app_id:e},{timeoutMs:8e3})}function gr(){return V(G,`updates_status`)}function _r(){return V(G,`firmware_status`)}function vr(){return V(G,`firmware_check`)}function yr(){return V(G,`firmware_prepare`)}function br(e){return V(G,`firmware_validate_upload`,{filename:e})}function xr(e){return V(G,`firmware_install`,{confirm:`install`,keep_settings:e})}function Sr(){return V(G,`firmware_discard`)}function Cr(){return V(G,`updates_check`)}function wr(){return V(G,`updates_install`,{confirm:`install`})}function Tr(e){return V(G,`updates_settings_update`,{check_enabled:e.checkEnabled,check_interval_s:e.checkIntervalSeconds,auto_install:e.autoInstall,auto_install_time:e.autoInstallTime})}function Er(){return V(G,`system_time_settings`)}function Dr(e){return V(G,`system_timezone_initialize`,{zonename:e})}function Or(e){return V(G,`system_timezone_update`,{zonename:e})}function kr(){return V(G,`system_time_sync`)}function Ar(){return V(G,`system_scheduled_reboot_settings`)}function jr(e){return V(G,`system_scheduled_reboot_update`,{enabled:e.enabled,frequency:e.frequency,day_of_week:e.dayOfWeek,time:e.time})}function Mr(e){return V(G,`system_backup_validate`,{filename:e})}function Nr(){return V(G,`system_backup_restore`,{confirm:`restore`},{timeoutMs:35e3})}function Pr(){return V(G,`system_backup_discard`)}function Fr(){return V(G,`system_reboot`,{confirm:`reboot`})}function Ir(){return V(qn,`wan_settings`)}function Lr(e){return V(qn,`wan_update`,{protocol:e.protocol,pppoe_username:e.pppoeUsername,pppoe_password:e.pppoePassword,pppoe_password_changed:e.pppoePasswordChanged,static_address:e.staticAddress,static_prefix_length:e.staticPrefixLength,static_gateway:e.staticGateway,dns_primary:e.dnsPrimary,dns_secondary:e.dnsSecondary,confirm:`apply`},{timeoutMs:1e4})}function Rr(){return V(qn,`wan_reconnect`,{confirm:`reconnect`},{timeoutMs:1e4})}function zr(){return V(qn,`lan_settings`)}function Br(e){return V(qn,`lan_update`,{ip_address:e.ipAddress,prefix_length:e.prefixLength,dhcp_enabled:e.dhcpEnabled,dhcp_start:e.dhcpStart,dhcp_end:e.dhcpEnd,lease_time:e.leaseTime,confirm:`apply`},{timeoutMs:1e4})}function Vr(){return V(qn,`lan_auto_subnet`,{confirm:`apply`},{timeoutMs:1e4})}function Hr(){return V(qn,`iptv_settings`)}function Ur(e){return V(qn,`iptv_update`,{enabled:e.enabled,provider:e.provider,confirm:`apply`},{timeoutMs:2e4})}function Wr(e){return V(G,`wifi_qr`,{section:e},{timeoutMs:8e3})}function Gr(){return V(Kn,`wifi_guest_qr`,{},{timeoutMs:8e3})}async function Kr(){let e=await V(G,`wifi_summary`);try{let t=await V(Kn,`wifi_guest_summary`);return{...e,guest:t,guestError:!1}}catch{return{...e,guest:null,guestError:!0}}}function qr(e){return V(G,`wifi_update`,{...e},{timeoutMs:35e3})}function Jr(e){return V(Kn,`wifi_guest_update`,{...e},{timeoutMs:95e3})}var Yr=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:`apps`,hash:`#apps`,label:`Apps`,title:`SmartSafeHub Apps`,description:`SmartSafeHub에서 검증한 앱과 확장 기능을 살펴봅니다.`},{route:`system`,hash:`#system`,label:`업데이트`,title:`업데이트`,description:`기기의 펌웨어와 관리 소프트웨어 업데이트를 관리합니다.`},{route:`settings`,hash:`#settings`,label:`설정`,title:`설정`,description:`시스템 상태와 시간대를 확인하고 장치 관리 및 진단 기능을 설정합니다.`}].map(e=>[e.route,e])),Xr=[`home`,`activity`,`network`,`wifi`,`devices`,`safeshield`,`rules`,`account`,`apps`,`system`];function Zr({description:e,loading:t,onRefresh:n,onToggleTheme:r,refreshing:i,theme:a,title:o}){return H(`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:H(`div`,{class:`flex w-full items-center justify-between gap-4 md:h-full`,children:[H(`div`,{class:`min-w-0`,children:[H(`p`,{class:`m-0 text-[0.68rem] font-extrabold uppercase tracking-[0.18em] text-teal-700`,children:`SmartSafeHub`}),H(`div`,{class:`mt-1 flex min-w-0 items-baseline gap-3`,children:[H(`h1`,{class:`m-0 truncate text-xl font-black tracking-tight text-slate-950 sm:text-2xl`,children:o}),H(`p`,{class:`m-0 hidden truncate text-sm text-slate-500 lg:block`,children:e})]})]}),H(`div`,{class:`flex shrink-0 items-center gap-2`,children:[H(`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:H(a===`dark`?In:Fn,{class:`size-5`})}),H(`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?H(hn,{class:`size-5 animate-spin`}):H(hn,{class:`size-5`})})]})]})})}var Qr=[{label:`Overview`,routes:[`home`,`activity`]},{label:`Network`,routes:[`network`,`wifi`,`iptv`,`devices`]},{label:`Security`,routes:[`safeshield`,`rules`]},{label:`Explore`,routes:[`apps`]},{label:`System`,routes:[`account`,`system`,`settings`]}];function $r({route:e}){switch(e){case`home`:return H(Qt,{class:`size-5`});case`activity`:return H(pn,{class:`size-5`});case`network`:return H(dn,{class:`size-5`});case`wifi`:return H(cn,{class:`size-5`});case`iptv`:return H(fn,{class:`size-5`});case`devices`:return H(Zt,{class:`size-5`});case`safeshield`:return H(Yt,{class:`size-5`});case`rules`:return H($t,{class:`size-5`});case`account`:return H(yn,{class:`size-5`});case`apps`:return H(en,{class:`size-5`});case`system`:return H(on,{class:`size-5`});case`settings`:return H(sn,{class:`size-5`})}}function ei({theme:e}){return H(e===`dark`?In:Fn,{class:`size-5`})}function ti(e){return e===`dark`?`라이트 모드`:`다크 모드`}function ni(e,t=!1,n){let r=t?`justify-center px-2`:`gap-3 px-3`;return e?`relative flex min-h-11 min-w-0 items-center rounded-xl py-2.5 text-sm font-extrabold no-underline transition ${r} bg-teal-50 text-teal-800 ring-1 ring-inset ring-teal-200`:n===`apps`?`ssh-apps-nav-link relative flex min-h-11 min-w-0 items-center rounded-xl py-2.5 text-sm font-extrabold no-underline transition ${r}`:`relative flex min-h-11 min-w-0 items-center rounded-xl py-2.5 text-sm font-extrabold text-slate-600 no-underline transition ${r} hover:bg-slate-50 hover:text-slate-950`}function ri(e){return e===`apps`?`앱으로 확장하기`:null}function ii({accountRegistered:e,collapsed:t=!1,route:n,updateCount:r,demo:i=!1}){return H(`div`,{class:t?`space-y-3`:`space-y-5`,children:Qr.filter(e=>!i||e.routes.some(e=>Xr.includes(e))).map((a,o)=>H(`section`,{"aria-label":a.label,class:t&&o>0?`border-t border-slate-100 pt-3`:void 0,children:[t?null:H(`p`,{class:`mb-2 px-3 text-[0.65rem] font-black uppercase tracking-[0.18em] text-slate-400`,children:a.label}),H(`div`,{class:`space-y-1`,children:a.routes.filter(e=>!i||Xr.includes(e)).map(i=>{let a=Yr[i],o=n===i;return H(`a`,{"aria-current":o?`page`:void 0,"aria-label":i===`account`&&e===!1?`${a.label} (계정 연결 필요)`:t?a.label:void 0,class:ni(o,t,i),href:a.hash,title:i===`account`&&e===!1?`SmartSafeHub 계정 연결 필요`:t?a.label:void 0,children:[H(`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:H($r,{route:i})}),t?null:H(`span`,{class:`flex min-w-0 flex-1 items-center gap-2`,children:[H(`span`,{class:`min-w-0 flex-1`,children:[H(`span`,{class:`block truncate`,children:a.label}),ri(i)?H(`span`,{class:`ssh-apps-nav-description mt-0.5 block truncate text-[11px] font-semibold tracking-normal`,children:ri(i)}):null]}),i===`iptv`?H(`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`?H(`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?H(`span`,{"aria-label":`계정 연결 필요`,class:`ssh-account-nav-dot ${t?`ssh-account-nav-dot-collapsed`:``}`,"aria-hidden":`true`,title:`계정 연결 필요`}):null,i===`system`&&r>0?t?H(`span`,{"aria-label":`${r}개의 업데이트 유형`,class:`ssh-update-nav-badge`,"data-collapsed":`true`,children:r}):H(`span`,{class:`ssh-update-nav-badge`,"data-collapsed":`false`,children:r}):null]},i)})})]},a.label))})}function ai({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=ti(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]),H(b,{children:[H(`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:[H(`div`,{class:`flex min-h-16 items-center justify-between gap-3`,children:[H(`a`,{class:`flex min-w-0 items-center gap-3 no-underline`,href:`#home`,children:[H(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-950 text-teal-300`,children:H(Yt,{class:`size-5`})}),H(`span`,{class:`min-w-0`,children:[H(`strong`,{class:`block truncate text-sm font-black text-slate-950`,children:`SmartSafeHub`}),H(`span`,{class:`block truncate text-xs font-semibold text-slate-500`,children:l})]})]}),H(`div`,{class:`flex shrink-0 items-center gap-2`,children:[H(`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:H(ei,{theme:c})}),H(`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?H(hn,{class:`size-5 animate-spin`}):H(hn,{class:`size-5`})}),H(`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:H(f?Mn:An,{class:`size-5`})})]})]}),f?H(`div`,{class:`ssh-mobile-menu border-t border-slate-100 py-4`,id:`smartsafehub-mobile-menu`,children:[H(ii,{accountRegistered:e,demo:d,route:s,updateCount:u}),d?null:H(`div`,{class:`mt-5 space-y-1 border-t border-slate-100 pt-4`,children:H(`a`,{class:`${ni(!1)} text-rose-700 hover:bg-rose-50 hover:text-rose-800`,href:be(`/admin/logout`),children:[H(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg bg-rose-50 text-rose-600`,children:H(kn,{class:`size-5`})}),`로그아웃`]})})]}):null]}),H(`aside`,{class:`hidden min-h-screen border-r border-slate-200 bg-white md:flex md:flex-col`,"data-collapsed":t?`true`:`false`,children:H(`div`,{class:`sticky top-0 flex h-screen flex-col`,children:[H(`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:[H(`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:[H(`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:H(Yt,{class:`size-5`})}),t?null:H(`div`,{class:`min-w-0`,children:[H(`strong`,{class:`block truncate text-sm font-black tracking-tight text-slate-950`,children:`SmartSafeHub`}),H(`span`,{class:`block text-[0.68rem] font-bold uppercase tracking-[0.14em] text-slate-400`,children:`Home Gateway`})]})]}),H(`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:H(t?Pn:Nn,{class:`size-4`})})]}),H(`nav`,{"aria-label":`SmartSafeHub 메뉴`,class:`flex-1 overflow-y-auto py-5 ${t?`px-2`:`px-3`}`,id:`smartsafehub-desktop-navigation`,children:H(ii,{accountRegistered:e,collapsed:t,demo:d,route:s,updateCount:u})}),d?null:H(`div`,{class:`border-t border-slate-100 ${t?`p-2`:`p-3`}`,children:H(`a`,{"aria-label":`SmartSafeHub에서 로그아웃`,class:`${ni(!1,t)} mt-1 text-rose-700 hover:bg-rose-50 hover:text-rose-800`,href:be(`/admin/logout`),title:t?`로그아웃`:void 0,children:[H(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg bg-rose-50 text-rose-600`,children:H(kn,{class:`size-5`})}),t?null:`로그아웃`]})})]})})]})}var oi=`smartsafehub.sidebar.collapsed`;function si(){if(typeof window>`u`)return!1;try{return window.localStorage.getItem(oi)===`1`}catch{return!1}}function ci({accountRegistered:e,children:t,loading:n,onRefresh:r,refreshing:i,route:a,updateCount:o,demo:s=!1}){let c=Yr[a],[l,u]=F(si),[d,f]=F(Rn);return I(()=>{try{window.localStorage.setItem(oi,l?`1`:`0`)}catch{}},[l]),I(()=>{zn(d),Bn(d)},[d]),H(`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:[H(ai,{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}),H(`div`,{class:`min-w-0 md:min-h-screen`,children:[H(Zr,{description:c.description,loading:n,onRefresh:r,onToggleTheme:()=>f(e=>e===`dark`?`light`:`dark`),refreshing:i,theme:d,title:c.title}),H(`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})]})]})}function li({active:e,label:t,tabs:n}){return H(`nav`,{"aria-label":t,class:`mb-5 min-w-0`,role:`tablist`,children:H(`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 H(`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)})})})}function K(e,t){return e instanceof Error?e.message:t}function ui({active:e=!0,fallbackError:t,loader:n,pollInterval:r,refreshOnFocus:i=!1,refreshOnVisible:a=!1,staleTimeMs:o=3e4}){let[s,c]=F({data:null,error:null,loading:e,refreshing:!1}),l=L(!1),u=L(!0),d=L(null),f=L(null),p=L(0),m=L(null),h=L(null),g=L(0);I(()=>(u.current=!0,()=>{u.current=!1,p.current+=1,f.current?.abort()}),[]);let _=R((e=!1)=>{if(d.current)return d.current;u.current&&c(t=>({...t,error:null,loading:t.data===null,refreshing:e}));let r,i=++p.current,a=new AbortController;return f.current=a,m.current=Date.now(),r=n(a.signal).then(e=>{i!==p.current||a.signal.aborted||(h.current=Date.now(),g.current=0,u.current&&c({data:e,error:null,loading:!1,refreshing:!1}))}).catch(e=>{i!==p.current||a.signal.aborted||(g.current+=1,u.current&&c(n=>({...n,error:K(e,t),loading:!1,refreshing:!1})))}).finally(()=>{d.current===r&&(d.current=null),f.current===a&&(f.current=null)}),d.current=r,r},[t,n]);I(()=>{e?l.current||(l.current=!0,s.data!==null&&h.current!==null&&Date.now()-h.current{if(!e||v==null||v<=0)return;let t=!1,n=null,r=()=>{n!==null&&(window.clearTimeout(n),n=null)},o=()=>{let e=Date.now();if(g.current>0&&m.current!==null){let t=Math.min(3e4,3e3*2**Math.min(g.current-1,4));return Math.max(0,m.current+t-e)}return h.current===null?0:Math.max(0,h.current+v-e)},s=()=>{r(),!(t||document.visibilityState===`hidden`)&&(n=window.setTimeout(()=>{n=null,!(t||document.visibilityState===`hidden`)&&(o()>0?s():_(!1).finally(s))},o()))},c=()=>{r(),!(t||document.visibilityState===`hidden`)&&(o()>0?s():_(!1).finally(s))},l=()=>{document.visibilityState===`hidden`?r():a?_(!1).finally(s):c()},u=()=>{c()};return document.addEventListener(`visibilitychange`,l),i&&window.addEventListener(`focus`,u),s(),()=>{t=!0,r(),document.removeEventListener(`visibilitychange`,l),i&&window.removeEventListener(`focus`,u)}},[e,v,_,i,a]);let y=R(()=>_(!0),[_]),b=R(e=>{u.current&&(h.current=Date.now(),g.current=0,c(t=>({...t,data:e,error:null,loading:!1,refreshing:!1})))},[]);return{...s,refresh:y,replaceData:b}}var di=6e4;function fi(e){let t=ui({active:e,fallbackError:`최근 활동을 불러오지 못했습니다.`,loader:()=>Yn(),pollInterval:di,refreshOnFocus:!0}),[n,r]=F({savingCloudSync:!1,actionError:null,actionMessage:null}),i=R(async e=>{r({savingCloudSync:!0,actionError:null,actionMessage:null});try{let n=await Xn(e);return t.replaceData(n),r({savingCloudSync:!1,actionError:null,actionMessage:e?`Cloud 활동 기록 전송을 켰습니다. 지금부터 발생하는 활동을 Cloud에 전송합니다.`:`Cloud 활동 기록 전송을 껐습니다. 로컬 최근 활동은 계속 기록됩니다.`}),!0}catch(e){return r({savingCloudSync:!1,actionError:K(e,`Cloud 활동 기록 설정을 변경하지 못했습니다.`),actionMessage:null}),!1}},[t.replaceData]),a=R(()=>{r(e=>({...e,actionError:null,actionMessage:null}))},[]);return{...t,...n,dismissActionFeedback:a,setCloudSyncEnabled:i}}var pi=`/tmp/smartsafehub/config-backup.tar.gz`,mi=`/cgi-backup`,hi=`/cgi-upload`;function gi(){let e=window.__SMARTHUB_BOOTSTRAP__?.sessionId;if(!e)throw new B(`BOOTSTRAP_MISSING`,`LuCI 세션 정보를 찾을 수 없습니다.`);return e}function _i(e){return(e?.match(/filename="?([^";]+)"?/i))?.[1]?.trim()||`backup-openwrt.tar.gz`}function vi(e,t){let n=URL.createObjectURL(t),r=document.createElement(`a`);r.href=n,r.download=e,r.style.display=`none`,(document.body??document.documentElement).appendChild(r),r.click(),r.remove(),window.setTimeout(()=>URL.revokeObjectURL(n),1e3)}async function yi(){let e=ve(mi),t=await fetch(e,{method:`POST`,credentials:`same-origin`,headers:{"Content-Type":`application/x-www-form-urlencoded;charset=UTF-8`},body:new URLSearchParams({sessionid:gi()}).toString()});if(!t.ok)throw new B(`SYSTEM_BACKUP_DOWNLOAD_FAILED`,`설정 백업 생성에 실패했습니다. (HTTP ${t.status})`);let n=await t.blob();if(n.size<=0)throw new B(`SYSTEM_BACKUP_DOWNLOAD_EMPTY`,`설정 백업 파일이 비어 있습니다.`);vi(_i(t.headers.get(`Content-Disposition`)),n)}function bi(e,t){let n;try{n=gi()}catch(e){return Promise.reject(e)}return new Promise((r,i)=>{let a=new XMLHttpRequest,o=new FormData;o.append(`sessionid`,n),o.append(`filename`,pi),o.append(`filedata`,e),a.open(`POST`,ve(hi)),a.timeout=0,a.upload.addEventListener(`progress`,e=>{!e.lengthComputable||e.total<=0||t?.(Math.min(100,Math.max(0,e.loaded/e.total*100)))}),a.addEventListener(`error`,()=>{i(new B(`SYSTEM_BACKUP_UPLOAD_FAILED`,`설정 백업 파일을 공유기에 업로드하지 못했습니다.`))}),a.addEventListener(`load`,()=>{if(a.status<200||a.status>=300){i(new B(`SYSTEM_BACKUP_UPLOAD_HTTP_ERROR`,`설정 백업 업로드가 HTTP ${a.status} 오류를 반환했습니다.`));return}let n;try{n=JSON.parse(a.responseText)}catch{i(new B(`SYSTEM_BACKUP_UPLOAD_INVALID_RESPONSE`,`설정 백업 업로드 응답을 확인하지 못했습니다.`));return}n.failure?i(new B(n.failure,n.message||`설정 백업 파일 업로드가 거부되었습니다.`)):(t?.(100),r({...n,name:e.name}))}),a.send(o)})}var xi=16777216;function Si(){let[e,t]=F(null),[n,r]=F(null),[i,a]=F(null),[o,s]=F(null),[c,l]=F(null),[u,d]=F(!1),f=R(async()=>{t(`download`),r(null),a(null);try{await yi(),a(`현재 설정 백업을 다운로드했습니다.`)}catch(e){r(K(e,`설정 백업을 다운로드하지 못했습니다.`))}finally{t(null)}},[]),p=R(async e=>{if(r(null),a(null),s(null),d(!1),e.size<=0||e.size>xi)return r(`설정 백업 파일은 16MB 이하여야 합니다.`),!1;try{t(`upload`),l(0),await bi(e,l),t(`validate`);let n=await Mr(e.name);return s(n),a(`백업 파일 검증이 완료되었습니다. 복원 내용을 확인해 주세요.`),!0}catch(e){return r(K(e,`설정 백업 파일을 검증하지 못했습니다.`)),!1}finally{t(null),l(null)}},[]),m=R(async()=>{t(`discard`),r(null),a(null);try{return await Pr(),s(null),d(!1),a(`업로드한 백업 파일을 삭제했습니다.`),!0}catch(e){return r(K(e,`업로드한 백업 파일을 삭제하지 못했습니다.`)),!1}finally{t(null)}},[]),h=R(async()=>{if(!o)return r(`먼저 복원할 백업 파일을 업로드하고 검증해 주세요.`),!1;t(`restore`),r(null),a(null);try{let e=await Nr();return d(e.accepted&&e.rebooting),s(null),a(e.accepted?`설정을 복원했습니다. 공유기가 재부팅되며 네트워크 설정에 따라 다시 연결해야 할 수 있습니다.`:`설정 복원 요청이 접수되지 않았습니다.`),e.accepted}catch(e){return r(K(e,`설정 백업을 복원하지 못했습니다.`)),!1}finally{t(null)}},[o]);return{action:e,discard:m,dismissFeedback:R(()=>{r(null),a(null)},[]),download:f,error:n,message:i,restore:h,restoreAccepted:u,upload:p,uploadProgress:c,validated:o}}var Ci=15e3;function wi(e,t=!0){return ui({active:e,fallbackError:`연결된 기기 목록을 불러오지 못했습니다.`,loader:Jn,...t?{pollInterval:Ci}:{}})}var Ti=`/tmp/smartsafehub/firmware.bin`,Ei=`/cgi-upload`;function Di(e,t){let n=window.__SMARTHUB_BOOTSTRAP__;return n?.sessionId?new Promise((r,i)=>{let a=new XMLHttpRequest,o=new FormData;o.append(`sessionid`,n.sessionId),o.append(`filename`,Ti),o.append(`filedata`,e);let s=ve(Ei);a.open(`POST`,s),a.timeout=0,a.upload.addEventListener(`progress`,e=>{!e.lengthComputable||e.total<=0||t?.(Math.min(100,Math.max(0,e.loaded/e.total*100)))}),a.addEventListener(`error`,()=>{console.error(`[SmartSafeHub] firmware upload network error`,{endpoint:s,destination:Ti,fileName:e.name,fileSize:e.size,readyState:a.readyState,status:a.status}),i(new B(`FIRMWARE_UPLOAD_FAILED`,`펌웨어 파일 업로드에 실패했습니다. (${s}, 네트워크 오류)`))}),a.addEventListener(`load`,()=>{if(a.status<200||a.status>=300){console.error(`[SmartSafeHub] firmware upload HTTP error`,{endpoint:s,destination:Ti,fileName:e.name,fileSize:e.size,readyState:a.readyState,status:a.status,response:a.responseText}),i(new B(`FIRMWARE_UPLOAD_HTTP_ERROR`,`펌웨어 업로드가 HTTP ${a.status} 오류를 반환했습니다. (${s})`));return}let n;try{n=JSON.parse(a.responseText)}catch{console.error(`[SmartSafeHub] firmware upload returned invalid JSON`,{endpoint:s,destination:Ti,fileName:e.name,fileSize:e.size,readyState:a.readyState,status:a.status,response:a.responseText}),i(new B(`FIRMWARE_UPLOAD_INVALID_RESPONSE`,`펌웨어 업로드 응답을 확인하지 못했습니다. (${s})`));return}n.failure?i(new B(n.failure,n.message||`펌웨어 파일 업로드가 거부되었습니다.`)):(t?.(100),r({...n,name:e.name}))}),a.send(o)}):Promise.reject(new B(`BOOTSTRAP_MISSING`,`LuCI 세션 정보를 찾을 수 없습니다.`))}var Oi=3e5,ki=1e3,Ai=500,ji=3e3,Mi=3e4,Ni=15e3,Pi=5e3;function Fi(e){return new Promise(t=>window.setTimeout(t,e))}function Ii(e){return e===`checking`||e===`downloading`||e===`verifying`}function Li(e=!0){let[t,n]=F(null),r=ui({active:e||Ii(t??void 0),fallbackError:`펌웨어 업데이트 상태를 불러오지 못했습니다.`,loader:_r,pollInterval:e=>Ii(e?.phase)?ki:Oi,refreshOnFocus:!0,refreshOnVisible:Ii(t??void 0)});I(()=>{n(r.data?.phase??null)},[r.data?.phase]);let[i,a]=F(null),[o,s]=F(null),[c,l]=F(null),[u,d]=F(null),[f,p]=F(!1),m=L(null);I(()=>()=>{m.current!==null&&window.clearTimeout(m.current)},[]);let h=R(e=>{n(e),r.data&&r.replaceData({...r.data,phase:e,lastError:null})},[r]),g=R(async(e,t,n,i)=>{a(e),s(null),l(null);try{if(!(await n()).accepted)throw Error(`펌웨어 작업 요청이 접수되지 않았습니다.`);h(t),a(null),window.setTimeout(()=>void r.refresh(),400)}catch(e){a(null),s(K(e,i))}},[h,r]),_=R(()=>g(`check`,`checking`,vr,`펌웨어 업데이트 확인을 시작하지 못했습니다.`),[g]),v=R(()=>g(`prepare`,`downloading`,yr,`펌웨어 다운로드를 시작하지 못했습니다.`),[g]),y=R(async()=>{let e=Date.now(),t=null;for(;Date.now()-e{a(`upload`),s(null),l(null),d(0);try{if(await Di(e,d),a(`validate-upload`),!(await br(e.name)).accepted)throw Error(`업로드한 펌웨어 검증 요청이 접수되지 않았습니다.`);h(`verifying`),d(null);let t=await y();if(a(null),t.phase===`error`)return!1;if(!t.prepared)throw Error(`검증된 펌웨어의 설치 준비 정보를 확인하지 못했습니다. 다시 시도해 주세요.`);return l(`펌웨어 검증이 완료되었습니다. 설치 옵션을 확인해 주세요.`),!0}catch(e){return a(null),d(null),s(K(e,`펌웨어 파일을 업로드하지 못했습니다.`)),!1}},[h,y]),x=R(()=>{p(!0);let e=async()=>{try{if((await fetch(window.location.pathname,{cache:`no-store`,credentials:`same-origin`})).ok){window.location.reload();return}}catch{}m.current=window.setTimeout(e,Pi)};m.current=window.setTimeout(e,Ni)},[]),S=R(async e=>{a(`install`),s(null),l(null);try{if(!(await xr(e)).accepted)throw Error(`펌웨어 설치 요청이 접수되지 않았습니다.`);return h(`flashing`),a(null),x(),!0}catch(e){return a(null),s(K(e,`펌웨어 설치를 시작하지 못했습니다.`)),!1}},[h,x]),C=R(async()=>{a(`discard`),s(null),l(null);try{if(!(await Sr()).accepted)throw Error(`준비된 펌웨어 정리 요청이 접수되지 않았습니다.`);return a(null),l(`준비된 펌웨어 파일을 삭제했습니다.`),await r.refresh(),!0}catch(e){return a(null),s(K(e,`준비된 펌웨어를 정리하지 못했습니다.`)),!1}},[r]),w=R(()=>{s(null),l(null)},[]);return{...r,action:i,actionError:o,message:c,uploadProgress:u,reconnecting:f,check:_,prepare:v,upload:b,install:S,discard:C,dismissFeedback:w}}var Ri={"#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`,"#apps":`apps`,"#system":`system`,"#settings":`settings`,"#settings/diagnostics":`settings`,"#settings/time":`settings`,"#settings/management":`settings`};function zi(){return Ri[window.location.hash]??`home`}function Bi(){let[e,t]=F(zi);return I(()=>{let e=()=>t(zi());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),e}function Vi(e){return e===`#lan`||e===`#network/lan`?`lan`:`internet`}function Hi(e){return e===`#settings/time`?`time`:e===`#settings/management`?`management`:`diagnostics`}function Ui(){let e=window.location.hash;return{network:Vi(e),settings:Hi(e)}}function Wi(){let[e,t]=F(Ui);return I(()=>{let e=()=>t(Ui());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),e}var Gi=6e4,Ki=1e3,qi=[400,800,1200,1600],Ji=[400,800,1200,2e3,4e3];function Yi(e){return new Promise(t=>window.setTimeout(t,e))}function Xi(e){let t=ui({active:e,fallbackError:`장치 진단 상태를 불러오지 못했습니다.`,loader:ir,pollInterval:e=>e?.generatedAt?Gi:Ki,refreshOnFocus:!0}),n=L(0),[r,i]=F({running:!1,savingReporter:!1,actionError:null,actionMessage:null}),a=R(async()=>{i({running:!0,savingReporter:!1,actionError:null,actionMessage:null});try{let e=t.data?.generatedAt??0;if(!(await ar()).accepted)throw Error(`장치가 진단 요청을 접수하지 않았습니다.`);let n=t.data;for(let t of qi)if(await Yi(t),n=await ir(),n.generatedAt>0&&n.generatedAt!==e)break;return n&&t.replaceData(n),i({running:!1,savingReporter:!1,actionError:null,actionMessage:`장치 상태를 다시 진단했습니다.`}),!0}catch(e){return i({running:!1,savingReporter:!1,actionError:K(e,`장치 상태 진단을 실행하지 못했습니다.`),actionMessage:null}),!1}},[t.data,t.replaceData]),o=R(async(e,r)=>{for(let i of Ji){if(await Yi(i),n.current!==r)return;try{let i=await ir();if(n.current!==r)return;if(t.replaceData(i),i.reporter.enabled!==e)continue;if(!e||![`disabled`,`never`,`initializing`].includes(i.reporter.lastResult))return}catch{}}},[t.replaceData]),s=R(async e=>{let r=n.current+1;n.current=r;let a=t.data;a&&t.replaceData({...a,reporter:{...a.reporter,enabled:e,lastResult:e?`initializing`:`disabled`,lastErrorCode:null,nextReportAt:e?a.reporter.nextReportAt:0}}),i({running:!1,savingReporter:!0,actionError:null,actionMessage:null});try{let a=await or(e);return n.current===r&&(t.replaceData(a),i({running:!1,savingReporter:!1,actionError:null,actionMessage:e?`원격 상태 보고를 켰습니다. 첫 서버 보고를 준비합니다.`:`원격 상태 보고를 껐습니다. 로컬 진단은 계속 동작합니다.`}),o(e,r)),!0}catch(e){return n.current===r&&(a&&t.replaceData(a),i({running:!1,savingReporter:!1,actionError:K(e,`원격 상태 보고 설정을 변경하지 못했습니다.`),actionMessage:null})),!1}},[o,t.data,t.replaceData]),c=R(()=>{i(e=>({...e,actionError:null,actionMessage:null}))},[]);return{...t,...r,dismissActionFeedback:c,runDiagnostic:a,setReporterEnabled:s}}function Zi(e){let t=ui({active:e,fallbackError:`IPTV 설정을 불러오지 못했습니다.`,loader:Hr,refreshOnFocus:!0}),[n,r]=F(!1),[i,a]=F(null),o=R(async e=>{r(!0),a(null);try{let n=await Ur(e);return t.replaceData(n.settings),r(!1),a({kind:`success`,message:e.enabled?n.changed?`IPTV 설정을 적용했습니다.`:`IPTV 설정을 다시 적용했습니다.`:`IPTV 기능을 껐습니다.`}),!0}catch(e){return r(!1),a({kind:`error`,message:K(e,`IPTV 설정을 적용하지 못했습니다.`)}),!1}},[t.replaceData]),s=R(()=>a(null),[]);return{...t,saving:n,feedback:i,save:o,dismissFeedback:s}}var Qi=4e3;function $i(e){let t=ui({active:e,fallbackError:`내부 네트워크 설정을 불러오지 못했습니다.`,loader:zr}),n=L(e),[r,i]=F({action:null,feedback:null});I(()=>{n.current=e},[e]);let a=R((e,r)=>{t.replaceData(e.settings),e.changed?e.addressChanged?i({action:null,feedback:{kind:`warning`,message:r?`추천 대역 ${e.settings.lan.subnet}을 저장했습니다. 네트워크가 다시 적용되면 새 공유기 주소로 다시 연결해 주세요.`:`내부 네트워크 주소를 저장했습니다. 네트워크가 다시 적용되면 새 공유기 주소로 다시 연결해 주세요.`,newAddress:e.newAddress??e.settings.lan.address}}):(i({action:null,feedback:{kind:`success`,message:r?`추천 내부 네트워크 설정을 적용했습니다.`:`내부 네트워크 설정을 저장했습니다.`}}),e.reloadScheduled&&window.setTimeout(()=>{n.current&&t.refresh()},Qi)):i({action:null,feedback:{kind:`success`,message:`변경된 내부 네트워크 설정이 없습니다.`}})},[t.refresh,t.replaceData]),o=R(async e=>{i({action:`saving`,feedback:null});try{let t=await Br(e);return a(t,!1),!0}catch(e){return i({action:null,feedback:{kind:`error`,message:K(e,`내부 네트워크 설정을 저장하지 못했습니다.`)}}),!1}},[a]),s=R(async()=>{i({action:`auto`,feedback:null});try{let e=await Vr();return a(e,!0),!0}catch(e){return i({action:null,feedback:{kind:`error`,message:K(e,`추천 내부 네트워크를 적용하지 못했습니다.`)}}),!1}},[a]),c=R(()=>{i(e=>({...e,feedback:null}))},[]);return{...t,...r,save:o,applyRecommendation:s,dismissFeedback:c}}var ea=5e3;function ta(e){let t=ui({active:e,fallbackError:`인터넷 연결 설정을 불러오지 못했습니다.`,loader:Ir}),n=L(e),[r,i]=F({action:null,feedback:null});I(()=>{n.current=e},[e]);let a=R(()=>{window.setTimeout(()=>{n.current&&t.refresh()},ea)},[t.refresh]),o=R(async e=>{i({action:`saving`,feedback:null});try{let n=await Lr(e);return t.replaceData(n.settings),i({action:null,feedback:{kind:n.changed?`warning`:`success`,message:n.changed?`인터넷 연결 설정을 저장했습니다. WAN을 다시 연결하는 동안 잠시 인터넷이 끊길 수 있습니다.`:`변경된 인터넷 연결 설정이 없습니다.`}}),n.reconnectScheduled&&a(),!0}catch(e){return i({action:null,feedback:{kind:`error`,message:K(e,`인터넷 연결 설정을 저장하지 못했습니다.`)}}),!1}},[t.replaceData,a]),s=R(async()=>{i({action:`reconnecting`,feedback:null});try{let e=await Rr();t.replaceData(e.settings),i({action:null,feedback:{kind:`warning`,message:`WAN 재연결을 시작했습니다. 인터넷 연결이 다시 올라오면 상태를 자동으로 새로고침합니다.`}}),e.reconnectScheduled&&a()}catch(e){i({action:null,feedback:{kind:`error`,message:K(e,`WAN 재연결을 시작하지 못했습니다.`)}})}},[t.replaceData,a]),c=R(()=>{i(e=>({...e,feedback:null}))},[]);return{...t,...r,save:o,reconnect:s,dismissFeedback:c}}var na=`safeshield`,ra=`smartsafehub`;function ia(e,t){return typeof e==`string`&&e.length>0?e:t}function aa(e){return typeof e==`string`?e:``}function oa(e){return typeof e==`number`&&Number.isFinite(e)?e:0}function sa(e){return e===!0}function ca(e){return Array.isArray(e)?e.length:0}function la(e){return Array.isArray(e)?e.filter(e=>typeof e==`string`):[]}function ua(e){return typeof e==`object`&&e&&!Array.isArray(e)?e:{}}function da(e){let t=ua(e.error),n=aa(t.code)||`UNKNOWN_ERROR`,r=aa(t.message)||`SafeShield API 요청을 처리하지 못했습니다.`;return new B(`SAFESHIELD_${n.toUpperCase()}`,r)}async function fa(e,t={},n){let r=await Wt(na,e,t,{...n?{signal:n}:{},priority:n?`low`:`normal`}),i=ua(r);if(i.ok===!1)throw da(i);return r}async function pa(e,t,n,r=t){try{return await V(ra,e,t)}catch(e){if(e instanceof B&&[`RPC_PERMISSION_DENIED`,`UBUS_3`,`UBUS_6`].includes(e.code))return fa(n,r);throw e}}function ma(){return{enabled:!1,collectorRunning:!1,available:!1,schemaVersion:0,volatile:!0,startedAt:0,updatedAt:0,retentionHours:0,totals:{queries:0,blocked:0},hourly:[],deviceLimit:0,devicesTruncated:!1,devices:[]}}function ha(e){let t=ua(e);if(Object.keys(t).length===0)return ma();let n=ua(t.schema),r=ua(t.totals),i=Array.isArray(t.hourly)?t.hourly.map(e=>ua(e)).map(e=>({bucketStart:oa(e.bucket_start),queries:oa(e.queries),blocked:oa(e.blocked)})).filter(e=>e.bucketStart>0).sort((e,t)=>e.bucketStart-t.bucketStart):[],a=Array.isArray(t.devices)?t.devices.map(e=>ua(e)).map(e=>{let t=aa(e.ip),n=aa(e.mac);return{id:aa(e.id)||n||(t?`ip:${t}`:``),mac:n,ip:t,hostname:aa(e.hostname),identified:sa(e.identified),queries:oa(e.queries),blocked:oa(e.blocked)}}).filter(e=>e.id.length>0):[];return{enabled:sa(t.enabled),collectorRunning:sa(t.collector_running),available:t.available!==!1,schemaVersion:oa(n.version),volatile:t.volatile!==!1,startedAt:oa(t.started_at),updatedAt:oa(t.updated_at),retentionHours:oa(t.retention_hours),totals:{queries:oa(r.queries),blocked:oa(r.blocked)},hourly:i,deviceLimit:oa(t.device_limit),devicesTruncated:sa(t.devices_truncated),devices:a}}function ga(){return{available:!1,version:null,enabled:!1,active:!1,status:`unavailable`,stage:null,summary:{label:`Unavailable`,message:`SafeShield status API is unavailable`,severity:`warning`},runtime:{refreshdRunning:!1,dnsmasqRunning:!1,dnsRuntimeOk:!1,lastResult:null,lastErrorCode:null},entitlement:{plan:null,status:null},artifact:{resolved:!1,tier:null,version:null,uniqueDomains:0,rules:0},localOverrides:{enabled:!1,allowlistPath:null,blocklistPath:null},blocklist:{installed:!1,validLineCount:0,fileSizeKb:0,verificationOk:!1},health:{overall:`unavailable`},issueCounts:{warnings:0,errors:0},timestamps:{lastAttempt:0,lastSuccess:0,lastFailure:0,lastLocalApply:0,lastLocalApplyFailure:0,nextRefreshAt:0,refreshIntervalS:0}}}function _a(e){let t=ua(e);if(Object.keys(t).length===0)return ga();let n=ua(t.summary),r=ua(t.runtime),i=ua(t.entitlement),a=ua(t.artifact),o=ua(t.local_overrides),s=ua(t.blocklist),c=ua(t.health),l=ua(t.timestamps);return{available:!0,version:ia(t.version,null),enabled:sa(t.enabled),active:sa(t.active),status:ia(t.status,`unknown`)??`unknown`,stage:ia(t.stage,null),summary:{label:ia(n.label,`Unknown`)??`Unknown`,message:ia(n.message,`SafeShield status is unknown`)??`SafeShield status is unknown`,severity:ia(n.severity,`warning`)??`warning`},runtime:{refreshdRunning:sa(r.refreshd_running),dnsmasqRunning:sa(r.dnsmasq_running),dnsRuntimeOk:sa(r.dns_runtime_ok),lastResult:ia(r.last_result,null),lastErrorCode:ia(r.last_error_code,null)},entitlement:{plan:ia(i.plan,null),status:ia(i.status,null)},artifact:{resolved:sa(a.resolved),tier:ia(a.tier,null),version:ia(a.version,null),uniqueDomains:oa(a.unique_domains),rules:oa(a.rules)},localOverrides:{enabled:sa(o.enabled),allowlistPath:ia(o.allowlist_path,null),blocklistPath:ia(o.blocklist_path,null)},blocklist:{installed:sa(s.installed),validLineCount:oa(s.valid_line_count),fileSizeKb:oa(s.file_size_kb),verificationOk:sa(s.verification_ok)},health:{overall:ia(c.overall,`unknown`)??`unknown`},issueCounts:{warnings:ca(t.warnings),errors:ca(t.errors)},timestamps:{lastAttempt:oa(l.last_attempt),lastSuccess:oa(l.last_success),lastFailure:oa(l.last_failure),lastLocalApply:oa(l.last_local_apply),lastLocalApplyFailure:oa(l.last_local_apply_failure),nextRefreshAt:oa(l.next_refresh_at),refreshIntervalS:oa(l.refresh_interval_s)}}}function va(e){let t=ua(ua(e).values);return{enabled:sa(t.enabled),applyLocalOverrides:sa(t.apply_local_overrides)}}function ya(e){let t=ua(e);return{requested:sa(t.requested),accepted:sa(t.accepted),reason:aa(t.reason)}}async function ba(){try{return ha(await fa(`statistics`))}catch(e){if(e instanceof B&&[`UBUS_3`,`UBUS_4`,`UBUS_5`].includes(e.code))return ma();throw e}}async function xa(e){let t=await pa(`safeshield_statistics_update`,{enabled:e},`config_update`,{values:{statistics_enabled:e}});return{changed:Array.isArray(t.changed)?t.changed.some(e=>e===`statistics_enabled`):!1,enabled:e,restarted:sa(t.restarted),reconciled:sa(t.reconciled)}}async function Sa(){try{return _a(await fa(`status`))}catch(e){if(e instanceof B&&[`UBUS_3`,`UBUS_4`,`UBUS_5`].includes(e.code))return ga();throw e}}async function Ca(e){return va(await fa(`config`,{},e))}async function wa(e){let t=await pa(`safeshield_set_enabled`,{enabled:e},`set_enabled`);return{changed:sa(t.changed),accepted:sa(t.accepted),targetEnabled:sa(t.target_enabled),reconciled:sa(t.reconciled)}}async function Ta(){let e=await pa(`safeshield_refresh`,{},`refresh`);return{accepted:sa(e.accepted),reason:aa(e.reason),status:_a(e.status)}}async function Ea(e){let[t,n]=await Promise.all([fa(`rules_list`,{},e),Ca(e)]),r=la(t.allow),i=la(t.block);return{allow:r,block:i,counts:{allow:r.length,block:i.length,total:r.length+i.length},safeshieldEnabled:n.enabled,applyLocalOverrides:n.applyLocalOverrides}}async function Da(e,t,n,r=!0){let i=await pa(e===`rule_add`?`safeshield_rule_add`:`safeshield_rule_delete`,{action:t,domain:n,refresh:r},e),a=sa(e===`rule_add`?i.added:i.deleted);return{action:t,domain:aa(i.domain)||n,changed:a,refresh:ya(i.refresh),rules:await Ea()}}function Oa(e,t,n=!0){return Da(`rule_add`,e,t,n)}function ka(e,t,n=!0){return Da(`rule_delete`,e,t,n)}var Aa=4500;function ja(e,t){let[n,r]=F({action:null,error:null,message:null}),i=L([]),a=L([]),o=L(null),s=R(()=>{for(let e of i.current)window.clearTimeout(e);i.current=[]},[]),c=R(()=>{for(let e of a.current)window.clearTimeout(e);a.current=[]},[]),l=R(()=>{o.current!==null&&(window.clearTimeout(o.current),o.current=null)},[]),u=R(e=>{l(),r({action:e,error:null,message:null})},[l]),d=R(e=>{l(),r({action:null,error:null,message:e}),o.current=window.setTimeout(()=>{o.current=null,r(e=>e.error!==null||e.action!==null?e:{...e,message:null})},Aa)},[l]),f=R(t=>{s(),i.current=t.map(t=>window.setTimeout(()=>{e()},t))},[s,e]),p=R(e=>{c(),a.current=e.map(e=>window.setTimeout(()=>{t()},e))},[c,t]);I(()=>()=>{s(),c(),l()},[l,c,s]);let m=R(async t=>{u(t?`enable`:`disable`);try{let n=await wa(t);d(n.changed?t?`SafeShield 보호 활성화 요청을 적용했습니다.`:`SafeShield 보호 비활성화 요청을 적용했습니다.`:t?`SafeShield 보호가 이미 활성화 상태입니다.`:`SafeShield 보호가 이미 비활성화 상태입니다.`),await e(),f([800,2e3,5e3])}catch(e){r({action:null,error:K(e,`SafeShield 작업을 수행하지 못했습니다.`),message:null})}},[u,e,f,d]),h=R(async e=>{u(e?`statistics-enable`:`statistics-disable`);try{let n=await xa(e);await t(),d(n.changed?n.reconciled?e?`차단 통계 수집을 활성화했습니다.`:`차단 통계 수집을 비활성화했습니다.`:`차단 통계 설정을 저장했습니다.`:e?`차단 통계 수집이 이미 활성화되어 있습니다.`:`차단 통계 수집이 이미 비활성화되어 있습니다.`),n.changed&&p([500,1500])}catch(e){r({action:null,error:K(e,`차단 통계 설정을 변경하지 못했습니다.`),message:null})}},[u,t,p,d]),g=R(async()=>{u(`refresh`);try{await Ta(),r({action:null,error:null,message:null}),f([700,2500,6e3,12e3])}catch(e){r({action:null,error:K(e,`SafeShield 작업을 수행하지 못했습니다.`),message:null})}},[u,f]),_=R(()=>{l(),r(e=>({...e,error:null,message:null}))},[l]);return{...n,dismissFeedback:_,refreshBlocklist:g,setEnabled:m,setStatisticsEnabled:h}}var Ma=500,Na=45e3;function Pa(e){return new Promise(t=>window.setTimeout(t,e))}function Fa(e){return e===`allow`?`허용`:`차단`}function Ia(e,t){if(!t.changed)return e===`add`?t.action===`allow`?`이미 허용 목록에 있는 도메인입니다.`:`이미 차단 목록에 있는 도메인입니다.`:`이미 삭제된 규칙입니다.`;let n=Fa(t.action),r=e===`add`?`저장`:`삭제`;return t.rules.safeshieldEnabled?t.rules.applyLocalOverrides?`${t.domain} ${n} 규칙을 ${r}했습니다.`:`${t.domain} ${n} 규칙을 ${r}했습니다. 로컬 규칙 적용 설정이 꺼져 있어 아직 DNS에는 반영되지 않습니다.`:`${t.domain} ${n} 규칙을 ${r}했습니다. SafeShield를 켜면 다음 갱신에서 적용됩니다.`}function La(e,t){let n=e===`add`?`추가`:`삭제`;return`${t.domain} ${Fa(t.action)} 규칙 ${n}를 저장했습니다. 로컬 규칙을 DNS에 적용하고 있습니다…`}function Ra(e,t){let n=e===`add`?`추가`:`삭제`;return`${t.domain} ${Fa(t.action)} 규칙 ${n}와 DNS 적용이 완료되었습니다.`}async function za(e){let t=Date.now()+Na,n=null;for(;Date.now()e.timestamps.lastLocalApply)return{kind:`success`,status:n};if(n.timestamps.lastLocalApplyFailure>e.timestamps.lastLocalApplyFailure)return{kind:`failure`,status:n};await Pa(Ma)}return{kind:`timeout`,status:n}}function Ba(e){let t=ui({active:e,fallbackError:`사용자 규칙을 불러오지 못했습니다.`,loader:Ea}),[n,r]=F({action:null,error:null,feedback:null}),i=L(0),a=R(async(e,n,a,o)=>{let s=++i.current,c=e=>{i.current===s&&r(e)};c({action:{operation:e,action:n,domain:a},error:null,feedback:null});try{let r=await Sa(),i=e===`add`?await Oa(n,a,!0):await ka(n,a,!0);if(t.replaceData(i.rules),o?.(),!(i.changed&&i.rules.safeshieldEnabled&&i.rules.applyLocalOverrides))return c({action:null,error:null,feedback:Ia(e,i)}),!0;if(!i.refresh.accepted)return c({action:null,error:`규칙은 저장되었지만 로컬 DNS 적용 요청을 시작하지 못했습니다${i.refresh.reason?` (${i.refresh.reason})`:``}.`,feedback:null}),!0;c({action:null,error:null,feedback:La(e,i)});let s;try{s=await za(r)}catch{return c({action:null,error:`규칙은 저장되었지만 DNS 적용 상태를 확인하지 못했습니다. SafeShield 상태를 확인해 주세요.`,feedback:null}),!0}return s.kind===`success`?(c({action:null,error:null,feedback:Ra(e,i)}),!0):s.kind===`disabled`?(c({action:null,error:null,feedback:`규칙은 저장되었지만 SafeShield가 꺼져 있어 DNS 적용을 대기하고 있습니다.`}),!0):s.kind===`failure`?(c({action:null,error:`규칙은 저장되었지만 로컬 DNS 적용이 실패했습니다. SafeShield 상태를 확인해 주세요.`,feedback:null}),!0):(c({action:null,error:`규칙은 저장되었지만 제한 시간 안에 로컬 DNS 적용 완료를 확인하지 못했습니다.`,feedback:null}),!0)}catch(e){return c({action:null,error:K(e,`사용자 규칙을 변경하지 못했습니다.`),feedback:null}),!1}},[t.replaceData]),o=R((e,t,n)=>a(`add`,e,t,n),[a]),s=R((e,t)=>a(`delete`,e,t),[a]),c=R(async()=>{r(e=>({...e,error:null})),await t.refresh()},[t.refresh]),l=R(()=>{r(e=>({...e,feedback:null}))},[]);return{...t,...n,error:n.error??t.error,addRule:o,deleteRule:s,dismissFeedback:l,refresh:c}}var Va=6e4;function Ha(e,t=!0){return ui({active:e,fallbackError:`SafeShield 통계를 불러오지 못했습니다.`,loader:ba,...t?{pollInterval:Va}:{}})}var Ua=[{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`]}],Wa=new Map;Ua.forEach((e,t)=>{e.stages.forEach(e=>Wa.set(e,t))});function Ga(e,t){return e===`running`||t!==null&&Wa.has(t)}function Ka(e){let t=e===null?0:Wa.get(e)??0,n=Ua[t];return{description:n.description,label:n.label,number:t+1,total:Ua.length}}function qa(e){return(e??``).trim().toLowerCase()}function Ja(e,t){let n=qa(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=Ka(t);return r.number===2?`최신 차단 목록 정보를 확인하는 중 문제가 발생했습니다. 인터넷 연결을 확인한 뒤 다시 갱신해 주세요.`:r.number===3?`차단 목록을 다운로드하는 중 문제가 발생했습니다. 인터넷 연결을 확인한 뒤 다시 갱신해 주세요.`:r.number===4?`사용자 규칙을 적용하는 중 문제가 발생했습니다. 사용자 규칙을 확인한 뒤 다시 갱신해 주세요.`:r.number===5?`새 보호 규칙을 적용하는 중 문제가 발생했습니다. 잠시 후 다시 갱신해 주세요.`:r.number===6?`새 차단 목록의 보호 상태를 확인하지 못했습니다. 다시 갱신해 주세요.`:`차단 목록 갱신을 완료하지 못했습니다. 잠시 후 다시 갱신해 주세요.`}var Ya=3e3;function Xa(e){return e?Ga(e.status,e.stage):!1}function Za(e){return ui({active:e,fallbackError:`SafeShield 상태를 불러오지 못했습니다.`,loader:Sa,pollInterval:e=>Xa(e)?Ya:null})}var Qa=3e5,$a=1e3,eo=3e3,to=`luci-app-smartsafehub`;function no(e=!0){let[t,n]=F(null),r=ui({active:e||t===`checking`||t===`installing`,fallbackError:`소프트웨어 업데이트 상태를 불러오지 못했습니다.`,loader:gr,pollInterval:e=>e?.phase===`checking`?$a:e?.phase===`installing`?eo:Qa,refreshOnFocus:!0,refreshOnVisible:t===`checking`||t===`installing`});I(()=>{n(r.data?.phase??null)},[r.data?.phase]);let[i,a]=F(null),[o,s]=F(null),[c,l]=F(null),u=L(!1),d=L(void 0);I(()=>{if(u.current||!r.data)return;let e=r.data.lastInstallAt??null,t=d.current;if(t===void 0){d.current=e;return}if(d.current=e,e===null||e===t||r.data.phase!==`idle`)return;let n=r.data.packages.find(e=>e.name===to)?.installedVersion,i=window.__SMARTHUB_BOOTSTRAP__?.assetVersion;n&&i&&n!==i&&(u.current=!0,window.location.reload())},[r.data]);let f=R(e=>{n(e),r.data&&r.replaceData({...r.data,phase:e,lastError:null})},[r]),p=R(async()=>{a(`check`),s(null),l(null);try{if(!(await Cr()).accepted)throw Error(`업데이트 확인 요청이 접수되지 않았습니다.`);f(`checking`),a(null),window.setTimeout(()=>void r.refresh(),500)}catch(e){a(null),s(K(e,`업데이트 확인을 시작하지 못했습니다.`))}},[f,r]),m=R(async()=>{a(`install`),s(null),l(null);try{if(!(await wr()).accepted)throw Error(`업데이트 설치 요청이 접수되지 않았습니다.`);f(`installing`),a(null),window.setTimeout(()=>void r.refresh(),750)}catch(e){a(null),s(K(e,`업데이트 설치를 시작하지 못했습니다.`))}},[f,r]),h=R(async e=>{a(`settings`),s(null),l(null);try{let t=await Tr(e);return r.data&&r.replaceData({...r.data,settings:t}),a(null),l(`자동 업데이트 설정을 저장했습니다.`),!0}catch(e){return a(null),s(K(e,`자동 업데이트 설정을 저장하지 못했습니다.`)),!1}},[r]),g=R(()=>{s(null),l(null)},[]);return{...r,action:i,actionError:o,message:c,check:p,dismissFeedback:g,install:m,saveSettings:h}}var ro=6e4;function io(e){return ui({active:e,fallbackError:`장치 상태를 불러오지 못했습니다.`,loader:Zn,pollInterval:ro})}function ao(e){let t=ui({active:e,fallbackError:`예약 재부팅 설정을 불러오지 못했습니다.`,loader:Ar,refreshOnFocus:!0}),[n,r]=F({saving:!1,saveError:null,saveMessage:null}),i=R(async e=>{r({saving:!0,saveError:null,saveMessage:null});try{let n=await jr(e);return t.replaceData(n),r({saving:!1,saveError:null,saveMessage:e.enabled?`예약 재부팅 일정을 저장했습니다.`:`예약 재부팅을 껐습니다.`}),!0}catch(e){return r({saving:!1,saveError:K(e,`예약 재부팅 설정을 저장하지 못했습니다.`),saveMessage:null}),!1}},[t.replaceData]),a=R(()=>{r(e=>({...e,saveError:null,saveMessage:null}))},[]);return{...t,...n,dismissSaveFeedback:a,saveSettings:i}}function oo(e){return e.trim().toLowerCase().replace(/[^a-z0-9._-]+/g,`-`)||`openwrt`}function so(e,t){let n=new Date(t*1e3).toISOString().replace(/[:.]/g,`-`);return`smartsafehub-diagnostics-${oo(e)}-${n}.json`}function co(e,t){let n=new Blob([`${JSON.stringify(t,null,2)}\n`],{type:`application/json;charset=utf-8`}),r=URL.createObjectURL(n),i=document.createElement(`a`);i.href=r,i.download=e,i.style.display=`none`,(document.body??document.documentElement).appendChild(i),i.click(),i.remove(),window.setTimeout(()=>URL.revokeObjectURL(r),1e3)}function lo(e){return{enabled:e.available?e.enabled:null,status:e.status,lastResult:e.runtime.lastResult,validLineCount:e.blocklist.validLineCount,blocklistInstalled:e.blocklist.installed,dnsmasqOk:e.runtime.dnsRuntimeOk}}function uo(){return{networks:[],totalClients:0,guest:null}}function fo(){return{enabled:null,status:`unavailable`,lastResult:null,validLineCount:0,blocklistInstalled:!1,dnsmasqOk:!1}}async function po(e){if(!e)throw Error(`시스템 상태가 준비되지 않았습니다. 새로고침한 뒤 다시 시도해 주세요.`);let[t,n,r]=await Promise.allSettled([Kr(),Sa(),ir()]);return{generatedAt:Math.floor(Date.now()/1e3),health:r.status===`fulfilled`?r.value:null,system:e,wifi:t.status===`fulfilled`?t.value:uo(),safeshield:n.status===`fulfilled`?lo(n.value):fo()}}function mo(e){let[t,n]=F({action:null,error:null,message:null,rebootAccepted:!1}),r=R(async()=>{n(e=>({...e,action:`diagnostics`,error:null,message:null}));try{let t=await po(e);co(so(t.system.device.hostname,t.generatedAt),t),n(e=>({...e,action:null,message:`진단 정보 파일을 만들었습니다.`}))}catch(e){n(t=>({...t,action:null,error:K(e,`시스템 작업을 완료하지 못했습니다.`)}))}},[e]),i=R(async()=>{n(e=>({...e,action:`reboot`,error:null,message:null}));try{let e=await Fr();n({action:null,error:null,message:e.accepted?`재부팅을 시작했습니다. 잠시 후 공유기에 다시 연결해 주세요.`:`재부팅 요청이 접수되지 않았습니다.`,rebootAccepted:e.accepted})}catch(e){n(t=>({...t,action:null,error:K(e,`시스템 작업을 완료하지 못했습니다.`)}))}},[]),a=R(()=>{n(e=>({...e,error:null,message:null}))},[]);return{...t,dismissFeedback:a,downloadDiagnostics:r,reboot:i}}function ho(e){let t=ui({active:e,fallbackError:`시간대 설정을 불러오지 못했습니다.`,loader:Er,refreshOnFocus:!0}),[n,r]=F({saving:!1,syncing:!1,saveError:null,saveMessage:null}),i=R(async e=>{r({saving:!0,syncing:!1,saveError:null,saveMessage:null});try{let n=await Or(e);return t.replaceData(n),r({saving:!1,syncing:!1,saveError:null,saveMessage:`시간대를 저장하고 시스템 시간 설정에 적용했습니다.`}),!0}catch(e){return r({saving:!1,syncing:!1,saveError:K(e,`시간대 설정을 저장하지 못했습니다.`),saveMessage:null}),!1}},[t.replaceData]),a=R(async()=>{r({saving:!1,syncing:!0,saveError:null,saveMessage:null});try{await kr(),await new Promise(e=>{window.setTimeout(e,1500)});let e=await Er();return t.replaceData(e),r({saving:!1,syncing:!1,saveError:null,saveMessage:`NTP 시간 동기화를 요청하고 장치 시간을 다시 확인했습니다.`}),!0}catch(e){return r({saving:!1,syncing:!1,saveError:K(e,`시간을 동기화하지 못했습니다.`),saveMessage:null}),!1}},[t.replaceData]),o=R(()=>{r(e=>({...e,saveError:null,saveMessage:null}))},[]);return{...t,...n,dismissSaveFeedback:o,saveTimezone:i,syncTime:a}}var go=5e3,_o=5e3;function vo(e){if(!e?.pairingCode||e.accountRegistered===!0)return!1;if(!e.pairingExpiresAt)return!0;let t=Date.parse(e.pairingExpiresAt);return!Number.isFinite(t)||t>Date.now()}function yo(e){let[t,n]=F(null),[r,i]=F(null),[a,o]=F(null),[s,c]=F(e),[l,u]=F(!1),[d,f]=F(!1),p=L(null),m=L(0),h=L(!0),g=R((e,t)=>!h.current||t!==m.current?!1:(n(e),i(null),c(!1),!0),[]),_=R(()=>{if(!e)return Promise.resolve(null);if(p.current)return p.current;let t=++m.current;o(null),u(!0);let n=(async()=>{try{let e=await nr();return g(e,t)&&o(null),e}catch{let e=await tr().catch(()=>null);return e&&g(e,t),h.current&&t===m.current&&o(`SmartSafeHub 서버의 최신 상태 확인이 지연되고 있습니다.`),e}finally{h.current&&u(!1),p.current=null}})();return p.current=n,n},[e,g]),v=R(async()=>{let e=++m.current;f(!0);try{let t=await rr();return g(t,e)&&o(null),t}finally{f(!1)}},[g]);I(()=>{if(!e)return;let t=!0;h.current=!0;let n=++m.current;return c(!0),i(null),o(null),tr().then(e=>{t&&(g(e,n)&&c(!1),_())}).catch(()=>{t&&(n===m.current&&(i(`기기 등록 상태를 확인하지 못했습니다.`),c(!1)),_())}),()=>{t=!1,m.current+=1}},[e,_,g]),I(()=>{if(!e)return;let t=!0,n=window.setInterval(()=>{if(document.visibilityState===`hidden`||p.current)return;let e=++m.current;tr().then(n=>{t&&g(n,e)&&o(null)}).catch(()=>{})},_o);return()=>{t=!1,window.clearInterval(n)}},[e,g]),I(()=>()=>{h.current=!1},[]);let y=Qe(()=>vo(t)||t?.pairingOperation?.phase===`starting`||t?.pairingOperation?.phase===`running`,[t]);return I(()=>{if(!e||!y)return;let t=!0,n=0,r=async()=>{let e=++m.current,i=await tr().catch(()=>null);t&&i&&g(i,e),t&&(n=window.setTimeout(()=>void r(),go))};return n=window.setTimeout(()=>void r(),go),()=>{t=!1,window.clearTimeout(n)}},[e,g,y]),{data:t,error:r,loading:s,pairingBusy:d,refresh:_,refreshing:l,requestPairing:v,syncError:a}}var bo=6e4;function xo(e,t){let[n,r]=F(null),i=L(0);return I(()=>{e&&t!==null&&(i.current+=1,r(t))},[e,t]),I(()=>{if(e)return;let t=!1,n=null,a=!1,o=0,s=async(e=!1)=>{if(t||document.visibilityState===`hidden`||a||!e&&Date.now()-oe??null)}finally{a=!1}},c=()=>{document.visibilityState===`visible`&&s()},l=()=>void s();return s(!0),n=window.setInterval(()=>void s(),bo),document.addEventListener(`visibilitychange`,c),window.addEventListener(`focus`,l),()=>{t=!0,i.current+=1,n!==null&&window.clearInterval(n),document.removeEventListener(`visibilitychange`,c),window.removeEventListener(`focus`,l)}},[e]),n}var So=[2e3,3e3];function Co(e){return new Promise(t=>window.setTimeout(t,e))}function wo(e){let t=ui({active:e,fallbackError:`Wi-Fi 설정을 처리하지 못했습니다.`,loader:Kr}),n=L(!0),r=L(e),[i,a]=F({updatingSection:null,feedback:null});I(()=>(n.current=!0,()=>{n.current=!1}),[]),I(()=>{r.current=e},[e]);let o=R(async()=>{for(let e of So){if(await Co(e),!n.current||!r.current)return;await t.refresh()}},[t.refresh]),s=R(async e=>{a({updatingSection:e.section,feedback:null});try{let n=await qr(e);return t.replaceData({...n.summary,guest:t.data?.guest??null,guestError:t.data?.guestError??!1}),a({updatingSection:null,feedback:{kind:`success`,message:n.changed?`Wi-Fi 설정을 저장하고 무선 네트워크를 다시 불러왔습니다.`:`변경된 Wi-Fi 설정이 없습니다.`}}),n.reloaded&&o(),!0}catch(e){let t=e instanceof B&&[`NETWORK_ERROR`,`RPC_TIMEOUT`].includes(e.code);return a({updatingSection:null,feedback:{kind:t?`warning`:`error`,message:t?`Wi-Fi가 다시 시작되면서 연결이 끊어졌을 수 있습니다. 변경한 SSID로 다시 연결한 뒤 화면을 새로고침해 주세요.`:K(e,`Wi-Fi 설정을 처리하지 못했습니다.`)}}),!1}},[o,t.replaceData,t.data]),c=R(async e=>{a({updatingSection:`ssh_guest`,feedback:null});try{let n=await Jr(e);return await t.refresh(),a({updatingSection:null,feedback:{kind:`success`,message:n.changed?`게스트 Wi-Fi 설정이 저장되었습니다.`:`변경된 게스트 Wi-Fi 설정이 없습니다.`}}),n.reloaded&&o(),!0}catch(e){let t=e instanceof B&&[`NETWORK_ERROR`,`RPC_TIMEOUT`].includes(e.code);return a({updatingSection:null,feedback:{kind:t?`warning`:`error`,message:t?`무선 네트워크가 다시 시작되어 연결이 끊어졌을 수 있습니다. 다시 연결한 뒤 확인해 주세요.`:K(e,`게스트 Wi-Fi 설정을 처리하지 못했습니다.`)}}),!1}},[t.refresh,o]),l=R(()=>{a(e=>({...e,feedback:null}))},[]);return{...t,...i,update:s,updateGuest:c,dismissFeedback:l}}function q({action:e,description:t,eyebrow:n,id:r,title:i}){return H(`div`,{class:`ssh-section-heading mb-4 min-w-0`,children:[H(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.18em] text-teal-700`,children:n}),H(`div`,{class:`mt-2 flex min-w-0 flex-wrap items-center justify-between gap-3`,children:[H(`h2`,{class:`m-0 min-w-0 text-xl font-black tracking-tight text-slate-950`,id:r,children:i}),e?H(`div`,{class:`shrink-0`,children:e}):null]}),H(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500`,children:t})]})}var To=new Intl.NumberFormat(`ko-KR`),Eo=new Intl.DateTimeFormat(`ko-KR`,{dateStyle:`medium`,timeStyle:`short`});function Do(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 Oo(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?jo(e):Mo(e,t)}function Po(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 Fo(e,t){if(!Number.isFinite(e)||e<=0)return`확인 불가`;let n=t.replaceAll(` `,`_`);try{return new Intl.DateTimeFormat(`ko-KR`,{dateStyle:`medium`,timeStyle:`medium`,timeZone:n}).format(new Date(e*1e3))}catch{return Eo.format(new Date(e*1e3))}}function Io(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 Lo(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 Ro(e,t){let n=e.metadata[t];return typeof n==`string`&&n.trim().length>0?n.trim():null}function zo(e,t){let n=e.metadata[t];return typeof n==`number`&&Number.isFinite(n)?n:null}function Bo(e,t){let n=e.metadata[t];return typeof n==`boolean`?n:null}function Vo(e,t){let n=Ro(e,`error_code`);return n?`${t} (오류 코드: ${n})`:t}function Ho(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 Uo(e){let t=Ro(e,`from_version`),n=Ro(e,`to_version`);return t&&n?`${t} → ${n}`:n?`${n} 버전이 적용되었습니다.`:null}function Wo(e){return e?e.toUpperCase():null}function Go(e){switch(e){case`dhcp`:return`자동 IP(DHCP)`;case`pppoe`:return`PPPoE`;case`static`:return`고정 IPv4`;default:return e.toUpperCase()}}function Ko(e){switch(e.eventType){case`system.booted`:return{title:`공유기 부팅 감지`,description:`공유기가 켜지거나 재시작된 것을 확인했습니다.`};case`network.internet.disconnected`:return{title:`인터넷 연결 끊김 감지`,description:`공유기에서 인터넷 연결을 확인할 수 없었습니다.`};case`network.internet.recovered`:{let t=zo(e,`downtime_seconds`);return{title:`인터넷 연결 복구`,description:t!==null&&t>0?`약 ${Ho(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=zo(e,`domain_count`),n=Ro(e,`artifact_version`),r=[];return t!==null&&t>0&&r.push(`${J(t)}개 도메인 적용`),n&&r.push(`목록 ${n}`),{title:`SafeShield 차단 목록 업데이트 완료`,description:r.length>0?r.join(` · `):`최신 차단 목록을 적용했습니다.`}}case`safeshield.blocklist.update_failed`:return{title:`SafeShield 차단 목록 업데이트 실패`,description:Vo(e,`차단 목록을 업데이트하지 못했습니다.`)};case`safeshield.statistics.enabled`:return{title:`SafeShield 차단 통계 수집 활성화`,description:`기기별 차단 통계 수집을 활성화했습니다.`};case`safeshield.statistics.disabled`:return{title:`SafeShield 차단 통계 수집 비활성화`,description:`기기별 차단 통계 수집을 비활성화했습니다.`};case`safeshield.rule.added`:return{title:Ro(e,`rule_action`)===`allow`?`사용자 허용 규칙 추가`:`사용자 차단 규칙 추가`,description:`SafeShield 사용자 규칙이 변경되었습니다.`};case`safeshield.rule.removed`:return{title:Ro(e,`rule_action`)===`allow`?`사용자 허용 규칙 삭제`:`사용자 차단 규칙 삭제`,description:`SafeShield 사용자 규칙이 변경되었습니다.`};case`settings.scheduled_reboot.updated`:{let t=Bo(e,`enabled`);return{title:`예약 재부팅 설정 변경`,description:t===!0?`예약 재부팅 설정을 저장하고 새 일정에 적용했습니다.`:t===!1?`예약 재부팅을 비활성화했습니다.`:`예약 재부팅 설정을 변경했습니다.`}}case`settings.software_updates.updated`:return{title:`관리 소프트웨어 업데이트 설정 변경`,description:Bo(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=Ro(e,`zonename`);return{title:`시간대 설정 변경`,description:t?`공유기 시간대를 ${t}(으)로 변경했습니다.`:`공유기 시간대를 변경했습니다.`}}case`settings.wifi.updated`:return{title:`Wi-Fi 설정 변경`,description:`Wi-Fi 설정을 저장하고 무선 네트워크에 적용했습니다.`};case`settings.wan.updated`:{let t=Ro(e,`protocol`);return{title:`인터넷 설정 변경`,description:t?`${Go(t)} 방식으로 WAN 설정을 변경했습니다.`:`WAN 인터넷 연결 설정을 변경했습니다.`}}case`settings.lan.updated`:return{title:`LAN 설정 변경`,description:`내부 네트워크 및 DHCP 설정을 변경했습니다.`};case`settings.iptv.updated`:{let t=Bo(e,`enabled`),n=Ro(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:Uo(e)};case`software.update.failed`:return{title:`관리 소프트웨어 업데이트 실패`,description:Vo(e,`관리 소프트웨어를 업데이트하지 못했습니다.`)};case`firmware.update.started`:return{title:`펌웨어 업데이트 시작`,description:`펌웨어 설치를 시작했습니다. 완료 과정에서 공유기가 재부팅됩니다.`};case`firmware.update.failed`:return{title:`펌웨어 업데이트 실패`,description:Vo(e,`펌웨어 설치 준비 또는 검증에 실패했습니다.`)};case`license.activated`:{let t=Wo(Ro(e,`to_plan`));return{title:`요금제 활성화`,description:t?`${t} 요금제가 활성화되었습니다.`:`요금제가 활성화되었습니다.`}}case`license.changed`:{let t=Wo(Ro(e,`from_plan`)),n=Wo(Ro(e,`to_plan`));return{title:`요금제 변경`,description:t&&n?`${t} → ${n}`:n?`${n} 요금제로 변경되었습니다.`:`요금제 상태가 변경되었습니다.`}}case`license.cleared`:return{title:`계정 권한 해제`,description:Ro(e,`reason`)===`manual`?`이 공유기에서 계정 권한 연결을 해제했습니다.`:`서버의 계정 권한 상태가 반영되어 이 공유기의 권한 연결이 해제되었습니다.`};case`health.issue.started`:{let t=zo(e,`warning_count`)??0,n=zo(e,`critical_count`)??0,r=[];return n>0&&r.push(`이상 ${J(n)}건`),t>0&&r.push(`주의 ${J(t)}건`),{title:`확인이 필요한 장치 상태 발견`,description:r.length>0?r.join(` · `):`장치 상태에서 확인이 필요한 항목이 발견되었습니다.`}}case`health.issue.changed`:{let t=zo(e,`warning_count`)??0,n=zo(e,`critical_count`)??0,r=[];return n>0&&r.push(`이상 ${J(n)}건`),t>0&&r.push(`주의 ${J(t)}건`),{title:`확인이 필요한 장치 상태 변경`,description:r.length>0?r.join(` · `):`확인이 필요한 장치 상태 항목이 변경되었습니다.`}}case`health.issue.resolved`:return{title:`장치 상태 정상으로 복구`,description:`이전에 확인이 필요했던 장치 상태가 정상으로 돌아왔습니다.`};default:return{title:`SmartSafeHub 활동`,description:`장치에서 새로운 활동이 기록되었습니다.`}}}function qo(e){return e===`error`?{icon:H(W,{class:`size-4`}),surface:`bg-rose-50 text-rose-700 ring-rose-200`}:e===`warning`?{icon:H(W,{class:`size-4`}),surface:`bg-amber-50 text-amber-800 ring-amber-200`}:e===`success`?{icon:H(wn,{class:`size-4`}),surface:`bg-emerald-50 text-emerald-700 ring-emerald-200`}:{icon:H(pn,{class:`size-4`}),surface:`bg-slate-100 text-slate-600 ring-slate-200`}}function Jo(e){let t=new Date(e*1e3);return new Intl.DateTimeFormat(`ko-KR`,{hour:`2-digit`,minute:`2-digit`,hour12:!1}).format(t)}function Yo(e){let t=new Date(e*1e3);return`${t.getFullYear()}-${t.getMonth()+1}-${t.getDate()}`}function Xo(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 Zo({compact:e=!1,events:t}){if(t.length===0)return H(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 px-4 py-5 text-center`,children:[H(pn,{class:`mx-auto size-5 text-slate-400`}),H(`p`,{class:`mt-2 mb-0 text-sm font-extrabold text-slate-700`,children:`아직 기록된 활동이 없습니다.`}),H(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`상태가 실제로 변경되면 이곳에 시간순으로 기록됩니다.`})]});let n=``;return H(`div`,{class:e?`space-y-2`:`space-y-1`,children:t.map(t=>{let r=Ko(t),i=qo(t.severity),a=Yo(t.occurredAt),o=!e&&a!==n;return n=a,H(`div`,{children:[o?H(`p`,{class:`mt-5 mb-2 first:mt-0 text-xs font-black text-slate-500`,children:Xo(t.occurredAt)}):null,H(`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:[H(`div`,{class:`w-12 shrink-0 pt-1 text-xs font-bold tabular-nums text-slate-400`,children:H(`time`,{dateTime:new Date(t.occurredAt*1e3).toISOString(),title:jo(t.occurredAt),children:Jo(t.occurredAt)})}),H(`span`,{class:`mt-0.5 grid size-8 shrink-0 place-items-center rounded-full ring-1 ring-inset ${i.surface}`,children:i.icon}),H(`div`,{class:`min-w-0 flex-1`,children:[H(`p`,{class:`m-0 text-sm font-black leading-5 text-slate-950`,children:r.title}),r.description?H(`p`,{class:`mt-1 mb-0 break-words text-xs font-medium leading-5 text-slate-500`,children:r.description}):null]})]})]},t.eventId)})})}function Qo({error:e,loading:t,onRetry:n}){return t?H(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 px-4 py-5 text-center`,children:[H(hn,{class:`mx-auto size-5 animate-spin text-teal-700`}),H(`p`,{class:`mt-2 mb-0 text-sm font-bold text-slate-500`,children:`최근 활동을 불러오는 중입니다.`})]}):e?H(`div`,{class:`rounded-xl border border-amber-200 bg-amber-50/70 px-4 py-4`,children:H(`div`,{class:`flex items-start gap-3`,children:[H(W,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),H(`div`,{class:`min-w-0 flex-1`,children:[H(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:`최근 활동을 불러오지 못했습니다.`}),H(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:e}),n?H(`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 $o(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 es(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 ts({actionError:e,actionMessage:t,cloud:n,onDismissFeedback:r,onSetEnabled:i,saving:a}){let o=$o(n),s=n.plan?.toUpperCase()??null,c=n.eligible===!0,l=n.enabled||c;return H(`section`,{"aria-label":`Cloud 활동 기록 설정`,class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:[H(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[H(`div`,{class:`min-w-0 flex-1`,children:[H(`div`,{class:`flex flex-wrap items-center gap-2`,children:H(`span`,{class:`rounded-full px-2.5 py-1 text-xs font-extrabold ring-1 ring-inset ${o.tone}`,children:a?`설정 중`:o.label})}),H(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:o.description})]}),H(`div`,{class:`flex shrink-0 items-center justify-end gap-3 sm:justify-start`,children:[H(`span`,{class:`text-xs font-bold text-slate-600`,children:[`Cloud 전송 `,n.enabled?`켜짐`:`꺼짐`]}),H(`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:H(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${n.enabled?`left-6`:`left-1`}`})})]})]}),(e||t)&&H(`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:[H(`span`,{class:`min-w-0`,children:e||t}),H(`button`,{class:`shrink-0 rounded-lg px-2 py-1 text-xs font-extrabold hover:bg-black/5`,onClick:r,type:`button`,children:`닫기`})]}),n.enabled?H(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`Cloud 전송을 끄면 아직 전송하지 않은 대기 기록이 삭제됩니다. 공유기의 로컬 기록은 유지됩니다.`}):null,H(`div`,{class:`mt-4 flex flex-wrap items-center gap-x-5 gap-y-1 border-t border-slate-200 pt-3 text-xs font-semibold text-slate-500`,children:[H(`span`,{children:[`마지막 동기화: `,n.lastSuccessAt>0?Mo(n.lastSuccessAt):`아직 없음`]}),H(`span`,{children:[`Cloud 보관: `,es(n)]}),n.pendingEvents>0?H(`span`,{children:[`전송 대기: `,J(n.pendingEvents),`건`]}):null]}),n.enabled&&n.phase===`error`&&n.lastErrorCode?H(`p`,{class:`mt-3 mb-0 text-xs font-semibold text-amber-700`,children:[`오류 코드: `,H(`code`,{children:n.lastErrorCode})]}):null,H(`details`,{class:`mt-3 border-t border-slate-200 pt-3`,children:[H(`summary`,{class:`cursor-pointer text-xs font-extrabold text-teal-700 hover:text-teal-800 focus-visible:rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-teal-500`,children:`Cloud 동기화 상세 정보`}),H(`dl`,{class:`mt-3 grid gap-3 sm:grid-cols-4`,children:[H(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[H(`dt`,{class:`text-xs font-bold text-slate-500`,children:`멤버십`}),H(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:s??(n.eligible===!1?`Free`:`확인 중`)})]}),H(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[H(`dt`,{class:`text-xs font-bold text-slate-500`,children:`전송 대기`}),H(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:[J(n.pendingEvents),`건`]})]}),H(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[H(`dt`,{class:`text-xs font-bold text-slate-500`,children:`마지막 동기화`}),H(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:n.lastSuccessAt>0?Mo(n.lastSuccessAt):`아직 없음`})]}),H(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[H(`dt`,{class:`text-xs font-bold text-slate-500`,children:`Cloud 보관`}),H(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:es(n)})]})]}),H(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`SmartSafeHub 계정에 연결된 Free는 최근 7일, Pro와 Ultimate는 최근 90일 동안 Cloud 활동 기록을 보관합니다. 끄면 전송 대기 데이터와 임시 인증 정보를 삭제하며, 다시 켠 뒤 새로 발생한 활동부터 Cloud에 전송합니다. 로컬 최근 활동은 항상 유지됩니다.`})]})]})}function ns({actionError:e,actionMessage:t,data:n,error:r,loading:i,onDismissActionFeedback:a,onRetry:o,onSetCloudSync:s,savingCloudSync:c}){let[l,u]=F(20),d=n?.events??[],f=d.slice(0,l),p=d.length-f.length;return H(`div`,{class:`min-w-0 space-y-6`,children:[H(`section`,{"aria-labelledby":`activity-cloud-title`,class:`min-w-0`,children:[H(q,{id:`activity-cloud-title`,eyebrow:`Cloud history`,title:`Cloud 활동 기록`,description:`활동 기록을 Cloud에 자동으로 전송할지 설정합니다. 공유기 내 최근 활동은 항상 기록됩니다.`}),H(ts,{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})]}),H(`section`,{"aria-labelledby":`activity-history-title`,class:`min-w-0`,children:[H(q,{id:`activity-history-title`,eyebrow:`Local history`,title:`현재 부팅 이후의 최근 활동`,description:`인터넷, SafeShield, 업데이트, 계정 권한과 주요 설정에서 실제 상태가 변경된 시점만 기록합니다.`,action:H(`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,`건`]})}),H(`div`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:i||r&&!n?H(Qo,{error:r,loading:i,onRetry:o}):H(b,{children:[H(Zo,{events:f}),d.length>20?H(`div`,{class:`mt-5 flex flex-wrap items-center justify-between gap-3 border-t border-slate-200 pt-4`,children:[H(`p`,{class:`m-0 text-xs font-semibold text-slate-500`,children:[J(f.length),` / `,J(d.length),`건 표시 중`]}),H(`div`,{class:`flex flex-wrap items-center gap-2`,children:[l>20?H(`button`,{class:`inline-flex min-h-10 items-center justify-center rounded-xl border border-slate-200 bg-white px-4 py-2 text-xs font-extrabold text-slate-700 transition hover:bg-slate-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-teal-500`,onClick:()=>u(20),type:`button`,children:`최신 20건만 보기`}):null,p>0?H(`button`,{class:`inline-flex min-h-10 items-center justify-center rounded-xl border border-teal-200 bg-teal-50 px-4 py-2 text-xs font-extrabold text-teal-800 transition hover:bg-teal-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-teal-500`,onClick:()=>u(e=>Math.min(e+20,d.length)),type:`button`,children:[`이전 활동 더 보기 (`,J(Math.min(20,p)),`건)`]}):null]})]}):null]})})]}),H(`aside`,{class:`rounded-2xl border border-slate-200 bg-slate-50 px-5 py-4`,children:[H(`p`,{class:`m-0 text-sm font-extrabold text-slate-800`,children:`로컬 활동 기록 안내`}),H(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`이 기록은 공유기의 메모리 영역에만 보관되어 재부팅하면 초기화됩니다. 직접 설정을 변경한 작업은 성공 시점에 즉시 기록하고, 인터넷 및 장치 진단처럼 외부 상태를 확인해야 하는 항목은 실제 상태 변화가 관찰된 경우에만 추가합니다.`})]})]})}function rs(e){return!Number.isFinite(e)||e<0?`—`:e<1024?`${Math.round(e)} B`:e<1024**2?`${(e/1024).toFixed(1)} KB`:e<1024**3?`${(e/1024**2).toFixed(1)} MB`:`${(e/1024**3).toFixed(1)} GB`}function is(e){if(!e||!Number.isSafeInteger(e.totalBytes)||!Number.isSafeInteger(e.availableBytes)||e.totalBytes<=0||e.availableBytes<0||e.availableBytes>e.totalBytes||e.mount!==`/overlay`&&e.mount!==`/`)return null;let t=e.availableBytes/e.totalBytes;return{availablePercent:Math.round(t*100),availableRatio:t,availableText:rs(e.availableBytes),usedText:rs(e.totalBytes-e.availableBytes),totalText:rs(e.totalBytes),level:t<=.1?`critical`:t<=.2?`warning`:`normal`}}var as=2*Math.PI*39;function os({storage:e}){let t=is(e),n=t?(t.availableRatio*as).toFixed(2):`0`;return H(`aside`,{"aria-label":`앱 설치 저장 공간`,"data-level":t?.level??`unavailable`,class:`ssh-apps-storage-card flex min-w-0 items-center gap-3 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5`,children:[H(`div`,{class:`ssh-apps-storage-ring relative grid size-24 shrink-0 place-items-center`,"data-level":t?.level??`unavailable`,children:[H(`svg`,{"aria-hidden":`true`,class:`size-24`,viewBox:`0 0 96 96`,children:[H(`circle`,{class:`ssh-apps-storage-track`,cx:`48`,cy:`48`,r:`39`}),t&&t.availableRatio>0?H(`circle`,{class:`ssh-apps-storage-free`,cx:`48`,cy:`48`,r:`39`,"stroke-dasharray":`${n} ${as.toFixed(2)}`}):null]}),H(`div`,{class:`absolute inset-0 flex flex-col items-center justify-center text-center`,children:[H(`strong`,{class:`text-lg leading-6 font-black text-slate-950`,children:t?`${t.availablePercent}%`:`—`}),H(`span`,{class:`text-[10px] font-bold text-slate-500`,children:`여유`})]})]}),H(`div`,{"aria-live":`polite`,class:`min-w-0 flex-1`,children:[H(`h3`,{class:`m-0 text-sm font-black text-slate-950`,children:`저장 공간`}),t?H(b,{children:[H(`p`,{class:`ssh-apps-storage-available mb-0 mt-1 text-base font-extrabold text-teal-700`,children:[t.availableText,` 남음`]}),H(`p`,{class:`mb-0 mt-1 text-[11px] leading-4 text-slate-500`,title:`앱 설치 가능 용량을 기준으로 계산하며, 파일시스템 예약 공간이 사용량에 포함될 수 있습니다.`,children:[`사용 `,t.usedText,` / 전체 `,t.totalText]})]}):H(`p`,{class:`mb-0 mt-1 text-xs leading-5 text-slate-500`,children:`용량 정보를 확인할 수 없습니다.`})]})]})}function ss(e){switch(e){case`network`:return`network`;case`security`:return`security`;case`entertainment`:return`entertainment`;case`utilities`:return`utilities`;case`other`:return`other`;case`smart_home`:case`iot`:return`smart_home`;default:return`other`}}function cs({category:e,class:t}){switch(e){case`all`:return H(en,{class:t});case`network`:return H(tn,{class:t});case`security`:return H(Yt,{class:t});case`smart_home`:return H(Qt,{class:t});case`entertainment`:return H(nn,{class:t});case`utilities`:return H(rn,{class:t});case`other`:return H(an,{class:t});default:return H(an,{class:t})}}function ls(e){return!e||typeof e.path!=`string`||e.path.length>160?null:e.type===`static`?e.path===`/retro-arcade/`?e.path:null:e.type===`luci`&&/^\/cgi-bin\/luci\/admin\/[a-z0-9_-]+(?:\/[a-z0-9_-]+)*$/.test(e.path)?e.path:null}var us=`smartsafehub:apps:dismissed-operation:v1`;function ds(e){return!e||e.phase===`idle`?``:e.operationId?`operation:${e.operationId}`:`legacy:${e.phase}:${e.action}:${e.appId}:${e.code}`}function fs(){try{return typeof window>`u`?null:window.localStorage.getItem(us)}catch{return null}}function ps(e){if(e)try{typeof window<`u`&&window.localStorage.setItem(us,e)}catch{}}var ms={E21:`서버에서 승인 정보를 확인하지 못했습니다. 인터넷 연결을 확인해 주세요.`,E22:`이 펌웨어에서 승인되지 않은 앱입니다.`,E24:`공유기의 패키지 아키텍처와 앱이 일치하지 않습니다.`,E25:`배포 저장소가 승인 당시와 달라졌거나 접근할 수 없습니다. 관리자 확인 후 다시 시도해 주세요.`,E26:`앱 설치에 필요한 메모리 또는 저장 공간이 부족합니다.`,E27:`승인된 패키지 정보가 올바르지 않습니다.`,E28:`이전에 설치한 앱의 구성이 현재 승인 정보와 다릅니다.`,E29:`동일한 패키지가 이미 다른 방법으로 설치되어 있습니다.`,E31:`앱 파일을 다운로드하지 못했습니다.`,E32:`다운로드된 앱 파일이 올바르지 않습니다.`,E33:`앱 파일의 검증 값이 일치하지 않습니다.`,E34:`패키지 설치 사전 확인에 실패했습니다.`,E35:`패키지를 설치하지 못했습니다.`,E37:`공식 OpenWrt LuCI 저장소가 승인 당시와 다릅니다. 관리자에게 확인해 주세요.`,E38:`기존 공유 라이브러리의 버전이 승인된 버전과 달라 설치할 수 없습니다.`,E51:`SmartSafeHub Apps에서 관리하는 앱만 업데이트할 수 있습니다.`,E52:`현재 설치 버전과 승인된 업데이트가 일치하지 않습니다. 앱 구성과 업데이트 버전을 확인해 주세요.`,E53:`다른 시스템 패키지 변경이 필요하여 앱 업데이트를 중단했습니다.`,E54:`앱 업데이트에 실패했습니다. 일부 패키지가 변경되었을 수 있으니 관리자에게 확인해 주세요.`,E55:`업데이트 후 설치 기록을 저장하지 못했습니다. 관리자에게 확인해 주세요.`,E41:`SmartSafeHub Apps에서 설치한 앱만 여기서 제거할 수 있습니다.`,E44:`기존 시스템에 영향을 줄 수 있어 앱 제거가 차단됐습니다.`,E45:`앱 제거에 실패했습니다.`},hs=[{id:`all`,label:`전체`},{id:`network`,label:`네트워크`},{id:`security`,label:`보안`},{id:`smart_home`,label:`스마트홈`},{id:`entertainment`,label:`엔터테인먼트`},{id:`utilities`,label:`도구`},{id:`other`,label:`기타`}];function gs({action:e}){let t=e===`remove`;return H(`span`,{"aria-live":`polite`,class:`inline-flex items-center gap-1.5 text-xs font-bold ${t?`text-red-700`:`text-teal-700`}`,role:`status`,children:[H(`svg`,{"aria-hidden":`true`,class:`size-4 animate-spin`,fill:`none`,viewBox:`0 0 24 24`,children:[H(`circle`,{cx:`12`,cy:`12`,r:`9`,stroke:`currentColor`,"stroke-width":`3`,opacity:`0.25`}),H(`path`,{d:`M21 12a9 9 0 0 0-9-9`,stroke:`currentColor`,"stroke-width":`3`,"stroke-linecap":`round`})]}),t?`제거 중…`:e===`update`?`업데이트 중…`:`설치 중…`]})}function _s({data:e,error:t,loading:n,refreshing:r,onRetry:i,installStatus:a,metrics:o,statusError:s,onRefreshStatus:c,onInstall:l,onUpdate:u,onRemove:d,demo:f=!1,onDemoAction:p}){let[m,h]=F(`all`),[g,_]=F(``),[v,y]=F(null),[x,S]=F(null),C=L(null),w=L(null),[T,E]=F(null),[D,O]=F(fs),k=e,A=k?.apps??[],j=k&&!t&&!n&&!r&&!s?Object.entries(a?.installed??{}).filter(([e,t])=>t.retired===!0&&t.managed&&!A.some(t=>t.id===e)):[],M=a?.restore,N=a?.job,P=N?.phase===`starting`||N?.phase===`running`,ee=M?.phase===`starting`||M?.phase===`running`,te=ds(N),ne=e=>{O(e),ps(e)},re=N&&N.phase!==`idle`&&te!==D;I(()=>{if(N?.phase!==`success`||!re)return;let e=window.setTimeout(()=>ne(te),7e3);return()=>window.clearTimeout(e)},[te,N?.phase,re]);let ie=N?.action===`install`||N?.action===`update`||N?.action===`remove`?N.action:null,ae=Qe(()=>A.filter(e=>(m===`all`||ss(e.category)===m)&&`${e.name} ${e.summary}`.toLocaleLowerCase().includes(g.trim().toLocaleLowerCase())),[A,m,g]);I(()=>{if(!x)return;C.current?.focus();let e=e=>{e.key===`Escape`&&(e.preventDefault(),S(null),w.current?.focus())};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[x]);let oe=()=>{S(null),w.current?.focus()},se=(e,t)=>{!a||s||v!==null||P||ee||(w.current=t,S(e))},ce=async(e,t)=>{if(!(!a||s||v!==null||P||ee)){if(f)t===`remove`&&oe(),p?.();else if((t!==`update`||window.confirm(`${e.name} 앱을 최신 승인 버전으로 업데이트하시겠습니까?\n\n업데이트 중 공유기를 재부팅하지 마세요.`))&&(t!==`install`||window.confirm(`${e.name} 앱을 설치하시겠습니까?\n\n앱을 변경하는 동안 공유기를 재부팅하지 마세요.`))){t===`remove`&&oe(),E(null),y({appId:e.id,action:t});try{t===`install`?await l(e.id):t===`update`?await u(e.id):await d(e.id),c()}catch(t){E(K(t,`${e.name} 앱 작업을 시작하지 못했습니다.`))}finally{y(null)}}}};return H(`section`,{class:`min-w-0 space-y-6`,children:[x?H(`div`,{class:`fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70 p-4`,onClick:e=>{e.target===e.currentTarget&&oe()},role:`presentation`,children:H(`div`,{"aria-describedby":`apps-remove-description`,"aria-labelledby":`apps-remove-title`,"aria-modal":`true`,class:`w-full max-w-md rounded-2xl border border-slate-200 bg-white p-5 shadow-xl`,onKeyDown:e=>{e.key===`Tab`&&(e.shiftKey&&e.target===C.current?(e.preventDefault(),e.currentTarget.querySelector(`[data-confirm-remove]`)?.focus()):!e.shiftKey&&e.target instanceof HTMLButtonElement&&e.target.hasAttribute(`data-confirm-remove`)&&(e.preventDefault(),C.current?.focus()))},role:`dialog`,children:[H(`h2`,{class:`m-0 text-lg font-extrabold text-slate-950`,id:`apps-remove-title`,children:`앱 제거 확인`}),H(`p`,{class:`mt-3 break-words text-sm text-slate-700`,id:`apps-remove-description`,children:[H(`strong`,{children:x.name}),` 앱을 이 공유기에서 제거하시겠습니까?`]}),H(`p`,{class:`mt-2 text-xs leading-5 text-slate-600`,children:`앱에서 제공하던 기능을 더 이상 사용할 수 없습니다. 제거하는 동안 공유기를 재부팅하지 마세요.`}),H(`div`,{class:`mt-5 flex justify-end gap-2`,children:[H(`button`,{class:`min-h-10 rounded-lg border border-slate-200 bg-white px-4 text-sm font-bold text-slate-700 transition hover:bg-slate-50`,onClick:oe,ref:C,type:`button`,children:`취소`}),H(`button`,{class:`min-h-10 rounded-lg bg-red-700 px-4 text-sm font-bold text-white transition hover:bg-red-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-100 disabled:opacity-50`,"data-confirm-remove":!0,disabled:!a||!!s||v!==null||P||ee,onClick:()=>void ce(x,`remove`),type:`button`,children:`앱 제거`})]})]})}):null,H(`div`,{class:`min-w-0`,children:[H(q,{eyebrow:`SmartSafeHub Apps`,title:`Apps`,description:`SmartSafeHub에서 직접 검증한 앱으로 공유기의 기능을 확장하세요.`}),H(`div`,{class:`grid min-w-0 gap-3 lg:grid-cols-[minmax(0,1fr)_minmax(0,18.5rem)]`,children:[H(`div`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:[H(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[H(`div`,{class:`flex min-w-0 items-center gap-3`,children:[H(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-teal-50 text-teal-700`,children:H(en,{class:`size-6`})}),H(`div`,{class:`min-w-0`,children:[H(`h3`,{class:`m-0 text-base font-black text-slate-950`,children:`검증된 앱`}),H(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`현재 펌웨어에서 확인된 앱만 표시합니다.`})]})]}),H(`div`,{class:`flex flex-wrap items-center gap-2`,children:[H(`span`,{class:`rounded-full border border-teal-200 bg-teal-50 px-3 py-1 text-xs font-bold text-teal-800`,children:k?`${A.length}개`:`확인 전`}),t&&k?H(`span`,{class:`rounded-full border border-amber-200 bg-amber-50 px-2 py-1 text-[11px] font-semibold text-amber-800`,title:t,role:`status`,children:`이전 목록`}):null,H(`button`,{class:`inline-flex min-h-10 items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 text-xs font-bold text-slate-700 transition hover:border-teal-300 hover:text-teal-700 disabled:opacity-50`,disabled:n||r,onClick:i,type:`button`,children:[H(hn,{class:`size-4 ${r?`animate-spin`:``}`}),r?`확인 중`:`목록 새로고침`]})]})]}),H(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`검증된 APK를 설치·업데이트·제거할 수 있습니다. 작업 중에는 다른 앱 작업을 시작할 수 없습니다.`})]}),H(os,{storage:o?.storage})]})]}),M&&(M.phase===`starting`||M.phase===`running`||M.phase===`failed`)?H(`div`,{"aria-live":`polite`,class:`rounded-2xl border p-4 text-sm ${M.phase===`failed`?`border-amber-300 bg-amber-50 text-amber-900`:`border-teal-200 bg-teal-50 text-teal-900`}`,role:`status`,children:[H(`strong`,{children:M.phase===`failed`?`Apps 자동 복원을 완료하지 못했습니다.`:`펌웨어 업데이트 후 Apps를 복원하는 중입니다.`}),M.appId?H(`span`,{children:[` · `,M.appId]}):null,M.phase===`failed`?H(`p`,{class:`mb-0 mt-1 text-xs`,children:[ms[M.code]??`승인·연결·설치 상태를 확인해 주세요.`,` 필요한 경우 앱 카드에서 설치 마무리를 진행할 수 있습니다.`]}):H(`p`,{class:`mb-0 mt-1 text-xs`,children:`검증된 패키지만 다시 설치합니다. 완료될 때까지 앱 설치·제거를 시작하지 마세요.`})]}):null,re&&N?H(`div`,{"aria-live":`polite`,class:`rounded-2xl border p-4 text-sm ${N.phase===`failed`?`border-rose-200 bg-rose-50 text-rose-800`:P&&N.action===`remove`?`border-red-200 bg-red-50 text-red-800`:`border-teal-200 bg-teal-50 text-teal-900`}`,role:`status`,children:H(`div`,{class:`flex items-start justify-between gap-3`,children:[H(`div`,{class:`min-w-0`,children:[P?N.action===`remove`?`앱 제거 중`:N.action===`update`?`앱 업데이트 중`:`앱 설치 중`:N.phase===`success`?`마지막 앱 작업 완료`:`마지막 앱 작업 실패`,N.appId?` · ${N.appId}`:``,N.code&&N.code!==`NONE`?` · 오류 코드 ${N.code}`:``,N.phase===`failed`&&N.code&&ms[N.code]?H(`p`,{class:`mb-0 mt-1 text-xs`,children:ms[N.code]}):null,P?H(`p`,{class:`mb-0 mt-1 text-xs`,children:[N.action===`remove`?`앱 제거와 패키지 정리가 진행 중입니다.`:N.action===`update`?`승인된 버전으로 앱을 업데이트하고 있습니다.`:`설치 확인과 패키지 처리가 진행 중입니다.`,` 화면을 닫아도 작업은 계속됩니다.`]}):null]}),P?null:H(`button`,{"aria-label":`앱 작업 알림 닫기`,class:`shrink-0 rounded-lg px-2 py-1 text-xs font-bold hover:bg-black/5 focus-visible:outline-none focus-visible:ring-2`,onClick:()=>ne(te),type:`button`,children:`닫기 ×`})]})}):null,T?H(`p`,{class:`rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-800`,role:`alert`,children:T}):null,s?H(`div`,{class:`rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900`,role:`alert`,children:[`설치 상태를 확인하지 못했습니다. 설치 및 제거 버튼이 비활성화됩니다. `,H(`button`,{class:`underline`,onClick:c,type:`button`,children:`다시 확인`})]}):null,t&&!k?H(`div`,{class:`rounded-2xl border border-rose-200 bg-rose-50 p-5`,role:`alert`,children:[H(`p`,{class:`m-0 text-sm font-bold text-rose-800`,children:`앱 목록을 불러오지 못했습니다.`}),H(`p`,{class:`mt-2 mb-0 text-sm text-rose-700`,children:t}),H(`button`,{class:`mt-3 rounded-lg border border-rose-300 px-3 py-2 text-xs font-bold text-rose-800`,onClick:i,type:`button`,children:`다시 시도`})]}):null,n&&!k?H(`div`,{"aria-live":`polite`,class:`rounded-2xl border border-slate-200 bg-white p-8 text-center text-sm text-slate-500`,role:`status`,children:`검증된 앱 목록을 확인하는 중입니다.`}):null,k?H(b,{children:[A.length>0?H(`div`,{class:`space-y-4`,children:[H(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[H(`div`,{class:`flex flex-wrap items-center gap-2`,role:`group`,"aria-label":`앱 카테고리`,children:hs.map(e=>H(`button`,{class:`inline-flex min-h-10 items-center gap-1.5 rounded-xl border px-3 text-xs font-bold ${m===e.id?`border-teal-400 bg-teal-50 text-teal-800`:`border-slate-200 bg-white text-slate-600`}`,onClick:()=>h(e.id),type:`button`,children:[H(cs,{category:e.id,class:`size-4 shrink-0`}),e.label]},e.id))}),H(`input`,{"aria-label":`앱 검색`,class:`min-h-10 w-full rounded-xl border border-slate-200 bg-white px-3 text-sm text-slate-900 outline-none focus:border-teal-500 sm:w-52`,onInput:e=>_(e.currentTarget.value),placeholder:`앱 검색`,type:`search`,value:g})]}),H(`div`,{class:`grid min-w-0 gap-3 sm:grid-cols-2 xl:grid-cols-3`,children:ae.map(e=>{let t=a?.installed[e.id],n=t?.installed??!1,r=o?.installedSizes?.[e.id],i=n&&typeof r==`number`&&Number.isSafeInteger(r)&&r>=0?rs(r):null,c=t?.managed??!1,l=!!(n&&c&&t?.updateAvailable),u=n?t?.installedVersion:null,d=!!(n&&u&&(l||u!==e.version)),m=v?.appId===e.id?v.action:P&&N?.appId===e.id?ie:null,h=!a||!!s||v!==null||P||ee,g=c&&(n||!!t?.partial),_=n&&t?.webUiReady?ls(e.webUi):null;return H(`article`,{class:`flex min-w-0 flex-col rounded-2xl border border-slate-200 bg-white p-5 shadow-sm`,children:[H(`div`,{class:`flex min-w-0 items-start gap-3`,children:[H(`span`,{class:`grid size-14 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-600`,children:H(cs,{category:ss(e.category),class:`size-7`})}),H(`div`,{class:`min-w-0 flex-1`,children:[H(`div`,{class:`flex min-w-0 flex-wrap items-start justify-between gap-x-3 gap-y-2`,children:[H(`h3`,{class:`m-0 min-w-0 flex-1 break-words text-sm font-black text-slate-950`,children:e.name}),n&&c?H(`span`,{class:`ssh-apps-installed-badge shrink-0 rounded-full border px-2 py-0.5 text-[11px] font-bold`,children:`✓ 설치됨`}):null]}),H(`div`,{class:`mt-1 flex flex-wrap items-center gap-x-2 gap-y-1`,children:[H(`span`,{class:`text-xs text-slate-500`,title:n?`공유기에 설치된 대표 APK 패키지 버전입니다.`:`현재 제공 중인 승인 버전입니다.`,children:n?u?`설치 v${u}`:`설치 버전 확인 불가`:`v${e.version}`}),l?H(`span`,{class:`ssh-apps-update-badge rounded-full border px-2 py-0.5 text-[11px] font-bold`,children:`업데이트 가능`}):null]}),d||i!==null?H(`p`,{class:`mb-0 mt-1 flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-0.5 text-[11px] text-slate-500`,children:[d?H(`span`,{title:`현재 서버가 승인한 앱 릴리스 버전입니다. 설치된 APK 패키지의 버전과 다를 수 있습니다.`,children:[`제공 v`,e.version]}):null,d&&i!==null?H(`span`,{"aria-hidden":`true`,children:`·`}):null,i===null?null:H(`span`,{title:`설치된 APK 패키지의 용량 합계입니다. 앱 데이터 및 캐시는 포함되지 않습니다.`,children:[`설치 용량 약 `,i]})]}):null]})]}),H(`p`,{class:`mt-4 mb-4 min-h-10 break-words text-sm leading-5 text-slate-600`,children:e.summary}),H(`div`,{class:`mt-auto flex flex-wrap items-center justify-between gap-2 border-t border-slate-100 pt-3`,children:[H(`span`,{class:`text-xs font-bold text-teal-700`,children:`✓ 검증된 앱`}),m?H(gs,{action:m}):H(`span`,{class:`flex flex-wrap items-center gap-2`,children:[c&&t?.partial&&!n?H(`button`,{class:`min-h-9 rounded-lg border border-teal-300 bg-white px-3 text-xs font-bold text-teal-800 hover:bg-teal-50 disabled:opacity-50`,disabled:h,onClick:()=>void ce(e,`install`),type:`button`,children:`설치 마무리`}):null,l?H(`button`,{class:`ssh-apps-update-button min-h-9 rounded-lg border px-3 text-xs font-bold transition disabled:opacity-50`,disabled:h,onClick:()=>void ce(e,`update`),type:`button`,children:`업데이트`}):null,_?f?H(`button`,{"aria-label":`${e.name} 데모 실행 안내`,class:`inline-flex min-h-9 items-center rounded-lg border border-teal-700 bg-teal-700 px-3 text-xs font-bold text-white transition hover:bg-teal-800`,onClick:p,type:`button`,children:`열기 ↗`}):H(`a`,{"aria-label":`${e.name} 새 탭에서 열기`,class:`inline-flex min-h-9 items-center rounded-lg border border-teal-700 bg-teal-700 px-3 text-xs font-bold text-white transition hover:bg-teal-800`,href:_,rel:`noopener noreferrer`,target:`_blank`,children:`열기 ↗`}):n&&e.webUi?H(`span`,{class:`text-xs text-amber-700`,title:`이 앱의 웹 UI 패키지를 설치해야 열 수 있습니다.`,children:`웹 UI 미설치`}):null,n&&!c?H(`span`,{class:`text-xs text-slate-500`,title:`공유기 소프트웨어 메뉴 또는 SSH로 설치한 앱은 Apps에서 제거하지 않습니다.`,children:`기존에 설치됨`}):t?.partial&&!c?H(`span`,{class:`text-xs text-amber-700`,children:`일부 패키지 설치됨`}):H(`button`,{class:`min-h-9 rounded-lg border bg-white px-3 text-xs font-bold transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50 ${g?`border-red-300 text-red-700 hover:bg-red-50 focus-visible:ring-red-100`:`border-teal-300 text-teal-800 hover:bg-teal-50 focus-visible:ring-teal-200`}`,disabled:h||!!t?.partial&&!c,onClick:t=>g?se(e,t.currentTarget):void ce(e,`install`),type:`button`,children:g?`제거`:c?`재설치`:`설치`})]})]})]},e.id)})}),ae.length===0?H(`p`,{class:`rounded-xl bg-slate-100 p-5 text-center text-sm text-slate-600`,children:`검색 조건에 맞는 앱이 없습니다.`}):null]}):H(`div`,{class:`rounded-2xl border border-dashed border-slate-300 bg-white px-5 py-12 text-center`,children:[H(en,{class:`mx-auto size-10 text-slate-400`}),H(`h3`,{class:`mt-4 mb-2 text-base font-black text-slate-900`,children:`아직 등록된 앱이 없습니다.`}),H(`p`,{class:`m-0 text-sm leading-6 text-slate-500`,children:`현재 기기와 펌웨어에 대해 승인된 앱이 준비되면 이곳에서 확인할 수 있습니다.`})]}),H(`p`,{class:`text-xs text-slate-400`,children:[`기기 `,k.deviceCode,` · 펌웨어 `,k.firmwareBuildId,` · `,k.channel,` 채널`]})]}):null,j.length>0?H(`div`,{class:`rounded-2xl border border-amber-200 bg-amber-50 p-5`,children:[H(`h3`,{class:`m-0 text-sm font-black text-amber-900`,children:`제공이 중단된 앱`}),H(`p`,{class:`mb-3 mt-1 text-xs text-amber-800`,children:`현재 승인 목록에서 제외되었지만 기기에 남아 있는 앱입니다. 설치는 불가능하며 제거할 수 있습니다.`}),H(`div`,{class:`space-y-2`,children:j.map(([e])=>{let t=v?.appId===e?v.action:P&&N?.appId===e?ie:null;return H(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[H(`span`,{class:`break-all text-sm text-amber-900`,children:e}),t?H(gs,{action:t}):H(`button`,{class:`min-h-9 rounded-lg border border-red-300 bg-white px-3 text-xs font-bold text-red-700 transition hover:bg-red-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-100 disabled:cursor-not-allowed disabled:opacity-50`,disabled:!a||P||ee||v!==null||!!s,onClick:t=>se({id:e,name:e,summary:``,description:``,category:``,version:``},t.currentTarget),type:`button`,children:`제거`})]},e)})})]}):null]})}function vs(){return H(`section`,{"aria-busy":`true`,"aria-live":`polite`,class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:H(`div`,{class:`flex items-center gap-4`,children:[H(hn,{class:`size-8 shrink-0 animate-spin text-teal-700`}),H(`div`,{class:`min-w-0 text-left`,children:H(`h2`,{class:`m-0 text-lg font-extrabold text-slate-950`,children:`장치 상태를 확인하고 있습니다`})})]})})}function ys({message:e,onRetry:t}){return H(`section`,{"aria-live":`assertive`,class:`rounded-2xl border border-red-200 bg-red-50 p-5 sm:p-6`,children:[H(`p`,{class:`m-0 text-xs font-bold uppercase tracking-[0.16em] text-red-700`,children:`연결 오류`}),H(`h2`,{class:`mt-2 mb-0 text-lg font-extrabold text-red-950`,children:`장치 상태를 불러오지 못했습니다`}),H(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-red-800`,children:e}),H(`button`,{class:`mt-5 inline-flex min-h-10 items-center justify-center gap-2 rounded-xl border-0 bg-red-700 px-4 py-2 text-sm font-bold text-white shadow-sm transition hover:bg-red-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-red-200`,onClick:t,type:`button`,children:[H(hn,{class:`size-4`}),`다시 시도`]})]})}var bs=[{value:`all`,label:`전체`},{value:`online`,label:`현재 연결`},{value:`wifi`,label:`Wi-Fi`},{value:`ethernet`,label:`유선/기타`},{value:`offline`,label:`최근 기기`}],xs=new Intl.Collator(`ko`),Ss=new Intl.DateTimeFormat(`ko-KR`,{month:`numeric`,day:`numeric`,hour:`2-digit`,minute:`2-digit`});function Cs(e){switch(e){case`wifi`:return`Wi-Fi`;case`ethernet`:return`유선/기타`;default:return`연결 방식 미확인`}}function ws(e){return e.hostname??e.ipv4Address??`이름 없는 기기`}function Ts(e){return e===null?null:e>=-50?`매우 좋음`:e>=-60?`좋음`:e>=-70?`보통`:`약함`}function Es(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 Ds(e){return e.leaseActive?e.leaseExpiresAt===null?`DHCP 임대 활성`:`DHCP ${Ss.format(new Date(e.leaseExpiresAt*1e3))}까지`:null}function Os(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 ks({device:e}){return H(`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:[H(`span`,{class:`size-2 rounded-full ${e.online?`bg-emerald-500`:`bg-slate-400`}`}),e.online?`현재 연결`:`최근 임대`]})}function As({device:e}){let t=Ts(e.signalDbm),n=Es(e.connectedSeconds),r=Ds(e);return H(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:[H(`div`,{class:`flex min-w-0 flex-col items-start gap-4 sm:flex-row sm:justify-between`,children:[H(`div`,{class:`flex min-w-0 items-start gap-3`,children:[H(`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`?H(cn,{class:`size-6`}):e.connection===`ethernet`?H(un,{class:`size-6`}):H(Zt,{class:`size-6`})}),H(`div`,{class:`min-w-0`,children:[H(`h3`,{class:`m-0 break-words text-base font-extrabold text-slate-950`,children:ws(e)}),H(`p`,{class:`mt-1 mb-0 break-all font-mono text-xs leading-5 text-slate-500`,children:e.mac})]})]}),H(`span`,{class:`shrink-0 self-start`,children:H(ks,{device:e})})]}),H(`dl`,{class:`mt-5 grid gap-3 text-sm sm:grid-cols-2`,children:[H(`div`,{class:`rounded-xl bg-slate-50 p-3.5`,children:[H(`dt`,{class:`text-xs font-bold text-slate-500`,children:`IP 주소`}),H(`dd`,{class:`mt-1.5 mb-0 ml-0 font-extrabold text-slate-900`,children:e.ipv4Address??`확인되지 않음`})]}),H(`div`,{class:`rounded-xl bg-slate-50 p-3.5`,children:[H(`dt`,{class:`text-xs font-bold text-slate-500`,children:`연결 방식`}),H(`dd`,{class:`mt-1.5 mb-0 ml-0 font-extrabold text-slate-900`,children:Cs(e.connection)})]})]}),H(`div`,{class:`mt-4 flex flex-wrap gap-2 text-xs font-bold text-slate-600`,children:[e.ssid?H(`span`,{class:`rounded-full bg-teal-50 px-3 py-1.5 text-teal-800`,children:[e.ssid,e.bandLabel?` · ${e.bandLabel}`:``]}):null,t?H(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:[`신호 `,t,` · `,e.signalDbm,` dBm`]}):null,n?H(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:[`연결 `,n]}):null,r?H(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:r}):null,e.interface?H(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:e.interface}):null]})]})}function js({data:e,error:t,loading:n,onRetry:r}){let[i,a]=F(``),[o,s]=F(`all`),c=Qe(()=>{if(!e)return[];let t=i.trim().toLocaleLowerCase();return[...e.devices].filter(e=>Os(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?xs.compare(ws(e),ws(t)):e.connection===`wifi`?-1:1:e.online?-1:1)},[e,o,i]);return n?H(vs,{}):t?H(ys,{message:t,onRetry:r}):e?H(b,{children:[H(q,{eyebrow:`Overview`,title:`연결 기기 현황`,description:`네트워크에 연결되었거나 최근 확인된 기기를 요약합니다.`}),H(`section`,{"aria-label":`연결 기기 요약`,class:`grid gap-4 sm:grid-cols-2 xl:grid-cols-4`,children:[H(Ms,{description:`${e.totals.offline}대는 최근 DHCP 임대에서 확인`,icon:H(Zt,{class:`size-6`}),label:`확인된 기기`,value:e.totals.known}),H(Ms,{description:`Wi-Fi 또는 ARP에서 현재 확인됨`,icon:H(pn,{class:`size-6`}),label:`현재 연결`,value:e.totals.online}),H(Ms,{description:`무선 AP에 연결된 기기`,icon:H(cn,{class:`size-6`}),label:`Wi-Fi`,value:e.totals.wireless}),H(Ms,{description:`LAN ARP에서 확인된 비무선 기기`,icon:H(un,{class:`size-6`}),label:`유선/기타`,value:e.totals.ethernet})]}),H(`section`,{class:`mt-7`,"aria-label":`기기 목록 및 필터`,children:[H(q,{eyebrow:`Device inventory`,title:`기기 목록`,description:`연결된 기기를 검색하고 무선·유선 상태별로 확인합니다.`}),H(`div`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:H(`div`,{class:`flex min-w-0 flex-col gap-3 lg:flex-row lg:items-center lg:justify-between`,children:[H(`div`,{class:`ssh-touch-scroll flex min-w-0 gap-2 overflow-x-auto pb-1 lg:flex-1`,role:`group`,"aria-label":`기기 필터`,children:bs.map(e=>H(`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))}),H(`label`,{class:`relative block w-full lg:w-72 lg:shrink-0 xl:w-80`,children:[H(`span`,{class:`sr-only`,children:`기기 검색`}),H(`span`,{class:`pointer-events-none absolute inset-y-0 left-0 flex w-11 items-center justify-center text-slate-400`,children:H(Dn,{class:`size-5`})}),H(`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})]})]})})]}),c.length?H(`section`,{"aria-live":`polite`,class:`mt-5 grid gap-4 lg:grid-cols-2`,children:c.map(e=>H(As,{device:e},e.id))}):H(`section`,{class:`mt-5 rounded-2xl border border-dashed border-slate-300 bg-white p-5 text-center sm:p-8`,children:[H(Zt,{class:`mx-auto size-10 text-slate-400`}),H(`h2`,{class:`mt-4 mb-0 text-lg font-extrabold text-slate-950`,children:`조건에 맞는 기기가 없습니다`}),H(`p`,{class:`mt-2 mb-0 text-sm text-slate-600`,children:`검색어 또는 필터를 변경해 주세요.`})]}),H(`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 Ms({description:e,icon:t,label:n,value:r}){return H(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:[H(`div`,{class:`flex items-start justify-between gap-4`,children:[H(`div`,{children:[H(`p`,{class:`m-0 text-xs font-bold uppercase tracking-[0.14em] text-slate-500`,children:n}),H(`p`,{class:`mt-3 mb-0 text-3xl font-black tracking-tight text-slate-950`,children:r})]}),H(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:t})]}),H(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:e})]})}function Ns(e){return e+.5|0}var Ps=(e,t,n)=>Math.max(Math.min(e,n),t);function Fs(e){return Ps(Ns(e*2.55),0,255)}function Is(e){return Ps(Ns(e*255),0,255)}function Ls(e){return Ps(Ns(e/2.55)/100,0,1)}function Rs(e){return Ps(Ns(e*100),0,100)}var zs={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},Bs=[...`0123456789ABCDEF`],Vs=e=>Bs[e&15],Hs=e=>Bs[(e&240)>>4]+Bs[e&15],Us=e=>(e&240)>>4==(e&15),Ws=e=>Us(e.r)&&Us(e.g)&&Us(e.b)&&Us(e.a);function Gs(e){var t=e.length,n;return e[0]===`#`&&(t===4||t===5?n={r:255&zs[e[1]]*17,g:255&zs[e[2]]*17,b:255&zs[e[3]]*17,a:t===5?zs[e[4]]*17:255}:(t===7||t===9)&&(n={r:zs[e[1]]<<4|zs[e[2]],g:zs[e[3]]<<4|zs[e[4]],b:zs[e[5]]<<4|zs[e[6]],a:t===9?zs[e[7]]<<4|zs[e[8]]:255})),n}var Ks=(e,t)=>e<255?t(e):``;function qs(e){var t=Ws(e)?Vs:Hs;return e?`#`+t(e.r)+t(e.g)+t(e.b)+Ks(e.a,t):void 0}var Js=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Ys(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 Xs(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 Zs(e,t,n){let r=Ys(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 Qs(e,t,n,r,i){return e===i?(t-n)/r+(t.5?l/(2-i-a):l/(i+a),s=Qs(t,n,r,l,i),s=s*60+.5),[s|0,c||0,o]}function ec(e,t,n,r){return(Array.isArray(t)?e(t[0],t[1],t[2]):e(t,n,r)).map(Is)}function tc(e,t,n){return ec(Ys,e,t,n)}function nc(e,t,n){return ec(Zs,e,t,n)}function rc(e,t,n){return ec(Xs,e,t,n)}function ic(e){return(e%360+360)%360}function ac(e){let t=Js.exec(e),n=255,r;if(!t)return;t[5]!==r&&(n=t[6]?Fs(+t[5]):Is(+t[5]));let i=ic(+t[2]),a=t[3]/100,o=t[4]/100;return r=t[1]===`hwb`?nc(i,a,o):t[1]===`hsv`?rc(i,a,o):tc(i,a,o),{r:r[0],g:r[1],b:r[2],a:n}}function oc(e,t){var n=$s(e);n[0]=ic(n[0]+t),n=tc(n),e.r=n[0],e.g=n[1],e.b=n[2]}function sc(e){if(!e)return;let t=$s(e),n=t[0],r=Rs(t[1]),i=Rs(t[2]);return e.a<255?`hsla(${n}, ${r}%, ${i}%, ${Ls(e.a)})`:`hsl(${n}, ${r}%, ${i}%)`}var cc={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`},lc={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 uc(){let e={},t=Object.keys(lc),n=Object.keys(cc),r,i,a,o,s;for(r=0;r>16&255,a>>8&255,a&255]}return e}var dc;function fc(e){dc||(dc=uc(),dc.transparent=[0,0,0,0]);let t=dc[e.toLowerCase()];return t&&{r:t[0],g:t[1],b:t[2],a:t.length===4?t[3]:255}}var pc=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;function mc(e){let t=pc.exec(e),n=255,r,i,a;if(t){if(t[7]!==r){let e=+t[7];n=t[8]?Fs(e):Ps(e*255,0,255)}return r=+t[1],i=+t[3],a=+t[5],r=255&(t[2]?Fs(r):Ps(r,0,255)),i=255&(t[4]?Fs(i):Ps(i,0,255)),a=255&(t[6]?Fs(a):Ps(a,0,255)),{r,g:i,b:a,a:n}}}function hc(e){return e&&(e.a<255?`rgba(${e.r}, ${e.g}, ${e.b}, ${Ls(e.a)})`:`rgb(${e.r}, ${e.g}, ${e.b})`)}var gc=e=>e<=.0031308?e*12.92:e**(1/2.4)*1.055-.055,_c=e=>e<=.04045?e/12.92:((e+.055)/1.055)**2.4;function vc(e,t,n){let r=_c(Ls(e.r)),i=_c(Ls(e.g)),a=_c(Ls(e.b));return{r:Is(gc(r+n*(_c(Ls(t.r))-r))),g:Is(gc(i+n*(_c(Ls(t.g))-i))),b:Is(gc(a+n*(_c(Ls(t.b))-a))),a:e.a+n*(t.a-e.a)}}function yc(e,t,n){if(e){let r=$s(e);r[t]=Math.max(0,Math.min(r[t]+r[t]*n,t===0?360:1)),r=tc(r),e.r=r[0],e.g=r[1],e.b=r[2]}}function bc(e,t){return e&&Object.assign(t||{},e)}function xc(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=Is(e[3]))):(t=bc(e,{r:0,g:0,b:0,a:1}),t.a=Is(t.a)),t}function Sc(e){return e.charAt(0)===`r`?mc(e):ac(e)}var Cc=class e{constructor(t){if(t instanceof e)return t;let n=typeof t,r;n===`object`?r=xc(t):n===`string`&&(r=Gs(t)||fc(t)||Sc(t)),this._rgb=r,this._valid=!!r}get valid(){return this._valid}get rgb(){var e=bc(this._rgb);return e&&(e.a=Ls(e.a)),e}set rgb(e){this._rgb=xc(e)}rgbString(){return this._valid?hc(this._rgb):void 0}hexString(){return this._valid?qs(this._rgb):void 0}hslString(){return this._valid?sc(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=vc(this._rgb,e._rgb,t)),this}clone(){return new e(this.rgb)}alpha(e){return this._rgb.a=Is(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=Ns(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 yc(this._rgb,2,e),this}darken(e){return yc(this._rgb,2,-e),this}saturate(e){return yc(this._rgb,1,e),this}desaturate(e){return yc(this._rgb,1,-e),this}rotate(e){return oc(this._rgb,e),this}};function wc(){}var Tc=(()=>{let e=0;return()=>e++})();function Y(e){return e==null}function Ec(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 X(e){return e!==null&&Object.prototype.toString.call(e)===`[object Object]`}function Dc(e){return(typeof e==`number`||e instanceof Number)&&isFinite(+e)}function Oc(e,t){return Dc(e)?e:t}function Z(e,t){return e===void 0?t:e}var kc=(e,t)=>typeof e==`string`&&e.endsWith(`%`)?parseFloat(e)/100:+e/t,Ac=(e,t)=>typeof e==`string`&&e.endsWith(`%`)?parseFloat(e)/100*t:+e;function jc(e,t,n){if(e&&typeof e.call==`function`)return e.apply(n,t)}function Q(e,t,n,r){let i,a,o;if(Ec(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 Bc(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 Vc(e){let t=Bc(e);return e=>{for(let n of t){if(n===``)break;e=e&&e[n]}return e}}function Hc(e,t){return(zc[t]||(zc[t]=Vc(t)))(e)}function Uc(e){return e.charAt(0).toUpperCase()+e.slice(1)}var Wc=e=>e!==void 0,Gc=e=>typeof e==`function`,Kc=(e,t)=>{if(e.size!==t.size)return!1;for(let n of e)if(!t.has(n))return!1;return!0};function qc(e){return e.type===`mouseup`||e.type===`click`||e.type===`contextmenu`}var $=Math.PI,Jc=2*$,Yc=Jc+$,Xc=1/0,Zc=$/180,Qc=$/2,$c=$/4,el=$*2/3,tl=Math.log10,nl=Math.sign;function rl(e,t,n){return Math.abs(e-t)e-t).pop(),t}function ol(e){return typeof e==`symbol`||typeof e==`object`&&!!e&&!(Symbol.toPrimitive in e||`toString`in e||`valueOf`in e)}function sl(e){return!ol(e)&&!isNaN(parseFloat(e))&&isFinite(e)}function cl(e,t){let n=Math.round(e);return n-t<=e&&n+t>=e}function ll(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 xl(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 Sl=(e,t,n,r)=>xl(e,n,r?r=>{let i=e[r][t];return ie[r][t]xl(e,n,r=>e[r][t]>=n);function wl(e,t,n){let r=0,i=e.length;for(;rr&&e[i-1]>n;)i--;return r>0||i{let n=`_onData`+Uc(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 Dl(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)&&(Tl.forEach(t=>{delete e[t]}),delete e._chartjs)}function Ol(e){let t=new Set(e);return t.size===e.length?e:Array.from(t)}var kl=function(){return typeof window>`u`?function(e){return e()}:window.requestAnimationFrame}();function Al(e,t){let n=[],r=!1;return function(...i){n=i,r||(r=!0,kl.call(window,()=>{r=!1,e.apply(t,n)}))}}function jl(e,t){let n;return function(...r){return t?(clearTimeout(n),n=setTimeout(e,t,r)):e.apply(this,r),t}}var Ml=e=>e===`start`?`left`:e===`end`?`right`:`center`,Nl=(e,t,n)=>e===`start`?t:e===`end`?n:(t+n)/2;function Pl(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(Sl(c,u,d).lo,n?r:Sl(t,u,o.getPixelForValue(d)).lo),l){let e=c.slice(0,i+1).reverse().findIndex(e=>!Y(e[s.axis]));i-=Math.max(0,e)}i=vl(i,0,r-1)}if(m){let e=Math.max(Sl(c,o.axis,f,!0).hi+1,n?0:Sl(t,u,o.getPixelForValue(f),!0).hi+1);if(l){let t=c.slice(e-1).findIndex(e=>!Y(e[s.axis]));e+=Math.max(0,t)}a=vl(e,i,r)-i}else a=r-i}return{start:i,count:a}}function Fl(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 Il=e=>e===0||e===1,Ll=(e,t,n)=>-(2**(10*--e)*Math.sin((e-t)*Jc/n)),Rl=(e,t,n)=>2**(-10*e)*Math.sin((e-t)*Jc/n)+1,zl={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*Qc)+1,easeOutSine:e=>Math.sin(e*Qc),easeInOutSine:e=>-.5*(Math.cos($*e)-1),easeInExpo:e=>e===0?0:2**(10*(e-1)),easeOutExpo:e=>e===1?1:-(2**(-10*e))+1,easeInOutExpo:e=>Il(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=>Il(e)?e:Ll(e,.075,.3),easeOutElastic:e=>Il(e)?e:Rl(e,.075,.3),easeInOutElastic(e){let t=.1125,n=.45;return Il(e)?e:e<.5?.5*Ll(e*2,t,n):.5+.5*Rl(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-zl.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?zl.easeInBounce(e*2)*.5:zl.easeOutBounce(e*2-1)*.5+.5};function Bl(e){if(e&&typeof e==`object`){let t=e.toString();return t===`[object CanvasPattern]`||t===`[object CanvasGradient]`}return!1}function Vl(e){return Bl(e)?e:new Cc(e)}function Hl(e){return Bl(e)?e:new Cc(e).saturate(.5).darken(.1).hexString()}var Ul=[`x`,`y`,`borderWidth`,`radius`,`tension`],Wl=[`color`,`borderColor`,`backgroundColor`];function Gl(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:Wl},numbers:{type:`number`,properties:Ul}}),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 Kl(e){e.set(`layout`,{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})}var ql=new Map;function Jl(e,t){t=t||{};let n=e+JSON.stringify(t),r=ql.get(n);return r||(r=new Intl.NumberFormat(e,t),ql.set(n,r)),r}function Yl(e,t,n){return Jl(t,n).format(e)}var Xl={values(e){return Ec(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=Zl(e,n)}let o=tl(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),Yl(e,r,c)},logarithmic(e,t,n){if(e===0)return`0`;let r=n[t].significand||e/10**Math.floor(tl(e));return[1,2,3,5,10,15].includes(r)||t>.8*n.length?Xl.numeric.call(this,e,t,n):``}};function Zl(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 Ql={formatters:Xl};function $l(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:Ql.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 eu=Object.create(null),tu=Object.create(null);function nu(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)=>Hl(t.backgroundColor),this.hoverBorderColor=(e,t)=>Hl(t.borderColor),this.hoverColor=(e,t)=>Hl(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 ru(this,e,t)}get(e){return nu(this,e)}describe(e,t){return ru(tu,e,t)}override(e,t){return ru(eu,e,t)}route(e,t,n,r){let i=nu(this,e),a=nu(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 X(e)?Object.assign({},t,e):Z(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}},[Gl,Kl,$l]);function au(e){return!e||Y(e.size)||Y(e.family)?null:(e.style?e.style+` `:``)+(e.weight?e.weight+` `:``)+e.size+`px `+e.family}function ou(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 su(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 fu(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,_u(e,a),c=0;c+e||0;function Eu(e,t){let n={},r=X(t),i=r?Object.keys(t):t,a=X(e)?r?n=>Z(e[n],e[t[n]]):t=>e[t]:()=>e;for(let e of i)n[e]=Tu(a(e));return n}function Du(e){return Eu(e,{top:`y`,right:`x`,bottom:`y`,left:`x`})}function Ou(e){return Eu(e,[`topLeft`,`topRight`,`bottomLeft`,`bottomRight`])}function ku(e){let t=Du(e);return t.width=t.left+t.right,t.height=t.top+t.bottom,t}function Au(e,t){e=e||{},t=t||iu.font;let n=Z(e.size,t.size);typeof n==`string`&&(n=parseInt(n,10));let r=Z(e.style,t.style);r&&!(``+r).match(Cu)&&(console.warn(`Invalid font style specified: "`+r+`"`),r=void 0);let i={family:Z(e.family,t.family),lineHeight:wu(Z(e.lineHeight,t.lineHeight),n),size:n,style:r,weight:Z(e.weight,t.weight),string:``};return i.string=au(i),i}function ju(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 Nu(e,t){return Object.assign(Object.create(e),t)}function Pu(e,t=[``],n,r,i=()=>e[0]){let a=n||e;return r===void 0&&(r=Xu(`_fallback`,e)),new Proxy({[Symbol.toStringTag]:`Object`,_cacheable:!0,_scopes:e,_rootScopes:a,_fallback:r,_getTarget:i,override:n=>Pu([n,...e],t,a,r)},{deleteProperty(t,n){return delete t[n],delete t._keys,delete e[0][n],!0},get(n,r){return zu(n,r,()=>Yu(r,t,e,n))},getOwnPropertyDescriptor(e,t){return Reflect.getOwnPropertyDescriptor(e._scopes[0],t)},getPrototypeOf(){return Reflect.getPrototypeOf(e[0])},has(e,t){return Zu(e).includes(t)},ownKeys(e){return Zu(e)},set(e,t,n){let r=e._storage||(e._storage=i());return e[t]=r[t]=n,delete e._keys,!0}})}function Fu(e,t,n,r){let i={_cacheable:!1,_proxy:e,_context:t,_subProxy:n,_stack:new Set,_descriptors:Iu(e,r),setContext:t=>Fu(e,t,n,r),override:i=>Fu(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 zu(e,t,()=>Bu(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 Iu(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:Gc(n)?n:()=>n,isIndexable:Gc(r)?r:()=>r}}var Lu=(e,t)=>e?e+Uc(t):t,Ru=(e,t)=>X(t)&&e!==`adapters`&&(Object.getPrototypeOf(t)===null||t.constructor===Object);function zu(e,t,n){if(Object.prototype.hasOwnProperty.call(e,t)||t===`constructor`)return e[t];let r=n();return e[t]=r,r}function Bu(e,t,n){let{_proxy:r,_context:i,_subProxy:a,_descriptors:o}=e,s=r[t];return Gc(s)&&o.isScriptable(t)&&(s=Vu(t,s,e,n)),Ec(s)&&s.length&&(s=Hu(t,s,e,o.isIndexable)),Ru(t,s)&&(s=Fu(s,i,a&&a[t],o)),s}function Vu(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),Ru(e,c)&&(c=Ku(i._scopes,i,e,c)),c}function Hu(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(X(t[0])){let n=t,r=i._scopes.filter(e=>e!==n);t=[];for(let c of n){let n=Ku(r,i,e,c);t.push(Fu(n,a,o&&o[e],s))}}return t}function Uu(e,t,n){return Gc(e)?e(t,n):e}var Wu=(e,t)=>e===!0?t:typeof e==`string`?Hc(t,e):void 0;function Gu(e,t,n,r,i){for(let a of t){let t=Wu(n,a);if(t){e.add(t);let a=Uu(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 Ku(e,t,n,r){let i=t._rootScopes,a=Uu(t._fallback,n,r),o=[...e,...i],s=new Set;s.add(r);let c=qu(s,o,n,a||n,r);return c===null||a!==void 0&&a!==n&&(c=qu(s,o,a,c,r),c===null)?!1:Pu(Array.from(s),[``],i,a,()=>Ju(t,n,r))}function qu(e,t,n,r,i){for(;n;)n=Gu(e,t,n,r,i);return n}function Ju(e,t,n){let r=e._getTarget();t in r||(r[t]={});let i=r[t];return Ec(i)&&X(n)?n:i||{}}function Yu(e,t,n,r){let i;for(let a of t)if(i=Xu(Lu(a,e),n),i!==void 0)return Ru(e,i)?Ku(n,r,e,i):i}function Xu(e,t){for(let n of t){if(!n)continue;let t=n[e];if(t!==void 0)return t}}function Zu(e){let t=e._keys;return t||(t=e._keys=Qu(e._scopes)),t}function Qu(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 $u(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 rd(e,t,n,r){let i=e.skip?t:e,a=t,o=n.skip?t:n,s=ml(a,i),c=ml(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 id(e,t,n){let r=e.length,i,a,o,s,c,l=td(e,0);for(let u=0;u!e.skip)),t.cubicInterpolationMode===`monotone`)od(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 md(e,t){return pd(e).getPropertyValue(t)}var hd=[`top`,`right`,`bottom`,`left`];function gd(e,t,n){let r={};n=n?`-`+n:``;for(let i=0;i<4;i++){let a=hd[i];r[a]=parseFloat(e[t+`-`+a+n])||0}return r.width=r.left+r.right,r.height=r.top+r.bottom,r}var _d=(e,t,n)=>(e>0||t>0)&&(!n||!n.shadowRoot);function vd(e,t){let n=e.touches,r=n&&n.length?n[0]:e,{offsetX:i,offsetY:a}=r,o=!1,s,c;if(_d(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 yd(e,t){if(`native`in e)return e;let{canvas:n,currentDevicePixelRatio:r}=t,i=pd(n),a=i.boxSizing===`border-box`,o=gd(i,`padding`),s=gd(i,`border`,`width`),{x:c,y:l,box:u}=vd(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 bd(e,t,n){let r,i;if(t===void 0||n===void 0){let a=e&&dd(e);if(!a)t=e.clientWidth,n=e.clientHeight;else{let e=a.getBoundingClientRect(),o=pd(a),s=gd(o,`border`,`width`),c=gd(o,`padding`);t=e.width-c.width-s.width,n=e.height-c.height-s.height,r=fd(o.maxWidth,a,`clientWidth`),i=fd(o.maxHeight,a,`clientHeight`)}}return{width:t,height:n,maxWidth:r||Xc,maxHeight:i||Xc}}var xd=e=>Math.round(e*10)/10;function Sd(e,t,n,r){let i=pd(e),a=gd(i,`margin`),o=fd(i.maxWidth,e,`clientWidth`)||Xc,s=fd(i.maxHeight,e,`clientHeight`)||Xc,c=bd(e,t,n),{width:l,height:u}=c;if(i.boxSizing===`content-box`){let e=gd(i,`border`,`width`),t=gd(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=xd(Math.min(l,o,c.maxWidth)),u=xd(Math.min(u,s,c.maxHeight)),l&&!u&&(u=xd(l/2)),(t!==void 0||n!==void 0)&&r&&c.height&&u>c.height&&(u=c.height,l=xd(Math.floor(u*r))),{width:l,height:u}}function Cd(e,t,n){let r=t||1,i=xd(e.height*r),a=xd(e.width*r);e.height=xd(e.height),e.width=xd(e.width);let o=e.canvas;o.style&&(n||!o.style.height&&!o.style.width)&&(o.style.height=`${e.height}px`,o.style.width=`${e.width}px`);let s=Math.floor(i),c=Math.floor(a);return e.currentDevicePixelRatio!==r||o.height!==s||o.width!==c?(e.currentDevicePixelRatio=r,o.height=s,o.width=c,e.ctx.setTransform(r,0,0,r,0,0),!0):!1}var wd=function(){let e=!1;try{let t={get passive(){return e=!0,!1}};ud()&&(window.addEventListener(`test`,null,t),window.removeEventListener(`test`,null,t))}catch{}return e}();function Td(e,t){let n=md(e,t),r=n&&n.match(/^(\d+)(\.\d+)?px$/);return r?+r[1]:void 0}function Ed(e,t,n,r){return{x:e.x+n*(t.x-e.x),y:e.y+n*(t.y-e.y)}}function Dd(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 Od(e,t,n,r){let i={x:e.cp2x,y:e.cp2y},a={x:t.cp1x,y:t.cp1y},o=Ed(e,i,n),s=Ed(i,a,n),c=Ed(a,t,n);return Ed(Ed(o,s,n),Ed(s,c,n),n)}var kd=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}}},Ad=function(){return{x(e){return e},setWidth(e){},textAlign(e){return e},xPlus(e,t){return e+t},leftForLtr(e,t){return e}}};function jd(e,t,n){return e?kd(t,n):Ad()}function Md(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 Nd(e,t){t!==void 0&&(delete e.prevTextDirection,e.canvas.style.setProperty(`direction`,t[0],t[1]))}function Pd(e){return e===`angle`?{between:_l,compare:hl,normalize:gl}:{between:bl,compare:(e,t)=>e-t,normalize:e=>e}}function Fd({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 Id(e,t,n){let{property:r,start:i,end:a}=n,{between:o,normalize:s}=Pd(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(Fd({start:g,end:e,loop:f,count:o,style:p})),g=null),n=e,y=_));return g!==null&&m.push(Fd({start:g,end:d,loop:f,count:o,style:p})),m}function Rd(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 Bd(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 Vd(e,t){let n=e.points,r=e.options.spanGaps,i=n.length;if(!i)return[];let a=!!e._loop,{start:o,end:s}=zd(n,i,a,r);return r===!0?Hd(e,[{start:o,end:s,loop:a}],n,t):Hd(e,Bd(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=kl.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)}},Xd=`transparent`,Zd={boolean(e,t,n){return n>.5?t:e},color(e,t,n){let r=Vl(e||Xd),i=r.valid&&Vl(t||Xd);return i&&i.valid?i.mix(r,n).hexString():t},number(e,t,n){return e+(t-e)*n}},Qd=class{constructor(e,t,n,r){let i=t[n];r=ju([e.to,r,i,e.from]);let a=ju([e.from,i,r]);this._active=!0,this._fn=e.fn||Zd[e.type||typeof a],this._easing=zl[e.easing]||zl.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=ju([e.to,t,r,e.from]),this._from=ju([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(!X(i))return;let a={};for(let e of t)a[e]=i[e];(Ec(i.properties)&&i.properties||[r]).forEach(e=>{e!==r&&n.has(e)||n.set(e,a)})})}_animateOptions(e,t){let n=t.options,r=tf(e,n);if(!r)return[];let i=this._createAnimations(r,n);return n.$shared&&ef(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 Qd(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 Yd.add(this._chart,n),!0}};function ef(e,t){let n=[],r=Object.keys(t);for(let t=0;t0||!n&&t<0)return i.index}return null}function mf(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=uf(a,o,r),d=t.length,f;for(let e=0;en[e].axis===t).shift()}function gf(e,t){return Nu(e,{active:!1,dataset:void 0,datasetIndex:t,index:t,mode:`default`,type:`dataset`})}function _f(e,t,n){return Nu(e,{active:!1,dataIndex:t,parsed:void 0,raw:void 0,element:n,index:t,mode:`default`,type:`data`})}function vf(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;t&&t[r]!==void 0&&t[r][n]!==void 0&&(delete t[r][n],t[r]._visualValues!==void 0&&t[r]._visualValues[n]!==void 0&&delete t[r]._visualValues[n])}}}var yf=e=>e===`reset`||e===`none`,bf=(e,t)=>t?e:Object.assign({},e),xf=(e,t,n)=>e&&!t.hidden&&t._stacked&&{keys:of(n,!0),values:null},Sf=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=lf(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&&vf(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=Z(n.xAxisID,hf(e,`x`)),a=t.yAxisID=Z(n.yAxisID,hf(e,`y`)),o=t.rAxisID=Z(n.rAxisID,hf(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&&Dl(this._data,this),e._stacked&&vf(e)}_dataCheck(){let e=this.getDataset(),t=e.data||(e.data=[]),n=this._data;if(X(t)){let e=this._cachedMeta;this._data=cf(t,e)}else if(n!==t){if(n){Dl(n,this);let e=this._cachedMeta;vf(e),e._parsed=[]}t&&Object.isExtensible(t)&&El(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=lf(t.vScale,t),t.stack!==n.stack&&(r=!0,vf(t),t.stack=n.stack),this._resyncElements(e),(r||i!==t._stacked)&&(mf(this,t._parsed),t._stacked=lf(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=Ec(r[e])?this.parseArrayData(n,r,e,t):X(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(bf(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 $d(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||yf(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){yf(r)?Object.assign(e,n):this._resolveAnimations(t,r).update(e,n)}updateSharedOptions(e,t,n){e&&!yf(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 wf(e){let t=e.iScale,n=Cf(t,e.type),r=t._length,i,a,o,s,c=()=>{o!==32767&&o!==-32768&&(Wc(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 Of(e,t,n,r){return Ec(e)?Df(e,t,n,r):t[n.axis]=n.parse(e,r),t}function kf(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):nl(e)}function Mf(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(Y(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[Z(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;d_l(e,s,c,!0)?1:Math.max(t,t*n,r,r*n),m=(e,t,r)=>_l(e,s,c,!0)?-1:Math.min(t,t*n,r,r*n),h=p(0,l,d),g=p(Qc,u,f),_=m($,l,d),v=m($+Qc,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 Vf=class extends Sf{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(X(n[e])){let{key:e=`value`}=this._parsing;i=t=>+Hc(n[t],e)}let a,o;for(a=e,o=e+t;a0&&!isNaN(e)?Math.abs(e)/t*Jc:0}getLabelAndValue(e){let t=this._cachedMeta,n=this.chart,r=n.data.labels||[],i=Yl(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`&&e!==`spacingMode`,_indexable:e=>e!==`spacing`&&e!==`spacingMode`&&!e.startsWith(`borderDash`)&&!e.startsWith(`hoverBorderDash`)}),z(Vf,`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 Hf=class extends Sf{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}=Pl(t,r,a);this._drawStart=o,this._drawCount=s,Fl(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=sl(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=Y(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()}};z(Hf,`id`,`line`),z(Hf,`defaults`,{datasetElementType:`line`,dataElementType:`point`,showLine:!0,spanGaps:!1}),z(Hf,`overrides`,{scales:{_index_:{type:`category`},_value_:{type:`linear`}}});var Uf=class extends Sf{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=Yl(t._parsed[e].r,n.options.locale);return{label:r[e]||``,value:i}}parseObjectData(e,t,n,r){return $u.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*$,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)?ul(this.resolveDataElementOptions(e,t).angle||n):0}};z(Uf,`id`,`polarArea`),z(Uf,`defaults`,{dataElementType:`arc`,animation:{animateRotate:!0,animateScale:!0},animations:{numbers:{type:`number`,properties:[`x`,`y`,`startAngle`,`endAngle`,`innerRadius`,`outerRadius`]}},indexAxis:`r`,startAngle:0,spacing:0,spacingMode:`proportional`}),z(Uf,`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 Wf=class extends Vf{};z(Wf,`id`,`pie`),z(Wf,`defaults`,{cutout:0,rotation:0,circumference:360,radius:`100%`});var Gf=class extends Sf{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 $u.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}};z(Kf,`id`,`scatter`),z(Kf,`defaults`,{datasetElementType:!1,dataElementType:`point`,showLine:!1,fill:!1}),z(Kf,`overrides`,{interaction:{mode:`point`},scales:{x:{type:`linear`},y:{type:`linear`}}});function qf(){throw Error(`This method is not implemented: Check that a complete date adapter is provided.`)}var Jf={_date:class e{static override(t){Object.assign(e.prototype,t)}constructor(e){z(this,`options`,void 0),this.options=e||{}}init(){}formats(){return qf()}parse(){return qf()}format(){return qf()}add(){return qf()}diff(){return qf()}startOf(){return qf()}endOf(){return qf()}}};function Yf(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?Cl:Sl;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=>!Y(e[t.axis]));r.lo-=Math.max(0,a);let o=n.slice(r.hi).findIndex(e=>!Y(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 Xf(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 rp={evaluateInteractionItems:Xf,modes:{index(e,t,n,r){let i=yd(t,e),a=n.axis||`x`,o=n.includeInvisible||!1,s=n.intersect?Qf(e,i,a,r,o):tp(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=yd(t,e),a=n.axis||`xy`,o=n.includeInvisible||!1,s=n.intersect?Qf(e,i,a,r,o):tp(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 op(e,t){return e.filter(e=>ip.indexOf(e.pos)===-1&&e.box.axis===t)}function sp(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 cp(e){let t=[],n,r,i,a,o,s;for(n=0,r=(e||[]).length;ne.box.fullSize),!0),r=sp(ap(t,`left`),!0),i=sp(ap(t,`right`)),a=sp(ap(t,`top`),!0),o=sp(ap(t,`bottom`)),s=op(t,`x`),c=op(t,`y`);return{fullSize:n,leftAndTop:r.concat(a),rightAndBottom:i.concat(c).concat(o).concat(s),chartArea:ap(t,`chartArea`),vertical:r.concat(i).concat(c),horizontal:a.concat(o).concat(s)}}function fp(e,t,n,r){return Math.max(e[n],t[n])+Math.max(e[r],t[r])}function pp(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 mp(e,t,n,r){let{pos:i,box:a}=n,o=e.maxPadding;if(!X(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&&pp(o,a.getPadding());let s=Math.max(0,t.outerWidth-fp(o,e,`left`,`right`)),c=Math.max(0,t.outerHeight-fp(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 hp(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 gp(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 _p(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);pp(f,ku(r));let p=Object.assign({maxPadding:f,w:a,h:o,x:i.left,y:i.top},i),m=up(c.concat(l),d);_p(s.fullSize,p,d,m),_p(c,p,d,m),_p(l,p,d,m)&&_p(c,p,d,m),hp(p),yp(s.leftAndTop,p,d,m),p.x+=p.w,p.y+=p.h,yp(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},Q(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})})}},xp=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){}},Sp=class extends xp{acquireContext(e){return e&&e.getContext&&e.getContext(`2d`)||null}updateConfig(e){e.options.animation=!1}},Cp=`$chartjs`,wp={touchstart:`mousedown`,touchmove:`mousemove`,touchend:`mouseup`,pointerenter:`mouseenter`,pointerdown:`mousedown`,pointermove:`mousemove`,pointerup:`mouseup`,pointerleave:`mouseout`,pointerout:`mouseout`},Tp=e=>e===null||e===``;function Ep(e,t){let n=e.style,r=e.getAttribute(`height`),i=e.getAttribute(`width`);if(e[Cp]={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`,Tp(i)){let t=Td(e,`width`);t!==void 0&&(e.width=t)}if(Tp(r)){if(e.style.height===``)e.height=e.width/(t||2);else{let t=Td(e,`height`);t!==void 0&&(e.height=t)}}return e}var Dp=wd?{passive:!0}:!1;function Op(e,t,n){e&&e.addEventListener(t,n,Dp)}function kp(e,t,n){e&&e.canvas&&e.canvas.removeEventListener(t,n,Dp)}function Ap(e,t){let n=wp[e.type]||e.type,{x:r,y:i}=yd(e,t);return{type:n,chart:t,native:e,x:r===void 0?null:r,y:i===void 0?null:i}}function jp(e,t){for(let n of e)if(n===t||n.contains(t))return!0}function Mp(e,t,n){let r=e.canvas,i=new MutationObserver(e=>{let t=!1;for(let n of e)t=t||jp(n.addedNodes,r),t=t&&!jp(n.removedNodes,r);t&&n()});return i.observe(document,{childList:!0,subtree:!0}),i}function Np(e,t,n){let r=e.canvas,i=new MutationObserver(e=>{let t=!1;for(let n of e)t=t||jp(n.removedNodes,r),t=t&&!jp(n.addedNodes,r);t&&n()});return i.observe(document,{childList:!0,subtree:!0}),i}var Pp=new Map,Fp=0;function Ip(){let e=window.devicePixelRatio;e!==Fp&&(Fp=e,Pp.forEach((t,n)=>{n.currentDevicePixelRatio!==e&&t()}))}function Lp(e,t){Pp.size||window.addEventListener(`resize`,Ip),Pp.set(e,t)}function Rp(e){Pp.delete(e),Pp.size||window.removeEventListener(`resize`,Ip)}function zp(e,t,n){let r=e.canvas,i=r&&dd(r);if(!i)return;let a=Al((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),Lp(e,a),o}function Bp(e,t,n){n&&n.disconnect(),t===`resize`&&Rp(e)}function Vp(e,t,n){let r=e.canvas,i=Al(t=>{e.ctx!==null&&n(Ap(t,e))},e);return Op(r,t,i),i}var Hp=class extends xp{acquireContext(e,t){let n=e&&e.getContext&&e.getContext(`2d`);return n&&n.canvas===e?(Ep(e,t),n):null}releaseContext(e){let t=e.canvas;if(!t[Cp])return!1;let n=t[Cp].initial;[`height`,`width`].forEach(e=>{let r=n[e];Y(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[Cp],!0}addEventListener(e,t,n){this.removeEventListener(e,t);let r=e.$proxies||(e.$proxies={});r[t]=({attach:Mp,detach:Np,resize:zp}[t]||Vp)(e,t,n)}removeEventListener(e,t){let n=e.$proxies||(e.$proxies={}),r=n[t];r&&(({attach:Bp,detach:Bp,resize:Bp}[t]||kp)(e,t,r),n[t]=void 0)}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(e,t,n,r){return Sd(e,t,n,r)}isAttached(e){let t=e&&dd(e);return!!(t&&t.isConnected)}};function Up(e){return!ud()||typeof OffscreenCanvas<`u`&&e instanceof OffscreenCanvas?Sp:Hp}var Wp=class{constructor(){z(this,`x`,void 0),z(this,`y`,void 0),z(this,`active`,!1),z(this,`options`,void 0),z(this,`$animations`,void 0)}tooltipPosition(e){let{x:t,y:n}=this.getProps([`x`,`y`],e);return{x:t,y:n}}hasValue(){return sl(this.x)&&sl(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}};z(Wp,`defaults`,{}),z(Wp,`defaultRoutes`,void 0);function Gp(e,t){let n=e.options.ticks,r=Kp(e),i=Math.min(n.maxTicksLimit||r,r),a=n.major.enabled?Jp(t):[],o=a.length,s=a[0],c=a[o-1],l=[];if(o>i)return Yp(t,l,a,o/i),l;let u=qp(a,t,i);if(o>0){let e,n,r=o>1?Math.round((c-s)/(o-1)):null;for(Xp(t,l,u,Y(r)?0:s-r,s),e=0,n=o-1;ei)return t}return Math.max(i,1)}function Jp(e){let t=[],n,r;for(n=0,r=e.length;ne===`left`?`right`:e===`right`?`left`:e,$p=(e,t,n)=>t===`top`||t===`left`?e[t]+n:e[t]-n,em=(e,t)=>Math.min(t||e,e);function tm(e,t){let n=[],r=e.length/t,i=e.length,a=0;for(;ao+s)))return c}function rm(e,t){Q(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:Oc(t,Oc(n,t)),max:Oc(n,Oc(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(){jc(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=Mu(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=vl(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-im(e.grid)-t.padding-am(e.title,this.chart.options.font),c=Math.sqrt(u*u+d*d),a=dl(Math.min(Math.asin(vl((l.highest.height+6)/o,-1,1)),Math.asin(vl(s/c,-1,1))-Math.asin(vl(d/c,-1,1)))),a=Math.max(r,Math.min(i,a))),this.labelRotation=a}afterCalculateLabelRotation(){jc(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){jc(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=am(r,t.options.font);if(o?(e.width=this.maxWidth,e.height=im(i)+a):(e.height=this.maxHeight,e.width=im(i)+a),n.display&&this.ticks.length){let{first:t,last:r,widest:i,highest:a}=this._getLabelSizes(),s=n.padding*2,c=ul(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(){jc(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 yl(this._alignToPixels?cu(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=im(i),d=[],f=o.setContext(this.getContext()),p=f.display?f.width:0,m=p/2,h=function(e){return cu(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(X(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(X(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=Z(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(-ul(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(`.`);iu.route(a,i,c,s)})}function mm(e){return`id`in e&&`defaults`in e}var hm=new class{constructor(){this.controllers=new dm(Sf,`datasets`,!0),this.elements=new dm(Wp,`elements`),this.plugins=new dm(Object,`plugins`),this.scales=new dm(um,`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):Q(t,t=>{let r=n||this._getRegistryForType(t);this._exec(e,r,t)})})}_exec(e,t,n){let r=Uc(e);jc(n[`before`+r],[],n),t[e](n),jc(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 _m(e){let t={},n=[],r=Object.keys(hm.plugins.items);for(let e=0;e1&&wm(e[0].toLowerCase());if(t)return t}throw Error(`Cannot determine type of '${e}' axis. Please provide 'axis' or 'position' option.`)}function Dm(e,t,n){if(n[t+`AxisID`]===e)return{axis:t}}function Om(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 Dm(e,`x`,n[0])||Dm(e,`y`,n[0])}return{}}function km(e,t){let n=eu[e.type]||{scales:{}},r=t.scales||{},i=xm(e.type,t),a=Object.create(null);return Object.keys(r).forEach(t=>{let o=r[t];if(!X(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=Em(t,o,Om(t,e),iu.scales[o.type]),c=Cm(s,i),l=n.scales||{};a[t]=Lc(Object.create(null),[{axis:s},o,l[s],l[c]])}),e.data.datasets.forEach(n=>{let i=n.type||e.type,o=n.indexAxis||xm(i,t),s=(eu[i]||{}).scales||{};Object.keys(s).forEach(e=>{let t=Sm(e,o),i=n[t+`AxisID`]||t;a[i]=a[i]||Object.create(null),Lc(a[i],[{axis:t},r[i],s[e]])})}),Object.keys(a).forEach(e=>{let t=a[e];Lc(t,[iu.scales[t.type],iu.scale])}),a}function Am(e){let t=e.options||(e.options={});t.plugins=Z(t.plugins,{}),t.scales=km(e,t)}function jm(e){return e=e||{},e.datasets=e.datasets||[],e.labels=e.labels||[],e}function Mm(e){return e=e||{},e.data=jm(e.data),Am(e),e}var Nm=new Map,Pm=new Set;function Fm(e,t){let n=Nm.get(e);return n||(n=t(),Nm.set(e,n),Pm.add(n)),n}var Im=(e,t,n)=>{let r=Hc(t,n);r!==void 0&&e.add(r)},Lm=class{constructor(e){this._config=Mm(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=jm(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(),Am(e)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(e){return Fm(e,()=>[[`datasets.${e}`,``]])}datasetAnimationScopeKeys(e,t){return Fm(`${e}.transition.${t}`,()=>[[`datasets.${e}.transitions.${t}`,`transitions.${t}`],[`datasets.${e}`,``]])}datasetElementScopeKeys(e,t){return Fm(`${e}-${t}`,()=>[[`datasets.${e}.elements.${t}`,`datasets.${e}`,`elements.${t}`,``]])}pluginScopeKeys(e){let t=e.id,n=this.type;return Fm(`${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=>Im(s,e,t))),t.forEach(e=>Im(s,r,e)),t.forEach(e=>Im(s,eu[i]||{},e)),t.forEach(e=>Im(s,iu,e)),t.forEach(e=>Im(s,tu,e))});let c=Array.from(s);return c.length===0&&c.push(Object.create(null)),Pm.has(t)&&a.set(t,c),c}chartOptionScopes(){let{options:e,type:t}=this;return[e,eu[t]||{},iu.datasets[t]||{},{type:t},iu,tu]}resolveNamedOptions(e,t,n,r=[``]){let i={$shared:!0},{resolver:a,subPrefixes:o}=Rm(this._resolverCache,e,r),s=a;if(Bm(a,t)){i.$shared=!1,n=Gc(n)?n():n;let t=this.createResolver(e,n,o);s=Fu(a,n,t)}for(let e of t)i[e]=s[e];return i}createResolver(e,t,n=[``],r){let{resolver:i}=Rm(this._resolverCache,e,n);return X(t)?Fu(i,t,void 0,r):i}};function Rm(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:Pu(t,n),subPrefixes:n.filter(e=>!e.toLowerCase().includes(`hover`))},r.set(i,a)),a}var zm=e=>X(e)&&Object.getOwnPropertyNames(e).some(t=>Gc(e[t]));function Bm(e,t){let{isScriptable:n,isIndexable:r}=Iu(e);for(let i of t){let t=n(i),a=r(i),o=(a||t)&&e[i];if(t&&(Gc(o)||zm(o))||a&&Ec(o))return!0}return!1}var Vm=`4.6.1`,Hm=[`top`,`bottom`,`left`,`right`,`chartArea`];function Um(e,t){return e===`top`||e===`bottom`||Hm.indexOf(e)===-1&&t===`x`}function Wm(e,t){return function(n,r){return n[e]===r[e]?n[t]-r[t]:n[e]-r[e]}}function Gm(e){let t=e.chart,n=t.options.animation;t.notifyPlugins(`afterRender`),jc(n&&n.onComplete,[e],t)}function Km(e){let t=e.chart,n=t.options.animation;jc(n&&n.onProgress,[e],t)}function qm(e){return ud()&&typeof e==`string`?e=document.getElementById(e):e&&e.length&&(e=e[0]),e&&e.canvas&&(e=e.canvas),e}var Jm={},Ym=e=>{let t=qm(e);return Object.values(Jm).filter(e=>e.canvas===t).pop()};function Xm(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 Zm(e,t,n,r){return!n||e.type===`mouseout`?null:r?t:e}var Qm=class{static register(...e){hm.add(...e),$m()}static unregister(...e){hm.remove(...e),$m()}constructor(e,t){let n=this.config=new Lm(t),r=qm(e),i=Ym(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||(Up(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=Tc(),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 gm,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=jl(e=>this.update(e),a.resizeDelay||0),this._dataChanges=[],Jm[this.id]=this,!o||!s?console.error(`Failed to create chart: can't acquire context from the given item`):(Yd.listen(this,`complete`,Gm),Yd.listen(this,`progress`,Km),this._initialize(),this.attached&&this.update())}get aspectRatio(){let{options:{aspectRatio:e,maintainAspectRatio:t},width:n,height:r,_aspectRatio:i}=this;return Y(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 hm}_initialize(){return this.notifyPlugins(`beforeInit`),this.options.responsive?this.resize():Cd(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins(`afterInit`),this}clear(){return lu(this.canvas,this.ctx),this}stop(){return Yd.stop(this),this}resize(e,t){Yd.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,Cd(this,o,!0)&&(this.notifyPlugins(`resize`,{size:a}),jc(n.onResize,[this,a],this),this.attached&&this._doResize(s)&&this.render())}ensureScalesHaveIDs(){Q(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=Em(e,n),i=r===`r`,a=r===`x`;return{options:n,dposition:i?`chartArea`:a?`bottom`:`left`,dtype:i?`radialLinear`:a?`category`:`linear`}}))),Q(i,t=>{let i=t.options,a=i.id,o=Em(a,i),s=Z(i.type,t.dtype);(i.position===void 0||Um(i.position,o)!==Um(t.dposition))&&(i.position=t.dposition),r[a]=!0;let c=null;a in n&&n[a].type===s?c=n[a]:(c=new(hm.getScale(s))({id:a,type:s,ctx:this.ctx,chart:this}),n[c.id]=c),c.init(i,e)}),Q(r,(e,t)=>{e||delete n[t]}),Q(n,e=>{bp.configure(this,e,e.options),bp.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(Wm(`z`,`_idx`));let{_active:o,_lastEvent:s}=this;s?this._eventHandler(s,!0):o.length&&this._updateHoverStyles(o,o,!0),this.render()}_updateScales(){Q(this.scales,e=>{bp.removeBox(this,e)}),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){let e=this.options;Kc(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)Xm(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;bp.update(this,this.width,this.height,e);let t=this.chartArea,n=t.width<=0||t.height<=0;this._layers=[],Q(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=Jd(this,e);this.notifyPlugins(`beforeDatasetDraw`,n)!==!1&&(r&&pu(t,r),e.controller.draw(),r&&mu(t),n.cancelable=!1,this.notifyPlugins(`afterDatasetDraw`,n))}isPointInArea(e){return fu(e,this.chartArea,this._minPadding)}getElementsAtEventForMode(e,t,n,r){let i=rp.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=Nu(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);Wc(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(),Yd.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)};Q(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(){Q(this._listeners,(e,t)=>{this.platform.removeEventListener(this,t,e)}),this._listeners={},Q(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}});Mc(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=qc(e),c=Zm(e,this._lastEvent,n,s);n&&(this._lastEvent=null,jc(i.onHover,[e,o,this],this),s&&jc(i.onClick,[e,o,this],this));let l=!Mc(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)}};z(Qm,`defaults`,iu),z(Qm,`instances`,Jm),z(Qm,`overrides`,eu),z(Qm,`registry`,hm),z(Qm,`version`,Vm),z(Qm,`getChart`,Ym);function $m(){return Q(Qm.instances,e=>e._plugins.invalidate())}function eh(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,gl(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,gl(r-n));e.arc(i,a,s+l/2,n-t/2,r+t/2,!0)}else{let t=Math.min(l/2,o*gl(r-n));if(u===`round`)e.arc(i,a,t,n-$/2,r+$/2,!0);else if(u===`bevel`){let o=2*t*t,s=-o*Math.cos(n+$/2)+i,c=-o*Math.sin(n+$/2)+a,l=o*Math.cos(r+$/2)+i,u=o*Math.sin(r+$/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 th(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+Qc,r-Qc),e.closePath(),e.clip()}function nh(e){return Eu(e,[`outerStart`,`outerEnd`,`innerStart`,`innerEnd`])}function rh(e,t,n,r){let i=nh(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 vl(e,0,Math.min(a,t))};return{outerStart:s(i.outerStart),outerEnd:s(i.outerEnd),innerStart:vl(i.innerStart,0,o),innerEnd:vl(i.innerEnd,0,o)}}function ih(e,t,n,r){return{x:n+e*Math.cos(t),y:r+e*Math.sin(t)}}function ah(e,t,n,r){let{x:i,y:a,startAngle:o,pixelMargin:s,innerRadius:c}=t,l=Math.max(t.outerRadius+r+n-s,0),u=c>0?c+r+n+s:0;e.beginPath(),e.arc(i,a,l,o,o+Jc),u>0&&(e.moveTo(i+Math.cos(o)*u,a+Math.sin(o)*u),e.arc(i,a,u,o+Jc,o,!0)),e.closePath()}function oh(e,t,n,r,i,a){let{x:o,y:s,startAngle:c,pixelMargin:l,innerRadius:u}=t,{spacingMode:d=`angular`}=t.options,f=i-c;if(a&&t.options.selfJoin&&Math.abs(f)>=Jc-1e-4){ah(e,t,n,r);return}let p=Math.max(t.outerRadius+r+n-l,0),m=u>0?u+r+n+l:0,h=0,g=0,_=p>0?Math.max(.001,f*p-n/$)/p:.001,v=(f-_)/2;if(r){let e=((u>0?u-r:0)+(p>0?p-r:0))/2,t=(()=>{let t=e===0?f:f*e/(e+r);return(f-t)/2})(),n=e>0?Math.asin(Math.min(1,r/e)):0,i=Math.max(0,_/2-.001),a=Math.sin(i);if(d===`parallel`){if(m===0&&a>0){let e=r/a,t=Math.max(0,p-.001);m=Math.min(e,t)}let e=Math.min(p>0?p*a:1/0,m>0?m*a:1/0),t=Math.min(r,e);h=p>0?Math.asin(Math.min(1,t/p)):Math.min(i,n),g=m>0?Math.asin(Math.min(1,t/m)):h}else d===`proportional`?(h=Math.min(i,t),g=Math.min(i,t)):(h=Math.min(i,n),g=Math.min(i,n))}let y=c+v+h,b=i-v-h,x=c+v+g,S=i-v-g,C=Math.min(b-y,S-x),{outerStart:w,outerEnd:T,innerStart:E,innerEnd:D}=rh(t,m,p,C),O=p-w,k=p-T,A=y+w/O,j=b-T/k,M=m+E,N=m+D,P=x+E/M,ee=S-D/N;if(e.beginPath(),a){let t=(A+j)/2;if(e.arc(o,s,p,A,t),e.arc(o,s,p,t,j),T>0){let t=ih(k,j,o,s);e.arc(t.x,t.y,T,j,b+Qc)}let n=ih(N,S,o,s);if(e.lineTo(n.x,n.y),D>0){let t=ih(N,ee,o,s);e.arc(t.x,t.y,D,S+Qc,ee+Math.PI)}let r=(S-D/m+(x+E/m))/2;if(e.arc(o,s,m,S-D/m,r,!0),e.arc(o,s,m,r,x+E/m,!0),E>0){let t=ih(M,P,o,s);e.arc(t.x,t.y,E,P+Math.PI,x-Qc)}let i=ih(O,y,o,s);if(e.lineTo(i.x,i.y),w>0){let t=ih(O,A,o,s);e.arc(t.x,t.y,w,y-Qc,A)}}else{e.moveTo(o,s);let t=Math.cos(A)*p+o,n=Math.sin(A)*p+s;e.lineTo(t,n);let r=Math.cos(j)*p+o,i=Math.sin(j)*p+s;e.lineTo(r,i)}e.closePath()}function sh(e,t,n,r,i){let{fullCircles:a,startAngle:o,circumference:s}=t,c=t.endAngle;if(a){oh(e,t,n,r,c,i);for(let t=0;t=Jc-1e-4;if(a){oh(e,t,n,r,h,i);for(let t=0;t0)&&c.selfJoin&&h-o>=$&&p===0&&u!==`miter`&&eh(e,t,h),a||(oh(e,t,n,r,h,i),e.stroke())}var lh=class extends Wp{constructor(e){super(),z(this,`circumference`,void 0),z(this,`endAngle`,void 0),z(this,`fullCircles`,void 0),z(this,`innerRadius`,void 0),z(this,`outerRadius`,void 0),z(this,`pixelMargin`,void 0),z(this,`startAngle`,void 0),z(this,`circular`,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,this.circular=!1,e&&Object.assign(this,e)}inRange(e,t,n){let{angle:r,distance:i}=pl(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=Z(l,o-a),f=_l(r,a,o)&&a!==o,p=d>=Jc||f,m=bl(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>Jc?Math.floor(n/Jc):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($,n||0)));e.fillStyle=t.backgroundColor,e.strokeStyle=t.borderColor,sh(e,this,s,i,a),ch(e,this,s,i,a),e.restore()}};z(lh,`id`,`arc`),z(lh,`defaults`,{borderAlign:`center`,borderColor:`#fff`,borderDash:[],borderDashOffset:0,borderJoinStyle:void 0,borderRadius:0,borderWidth:2,offset:0,spacing:0,spacingMode:`angular`,angle:void 0,circular:!0,selfJoin:!1}),z(lh,`defaultRoutes`,{backgroundColor:`backgroundColor`}),z(lh,`descriptors`,{_scriptable:!0,_indexable:e=>e!==`borderDash`});function uh(e,t,n=t){e.lineCap=Z(n.borderCapStyle,t.borderCapStyle),e.setLineDash(Z(n.borderDash,t.borderDash)),e.lineDashOffset=Z(n.borderDashOffset,t.borderDashOffset),e.lineJoin=Z(n.borderJoinStyle,t.borderJoinStyle),e.lineWidth=Z(n.borderWidth,t.borderWidth),e.strokeStyle=Z(n.borderColor,t.borderColor)}function dh(e,t,n){e.lineTo(n.x,n.y)}function fh(e){return e.stepped?hu:e.tension||e.cubicInterpolationMode===`monotone`?gu:dh}function ph(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 gh(e){let t=e.options,n=t.borderDash&&t.borderDash.length;return!e._decimated&&!e._loop&&!t.tension&&t.cubicInterpolationMode!==`monotone`&&!t.stepped&&!n?hh:mh}function _h(e){return e.stepped?Dd:e.tension||e.cubicInterpolationMode===`monotone`?Od:Ed}function vh(e,t,n,r){let i=t._path;i||(i=t._path=new Path2D,t.path(i,n,r)&&i.closePath()),uh(e,t.options),e.stroke(i)}function yh(e,t,n,r){let{segments:i,options:a}=t,o=gh(t);for(let s of i)uh(e,a,s.style),e.beginPath(),o(e,t,s,{start:n,end:n+r-1})&&e.closePath(),e.stroke()}var bh=typeof Path2D==`function`;function xh(e,t,n,r){bh&&!t.options.segment?vh(e,t,n,r):yh(e,t,n,r)}var Sh=class extends Wp{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;ld(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=Vd(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=Rd(this,{property:t,start:r,end:r});if(!a.length)return;let o=[],s=_h(n),c,l;for(c=0,l=a.length;ce!==`borderDash`&&e!==`fill`});function Ch(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;a2&&(o.children=s>3?h.call(arguments,2):n),y(e,o,r,i,null)}function y(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:u,__v:o||++t,__i:-1,__u:0};return!o&&e.vnode&&e.vnode(s),s}function b(e){return e.children}function x(e,t){this.props=e,this.context=t,this.__g=0}function S(e,t){if(t==null)return e.__&&S(e.__,e.__i+1);for(var n;to&&n.sort(a),t=n.shift(),o=n.length,8&t.__g&&(c=void 0,l=void 0,d=(l=(s=t).__v).__e,f=[],p=[],(m=s.__P)&&((c=g({constructor:u},l)).__v=l.__v+1,e.vnode&&e.vnode(c),P(m,c,l,s.__n,m.namespaceURI,32&l.__u?[d]:null,f,d||S(l),32&l.__u,p),c.__v=l.__v,c.__.__k[c.__i]=c,ee(f,c,p),l.__=l.__e=null,c.__e!=d&&C(c)))}finally{(i=n.length)&&(r||queueMicrotask)(T)}}function E(e,t,n,r,i,a,o,s,c,l,u){var p,m,h,g,_,v,y=r.__k||f,b=t.length;for(c=D(n,t,y,c,b),p=0;pc?v--:v++,x=1)):(~l||(i>g?v--:i>1]+!!c){for(i=n-1;i>=0;i--)if((s=t[i])&&!(2&s.__u)&&a==s.key&&o==s.type)return i;for(i=n+1;i=0?e.slice(0,t+13):pe}function he(){let e=me();return e.endsWith(`/luci`)?e.slice(0,-5):fe}function ge(e){let t=e.startsWith(`/`)?e:`/${e}`;return`${he()}${t}`}function _e(e){let t=e.startsWith(`/`)?e:`/${e}`;return`${me()}${t}`}function ve(e){return _e(e)}function ye(e=``){return`/${e?e.startsWith(`#`)?e:`#${e}`:``}`}var be=/^[0-9a-f]{32}$/i;function xe(){return _e(`/smartsafehub/session`)}function Se(e){return e.headers.get(`X-LuCI-Login-Required`)===`yes`}async function Ce(e){if(e.status===401||e.status===403||Se(e))return null;if(!e.ok)throw Error(`LuCI session endpoint returned HTTP ${e.status}`);let t=(await e.text()).trim();if(be.test(t))return t;if(e.redirected||/^(?:access|permission)\s+denied\.?$/i.test(t)||/^)/i.test(t))return null;throw Error(`LuCI session endpoint returned an invalid session id`)}async function we(e={}){return Ce(await fetch(xe(),{cache:`no-store`,credentials:`same-origin`,redirect:`follow`,...e}))}function Te(){return we()}async function Ee(){await fetch(_e(`/admin/logout`),{cache:`no-store`,credentials:`same-origin`,redirect:`manual`})}function De(e,t){let n=new URLSearchParams;return n.set(`luci_username`,e),n.set(`luci_password`,t),we({method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded;charset=UTF-8`},body:n})}var Oe=`smartsafehub:session-expired`,ke=`로그인 세션이 만료되었습니다. 계속하려면 다시 로그인해 주세요.`,Ae=null;function je(){Ae=null}function Me(e){Ae!==e&&(Ae=e,window.dispatchEvent(new CustomEvent(Oe)))}var Ne,Pe,Fe,Ie,Le=Object.is,Re=0,ze=[],Be=[],Ve=e,He=Ve.__b,Ue=Ve.__r,We=Ve.diffed,Ge=Ve.__c,Ke=Ve.unmount,qe=Ve.__;function Je(e,t){Ve.__h&&Ve.__h(Pe,e,Re||t),Re=0;var n=Pe.__H||(Pe.__H={__:[],__h:[]});return e>=n.__.length&&n.__.push({}),n.__[e]}function L(e){return Re=1,Ye(at,e)}function Ye(e,t,n){var r=Je(Ne++,2);if(r.t=e,!r.__c&&(r.__=[n?n(t):at(void 0,t),function(e){var t=r.__N?r.__N[0]:r.__[0],n=r.t(t,e);Le(t,n)||(r.__N=[n,r.__[1]],r.__c.setState({}))}],r.__c=Pe,!Pe.__f)){Pe.__f=!0;var i=Pe.shouldComponentUpdate;Pe.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,Le(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 R(e,t){var n=Je(Ne++,3);!Ve.__s&&it(n.__H,t)&&(n.__P=!0,n.__=e,n.u=t,Pe.__H.__h.push(n))}function z(e){return Re=5,Xe(function(){return{current:e}},[])}function Xe(e,t){var n=Je(Ne++,7);return it(n.__H,t)&&(n.__=e(),n.__H=t),n.__}function B(e,t){return Re=8,Xe(function(){return e},t)}function Ze(){var e=Je(Ne++,11);if(!e.__){for(var t=Pe.__v;!t.__m&&t.__;)t=t.__;var n=t.__m||(t.__m=[0,0]);e.__=`P`+n[0]+`-`+n[1]++}return e.__}function Qe(){var e;do{for(;e=Be.shift();)try{nt(e)}catch(t){Ve.__e(t,{__:(e=e.__P)&&e.__v})}for(;e=ze.shift();){var t=e.__H;if(e.__P&&t)try{t.__h.some(nt),t.__h.some(rt),t.__h=[]}catch(n){t.__h=[],Ve.__e(n,e.__v)}}}while(Be.length)}Ve.__b=function(e){Pe=null,He&&He(e)},Ve.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),qe&&qe(e,t)},Ve.__r=function(e){Ue&&Ue(e),Ne=0;var t=(Pe=e.__c).__H;t&&(Fe==Pe?Pe.__h=[]:(t.__h.some(nt),t.__h.some(rt),Ne=0),t.__h=[],t.__.some(function(e){e.__N&&(e.__=e.__N),e.u=e.__N=void 0})),Fe=Pe},Ve.diffed=function(e){We&&We(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&tt(ze.push(t)),t.__H.__.some(function(e){e.u&&(e.__H=e.u)})),Fe=Pe=null},Ve.__c=function(e,t){t.some(function(e){try{e.__h.some(nt),e.__h=e.__h.filter(function(e){return!e.__||rt(e)})}catch(t){e.__h=[function(){throw t}]}}),Ge&&Ge(e,t)},Ve.unmount=function(e){Ke&&Ke(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,tt(Be.push(r))}else nt(r)}catch(e){t=e}}),r.__H=void 0,t&&Ve.__e(t,r.__v))};var $e=typeof requestAnimationFrame==`function`;function et(e){var t,n=function(){clearTimeout(r),$e&&cancelAnimationFrame(t),setTimeout(e)},r=setTimeout(n,35);$e&&(t=requestAnimationFrame(n))}function tt(e){e!=1&&Ie==Ve.requestAnimationFrame||((Ie=Ve.requestAnimationFrame)||et)(Qe)}function nt(e){var t=Pe,n=e.__c;typeof n==`function`&&(e.__c=void 0,n()),Pe=t}function rt(e){var t=Pe;e.__c=e.__(),Pe=t}function it(e,t){return!e||e.length!=t.length||t.some(function(t,n){return!Le(t,e[n])})}function at(e,t){return typeof t==`function`?t(e):t}var ot=1500,st=60,ct=64,lt=new Map,ut=[];function dt(e){return e===null?null:Math.max(0,Math.round(e))}function ft(e){let t=`${e.object}.${e.method}`,n=dt(e.elapsedMs)??0,r=n>=ot,i=e.result!==`ok`,a=lt.get(t);return!a&<.sizest&&ut.shift()),r}function pt(){return{slowThresholdMs:ot,summary:[...lt.values()].map(({totalMs:e,...t})=>({...t})),recent:ut.map(e=>({...e}))}}function mt(){lt.clear(),ut.length=0}var ht={snapshot:pt,reset:mt};function gt(){var e;(e=window).__SMARTHUB_RPC_DIAGNOSTICS__??(e.__SMARTHUB_RPC_DIAGNOSTICS__=ht)}var _t=4,vt=3,yt=0,bt=0,xt=[];function St(){for(;yt<_t;){let e=xt.findIndex(e=>e.priority!==`low`||bt{--yt,t.priority===`low`&&--bt,St()}))}}function Ct(e,t){return t?.aborted?Promise.reject(new DOMException(`Cancelled`,`AbortError`)):new Promise((n,r)=>{let i={priority:e,start:e=>{t?.removeEventListener(`abort`,i.abort),n(e)},abort:()=>{let e=xt.indexOf(i);e!==-1&&xt.splice(e,1),t?.removeEventListener(`abort`,i.abort),r(new DOMException(`Cancelled`,`AbortError`))}};t?.addEventListener(`abort`,i.abort,{once:!0});let a=xt.findIndex(t=>e===`high`&&t.priority!==`high`||e===`normal`&&t.priority===`low`);a===-1?xt.push(i):xt.splice(a,0,i),St()})}function wt(e){"@babel/helpers - typeof";return wt=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},wt(e)}function Tt(e,t){if(wt(e)!=`object`||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||`default`);if(wt(r)!=`object`)return r;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(t===`string`?String:Number)(e)}function Et(e){var t=Tt(e,`string`);return wt(t)==`symbol`?t:t+``}function V(e,t,n){return(t=Et(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var Dt={1:`잘못된 명령입니다.`,2:`요청 인자가 올바르지 않습니다.`,3:`요청한 메서드를 찾을 수 없습니다.`,4:`요청한 리소스를 찾을 수 없습니다.`,5:`응답 데이터가 없습니다.`,6:`접근 권한이 없습니다.`,7:`요청 시간이 초과되었습니다.`,8:`지원하지 않는 기능입니다.`,9:`장치에서 알 수 없는 오류가 발생했습니다.`,10:`장치 연결이 끊어졌습니다.`},Ot=2e4,kt=3e3,At=1e3,jt=6e4,Mt=1,H=class extends Error{constructor(e,t){super(t),V(this,`code`,void 0),this.name=`RpcError`,this.code=e}};function Nt(){let e=window.__SMARTHUB_BOOTSTRAP__;if(!e?.sessionId||!e.rpcUrl)throw new H(`BOOTSTRAP_MISSING`,`LuCI 세션 정보를 찾을 수 없습니다. 페이지를 새로고침해 주세요.`);return e}function Pt(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Ft(){return new H(`INVALID_RESPONSE`,`장치 API가 올바르지 않은 응답을 반환했습니다.`)}function It(e){return typeof e!=`number`||!Number.isFinite(e)?Ot:Math.min(jt,Math.max(At,Math.trunc(e)))}function Lt(e){let t=e.message.trim().toLowerCase();return e.code===`UBUS_6`||e.code===`JSON_RPC_-32002`||e.code===`HTTP_ERROR`&&/HTTP (401|403)\b/.test(e.message)||t.includes(`access denied`)||t.includes(`permission denied`)||t.includes(`접근 권한`)}function Rt(e){return window.__SMARTHUB_BOOTSTRAP__?.sessionId===e}async function zt(e){let t=Mt++,n=new AbortController,r=window.setTimeout(()=>n.abort(),kt);try{let r=await fetch(e.rpcUrl,{method:`POST`,credentials:`same-origin`,headers:{"Content-Type":`application/json`},body:JSON.stringify({jsonrpc:`2.0`,id:t,method:`call`,params:[e.sessionId,`smartsafehub`,`system_root_password_status`,{}]}),signal:n.signal});if(r.status===401||r.status===403)return`expired`;if(!r.ok)return`unknown`;let i=await r.json();if(!Pt(i)||i.jsonrpc!==`2.0`||i.id!==t)return`unknown`;if(`error`in i){let e=i.error;return Pt(e)&&typeof e.code==`number`&&e.code===-32002?`expired`:`unknown`}let a=i.result;return!Array.isArray(a)||a.length<1?`unknown`:a[0]===0?`active`:a[0]===6?`expired`:`unknown`}catch{return`unknown`}finally{window.clearTimeout(r)}}async function Bt(e,t){if(!Lt(e)||!Rt(t.sessionId))return e;let n=await zt(t);return Rt(t.sessionId)?n===`active`?new H(`RPC_PERMISSION_DENIED`,`현재 로그인 세션에 이 기능 권한이 없습니다. SmartSafeHub 업데이트 직후라면 로그아웃 후 다시 로그인해 주세요.`):n===`expired`?(Me(t.sessionId),new H(`SESSION_EXPIRED`,ke)):e:e}function Vt(e,t){if(!Pt(e)||e.jsonrpc!==`2.0`||e.id!==t)throw Ft();if(`error`in e){let t=e.error;throw!Pt(t)||typeof t.code!=`number`?Ft():new H(`JSON_RPC_${t.code}`,typeof t.message==`string`&&t.message.length>0?t.message:`JSON-RPC 요청에 실패했습니다.`)}let n=e.result;if(!Array.isArray(n)||n.length<1||n.length>2)throw Ft();let r=n[0];if(typeof r!=`number`||!Number.isInteger(r))throw Ft();if(r!==0)throw new H(`UBUS_${r}`,Dt[r]??`ubus 오류 코드 ${r}가 반환되었습니다.`);if(n.length<2||n[1]===void 0)throw new H(`EMPTY_RESPONSE`,`장치 API 응답이 비어 있습니다.`);return n[1]}async function Ht(e,t,n={},r={}){let i=Nt();gt();let a=performance.now(),o=null,s=null,c=`ok`,l=Mt++,u=null,d=null,f=new AbortController,p=!1,m=()=>{p=!0,f.abort()};r.signal?.addEventListener(`abort`,m,{once:!0});try{if(u=await Ct(r.priority??`normal`,r.signal),p||r.signal?.aborted)throw new H(`RPC_CANCELLED`,`요청이 취소되었습니다.`);d=window.setTimeout(()=>f.abort(),It(r.timeoutMs));let c=await fetch(i.rpcUrl,{method:`POST`,credentials:`same-origin`,headers:{"Content-Type":`application/json`},body:JSON.stringify({jsonrpc:`2.0`,id:l,method:`call`,params:[i.sessionId,e,t,n]}),signal:f.signal});if(o=performance.now()-a,!c.ok)throw new H(`HTTP_ERROR`,`장치 API가 HTTP ${c.status} 오류를 반환했습니다.`);let m;try{m=await c.json(),s=performance.now()-a-(o??0)}catch{throw Ft()}return Vt(m,l)}catch(e){if(p||r.signal?.aborted)throw c=`RPC_CANCELLED`,new H(`RPC_CANCELLED`,`요청이 취소되었습니다.`);if(f.signal.aborted)throw c=`RPC_TIMEOUT`,new H(`RPC_TIMEOUT`,`장치 응답 시간이 초과되었습니다. 잠시 후 다시 시도해 주세요.`);if(e instanceof DOMException&&e.name===`AbortError`)throw c=`RPC_CANCELLED`,new H(`RPC_CANCELLED`,`요청이 취소되었습니다.`);if(e instanceof H){let t=await Bt(e,i);throw c=t.code,t}throw c=`NETWORK_ERROR`,new H(`NETWORK_ERROR`,`공유기와 통신할 수 없습니다. 네트워크 연결을 확인해 주세요.`)}finally{d!==null&&window.clearTimeout(d),r.signal?.removeEventListener(`abort`,m),u?.();let n=Math.round(performance.now()-a);(ft({object:e,method:t,elapsedMs:n,responseMs:o,decodeMs:s,result:c})||c===`RPC_TIMEOUT`)&&console.warn(`[SmartSafeHub RPC] ${e}.${t}: ${n}ms (${c})`)}}async function U(e,t,n={},r={}){let i=await Ht(e,t,n,r);if(!i.ok)throw new H(i.error.code,i.error.message);return i.data}function Ut(){return U(`smartsafehub`,`system_root_password_status`)}function Wt(e){return U(`smartsafehub`,`system_root_password_set`,{password:e})}var Gt=Symbol.for(`pv`),Kt=0;Array.isArray;function W(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:Gt,__v:--Kt,__i:-1,__u:0};return(a||o)&&(u.__source=a,u.__self=o),e.vnode&&e.vnode(u),u}function G({children:e,title:t,...n}){return W(`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?W(`title`,{children:t}):null,e]})}function qt(e){return W(G,{...e,children:[W(`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`}),W(`path`,{d:`m9 12 2 2 4-4`})]})}function Jt(e){return W(G,{...e,children:[W(`rect`,{height:`7`,rx:`2`,width:`18`,x:`3`,y:`13`}),W(`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 Yt(e){return W(G,{...e,children:[W(`rect`,{height:`11`,rx:`2`,width:`16`,x:`4`,y:`3`}),W(`path`,{d:`M8 21h8M12 14v7`})]})}function Xt(e){return W(G,{...e,children:[W(`path`,{d:`m3 11 9-8 9 8`}),W(`path`,{d:`M5 10v10h14V10M9 20v-6h6v6`})]})}function Zt(e){return W(G,{...e,children:[W(`circle`,{cx:`12`,cy:`8`,r:`4`}),W(`path`,{d:`M4.5 21a7.5 7.5 0 0 1 15 0`})]})}function Qt(e){return W(G,{...e,children:[W(`rect`,{x:`3`,y:`3`,width:`7`,height:`7`,rx:`1.5`}),W(`rect`,{x:`14`,y:`3`,width:`7`,height:`7`,rx:`1.5`}),W(`rect`,{x:`3`,y:`14`,width:`7`,height:`7`,rx:`1.5`}),W(`path`,{d:`M17.5 14v7M14 17.5h7`})]})}function $t(e){return W(G,{...e,children:[W(`rect`,{x:`2.5`,y:`3.5`,width:`7`,height:`6`,rx:`1.5`}),W(`rect`,{x:`14.5`,y:`3.5`,width:`7`,height:`6`,rx:`1.5`}),W(`rect`,{x:`8.5`,y:`15`,width:`7`,height:`6`,rx:`1.5`}),W(`path`,{d:`M6 9.5v2.5h12V9.5M12 12v3`})]})}function en(e){return W(G,{...e,children:[W(`path`,{d:`M7.5 7h9a5 5 0 0 1 4.9 4l.8 5a2.5 2.5 0 0 1-4 2.3L15.5 16h-7l-2.7 2.3a2.5 2.5 0 0 1-4-2.3l.8-5A5 5 0 0 1 7.5 7Z`}),W(`path`,{d:`M6.5 11v4M4.5 13h4M16 11.5h.01M18.5 13.5h.01`})]})}function tn(e){return W(G,{...e,children:W(`path`,{d:`M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.287l-7.91 7.91a2.121 2.121 0 0 1-3-3l7.91-7.91a6 6 0 0 1 7.287-8.259c.438.12.54.662.218.984z`})})}function nn(e){return W(G,{...e,children:[W(`circle`,{cx:`12`,cy:`12`,r:`10`}),W(`path`,{d:`M8 12h.01M12 12h.01M16 12h.01`})]})}function rn(e){return W(G,{...e,children:[W(`path`,{d:`M12 16V4`}),W(`path`,{d:`m7 9 5-5 5 5`}),W(`path`,{d:`M5 14v5h14v-5`})]})}function an(e){return W(G,{...e,children:[W(`circle`,{cx:`12`,cy:`12`,r:`3`}),W(`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 on(e){return W(G,{...e,children:[W(`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`}),W(`path`,{d:`M12 20h.01`})]})}function sn(e){return W(G,{...e,children:[W(`path`,{d:`M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4z`}),W(`path`,{d:`M6.5 6.5h1M16.5 6.5h1M6.5 16.5h1`}),W(`path`,{d:`M14 14h2v2h-2zM18 14h2v2h-2zM16 16h2v2h-2zM18 18h2v2h-2zM14 18h1`})]})}function cn(e){return W(G,{...e,children:[W(`path`,{d:`M7 3v4M17 3v4M5 7h14v4a7 7 0 0 1-14 0V7Z`}),W(`path`,{d:`M12 18v3`})]})}function ln(e){return W(G,{...e,children:[W(`circle`,{cx:`12`,cy:`12`,r:`9`}),W(`path`,{d:`M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18`})]})}function un(e){return W(G,{...e,children:[W(`rect`,{height:`13`,rx:`2`,width:`18`,x:`3`,y:`6`}),W(`path`,{d:`m8 2 4 4 4-4M8 22h8`})]})}function dn(e){return W(G,{...e,children:[W(`circle`,{cx:`12`,cy:`12`,r:`9`}),W(`path`,{d:`M12 7v5l3 2`})]})}function fn(e){return W(G,{...e,children:[W(`rect`,{height:`14`,rx:`2`,width:`14`,x:`5`,y:`5`}),W(`path`,{d:`M9 9h6v6H9zM9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M2 15h3M19 9h3M19 15h3`})]})}function pn(e){return W(G,{...e,children:[W(`path`,{d:`M20 11a8 8 0 1 0-2.3 5.7`}),W(`path`,{d:`M20 4v7h-7`})]})}function mn(e){return W(G,{...e,children:[W(`path`,{d:`M12 2v10`}),W(`path`,{d:`M18.4 6.6a8 8 0 1 1-12.8 0`})]})}function hn(e){return W(G,{...e,children:[W(`path`,{d:`M12 3v12`}),W(`path`,{d:`m7 10 5 5 5-5`}),W(`path`,{d:`M5 21h14`})]})}function gn(e){return W(G,{...e,children:[W(`ellipse`,{cx:`12`,cy:`5`,rx:`8`,ry:`3`}),W(`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 _n(e){return W(G,{...e,children:[W(`circle`,{cx:`8`,cy:`15`,r:`4`}),W(`path`,{d:`m11 12 8-8M15 8l2 2M17 6l2 2`})]})}function vn(e){return W(G,{...e,children:[W(`rect`,{height:`13`,rx:`2`,width:`13`,x:`8`,y:`8`}),W(`path`,{d:`M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3`})]})}function yn(e){return W(G,{...e,children:[W(`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`}),W(`circle`,{cx:`12`,cy:`12`,r:`2.5`})]})}function bn(e){return W(G,{...e,children:[W(`path`,{d:`m3 3 18 18`}),W(`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`}),W(`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`}),W(`path`,{d:`M10.3 10.3a2.5 2.5 0 0 0 3.4 3.4`})]})}function xn(e){return W(G,{...e,children:[W(`rect`,{height:`17`,rx:`2`,width:`18`,x:`3`,y:`4`}),W(`path`,{d:`M8 2v4M16 2v4M3 9h18`})]})}function Sn(e){return W(G,{...e,children:[W(`circle`,{cx:`12`,cy:`12`,r:`9`}),W(`path`,{d:`m8 12 2.5 2.5L16 9`})]})}function Cn(e){return W(G,{...e,children:[W(`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`}),W(`path`,{d:`M12 9v4M12 17h.01`})]})}function wn(e){return W(G,{...e,children:W(`path`,{d:`M12 5v14M5 12h14`})})}function Tn(e){return W(G,{...e,children:W(`path`,{d:`M4 7h16M9 7V4h6v3M7 7l1 13h8l1-13M10 11v5M14 11v5`})})}function En(e){return W(G,{...e,children:[W(`circle`,{cx:`11`,cy:`11`,r:`7`}),W(`path`,{d:`m20 20-4-4`})]})}function Dn(e){return W(G,{...e,children:[W(`path`,{d:`M8 6h12M8 12h12M8 18h12`}),W(`path`,{d:`M4 6h.01M4 12h.01M4 18h.01`})]})}function On(e){return W(G,{...e,children:[W(`path`,{d:`M10 5H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h4`}),W(`path`,{d:`M14 8l4 4-4 4M8 12h10`})]})}function kn(e){return W(G,{...e,children:W(`path`,{d:`M4 7h16M4 12h16M4 17h16`})})}function An(e){return W(G,{...e,children:W(`path`,{d:`m6 9 6 6 6-6`})})}function jn(e){return W(G,{...e,children:W(`path`,{d:`m6 6 12 12M18 6 6 18`})})}function Mn(e){return W(G,{...e,children:W(`path`,{d:`m15 18-6-6 6-6`})})}function Nn(e){return W(G,{...e,children:W(`path`,{d:`m9 18 6-6-6-6`})})}function Pn(e){return W(G,{...e,children:W(`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 Fn(e){return W(G,{...e,children:[W(`circle`,{cx:`12`,cy:`12`,r:`4`}),W(`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`})]})}var In=`smartsafehub.theme`;function Ln(){if(typeof window>`u`)return`light`;try{let e=window.localStorage.getItem(In);if(e===`dark`||e===`light`)return e}catch{}return window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}function Rn(e){try{window.localStorage.setItem(In,e)}catch{}}function zn(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`}function Bn(e){return{length:e.length>=8,letter:/[A-Za-z]/.test(e),number:/[0-9]/.test(e)}}function Vn(e){return e.length&&e.letter&&e.number}function Hn({theme:e}){return W(e===`dark`?Fn:Pn,{})}function Un({met:e,children:t}){return W(`li`,{class:e?`ssh-password-requirement is-met`:`ssh-password-requirement`,children:[W(Sn,{"aria-hidden":`true`}),W(`span`,{children:t})]})}function Wn({onCompleted:e,recovery:t}){let n=z(null),r=z(null),[i,a]=L(``),[o,s]=L(``),[c,l]=L(!1),[u,d]=L(!1),[f,p]=L(!1),[m,h]=L(null),[g,_]=L(Ln),v=Bn(i),y=Vn(v),b=o.length>0&&i===o,x=g===`dark`?`라이트 모드로 전환`:`다크 모드로 전환`;R(()=>{Rn(g),zn(g)},[g]),R(()=>{let e=window.requestAnimationFrame(()=>n.current?.focus());return()=>window.cancelAnimationFrame(e)},[]);let S=e=>{d(e.getModifierState?.(`CapsLock`)??!1)};return W(`main`,{class:`ssh-login-page`,"data-theme":g,children:[W(`section`,{class:`ssh-login-brand`,"aria-labelledby":`ssh-password-setup-brand-title`,children:[W(`div`,{class:`ssh-login-brand-orb ssh-login-brand-orb-one`,"aria-hidden":`true`}),W(`div`,{class:`ssh-login-brand-orb ssh-login-brand-orb-two`,"aria-hidden":`true`}),W(`div`,{class:`ssh-login-brand-content`,children:[W(`span`,{class:`ssh-login-brand-mark`,"aria-hidden":`true`,children:W(qt,{})}),W(`p`,{class:`ssh-login-eyebrow`,children:t?`PASSWORD RECOVERY`:`FIRST SECURITY SETUP`}),W(`h1`,{id:`ssh-password-setup-brand-title`,children:[W(`span`,{class:`ssh-password-setup-brand-title-line`,children:`SmartSafeHub`}),W(`span`,{class:`ssh-password-setup-brand-title-line`,children:t?`비밀번호 복구`:`보호 시작`})]}),W(`p`,{class:`ssh-login-brand-description`,children:t?`물리 Reset 버튼으로 관리자 비밀번호 복구가 요청되었습니다. 네트워크, Wi-Fi, SafeShield 등 기존 설정은 그대로 유지됩니다.`:`관리자 비밀번호가 없는 상태에서는 SmartSafeHub 관리 기능을 사용할 수 없습니다. 먼저 이 공유기의 root 관리자 비밀번호를 설정해 주세요.`}),W(`div`,{class:`ssh-password-setup-summary`,children:[W(qt,{"aria-hidden":`true`}),W(`div`,{children:[W(`strong`,{children:t?`새 관리자 비밀번호를 설정해 주세요`:`초기 보안 설정이 필요합니다`}),W(`span`,{children:t?`새 비밀번호를 설정하기 전까지 SSH 접근은 비활성화되고 다른 관리 기능도 잠겨 있습니다.`:`설정이 완료되기 전까지 다른 SmartSafeHub 기능은 잠겨 있습니다.`})]})]})]})]}),W(`section`,{class:`ssh-login-panel`,"aria-labelledby":`ssh-password-setup-title`,children:[W(`button`,{"aria-label":x,class:`ssh-login-theme-toggle`,onClick:()=>_(e=>e===`dark`?`light`:`dark`),title:x,type:`button`,children:W(Hn,{theme:g})}),W(`div`,{class:`ssh-login-panel-inner`,children:[W(`div`,{class:`ssh-login-card`,children:[W(`div`,{class:`ssh-login-card-heading`,children:[W(`span`,{class:`ssh-login-mobile-mark`,"aria-hidden":`true`,children:W(_n,{})}),W(`div`,{children:[W(`p`,{class:`ssh-login-kicker`,children:`ADMIN PASSWORD`}),W(`h2`,{id:`ssh-password-setup-title`,children:t?`관리자 비밀번호 복구`:`관리자 비밀번호 설정`}),W(`p`,{class:`ssh-login-subtitle`,children:`비밀번호 설정 후 새 비밀번호로 다시 로그인해야 합니다.`})]})]}),m?W(`div`,{class:`ssh-login-alert`,role:`alert`,"aria-live":`polite`,children:m}):null,W(`form`,{class:`ssh-login-form`,onSubmit:async t=>{if(t.preventDefault(),h(null),!y)h(`비밀번호는 8자 이상이며 영문과 숫자를 각각 하나 이상 포함해야 합니다.`),window.requestAnimationFrame(()=>n.current?.focus());else if(i!==o)h(`비밀번호 확인 값이 일치하지 않습니다.`),window.requestAnimationFrame(()=>r.current?.focus());else{p(!0);try{await Wt(i),a(``),s(``);try{await Ee()}catch{}e()}catch(e){h(e instanceof H?e.message:`관리자 비밀번호를 설정하지 못했습니다. 잠시 후 다시 시도해 주세요.`)}finally{p(!1)}}},noValidate:!0,children:[W(`label`,{class:`ssh-login-field`,for:`smartsafehub-new-root-password`,children:[W(`span`,{class:`ssh-login-label`,children:`새 비밀번호`}),W(`span`,{class:`ssh-login-input-wrap`,children:[W(`span`,{class:`ssh-login-field-icon`,"aria-hidden":`true`,children:W(_n,{})}),W(`input`,{autoComplete:`new-password`,disabled:f,id:`smartsafehub-new-root-password`,name:`new-password`,onBlur:()=>d(!1),onInput:e=>a(e.currentTarget.value),onKeyDown:S,onKeyUp:S,placeholder:`새 관리자 비밀번호`,ref:n,...c?{type:`text`}:{type:`password`},value:i}),W(`button`,{"aria-label":c?`비밀번호 숨기기`:`비밀번호 표시`,"aria-pressed":c,class:`ssh-login-password-toggle`,disabled:f,onClick:()=>l(e=>!e),title:c?`비밀번호 숨기기`:`비밀번호 표시`,type:`button`,children:W(c?bn:yn,{})})]})]}),W(`ul`,{class:`ssh-password-requirements`,"aria-label":`비밀번호 요구 사항`,children:[W(Un,{met:v.length,children:`8자 이상`}),W(Un,{met:v.letter,children:`영문자 1자 이상 포함`}),W(Un,{met:v.number,children:`숫자 1자 이상 포함`})]}),W(`label`,{class:`ssh-login-field`,for:`smartsafehub-confirm-root-password`,children:[W(`span`,{class:`ssh-login-label`,children:`비밀번호 확인`}),W(`span`,{class:`ssh-login-input-wrap`,children:[W(`span`,{class:`ssh-login-field-icon`,"aria-hidden":`true`,children:W(_n,{})}),W(`input`,{autoComplete:`new-password`,disabled:f,id:`smartsafehub-confirm-root-password`,name:`confirm-password`,onBlur:()=>d(!1),onInput:e=>s(e.currentTarget.value),onKeyDown:S,onKeyUp:S,placeholder:`새 관리자 비밀번호 다시 입력`,ref:r,...c?{type:`text`}:{type:`password`},value:o})]})]}),o.length>0?W(`p`,{class:b?`ssh-password-match is-met`:`ssh-password-match`,role:`status`,children:b?`비밀번호가 일치합니다.`:`비밀번호가 일치하지 않습니다.`}):null,u?W(`p`,{class:`ssh-login-caps`,role:`status`,children:`Caps Lock이 켜져 있습니다.`}):null,W(`button`,{class:`ssh-login-submit`,disabled:f,type:`submit`,children:[f?W(pn,{class:`ssh-login-submit-spinner`,"aria-hidden":`true`}):null,W(`span`,{children:f?`설정 중…`:`비밀번호 설정`})]})]}),W(`div`,{class:`ssh-login-security-note`,children:[W(qt,{"aria-hidden":`true`}),W(`span`,{children:`비밀번호는 현재 공유기에 직접 설정되며 외부 서버로 전송되지 않습니다.`})]})]}),W(`footer`,{class:`ssh-login-footer`,children:[W(`span`,{children:`SmartSafeHub`}),W(`span`,{"aria-hidden":`true`,children:`·`}),W(`span`,{children:window.location.host})]})]})]})]})}var K=`smartsafehub`,Gn=`smartsafehub_guest`,Kn=`smartsafehub_network`;function qn(){return U(K,`connected_devices`)}async function Jn(){let e=await U(K,`activity_history`);return{...e,cloud:{...e.cloud,enabled:e.cloud.enabled??!1}}}function Yn(e){return U(K,`activity_cloud_sync_update`,{enabled:e})}function Xn(){return U(K,`status`)}function Zn(e){return U(K,`device_recommended_rules_status`,{},{...e?{signal:e}:{},priority:`low`,timeoutMs:5e3})}function Qn(){return U(K,`device_recommended_rules_refresh`,{},{priority:`high`,timeoutMs:6e3})}function $n(e){return U(K,`device_recommended_rules_revoke`,{rule_id:e},{priority:`high`,timeoutMs:6e3})}function er(e){return U(K,`device_registration_status`,{},{...e?{signal:e}:{},priority:`low`,timeoutMs:5e3})}function tr(){return U(K,`device_registration_refresh`,{},{priority:`high`,timeoutMs:6e3})}function nr(){return U(K,`device_pairing_refresh`,{},{priority:`high`,timeoutMs:6e3})}function rr(){return U(K,`health_status`)}function ir(){return U(K,`health_run`)}function ar(e){return U(K,`health_reporter_update`,{enabled:e})}function or(e){return U(K,`apps_catalog`,{},{...e?{signal:e}:{},priority:`low`,timeoutMs:6e3})}function sr(){return U(K,`apps_catalog`,{refresh:!0},{priority:`high`,timeoutMs:6e3})}function cr(e){return U(K,`apps_status`,{},{...e?{signal:e}:{},priority:`low`,timeoutMs:6e3})}function lr(){return U(K,`apps_status`,{refresh:!0},{priority:`high`,timeoutMs:6e3})}function ur(e){return U(K,`apps_metrics`,{},{...e?{signal:e}:{},priority:`low`,timeoutMs:6e3})}function dr(){return U(K,`apps_metrics`,{refresh:!0},{priority:`high`,timeoutMs:6e3})}function fr(e){return U(K,`apps_install`,{app_id:e},{timeoutMs:8e3})}function pr(e){return U(K,`apps_update`,{app_id:e},{timeoutMs:8e3})}function mr(e){return U(K,`apps_remove`,{app_id:e},{timeoutMs:8e3})}function hr(){return U(K,`updates_status`)}function gr(){return U(K,`firmware_status`)}function _r(){return U(K,`firmware_check`)}function vr(){return U(K,`firmware_prepare`)}function yr(e){return U(K,`firmware_validate_upload`,{filename:e})}function br(e){return U(K,`firmware_install`,{confirm:`install`,keep_settings:e})}function xr(){return U(K,`firmware_discard`)}function Sr(){return U(K,`updates_check`)}function Cr(){return U(K,`updates_install`,{confirm:`install`})}function wr(e){return U(K,`updates_settings_update`,{check_enabled:e.checkEnabled,check_interval_s:e.checkIntervalSeconds,auto_install:e.autoInstall,auto_install_time:e.autoInstallTime})}function Tr(){return U(K,`system_time_settings`)}function Er(e){return U(K,`system_timezone_initialize`,{zonename:e})}function Dr(e){return U(K,`system_timezone_update`,{zonename:e})}function Or(){return U(K,`system_time_sync`)}function kr(){return U(K,`system_scheduled_reboot_settings`)}function Ar(e){return U(K,`system_scheduled_reboot_update`,{enabled:e.enabled,frequency:e.frequency,day_of_week:e.dayOfWeek,time:e.time})}function jr(e){return U(K,`system_backup_validate`,{filename:e})}function Mr(){return U(K,`system_backup_restore`,{confirm:`restore`},{timeoutMs:35e3})}function Nr(){return U(K,`system_backup_discard`)}function Pr(){return U(K,`system_reboot`,{confirm:`reboot`})}function Fr(){return U(Kn,`wan_settings`)}function Ir(e){return U(Kn,`wan_update`,{protocol:e.protocol,pppoe_username:e.pppoeUsername,pppoe_password:e.pppoePassword,pppoe_password_changed:e.pppoePasswordChanged,static_address:e.staticAddress,static_prefix_length:e.staticPrefixLength,static_gateway:e.staticGateway,dns_primary:e.dnsPrimary,dns_secondary:e.dnsSecondary,confirm:`apply`},{timeoutMs:1e4})}function Lr(){return U(Kn,`wan_reconnect`,{confirm:`reconnect`},{timeoutMs:1e4})}function Rr(){return U(Kn,`lan_settings`)}function zr(e){return U(Kn,`lan_update`,{ip_address:e.ipAddress,prefix_length:e.prefixLength,dhcp_enabled:e.dhcpEnabled,dhcp_start:e.dhcpStart,dhcp_end:e.dhcpEnd,lease_time:e.leaseTime,confirm:`apply`},{timeoutMs:1e4})}function Br(){return U(Kn,`lan_auto_subnet`,{confirm:`apply`},{timeoutMs:1e4})}function Vr(){return U(Kn,`iptv_settings`)}function Hr(e){return U(Kn,`iptv_update`,{enabled:e.enabled,provider:e.provider,confirm:`apply`},{timeoutMs:2e4})}function Ur(e){return U(K,`wifi_qr`,{section:e},{timeoutMs:8e3})}function Wr(){return U(Gn,`wifi_guest_qr`,{},{timeoutMs:8e3})}async function Gr(){let e=await U(K,`wifi_summary`);try{let t=await U(Gn,`wifi_guest_summary`);return{...e,guest:t,guestError:!1}}catch{return{...e,guest:null,guestError:!0}}}function Kr(e){return U(K,`wifi_update`,{...e},{timeoutMs:35e3})}function qr(e){return U(Gn,`wifi_guest_update`,{...e},{timeoutMs:95e3})}var Jr=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:`netwatch`,hash:`#netwatch`,label:`Network Watch`,title:`Network Watch`,description:`연결된 기기의 외부 통신 변화를 가볍게 관찰하고 주의 신호를 확인합니다.`},{route:`rules`,hash:`#rules`,label:`사용자 규칙`,title:`사용자 규칙`,description:`직접 허용하거나 차단할 도메인을 관리합니다.`},{route:`account`,hash:`#account`,label:`SmartSafeHub 계정`,title:`SmartSafeHub 계정`,description:`이 기기의 SmartSafeHub 계정 연결 상태와 구독 플랜을 확인합니다.`},{route:`apps`,hash:`#apps`,label:`Apps`,title:`SmartSafeHub Apps`,description:`SmartSafeHub에서 검증한 앱과 확장 기능을 살펴봅니다.`},{route:`system`,hash:`#system`,label:`업데이트`,title:`업데이트`,description:`기기의 펌웨어와 관리 소프트웨어 업데이트를 관리합니다.`},{route:`settings`,hash:`#settings`,label:`설정`,title:`설정`,description:`시스템 상태와 시간대를 확인하고 장치 관리 및 진단 기능을 설정합니다.`}].map(e=>[e.route,e])),Yr=[`home`,`activity`,`network`,`wifi`,`devices`,`safeshield`,`netwatch`,`rules`,`account`,`apps`,`system`];function Xr({description:e,loading:t,onRefresh:n,onToggleTheme:r,refreshing:i,theme:a,title:o}){return W(`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:W(`div`,{class:`flex w-full items-center justify-between gap-4 md:h-full`,children:[W(`div`,{class:`min-w-0`,children:[W(`p`,{class:`m-0 text-[0.68rem] font-extrabold uppercase tracking-[0.18em] text-teal-700`,children:`SmartSafeHub`}),W(`div`,{class:`mt-1 flex min-w-0 items-baseline gap-3`,children:[W(`h1`,{class:`m-0 truncate text-xl font-black tracking-tight text-slate-950 sm:text-2xl`,children:o}),W(`p`,{class:`m-0 hidden truncate text-sm text-slate-500 lg:block`,children:e})]})]}),W(`div`,{class:`flex shrink-0 items-center gap-2`,children:[W(`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:W(a===`dark`?Fn:Pn,{class:`size-5`})}),W(`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?W(pn,{class:`size-5 animate-spin`}):W(pn,{class:`size-5`})})]})]})})}var Zr=[{label:`Overview`,routes:[`home`,`activity`]},{label:`Network`,routes:[`network`,`wifi`,`iptv`,`devices`]},{label:`Security`,routes:[`safeshield`,`netwatch`,`rules`]},{label:`Explore`,routes:[`apps`]},{label:`System`,routes:[`account`,`system`,`settings`]}];function Qr({route:e}){switch(e){case`home`:return W(Xt,{class:`size-5`});case`activity`:return W(dn,{class:`size-5`});case`network`:return W(ln,{class:`size-5`});case`wifi`:return W(on,{class:`size-5`});case`iptv`:return W(un,{class:`size-5`});case`devices`:return W(Yt,{class:`size-5`});case`safeshield`:return W(qt,{class:`size-5`});case`netwatch`:return W($t,{class:`size-5`});case`rules`:return W(Zt,{class:`size-5`});case`account`:return W(_n,{class:`size-5`});case`apps`:return W(Qt,{class:`size-5`});case`system`:return W(rn,{class:`size-5`});case`settings`:return W(an,{class:`size-5`})}}function $r({theme:e}){return W(e===`dark`?Fn:Pn,{class:`size-5`})}function ei(e){return e===`dark`?`라이트 모드`:`다크 모드`}function ti(e,t=!1,n){let r=t?`justify-center px-2`:`gap-3 px-3`;return e?`relative flex min-h-11 min-w-0 items-center rounded-xl py-2.5 text-sm font-extrabold no-underline transition ${r} bg-teal-50 text-teal-800 ring-1 ring-inset ring-teal-200`:n===`apps`?`ssh-apps-nav-link relative flex min-h-11 min-w-0 items-center rounded-xl py-2.5 text-sm font-extrabold no-underline transition ${r}`:`relative flex min-h-11 min-w-0 items-center rounded-xl py-2.5 text-sm font-extrabold text-slate-600 no-underline transition ${r} hover:bg-slate-50 hover:text-slate-950`}function ni(e){return e===`apps`?`앱으로 확장하기`:null}function ri({accountRegistered:e,collapsed:t=!1,route:n,updateCount:r,demo:i=!1}){return W(`div`,{class:t?`space-y-3`:`space-y-5`,children:Zr.filter(e=>!i||e.routes.some(e=>Yr.includes(e))).map((a,o)=>W(`section`,{"aria-label":a.label,class:t&&o>0?`border-t border-slate-100 pt-3`:void 0,children:[t?null:W(`p`,{class:`mb-2 px-3 text-[0.65rem] font-black uppercase tracking-[0.18em] text-slate-400`,children:a.label}),W(`div`,{class:`space-y-1`,children:a.routes.filter(e=>!i||Yr.includes(e)).map(i=>{let a=Jr[i],o=n===i;return W(`a`,{"aria-current":o?`page`:void 0,"aria-label":i===`account`&&e===!1?`${a.label} (계정 연결 필요)`:t?a.label:void 0,class:ti(o,t,i),href:a.hash,title:i===`account`&&e===!1?`SmartSafeHub 계정 연결 필요`:t?a.label:void 0,children:[W(`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:W(Qr,{route:i})}),t?null:W(`span`,{class:`flex min-w-0 flex-1 items-center gap-2`,children:[W(`span`,{class:`min-w-0 flex-1`,children:[W(`span`,{class:`block truncate`,children:a.label}),ni(i)?W(`span`,{class:`ssh-apps-nav-description mt-0.5 block truncate text-[11px] font-semibold tracking-normal`,children:ni(i)}):null]}),i===`iptv`?W(`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`?W(`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?W(`span`,{"aria-label":`계정 연결 필요`,class:`ssh-account-nav-dot ${t?`ssh-account-nav-dot-collapsed`:``}`,"aria-hidden":`true`,title:`계정 연결 필요`}):null,i===`system`&&r>0?t?W(`span`,{"aria-label":`${r}개의 업데이트 유형`,class:`ssh-update-nav-badge`,"data-collapsed":`true`,children:r}):W(`span`,{class:`ssh-update-nav-badge`,"data-collapsed":`false`,children:r}):null]},i)})})]},a.label))})}function ii({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]=L(!1),m=ei(c);return R(()=>{p(!1)},[s]),R(()=>{if(!f)return;let e=e=>{e.key===`Escape`&&p(!1)};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[f]),W(b,{children:[W(`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:[W(`div`,{class:`flex min-h-16 items-center justify-between gap-3`,children:[W(`a`,{class:`flex min-w-0 items-center gap-3 no-underline`,href:`#home`,children:[W(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-950 text-teal-300`,children:W(qt,{class:`size-5`})}),W(`span`,{class:`min-w-0`,children:[W(`strong`,{class:`block truncate text-sm font-black text-slate-950`,children:`SmartSafeHub`}),W(`span`,{class:`block truncate text-xs font-semibold text-slate-500`,children:l})]})]}),W(`div`,{class:`flex shrink-0 items-center gap-2`,children:[W(`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:W($r,{theme:c})}),W(`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?W(pn,{class:`size-5 animate-spin`}):W(pn,{class:`size-5`})}),W(`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:W(f?jn:kn,{class:`size-5`})})]})]}),f?W(`div`,{class:`ssh-mobile-menu border-t border-slate-100 py-4`,id:`smartsafehub-mobile-menu`,children:[W(ri,{accountRegistered:e,demo:d,route:s,updateCount:u}),d?null:W(`div`,{class:`mt-5 space-y-1 border-t border-slate-100 pt-4`,children:W(`a`,{class:`${ti(!1)} text-rose-700 hover:bg-rose-50 hover:text-rose-800`,href:ve(`/admin/logout`),children:[W(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg bg-rose-50 text-rose-600`,children:W(On,{class:`size-5`})}),`로그아웃`]})})]}):null]}),W(`aside`,{class:`hidden min-h-screen border-r border-slate-200 bg-white md:flex md:flex-col`,"data-collapsed":t?`true`:`false`,children:W(`div`,{class:`sticky top-0 flex h-screen flex-col`,children:[W(`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:[W(`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:[W(`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:W(qt,{class:`size-5`})}),t?null:W(`div`,{class:`min-w-0`,children:[W(`strong`,{class:`block truncate text-sm font-black tracking-tight text-slate-950`,children:`SmartSafeHub`}),W(`span`,{class:`block text-[0.68rem] font-bold uppercase tracking-[0.14em] text-slate-400`,children:`Home Gateway`})]})]}),W(`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:W(t?Nn:Mn,{class:`size-4`})})]}),W(`nav`,{"aria-label":`SmartSafeHub 메뉴`,class:`flex-1 overflow-y-auto py-5 ${t?`px-2`:`px-3`}`,id:`smartsafehub-desktop-navigation`,children:W(ri,{accountRegistered:e,collapsed:t,demo:d,route:s,updateCount:u})}),d?null:W(`div`,{class:`border-t border-slate-100 ${t?`p-2`:`p-3`}`,children:W(`a`,{"aria-label":`SmartSafeHub에서 로그아웃`,class:`${ti(!1,t)} mt-1 text-rose-700 hover:bg-rose-50 hover:text-rose-800`,href:ve(`/admin/logout`),title:t?`로그아웃`:void 0,children:[W(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg bg-rose-50 text-rose-600`,children:W(On,{class:`size-5`})}),t?null:`로그아웃`]})})]})})]})}var ai=`smartsafehub.sidebar.collapsed`;function oi(){if(typeof window>`u`)return!1;try{return window.localStorage.getItem(ai)===`1`}catch{return!1}}function si({accountRegistered:e,children:t,loading:n,onRefresh:r,refreshing:i,route:a,updateCount:o,demo:s=!1}){let c=Jr[a],[l,u]=L(oi),[d,f]=L(Ln);return R(()=>{try{window.localStorage.setItem(ai,l?`1`:`0`)}catch{}},[l]),R(()=>{Rn(d),zn(d)},[d]),W(`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:[W(ii,{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}),W(`div`,{class:`min-w-0 md:min-h-screen`,children:[W(Xr,{description:c.description,loading:n,onRefresh:r,onToggleTheme:()=>f(e=>e===`dark`?`light`:`dark`),refreshing:i,theme:d,title:c.title}),W(`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})]})]})}function ci({active:e,label:t,tabs:n}){return W(`nav`,{"aria-label":t,class:`mb-5 min-w-0`,role:`tablist`,children:W(`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 W(`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)})})})}function q(e,t){return e instanceof Error?e.message:t}function li({active:e=!0,fallbackError:t,loader:n,pollInterval:r,refreshOnFocus:i=!1,refreshOnVisible:a=!1,staleTimeMs:o=3e4}){let[s,c]=L({data:null,error:null,loading:e,refreshing:!1}),l=z(!1),u=z(!0),d=z(null),f=z(null),p=z(0),m=z(null),h=z(null),g=z(0);R(()=>(u.current=!0,()=>{u.current=!1,p.current+=1,f.current?.abort()}),[]);let _=B((e=!1)=>{if(d.current)return d.current;u.current&&c(t=>({...t,error:null,loading:t.data===null,refreshing:e}));let r,i=++p.current,a=new AbortController;return f.current=a,m.current=Date.now(),r=n(a.signal).then(e=>{i!==p.current||a.signal.aborted||(h.current=Date.now(),g.current=0,u.current&&c({data:e,error:null,loading:!1,refreshing:!1}))}).catch(e=>{i!==p.current||a.signal.aborted||(g.current+=1,u.current&&c(n=>({...n,error:q(e,t),loading:!1,refreshing:!1})))}).finally(()=>{d.current===r&&(d.current=null),f.current===a&&(f.current=null)}),d.current=r,r},[t,n]);R(()=>{e?l.current||(l.current=!0,s.data!==null&&h.current!==null&&Date.now()-h.current{if(!e||v==null||v<=0)return;let t=!1,n=null,r=()=>{n!==null&&(window.clearTimeout(n),n=null)},o=()=>{let e=Date.now();if(g.current>0&&m.current!==null){let t=Math.min(3e4,3e3*2**Math.min(g.current-1,4));return Math.max(0,m.current+t-e)}return h.current===null?0:Math.max(0,h.current+v-e)},s=()=>{r(),!(t||document.visibilityState===`hidden`)&&(n=window.setTimeout(()=>{n=null,!(t||document.visibilityState===`hidden`)&&(o()>0?s():_(!1).finally(s))},o()))},c=()=>{r(),!(t||document.visibilityState===`hidden`)&&(o()>0?s():_(!1).finally(s))},l=()=>{document.visibilityState===`hidden`?r():a?_(!1).finally(s):c()},u=()=>{c()};return document.addEventListener(`visibilitychange`,l),i&&window.addEventListener(`focus`,u),s(),()=>{t=!0,r(),document.removeEventListener(`visibilitychange`,l),i&&window.removeEventListener(`focus`,u)}},[e,v,_,i,a]);let y=B(()=>_(!0),[_]),b=B(e=>{u.current&&(h.current=Date.now(),g.current=0,c(t=>({...t,data:e,error:null,loading:!1,refreshing:!1})))},[]);return{...s,refresh:y,replaceData:b}}var ui=6e4;function di(e){let t=li({active:e,fallbackError:`최근 활동을 불러오지 못했습니다.`,loader:()=>Jn(),pollInterval:ui,refreshOnFocus:!0}),[n,r]=L({savingCloudSync:!1,actionError:null,actionMessage:null}),i=B(async e=>{r({savingCloudSync:!0,actionError:null,actionMessage:null});try{let n=await Yn(e);return t.replaceData(n),r({savingCloudSync:!1,actionError:null,actionMessage:e?`Cloud 활동 기록 전송을 켰습니다. 지금부터 발생하는 활동을 Cloud에 전송합니다.`:`Cloud 활동 기록 전송을 껐습니다. 로컬 최근 활동은 계속 기록됩니다.`}),!0}catch(e){return r({savingCloudSync:!1,actionError:q(e,`Cloud 활동 기록 설정을 변경하지 못했습니다.`),actionMessage:null}),!1}},[t.replaceData]),a=B(()=>{r(e=>({...e,actionError:null,actionMessage:null}))},[]);return{...t,...n,dismissActionFeedback:a,setCloudSyncEnabled:i}}var fi=`/tmp/smartsafehub/config-backup.tar.gz`,pi=`/cgi-backup`,mi=`/cgi-upload`;function hi(){let e=window.__SMARTHUB_BOOTSTRAP__?.sessionId;if(!e)throw new H(`BOOTSTRAP_MISSING`,`LuCI 세션 정보를 찾을 수 없습니다.`);return e}function gi(e){return(e?.match(/filename="?([^";]+)"?/i))?.[1]?.trim()||`backup-openwrt.tar.gz`}function _i(e,t){let n=URL.createObjectURL(t),r=document.createElement(`a`);r.href=n,r.download=e,r.style.display=`none`,(document.body??document.documentElement).appendChild(r),r.click(),r.remove(),window.setTimeout(()=>URL.revokeObjectURL(n),1e3)}async function vi(){let e=ge(pi),t=await fetch(e,{method:`POST`,credentials:`same-origin`,headers:{"Content-Type":`application/x-www-form-urlencoded;charset=UTF-8`},body:new URLSearchParams({sessionid:hi()}).toString()});if(!t.ok)throw new H(`SYSTEM_BACKUP_DOWNLOAD_FAILED`,`설정 백업 생성에 실패했습니다. (HTTP ${t.status})`);let n=await t.blob();if(n.size<=0)throw new H(`SYSTEM_BACKUP_DOWNLOAD_EMPTY`,`설정 백업 파일이 비어 있습니다.`);_i(gi(t.headers.get(`Content-Disposition`)),n)}function yi(e,t){let n;try{n=hi()}catch(e){return Promise.reject(e)}return new Promise((r,i)=>{let a=new XMLHttpRequest,o=new FormData;o.append(`sessionid`,n),o.append(`filename`,fi),o.append(`filedata`,e),a.open(`POST`,ge(mi)),a.timeout=0,a.upload.addEventListener(`progress`,e=>{!e.lengthComputable||e.total<=0||t?.(Math.min(100,Math.max(0,e.loaded/e.total*100)))}),a.addEventListener(`error`,()=>{i(new H(`SYSTEM_BACKUP_UPLOAD_FAILED`,`설정 백업 파일을 공유기에 업로드하지 못했습니다.`))}),a.addEventListener(`load`,()=>{if(a.status<200||a.status>=300){i(new H(`SYSTEM_BACKUP_UPLOAD_HTTP_ERROR`,`설정 백업 업로드가 HTTP ${a.status} 오류를 반환했습니다.`));return}let n;try{n=JSON.parse(a.responseText)}catch{i(new H(`SYSTEM_BACKUP_UPLOAD_INVALID_RESPONSE`,`설정 백업 업로드 응답을 확인하지 못했습니다.`));return}n.failure?i(new H(n.failure,n.message||`설정 백업 파일 업로드가 거부되었습니다.`)):(t?.(100),r({...n,name:e.name}))}),a.send(o)})}var bi=16777216;function xi(){let[e,t]=L(null),[n,r]=L(null),[i,a]=L(null),[o,s]=L(null),[c,l]=L(null),[u,d]=L(!1),f=B(async()=>{t(`download`),r(null),a(null);try{await vi(),a(`현재 설정 백업을 다운로드했습니다.`)}catch(e){r(q(e,`설정 백업을 다운로드하지 못했습니다.`))}finally{t(null)}},[]),p=B(async e=>{if(r(null),a(null),s(null),d(!1),e.size<=0||e.size>bi)return r(`설정 백업 파일은 16MB 이하여야 합니다.`),!1;try{t(`upload`),l(0),await yi(e,l),t(`validate`);let n=await jr(e.name);return s(n),a(`백업 파일 검증이 완료되었습니다. 복원 내용을 확인해 주세요.`),!0}catch(e){return r(q(e,`설정 백업 파일을 검증하지 못했습니다.`)),!1}finally{t(null),l(null)}},[]),m=B(async()=>{t(`discard`),r(null),a(null);try{return await Nr(),s(null),d(!1),a(`업로드한 백업 파일을 삭제했습니다.`),!0}catch(e){return r(q(e,`업로드한 백업 파일을 삭제하지 못했습니다.`)),!1}finally{t(null)}},[]),h=B(async()=>{if(!o)return r(`먼저 복원할 백업 파일을 업로드하고 검증해 주세요.`),!1;t(`restore`),r(null),a(null);try{let e=await Mr();return d(e.accepted&&e.rebooting),s(null),a(e.accepted?`설정을 복원했습니다. 공유기가 재부팅되며 네트워크 설정에 따라 다시 연결해야 할 수 있습니다.`:`설정 복원 요청이 접수되지 않았습니다.`),e.accepted}catch(e){return r(q(e,`설정 백업을 복원하지 못했습니다.`)),!1}finally{t(null)}},[o]);return{action:e,discard:m,dismissFeedback:B(()=>{r(null),a(null)},[]),download:f,error:n,message:i,restore:h,restoreAccepted:u,upload:p,uploadProgress:c,validated:o}}var Si=15e3;function Ci(e,t=!0){return li({active:e,fallbackError:`연결된 기기 목록을 불러오지 못했습니다.`,loader:qn,...t?{pollInterval:Si}:{}})}var wi=`/tmp/smartsafehub/firmware.bin`,Ti=`/cgi-upload`;function Ei(e,t){let n=window.__SMARTHUB_BOOTSTRAP__;return n?.sessionId?new Promise((r,i)=>{let a=new XMLHttpRequest,o=new FormData;o.append(`sessionid`,n.sessionId),o.append(`filename`,wi),o.append(`filedata`,e);let s=ge(Ti);a.open(`POST`,s),a.timeout=0,a.upload.addEventListener(`progress`,e=>{!e.lengthComputable||e.total<=0||t?.(Math.min(100,Math.max(0,e.loaded/e.total*100)))}),a.addEventListener(`error`,()=>{console.error(`[SmartSafeHub] firmware upload network error`,{endpoint:s,destination:wi,fileName:e.name,fileSize:e.size,readyState:a.readyState,status:a.status}),i(new H(`FIRMWARE_UPLOAD_FAILED`,`펌웨어 파일 업로드에 실패했습니다. (${s}, 네트워크 오류)`))}),a.addEventListener(`load`,()=>{if(a.status<200||a.status>=300){console.error(`[SmartSafeHub] firmware upload HTTP error`,{endpoint:s,destination:wi,fileName:e.name,fileSize:e.size,readyState:a.readyState,status:a.status,response:a.responseText}),i(new H(`FIRMWARE_UPLOAD_HTTP_ERROR`,`펌웨어 업로드가 HTTP ${a.status} 오류를 반환했습니다. (${s})`));return}let n;try{n=JSON.parse(a.responseText)}catch{console.error(`[SmartSafeHub] firmware upload returned invalid JSON`,{endpoint:s,destination:wi,fileName:e.name,fileSize:e.size,readyState:a.readyState,status:a.status,response:a.responseText}),i(new H(`FIRMWARE_UPLOAD_INVALID_RESPONSE`,`펌웨어 업로드 응답을 확인하지 못했습니다. (${s})`));return}n.failure?i(new H(n.failure,n.message||`펌웨어 파일 업로드가 거부되었습니다.`)):(t?.(100),r({...n,name:e.name}))}),a.send(o)}):Promise.reject(new H(`BOOTSTRAP_MISSING`,`LuCI 세션 정보를 찾을 수 없습니다.`))}var Di=3e5,Oi=1e3,ki=500,Ai=3e3,ji=3e4,Mi=15e3,Ni=5e3;function Pi(e){return new Promise(t=>window.setTimeout(t,e))}function Fi(e){return e===`checking`||e===`downloading`||e===`verifying`}function Ii(e=!0){let[t,n]=L(null),r=li({active:e||Fi(t??void 0),fallbackError:`펌웨어 업데이트 상태를 불러오지 못했습니다.`,loader:gr,pollInterval:e=>Fi(e?.phase)?Oi:Di,refreshOnFocus:!0,refreshOnVisible:Fi(t??void 0)});R(()=>{n(r.data?.phase??null)},[r.data?.phase]);let[i,a]=L(null),[o,s]=L(null),[c,l]=L(null),[u,d]=L(null),[f,p]=L(!1),m=z(null);R(()=>()=>{m.current!==null&&window.clearTimeout(m.current)},[]);let h=B(e=>{n(e),r.data&&r.replaceData({...r.data,phase:e,lastError:null})},[r]),g=B(async(e,t,n,i)=>{a(e),s(null),l(null);try{if(!(await n()).accepted)throw Error(`펌웨어 작업 요청이 접수되지 않았습니다.`);h(t),a(null),window.setTimeout(()=>void r.refresh(),400)}catch(e){a(null),s(q(e,i))}},[h,r]),_=B(()=>g(`check`,`checking`,_r,`펌웨어 업데이트 확인을 시작하지 못했습니다.`),[g]),v=B(()=>g(`prepare`,`downloading`,vr,`펌웨어 다운로드를 시작하지 못했습니다.`),[g]),y=B(async()=>{let e=Date.now(),t=null;for(;Date.now()-e{a(`upload`),s(null),l(null),d(0);try{if(await Ei(e,d),a(`validate-upload`),!(await yr(e.name)).accepted)throw Error(`업로드한 펌웨어 검증 요청이 접수되지 않았습니다.`);h(`verifying`),d(null);let t=await y();if(a(null),t.phase===`error`)return!1;if(!t.prepared)throw Error(`검증된 펌웨어의 설치 준비 정보를 확인하지 못했습니다. 다시 시도해 주세요.`);return l(`펌웨어 검증이 완료되었습니다. 설치 옵션을 확인해 주세요.`),!0}catch(e){return a(null),d(null),s(q(e,`펌웨어 파일을 업로드하지 못했습니다.`)),!1}},[h,y]),x=B(()=>{p(!0);let e=async()=>{try{if((await fetch(window.location.pathname,{cache:`no-store`,credentials:`same-origin`})).ok){window.location.reload();return}}catch{}m.current=window.setTimeout(e,Ni)};m.current=window.setTimeout(e,Mi)},[]),S=B(async e=>{a(`install`),s(null),l(null);try{if(!(await br(e)).accepted)throw Error(`펌웨어 설치 요청이 접수되지 않았습니다.`);return h(`flashing`),a(null),x(),!0}catch(e){return a(null),s(q(e,`펌웨어 설치를 시작하지 못했습니다.`)),!1}},[h,x]),C=B(async()=>{a(`discard`),s(null),l(null);try{if(!(await xr()).accepted)throw Error(`준비된 펌웨어 정리 요청이 접수되지 않았습니다.`);return a(null),l(`준비된 펌웨어 파일을 삭제했습니다.`),await r.refresh(),!0}catch(e){return a(null),s(q(e,`준비된 펌웨어를 정리하지 못했습니다.`)),!1}},[r]),w=B(()=>{s(null),l(null)},[]);return{...r,action:i,actionError:o,message:c,uploadProgress:u,reconnecting:f,check:_,prepare:v,upload:b,install:S,discard:C,dismissFeedback:w}}var Li={"#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`,"#netwatch":`netwatch`,"#account":`account`,"#apps":`apps`,"#system":`system`,"#settings":`settings`,"#settings/diagnostics":`settings`,"#settings/time":`settings`,"#settings/management":`settings`};function Ri(){return Li[window.location.hash]??`home`}function zi(){let[e,t]=L(Ri);return R(()=>{let e=()=>t(Ri());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),e}function Bi(e){return e===`#lan`||e===`#network/lan`?`lan`:`internet`}function Vi(e){return e===`#settings/time`?`time`:e===`#settings/management`?`management`:`diagnostics`}function Hi(){let e=window.location.hash;return{network:Bi(e),settings:Vi(e)}}function Ui(){let[e,t]=L(Hi);return R(()=>{let e=()=>t(Hi());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),e}var Wi=6e4,Gi=1e3,Ki=[400,800,1200,1600],qi=[400,800,1200,2e3,4e3];function Ji(e){return new Promise(t=>window.setTimeout(t,e))}function Yi(e){let t=li({active:e,fallbackError:`장치 진단 상태를 불러오지 못했습니다.`,loader:rr,pollInterval:e=>e?.generatedAt?Wi:Gi,refreshOnFocus:!0}),n=z(0),[r,i]=L({running:!1,savingReporter:!1,actionError:null,actionMessage:null}),a=B(async()=>{i({running:!0,savingReporter:!1,actionError:null,actionMessage:null});try{let e=t.data?.generatedAt??0;if(!(await ir()).accepted)throw Error(`장치가 진단 요청을 접수하지 않았습니다.`);let n=t.data;for(let t of Ki)if(await Ji(t),n=await rr(),n.generatedAt>0&&n.generatedAt!==e)break;return n&&t.replaceData(n),i({running:!1,savingReporter:!1,actionError:null,actionMessage:`장치 상태를 다시 진단했습니다.`}),!0}catch(e){return i({running:!1,savingReporter:!1,actionError:q(e,`장치 상태 진단을 실행하지 못했습니다.`),actionMessage:null}),!1}},[t.data,t.replaceData]),o=B(async(e,r)=>{for(let i of qi){if(await Ji(i),n.current!==r)return;try{let i=await rr();if(n.current!==r)return;if(t.replaceData(i),i.reporter.enabled!==e)continue;if(!e||![`disabled`,`never`,`initializing`].includes(i.reporter.lastResult))return}catch{}}},[t.replaceData]),s=B(async e=>{let r=n.current+1;n.current=r;let a=t.data;a&&t.replaceData({...a,reporter:{...a.reporter,enabled:e,lastResult:e?`initializing`:`disabled`,lastErrorCode:null,nextReportAt:e?a.reporter.nextReportAt:0}}),i({running:!1,savingReporter:!0,actionError:null,actionMessage:null});try{let a=await ar(e);return n.current===r&&(t.replaceData(a),i({running:!1,savingReporter:!1,actionError:null,actionMessage:e?`원격 상태 보고를 켰습니다. 첫 서버 보고를 준비합니다.`:`원격 상태 보고를 껐습니다. 로컬 진단은 계속 동작합니다.`}),o(e,r)),!0}catch(e){return n.current===r&&(a&&t.replaceData(a),i({running:!1,savingReporter:!1,actionError:q(e,`원격 상태 보고 설정을 변경하지 못했습니다.`),actionMessage:null})),!1}},[o,t.data,t.replaceData]),c=B(()=>{i(e=>({...e,actionError:null,actionMessage:null}))},[]);return{...t,...r,dismissActionFeedback:c,runDiagnostic:a,setReporterEnabled:s}}function Xi(e){let t=li({active:e,fallbackError:`IPTV 설정을 불러오지 못했습니다.`,loader:Vr,refreshOnFocus:!0}),[n,r]=L(!1),[i,a]=L(null),o=B(async e=>{r(!0),a(null);try{let n=await Hr(e);return t.replaceData(n.settings),r(!1),a({kind:`success`,message:e.enabled?n.changed?`IPTV 설정을 적용했습니다.`:`IPTV 설정을 다시 적용했습니다.`:`IPTV 기능을 껐습니다.`}),!0}catch(e){return r(!1),a({kind:`error`,message:q(e,`IPTV 설정을 적용하지 못했습니다.`)}),!1}},[t.replaceData]),s=B(()=>a(null),[]);return{...t,saving:n,feedback:i,save:o,dismissFeedback:s}}var Zi=4e3;function Qi(e){let t=li({active:e,fallbackError:`내부 네트워크 설정을 불러오지 못했습니다.`,loader:Rr}),n=z(e),[r,i]=L({action:null,feedback:null});R(()=>{n.current=e},[e]);let a=B((e,r)=>{t.replaceData(e.settings),e.changed?e.addressChanged?i({action:null,feedback:{kind:`warning`,message:r?`추천 대역 ${e.settings.lan.subnet}을 저장했습니다. 네트워크가 다시 적용되면 새 공유기 주소로 다시 연결해 주세요.`:`내부 네트워크 주소를 저장했습니다. 네트워크가 다시 적용되면 새 공유기 주소로 다시 연결해 주세요.`,newAddress:e.newAddress??e.settings.lan.address}}):(i({action:null,feedback:{kind:`success`,message:r?`추천 내부 네트워크 설정을 적용했습니다.`:`내부 네트워크 설정을 저장했습니다.`}}),e.reloadScheduled&&window.setTimeout(()=>{n.current&&t.refresh()},Zi)):i({action:null,feedback:{kind:`success`,message:`변경된 내부 네트워크 설정이 없습니다.`}})},[t.refresh,t.replaceData]),o=B(async e=>{i({action:`saving`,feedback:null});try{let t=await zr(e);return a(t,!1),!0}catch(e){return i({action:null,feedback:{kind:`error`,message:q(e,`내부 네트워크 설정을 저장하지 못했습니다.`)}}),!1}},[a]),s=B(async()=>{i({action:`auto`,feedback:null});try{let e=await Br();return a(e,!0),!0}catch(e){return i({action:null,feedback:{kind:`error`,message:q(e,`추천 내부 네트워크를 적용하지 못했습니다.`)}}),!1}},[a]),c=B(()=>{i(e=>({...e,feedback:null}))},[]);return{...t,...r,save:o,applyRecommendation:s,dismissFeedback:c}}var $i=5e3;function ea(e){let t=li({active:e,fallbackError:`인터넷 연결 설정을 불러오지 못했습니다.`,loader:Fr}),n=z(e),[r,i]=L({action:null,feedback:null});R(()=>{n.current=e},[e]);let a=B(()=>{window.setTimeout(()=>{n.current&&t.refresh()},$i)},[t.refresh]),o=B(async e=>{i({action:`saving`,feedback:null});try{let n=await Ir(e);return t.replaceData(n.settings),i({action:null,feedback:{kind:n.changed?`warning`:`success`,message:n.changed?`인터넷 연결 설정을 저장했습니다. WAN을 다시 연결하는 동안 잠시 인터넷이 끊길 수 있습니다.`:`변경된 인터넷 연결 설정이 없습니다.`}}),n.reconnectScheduled&&a(),!0}catch(e){return i({action:null,feedback:{kind:`error`,message:q(e,`인터넷 연결 설정을 저장하지 못했습니다.`)}}),!1}},[t.replaceData,a]),s=B(async()=>{i({action:`reconnecting`,feedback:null});try{let e=await Lr();t.replaceData(e.settings),i({action:null,feedback:{kind:`warning`,message:`WAN 재연결을 시작했습니다. 인터넷 연결이 다시 올라오면 상태를 자동으로 새로고침합니다.`}}),e.reconnectScheduled&&a()}catch(e){i({action:null,feedback:{kind:`error`,message:q(e,`WAN 재연결을 시작하지 못했습니다.`)}})}},[t.replaceData,a]),c=B(()=>{i(e=>({...e,feedback:null}))},[]);return{...t,...r,save:o,reconnect:s,dismissFeedback:c}}var ta=`safeshield`,na=`smartsafehub`;function ra(e,t){return typeof e==`string`&&e.length>0?e:t}function ia(e){return typeof e==`string`?e:``}function aa(e){return typeof e==`number`&&Number.isFinite(e)?e:0}function oa(e){return e===!0}function sa(e){return Array.isArray(e)?e.length:0}function ca(e){return Array.isArray(e)?e.filter(e=>typeof e==`string`):[]}function la(e){return typeof e==`object`&&e&&!Array.isArray(e)?e:{}}function ua(e){let t=la(e.error),n=ia(t.code)||`UNKNOWN_ERROR`,r=ia(t.message)||`SafeShield API 요청을 처리하지 못했습니다.`;return new H(`SAFESHIELD_${n.toUpperCase()}`,r)}async function da(e,t={},n){let r=await Ht(ta,e,t,{...n?{signal:n}:{},priority:n?`low`:`normal`}),i=la(r);if(i.ok===!1)throw ua(i);return r}async function fa(e,t,n,r=t){try{return await U(na,e,t)}catch(e){if(e instanceof H&&[`RPC_PERMISSION_DENIED`,`UBUS_3`,`UBUS_6`].includes(e.code))return da(n,r);throw e}}function pa(){return{enabled:!1,collectorRunning:!1,available:!1,schemaVersion:0,volatile:!0,startedAt:0,updatedAt:0,retentionHours:0,totals:{queries:0,blocked:0},hourly:[],deviceLimit:0,devicesTruncated:!1,devices:[]}}function ma(e){let t=la(e);if(Object.keys(t).length===0)return pa();let n=la(t.schema),r=la(t.totals),i=Array.isArray(t.hourly)?t.hourly.map(e=>la(e)).map(e=>({bucketStart:aa(e.bucket_start),queries:aa(e.queries),blocked:aa(e.blocked)})).filter(e=>e.bucketStart>0).sort((e,t)=>e.bucketStart-t.bucketStart):[],a=Array.isArray(t.devices)?t.devices.map(e=>la(e)).map(e=>{let t=ia(e.ip),n=ia(e.mac);return{id:ia(e.id)||n||(t?`ip:${t}`:``),mac:n,ip:t,hostname:ia(e.hostname),identified:oa(e.identified),queries:aa(e.queries),blocked:aa(e.blocked)}}).filter(e=>e.id.length>0):[];return{enabled:oa(t.enabled),collectorRunning:oa(t.collector_running),available:t.available!==!1,schemaVersion:aa(n.version),volatile:t.volatile!==!1,startedAt:aa(t.started_at),updatedAt:aa(t.updated_at),retentionHours:aa(t.retention_hours),totals:{queries:aa(r.queries),blocked:aa(r.blocked)},hourly:i,deviceLimit:aa(t.device_limit),devicesTruncated:oa(t.devices_truncated),devices:a}}function ha(){return{available:!1,version:null,enabled:!1,active:!1,status:`unavailable`,stage:null,summary:{label:`Unavailable`,message:`SafeShield status API is unavailable`,severity:`warning`},runtime:{refreshdRunning:!1,dnsmasqRunning:!1,dnsRuntimeOk:!1,lastResult:null,lastErrorCode:null},entitlement:{plan:null,status:null},artifact:{resolved:!1,tier:null,version:null,uniqueDomains:0,rules:0},localOverrides:{enabled:!1,allowlistPath:null,blocklistPath:null},blocklist:{installed:!1,validLineCount:0,fileSizeKb:0,verificationOk:!1},health:{overall:`unavailable`},issueCounts:{warnings:0,errors:0},timestamps:{lastAttempt:0,lastSuccess:0,lastFailure:0,lastLocalApply:0,lastLocalApplyFailure:0,nextRefreshAt:0,refreshIntervalS:0}}}function ga(e){let t=la(e);if(Object.keys(t).length===0)return ha();let n=la(t.summary),r=la(t.runtime),i=la(t.entitlement),a=la(t.artifact),o=la(t.local_overrides),s=la(t.blocklist),c=la(t.health),l=la(t.timestamps);return{available:!0,version:ra(t.version,null),enabled:oa(t.enabled),active:oa(t.active),status:ra(t.status,`unknown`)??`unknown`,stage:ra(t.stage,null),summary:{label:ra(n.label,`Unknown`)??`Unknown`,message:ra(n.message,`SafeShield status is unknown`)??`SafeShield status is unknown`,severity:ra(n.severity,`warning`)??`warning`},runtime:{refreshdRunning:oa(r.refreshd_running),dnsmasqRunning:oa(r.dnsmasq_running),dnsRuntimeOk:oa(r.dns_runtime_ok),lastResult:ra(r.last_result,null),lastErrorCode:ra(r.last_error_code,null)},entitlement:{plan:ra(i.plan,null),status:ra(i.status,null)},artifact:{resolved:oa(a.resolved),tier:ra(a.tier,null),version:ra(a.version,null),uniqueDomains:aa(a.unique_domains),rules:aa(a.rules)},localOverrides:{enabled:oa(o.enabled),allowlistPath:ra(o.allowlist_path,null),blocklistPath:ra(o.blocklist_path,null)},blocklist:{installed:oa(s.installed),validLineCount:aa(s.valid_line_count),fileSizeKb:aa(s.file_size_kb),verificationOk:oa(s.verification_ok)},health:{overall:ra(c.overall,`unknown`)??`unknown`},issueCounts:{warnings:sa(t.warnings),errors:sa(t.errors)},timestamps:{lastAttempt:aa(l.last_attempt),lastSuccess:aa(l.last_success),lastFailure:aa(l.last_failure),lastLocalApply:aa(l.last_local_apply),lastLocalApplyFailure:aa(l.last_local_apply_failure),nextRefreshAt:aa(l.next_refresh_at),refreshIntervalS:aa(l.refresh_interval_s)}}}function _a(e){let t=la(la(e).values);return{enabled:oa(t.enabled),applyLocalOverrides:oa(t.apply_local_overrides)}}function va(e){let t=la(e);return{requested:oa(t.requested),accepted:oa(t.accepted),reason:ia(t.reason)}}async function ya(){try{return ma(await da(`statistics`))}catch(e){if(e instanceof H&&[`UBUS_3`,`UBUS_4`,`UBUS_5`].includes(e.code))return pa();throw e}}async function ba(e){let t=await fa(`safeshield_statistics_update`,{enabled:e},`config_update`,{values:{statistics_enabled:e}});return{changed:Array.isArray(t.changed)?t.changed.some(e=>e===`statistics_enabled`):!1,enabled:e,restarted:oa(t.restarted),reconciled:oa(t.reconciled)}}async function xa(){try{return ga(await da(`status`))}catch(e){if(e instanceof H&&[`UBUS_3`,`UBUS_4`,`UBUS_5`].includes(e.code))return ha();throw e}}async function Sa(e){return _a(await da(`config`,{},e))}async function Ca(e){let t=await fa(`safeshield_set_enabled`,{enabled:e},`set_enabled`);return{changed:oa(t.changed),accepted:oa(t.accepted),targetEnabled:oa(t.target_enabled),reconciled:oa(t.reconciled)}}async function wa(){let e=await fa(`safeshield_refresh`,{},`refresh`);return{accepted:oa(e.accepted),reason:ia(e.reason),status:ga(e.status)}}async function Ta(e){let[t,n]=await Promise.all([da(`rules_list`,{},e),Sa(e)]),r=ca(t.allow),i=ca(t.block);return{allow:r,block:i,counts:{allow:r.length,block:i.length,total:r.length+i.length},safeshieldEnabled:n.enabled,applyLocalOverrides:n.applyLocalOverrides}}async function Ea(e,t,n,r=!0){let i=await fa(e===`rule_add`?`safeshield_rule_add`:`safeshield_rule_delete`,{action:t,domain:n,refresh:r},e),a=oa(e===`rule_add`?i.added:i.deleted);return{action:t,domain:ia(i.domain)||n,changed:a,refresh:va(i.refresh),rules:await Ta()}}function Da(e,t,n=!0){return Ea(`rule_add`,e,t,n)}function Oa(e,t,n=!0){return Ea(`rule_delete`,e,t,n)}var ka=4500;function Aa(e,t){let[n,r]=L({action:null,error:null,message:null}),i=z([]),a=z([]),o=z(null),s=B(()=>{for(let e of i.current)window.clearTimeout(e);i.current=[]},[]),c=B(()=>{for(let e of a.current)window.clearTimeout(e);a.current=[]},[]),l=B(()=>{o.current!==null&&(window.clearTimeout(o.current),o.current=null)},[]),u=B(e=>{l(),r({action:e,error:null,message:null})},[l]),d=B(e=>{l(),r({action:null,error:null,message:e}),o.current=window.setTimeout(()=>{o.current=null,r(e=>e.error!==null||e.action!==null?e:{...e,message:null})},ka)},[l]),f=B(t=>{s(),i.current=t.map(t=>window.setTimeout(()=>{e()},t))},[s,e]),p=B(e=>{c(),a.current=e.map(e=>window.setTimeout(()=>{t()},e))},[c,t]);R(()=>()=>{s(),c(),l()},[l,c,s]);let m=B(async t=>{u(t?`enable`:`disable`);try{let n=await Ca(t);d(n.changed?t?`SafeShield 보호 활성화 요청을 적용했습니다.`:`SafeShield 보호 비활성화 요청을 적용했습니다.`:t?`SafeShield 보호가 이미 활성화 상태입니다.`:`SafeShield 보호가 이미 비활성화 상태입니다.`),await e(),f([800,2e3,5e3])}catch(e){r({action:null,error:q(e,`SafeShield 작업을 수행하지 못했습니다.`),message:null})}},[u,e,f,d]),h=B(async e=>{u(e?`statistics-enable`:`statistics-disable`);try{let n=await ba(e);await t(),d(n.changed?n.reconciled?e?`차단 통계 수집을 활성화했습니다.`:`차단 통계 수집을 비활성화했습니다.`:`차단 통계 설정을 저장했습니다.`:e?`차단 통계 수집이 이미 활성화되어 있습니다.`:`차단 통계 수집이 이미 비활성화되어 있습니다.`),n.changed&&p([500,1500])}catch(e){r({action:null,error:q(e,`차단 통계 설정을 변경하지 못했습니다.`),message:null})}},[u,t,p,d]),g=B(async()=>{u(`refresh`);try{await wa(),r({action:null,error:null,message:null}),f([700,2500,6e3,12e3])}catch(e){r({action:null,error:q(e,`SafeShield 작업을 수행하지 못했습니다.`),message:null})}},[u,f]),_=B(()=>{l(),r(e=>({...e,error:null,message:null}))},[l]);return{...n,dismissFeedback:_,refreshBlocklist:g,setEnabled:m,setStatisticsEnabled:h}}var ja=500,Ma=45e3;function Na(e){return new Promise(t=>window.setTimeout(t,e))}function Pa(e){return e===`allow`?`허용`:`차단`}function Fa(e,t){if(!t.changed)return e===`add`?t.action===`allow`?`이미 허용 목록에 있는 도메인입니다.`:`이미 차단 목록에 있는 도메인입니다.`:`이미 삭제된 규칙입니다.`;let n=Pa(t.action),r=e===`add`?`저장`:`삭제`;return t.rules.safeshieldEnabled?t.rules.applyLocalOverrides?`${t.domain} ${n} 규칙을 ${r}했습니다.`:`${t.domain} ${n} 규칙을 ${r}했습니다. 로컬 규칙 적용 설정이 꺼져 있어 아직 DNS에는 반영되지 않습니다.`:`${t.domain} ${n} 규칙을 ${r}했습니다. SafeShield를 켜면 다음 갱신에서 적용됩니다.`}function Ia(e,t){let n=e===`add`?`추가`:`삭제`;return`${t.domain} ${Pa(t.action)} 규칙 ${n}를 저장했습니다. 로컬 규칙을 DNS에 적용하고 있습니다…`}function La(e,t){let n=e===`add`?`추가`:`삭제`;return`${t.domain} ${Pa(t.action)} 규칙 ${n}와 DNS 적용이 완료되었습니다.`}async function Ra(e){let t=Date.now()+Ma,n=null;for(;Date.now()e.timestamps.lastLocalApply)return{kind:`success`,status:n};if(n.timestamps.lastLocalApplyFailure>e.timestamps.lastLocalApplyFailure)return{kind:`failure`,status:n};await Na(ja)}return{kind:`timeout`,status:n}}function za(e){let t=li({active:e,fallbackError:`사용자 규칙을 불러오지 못했습니다.`,loader:Ta}),[n,r]=L({action:null,error:null,feedback:null}),i=z(0),a=B(async(e,n,a,o)=>{let s=++i.current,c=e=>{i.current===s&&r(e)};c({action:{operation:e,action:n,domain:a},error:null,feedback:null});try{let r=await xa(),i=e===`add`?await Da(n,a,!0):await Oa(n,a,!0);if(t.replaceData(i.rules),o?.(),!(i.changed&&i.rules.safeshieldEnabled&&i.rules.applyLocalOverrides))return c({action:null,error:null,feedback:Fa(e,i)}),!0;if(!i.refresh.accepted)return c({action:null,error:`규칙은 저장되었지만 로컬 DNS 적용 요청을 시작하지 못했습니다${i.refresh.reason?` (${i.refresh.reason})`:``}.`,feedback:null}),!0;c({action:null,error:null,feedback:Ia(e,i)});let s;try{s=await Ra(r)}catch{return c({action:null,error:`규칙은 저장되었지만 DNS 적용 상태를 확인하지 못했습니다. SafeShield 상태를 확인해 주세요.`,feedback:null}),!0}return s.kind===`success`?(c({action:null,error:null,feedback:La(e,i)}),!0):s.kind===`disabled`?(c({action:null,error:null,feedback:`규칙은 저장되었지만 SafeShield가 꺼져 있어 DNS 적용을 대기하고 있습니다.`}),!0):s.kind===`failure`?(c({action:null,error:`규칙은 저장되었지만 로컬 DNS 적용이 실패했습니다. SafeShield 상태를 확인해 주세요.`,feedback:null}),!0):(c({action:null,error:`규칙은 저장되었지만 제한 시간 안에 로컬 DNS 적용 완료를 확인하지 못했습니다.`,feedback:null}),!0)}catch(e){return c({action:null,error:q(e,`사용자 규칙을 변경하지 못했습니다.`),feedback:null}),!1}},[t.replaceData]),o=B((e,t,n)=>a(`add`,e,t,n),[a]),s=B((e,t)=>a(`delete`,e,t),[a]),c=B(async()=>{r(e=>({...e,error:null})),await t.refresh()},[t.refresh]),l=B(()=>{r(e=>({...e,feedback:null}))},[]);return{...t,...n,error:n.error??t.error,addRule:o,deleteRule:s,dismissFeedback:l,refresh:c}}var Ba=6e4;function Va(e,t=!0){return li({active:e,fallbackError:`SafeShield 통계를 불러오지 못했습니다.`,loader:ya,...t?{pollInterval:Ba}:{}})}var Ha=[{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`]}],Ua=new Map;Ha.forEach((e,t)=>{e.stages.forEach(e=>Ua.set(e,t))});function Wa(e,t){return e===`running`||t!==null&&Ua.has(t)}function Ga(e){let t=e===null?0:Ua.get(e)??0,n=Ha[t];return{description:n.description,label:n.label,number:t+1,total:Ha.length}}function Ka(e){return(e??``).trim().toLowerCase()}function qa(e,t){let n=Ka(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=Ga(t);return r.number===2?`최신 차단 목록 정보를 확인하는 중 문제가 발생했습니다. 인터넷 연결을 확인한 뒤 다시 갱신해 주세요.`:r.number===3?`차단 목록을 다운로드하는 중 문제가 발생했습니다. 인터넷 연결을 확인한 뒤 다시 갱신해 주세요.`:r.number===4?`사용자 규칙을 적용하는 중 문제가 발생했습니다. 사용자 규칙을 확인한 뒤 다시 갱신해 주세요.`:r.number===5?`새 보호 규칙을 적용하는 중 문제가 발생했습니다. 잠시 후 다시 갱신해 주세요.`:r.number===6?`새 차단 목록의 보호 상태를 확인하지 못했습니다. 다시 갱신해 주세요.`:`차단 목록 갱신을 완료하지 못했습니다. 잠시 후 다시 갱신해 주세요.`}var Ja=3e3;function Ya(e){return e?Wa(e.status,e.stage):!1}function Xa(e){return li({active:e,fallbackError:`SafeShield 상태를 불러오지 못했습니다.`,loader:xa,pollInterval:e=>Ya(e)?Ja:null})}var Za=3e5,Qa=1e3,$a=3e3,eo=`luci-app-smartsafehub`;function to(e=!0){let[t,n]=L(null),r=li({active:e||t===`checking`||t===`installing`,fallbackError:`소프트웨어 업데이트 상태를 불러오지 못했습니다.`,loader:hr,pollInterval:e=>e?.phase===`checking`?Qa:e?.phase===`installing`?$a:Za,refreshOnFocus:!0,refreshOnVisible:t===`checking`||t===`installing`});R(()=>{n(r.data?.phase??null)},[r.data?.phase]);let[i,a]=L(null),[o,s]=L(null),[c,l]=L(null),u=z(!1),d=z(void 0);R(()=>{if(u.current||!r.data)return;let e=r.data.lastInstallAt??null,t=d.current;if(t===void 0){d.current=e;return}if(d.current=e,e===null||e===t||r.data.phase!==`idle`)return;let n=r.data.packages.find(e=>e.name===eo)?.installedVersion,i=window.__SMARTHUB_BOOTSTRAP__?.assetVersion;n&&i&&n!==i&&(u.current=!0,window.location.reload())},[r.data]);let f=B(e=>{n(e),r.data&&r.replaceData({...r.data,phase:e,lastError:null})},[r]),p=B(async()=>{a(`check`),s(null),l(null);try{if(!(await Sr()).accepted)throw Error(`업데이트 확인 요청이 접수되지 않았습니다.`);f(`checking`),a(null),window.setTimeout(()=>void r.refresh(),500)}catch(e){a(null),s(q(e,`업데이트 확인을 시작하지 못했습니다.`))}},[f,r]),m=B(async()=>{a(`install`),s(null),l(null);try{if(!(await Cr()).accepted)throw Error(`업데이트 설치 요청이 접수되지 않았습니다.`);f(`installing`),a(null),window.setTimeout(()=>void r.refresh(),750)}catch(e){a(null),s(q(e,`업데이트 설치를 시작하지 못했습니다.`))}},[f,r]),h=B(async e=>{a(`settings`),s(null),l(null);try{let t=await wr(e);return r.data&&r.replaceData({...r.data,settings:t}),a(null),l(`자동 업데이트 설정을 저장했습니다.`),!0}catch(e){return a(null),s(q(e,`자동 업데이트 설정을 저장하지 못했습니다.`)),!1}},[r]),g=B(()=>{s(null),l(null)},[]);return{...r,action:i,actionError:o,message:c,check:p,dismissFeedback:g,install:m,saveSettings:h}}var no=6e4;function ro(e){return li({active:e,fallbackError:`장치 상태를 불러오지 못했습니다.`,loader:Xn,pollInterval:no})}function io(e){let t=li({active:e,fallbackError:`예약 재부팅 설정을 불러오지 못했습니다.`,loader:kr,refreshOnFocus:!0}),[n,r]=L({saving:!1,saveError:null,saveMessage:null}),i=B(async e=>{r({saving:!0,saveError:null,saveMessage:null});try{let n=await Ar(e);return t.replaceData(n),r({saving:!1,saveError:null,saveMessage:e.enabled?`예약 재부팅 일정을 저장했습니다.`:`예약 재부팅을 껐습니다.`}),!0}catch(e){return r({saving:!1,saveError:q(e,`예약 재부팅 설정을 저장하지 못했습니다.`),saveMessage:null}),!1}},[t.replaceData]),a=B(()=>{r(e=>({...e,saveError:null,saveMessage:null}))},[]);return{...t,...n,dismissSaveFeedback:a,saveSettings:i}}function ao(e){return e.trim().toLowerCase().replace(/[^a-z0-9._-]+/g,`-`)||`openwrt`}function oo(e,t){let n=new Date(t*1e3).toISOString().replace(/[:.]/g,`-`);return`smartsafehub-diagnostics-${ao(e)}-${n}.json`}function so(e,t){let n=new Blob([`${JSON.stringify(t,null,2)}\n`],{type:`application/json;charset=utf-8`}),r=URL.createObjectURL(n),i=document.createElement(`a`);i.href=r,i.download=e,i.style.display=`none`,(document.body??document.documentElement).appendChild(i),i.click(),i.remove(),window.setTimeout(()=>URL.revokeObjectURL(r),1e3)}function co(e){return{enabled:e.available?e.enabled:null,status:e.status,lastResult:e.runtime.lastResult,validLineCount:e.blocklist.validLineCount,blocklistInstalled:e.blocklist.installed,dnsmasqOk:e.runtime.dnsRuntimeOk}}function lo(){return{networks:[],totalClients:0,guest:null}}function uo(){return{enabled:null,status:`unavailable`,lastResult:null,validLineCount:0,blocklistInstalled:!1,dnsmasqOk:!1}}async function fo(e){if(!e)throw Error(`시스템 상태가 준비되지 않았습니다. 새로고침한 뒤 다시 시도해 주세요.`);let[t,n,r]=await Promise.allSettled([Gr(),xa(),rr()]);return{generatedAt:Math.floor(Date.now()/1e3),health:r.status===`fulfilled`?r.value:null,system:e,wifi:t.status===`fulfilled`?t.value:lo(),safeshield:n.status===`fulfilled`?co(n.value):uo()}}function po(e){let[t,n]=L({action:null,error:null,message:null,rebootAccepted:!1}),r=B(async()=>{n(e=>({...e,action:`diagnostics`,error:null,message:null}));try{let t=await fo(e);so(oo(t.system.device.hostname,t.generatedAt),t),n(e=>({...e,action:null,message:`진단 정보 파일을 만들었습니다.`}))}catch(e){n(t=>({...t,action:null,error:q(e,`시스템 작업을 완료하지 못했습니다.`)}))}},[e]),i=B(async()=>{n(e=>({...e,action:`reboot`,error:null,message:null}));try{let e=await Pr();n({action:null,error:null,message:e.accepted?`재부팅을 시작했습니다. 잠시 후 공유기에 다시 연결해 주세요.`:`재부팅 요청이 접수되지 않았습니다.`,rebootAccepted:e.accepted})}catch(e){n(t=>({...t,action:null,error:q(e,`시스템 작업을 완료하지 못했습니다.`)}))}},[]),a=B(()=>{n(e=>({...e,error:null,message:null}))},[]);return{...t,dismissFeedback:a,downloadDiagnostics:r,reboot:i}}function mo(e){let t=li({active:e,fallbackError:`시간대 설정을 불러오지 못했습니다.`,loader:Tr,refreshOnFocus:!0}),[n,r]=L({saving:!1,syncing:!1,saveError:null,saveMessage:null}),i=B(async e=>{r({saving:!0,syncing:!1,saveError:null,saveMessage:null});try{let n=await Dr(e);return t.replaceData(n),r({saving:!1,syncing:!1,saveError:null,saveMessage:`시간대를 저장하고 시스템 시간 설정에 적용했습니다.`}),!0}catch(e){return r({saving:!1,syncing:!1,saveError:q(e,`시간대 설정을 저장하지 못했습니다.`),saveMessage:null}),!1}},[t.replaceData]),a=B(async()=>{r({saving:!1,syncing:!0,saveError:null,saveMessage:null});try{await Or(),await new Promise(e=>{window.setTimeout(e,1500)});let e=await Tr();return t.replaceData(e),r({saving:!1,syncing:!1,saveError:null,saveMessage:`NTP 시간 동기화를 요청하고 장치 시간을 다시 확인했습니다.`}),!0}catch(e){return r({saving:!1,syncing:!1,saveError:q(e,`시간을 동기화하지 못했습니다.`),saveMessage:null}),!1}},[t.replaceData]),o=B(()=>{r(e=>({...e,saveError:null,saveMessage:null}))},[]);return{...t,...n,dismissSaveFeedback:o,saveTimezone:i,syncTime:a}}var ho=5e3,go=5e3;function _o(e){if(!e?.pairingCode||e.accountRegistered===!0)return!1;if(!e.pairingExpiresAt)return!0;let t=Date.parse(e.pairingExpiresAt);return!Number.isFinite(t)||t>Date.now()}function vo(e){let[t,n]=L(null),[r,i]=L(null),[a,o]=L(null),[s,c]=L(e),[l,u]=L(!1),[d,f]=L(!1),p=z(null),m=z(0),h=z(!0),g=B((e,t)=>!h.current||t!==m.current?!1:(n(e),i(null),c(!1),!0),[]),_=B(()=>{if(!e)return Promise.resolve(null);if(p.current)return p.current;let t=++m.current;o(null),u(!0);let n=(async()=>{try{let e=await tr();return g(e,t)&&o(null),e}catch{let e=await er().catch(()=>null);return e&&g(e,t),h.current&&t===m.current&&o(`SmartSafeHub 서버의 최신 상태 확인이 지연되고 있습니다.`),e}finally{h.current&&u(!1),p.current=null}})();return p.current=n,n},[e,g]),v=B(async()=>{let e=++m.current;f(!0);try{let t=await nr();return g(t,e)&&o(null),t}finally{f(!1)}},[g]);R(()=>{if(!e)return;let t=!0;h.current=!0;let n=++m.current;return c(!0),i(null),o(null),er().then(e=>{t&&(g(e,n)&&c(!1),_())}).catch(()=>{t&&(n===m.current&&(i(`기기 등록 상태를 확인하지 못했습니다.`),c(!1)),_())}),()=>{t=!1,m.current+=1}},[e,_,g]),R(()=>{if(!e)return;let t=!0,n=window.setInterval(()=>{if(document.visibilityState===`hidden`||p.current)return;let e=++m.current;er().then(n=>{t&&g(n,e)&&o(null)}).catch(()=>{})},go);return()=>{t=!1,window.clearInterval(n)}},[e,g]),R(()=>()=>{h.current=!1},[]);let y=Xe(()=>_o(t)||t?.pairingOperation?.phase===`starting`||t?.pairingOperation?.phase===`running`,[t]);return R(()=>{if(!e||!y)return;let t=!0,n=0,r=async()=>{let e=++m.current,i=await er().catch(()=>null);t&&i&&g(i,e),t&&(n=window.setTimeout(()=>void r(),ho))};return n=window.setTimeout(()=>void r(),ho),()=>{t=!1,window.clearTimeout(n)}},[e,g,y]),{data:t,error:r,loading:s,pairingBusy:d,refresh:_,refreshing:l,requestPairing:v,syncError:a}}var yo=6e4;function bo(e,t){let[n,r]=L(null),i=z(0);return R(()=>{e&&t!==null&&(i.current+=1,r(t))},[e,t]),R(()=>{if(e)return;let t=!1,n=null,a=!1,o=0,s=async(e=!1)=>{if(t||document.visibilityState===`hidden`||a||!e&&Date.now()-oe??null)}finally{a=!1}},c=()=>{document.visibilityState===`visible`&&s()},l=()=>void s();return s(!0),n=window.setInterval(()=>void s(),yo),document.addEventListener(`visibilitychange`,c),window.addEventListener(`focus`,l),()=>{t=!0,i.current+=1,n!==null&&window.clearInterval(n),document.removeEventListener(`visibilitychange`,c),window.removeEventListener(`focus`,l)}},[e]),n}var xo=[2e3,3e3];function So(e){return new Promise(t=>window.setTimeout(t,e))}function Co(e){let t=li({active:e,fallbackError:`Wi-Fi 설정을 처리하지 못했습니다.`,loader:Gr}),n=z(!0),r=z(e),[i,a]=L({updatingSection:null,feedback:null});R(()=>(n.current=!0,()=>{n.current=!1}),[]),R(()=>{r.current=e},[e]);let o=B(async()=>{for(let e of xo){if(await So(e),!n.current||!r.current)return;await t.refresh()}},[t.refresh]),s=B(async e=>{a({updatingSection:e.section,feedback:null});try{let n=await Kr(e);return t.replaceData({...n.summary,guest:t.data?.guest??null,guestError:t.data?.guestError??!1}),a({updatingSection:null,feedback:{kind:`success`,message:n.changed?`Wi-Fi 설정을 저장하고 무선 네트워크를 다시 불러왔습니다.`:`변경된 Wi-Fi 설정이 없습니다.`}}),n.reloaded&&o(),!0}catch(e){let t=e instanceof H&&[`NETWORK_ERROR`,`RPC_TIMEOUT`].includes(e.code);return a({updatingSection:null,feedback:{kind:t?`warning`:`error`,message:t?`Wi-Fi가 다시 시작되면서 연결이 끊어졌을 수 있습니다. 변경한 SSID로 다시 연결한 뒤 화면을 새로고침해 주세요.`:q(e,`Wi-Fi 설정을 처리하지 못했습니다.`)}}),!1}},[o,t.replaceData,t.data]),c=B(async e=>{a({updatingSection:`ssh_guest`,feedback:null});try{let n=await qr(e);return await t.refresh(),a({updatingSection:null,feedback:{kind:`success`,message:n.changed?`게스트 Wi-Fi 설정이 저장되었습니다.`:`변경된 게스트 Wi-Fi 설정이 없습니다.`}}),n.reloaded&&o(),!0}catch(e){let t=e instanceof H&&[`NETWORK_ERROR`,`RPC_TIMEOUT`].includes(e.code);return a({updatingSection:null,feedback:{kind:t?`warning`:`error`,message:t?`무선 네트워크가 다시 시작되어 연결이 끊어졌을 수 있습니다. 다시 연결한 뒤 확인해 주세요.`:q(e,`게스트 Wi-Fi 설정을 처리하지 못했습니다.`)}}),!1}},[t.refresh,o]),l=B(()=>{a(e=>({...e,feedback:null}))},[]);return{...t,...i,update:s,updateGuest:c,dismissFeedback:l}}function J({action:e,description:t,eyebrow:n,id:r,title:i}){return W(`div`,{class:`ssh-section-heading mb-4 min-w-0`,children:W(`div`,{class:e?`grid min-w-0 items-start gap-x-4 gap-y-3 sm:grid-cols-[minmax(0,1fr)_auto]`:``,children:[W(`div`,{class:`min-w-0`,children:[W(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.18em] text-teal-700`,children:n}),W(`h2`,{class:`mt-2 mb-0 min-w-0 text-xl font-black tracking-tight text-slate-950`,id:r,children:i}),W(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500`,children:t})]}),e?W(`div`,{class:`min-w-0 justify-self-start sm:mt-3 sm:justify-self-end`,children:e}):null]})})}var wo=new Intl.NumberFormat(`ko-KR`),To=new Intl.DateTimeFormat(`ko-KR`,{dateStyle:`medium`,timeStyle:`short`});function Eo(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 Do(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?Ao(e):jo(e,t)}function No(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 Po(e,t){if(!Number.isFinite(e)||e<=0)return`확인 불가`;let n=t.replaceAll(` `,`_`);try{return new Intl.DateTimeFormat(`ko-KR`,{dateStyle:`medium`,timeStyle:`medium`,timeZone:n}).format(new Date(e*1e3))}catch{return To.format(new Date(e*1e3))}}function Fo(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 Io(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 Lo(e,t){let n=e.metadata[t];return typeof n==`string`&&n.trim().length>0?n.trim():null}function Ro(e,t){let n=e.metadata[t];return typeof n==`number`&&Number.isFinite(n)?n:null}function zo(e,t){let n=e.metadata[t];return typeof n==`boolean`?n:null}function Bo(e,t){let n=Lo(e,`error_code`);return n?`${t} (오류 코드: ${n})`:t}function Vo(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 Ho(e){let t=Lo(e,`from_version`),n=Lo(e,`to_version`);return t&&n?`${t} → ${n}`:n?`${n} 버전이 적용되었습니다.`:null}function Uo(e){return e?e.toUpperCase():null}function Wo(e){switch(e){case`dhcp`:return`자동 IP(DHCP)`;case`pppoe`:return`PPPoE`;case`static`:return`고정 IPv4`;default:return e.toUpperCase()}}function Go(e){switch(e.eventType){case`system.booted`:return{title:`공유기 부팅 감지`,description:`공유기가 켜지거나 재시작된 것을 확인했습니다.`};case`network.internet.disconnected`:return{title:`인터넷 연결 끊김 감지`,description:`공유기에서 인터넷 연결을 확인할 수 없었습니다.`};case`network.internet.recovered`:{let t=Ro(e,`downtime_seconds`);return{title:`인터넷 연결 복구`,description:t!==null&&t>0?`약 ${Vo(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=Ro(e,`domain_count`),n=Lo(e,`artifact_version`),r=[];return t!==null&&t>0&&r.push(`${Y(t)}개 도메인 적용`),n&&r.push(`목록 ${n}`),{title:`SafeShield 차단 목록 업데이트 완료`,description:r.length>0?r.join(` · `):`최신 차단 목록을 적용했습니다.`}}case`safeshield.blocklist.update_failed`:return{title:`SafeShield 차단 목록 업데이트 실패`,description:Bo(e,`차단 목록을 업데이트하지 못했습니다.`)};case`safeshield.statistics.enabled`:return{title:`SafeShield 차단 통계 수집 활성화`,description:`기기별 차단 통계 수집을 활성화했습니다.`};case`safeshield.statistics.disabled`:return{title:`SafeShield 차단 통계 수집 비활성화`,description:`기기별 차단 통계 수집을 비활성화했습니다.`};case`safeshield.rule.added`:return{title:Lo(e,`rule_action`)===`allow`?`사용자 허용 규칙 추가`:`사용자 차단 규칙 추가`,description:`SafeShield 사용자 규칙이 변경되었습니다.`};case`safeshield.rule.removed`:return{title:Lo(e,`rule_action`)===`allow`?`사용자 허용 규칙 삭제`:`사용자 차단 규칙 삭제`,description:`SafeShield 사용자 규칙이 변경되었습니다.`};case`network_watch.anomaly.warning`:{let t=Lo(e,`source_ip`),n=Ro(e,`connections`);return{title:`Network Watch 통신 증가 감지`,description:`${t?`${t} 기기에서 `:`연결된 기기에서 `}외부 연결이 연속해서 높은 수준으로 관찰되었습니다${n===null?``:` (동시 연결 ${Y(n)}개)`}. 악성 통신으로 확정된 것은 아닙니다.`}}case`settings.netwatch.enabled`:return{title:`Network Watch 켜짐`,description:`네트워크 연결 관찰 기능을 활성화했습니다.`};case`settings.netwatch.disabled`:return{title:`Network Watch 꺼짐`,description:`네트워크 연결 관찰 기능을 비활성화했습니다.`};case`settings.scheduled_reboot.updated`:{let t=zo(e,`enabled`);return{title:`예약 재부팅 설정 변경`,description:t===!0?`예약 재부팅 설정을 저장하고 새 일정에 적용했습니다.`:t===!1?`예약 재부팅을 비활성화했습니다.`:`예약 재부팅 설정을 변경했습니다.`}}case`settings.software_updates.updated`:return{title:`관리 소프트웨어 업데이트 설정 변경`,description:zo(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=Lo(e,`zonename`);return{title:`시간대 설정 변경`,description:t?`공유기 시간대를 ${t}(으)로 변경했습니다.`:`공유기 시간대를 변경했습니다.`}}case`settings.wifi.updated`:return{title:`Wi-Fi 설정 변경`,description:`Wi-Fi 설정을 저장하고 무선 네트워크에 적용했습니다.`};case`settings.wan.updated`:{let t=Lo(e,`protocol`);return{title:`인터넷 설정 변경`,description:t?`${Wo(t)} 방식으로 WAN 설정을 변경했습니다.`:`WAN 인터넷 연결 설정을 변경했습니다.`}}case`settings.lan.updated`:return{title:`LAN 설정 변경`,description:`내부 네트워크 및 DHCP 설정을 변경했습니다.`};case`settings.iptv.updated`:{let t=zo(e,`enabled`),n=Lo(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:Ho(e)};case`software.update.failed`:return{title:`관리 소프트웨어 업데이트 실패`,description:Bo(e,`관리 소프트웨어를 업데이트하지 못했습니다.`)};case`firmware.update.started`:return{title:`펌웨어 업데이트 시작`,description:`펌웨어 설치를 시작했습니다. 완료 과정에서 공유기가 재부팅됩니다.`};case`firmware.update.failed`:return{title:`펌웨어 업데이트 실패`,description:Bo(e,`펌웨어 설치 준비 또는 검증에 실패했습니다.`)};case`license.activated`:{let t=Uo(Lo(e,`to_plan`));return{title:`요금제 활성화`,description:t?`${t} 요금제가 활성화되었습니다.`:`요금제가 활성화되었습니다.`}}case`license.changed`:{let t=Uo(Lo(e,`from_plan`)),n=Uo(Lo(e,`to_plan`));return{title:`요금제 변경`,description:t&&n?`${t} → ${n}`:n?`${n} 요금제로 변경되었습니다.`:`요금제 상태가 변경되었습니다.`}}case`license.cleared`:return{title:`계정 권한 해제`,description:Lo(e,`reason`)===`manual`?`이 공유기에서 계정 권한 연결을 해제했습니다.`:`서버의 계정 권한 상태가 반영되어 이 공유기의 권한 연결이 해제되었습니다.`};case`health.issue.started`:{let t=Ro(e,`warning_count`)??0,n=Ro(e,`critical_count`)??0,r=[];return n>0&&r.push(`이상 ${Y(n)}건`),t>0&&r.push(`주의 ${Y(t)}건`),{title:`확인이 필요한 장치 상태 발견`,description:r.length>0?r.join(` · `):`장치 상태에서 확인이 필요한 항목이 발견되었습니다.`}}case`health.issue.changed`:{let t=Ro(e,`warning_count`)??0,n=Ro(e,`critical_count`)??0,r=[];return n>0&&r.push(`이상 ${Y(n)}건`),t>0&&r.push(`주의 ${Y(t)}건`),{title:`확인이 필요한 장치 상태 변경`,description:r.length>0?r.join(` · `):`확인이 필요한 장치 상태 항목이 변경되었습니다.`}}case`health.issue.resolved`:return{title:`장치 상태 정상으로 복구`,description:`이전에 확인이 필요했던 장치 상태가 정상으로 돌아왔습니다.`};default:return{title:`SmartSafeHub 활동`,description:`장치에서 새로운 활동이 기록되었습니다.`}}}function Ko(e){return e===`error`?{icon:W(Cn,{class:`size-4`}),surface:`bg-rose-50 text-rose-700 ring-rose-200`}:e===`warning`?{icon:W(Cn,{class:`size-4`}),surface:`bg-amber-50 text-amber-800 ring-amber-200`}:e===`success`?{icon:W(Sn,{class:`size-4`}),surface:`bg-emerald-50 text-emerald-700 ring-emerald-200`}:{icon:W(dn,{class:`size-4`}),surface:`bg-slate-100 text-slate-600 ring-slate-200`}}function qo(e){let t=new Date(e*1e3);return new Intl.DateTimeFormat(`ko-KR`,{hour:`2-digit`,minute:`2-digit`,hour12:!1}).format(t)}function Jo(e){let t=new Date(e*1e3);return`${t.getFullYear()}-${t.getMonth()+1}-${t.getDate()}`}function Yo(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 Xo({compact:e=!1,events:t}){if(t.length===0)return W(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 px-4 py-5 text-center`,children:[W(dn,{class:`mx-auto size-5 text-slate-400`}),W(`p`,{class:`mt-2 mb-0 text-sm font-extrabold text-slate-700`,children:`아직 기록된 활동이 없습니다.`}),W(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`상태가 실제로 변경되면 이곳에 시간순으로 기록됩니다.`})]});let n=``;return W(`div`,{class:e?`space-y-2`:`space-y-1`,children:t.map(t=>{let r=Go(t),i=Ko(t.severity),a=Jo(t.occurredAt),o=!e&&a!==n;return n=a,W(`div`,{children:[o?W(`p`,{class:`mt-5 mb-2 first:mt-0 text-xs font-black text-slate-500`,children:Yo(t.occurredAt)}):null,W(`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:[W(`div`,{class:`w-12 shrink-0 pt-1 text-xs font-bold tabular-nums text-slate-400`,children:W(`time`,{dateTime:new Date(t.occurredAt*1e3).toISOString(),title:Ao(t.occurredAt),children:qo(t.occurredAt)})}),W(`span`,{class:`mt-0.5 grid size-8 shrink-0 place-items-center rounded-full ring-1 ring-inset ${i.surface}`,children:i.icon}),W(`div`,{class:`min-w-0 flex-1`,children:[W(`p`,{class:`m-0 text-sm font-black leading-5 text-slate-950`,children:r.title}),r.description?W(`p`,{class:`mt-1 mb-0 break-words text-xs font-medium leading-5 text-slate-500`,children:r.description}):null]})]})]},t.eventId)})})}function Zo({error:e,loading:t,onRetry:n}){return t?W(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 px-4 py-5 text-center`,children:[W(pn,{class:`mx-auto size-5 animate-spin text-teal-700`}),W(`p`,{class:`mt-2 mb-0 text-sm font-bold text-slate-500`,children:`최근 활동을 불러오는 중입니다.`})]}):e?W(`div`,{class:`rounded-xl border border-amber-200 bg-amber-50/70 px-4 py-4`,children:W(`div`,{class:`flex items-start gap-3`,children:[W(Cn,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),W(`div`,{class:`min-w-0 flex-1`,children:[W(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:`최근 활동을 불러오지 못했습니다.`}),W(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:e}),n?W(`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 Qo(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 $o(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 es({actionError:e,actionMessage:t,cloud:n,onDismissFeedback:r,onSetEnabled:i,saving:a}){let o=Qo(n),s=n.plan?.toUpperCase()??null,c=n.eligible===!0,l=n.enabled||c;return W(`section`,{"aria-label":`Cloud 활동 기록 설정`,class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:[W(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[W(`div`,{class:`min-w-0 flex-1`,children:[W(`div`,{class:`flex flex-wrap items-center gap-2`,children:W(`span`,{class:`rounded-full px-2.5 py-1 text-xs font-extrabold ring-1 ring-inset ${o.tone}`,children:a?`설정 중`:o.label})}),W(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:o.description})]}),W(`div`,{class:`flex shrink-0 items-center justify-end gap-3 sm:justify-start`,children:[W(`span`,{class:`text-xs font-bold text-slate-600`,children:[`Cloud 전송 `,n.enabled?`켜짐`:`꺼짐`]}),W(`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:W(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${n.enabled?`left-6`:`left-1`}`})})]})]}),(e||t)&&W(`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:[W(`span`,{class:`min-w-0`,children:e||t}),W(`button`,{class:`shrink-0 rounded-lg px-2 py-1 text-xs font-extrabold hover:bg-black/5`,onClick:r,type:`button`,children:`닫기`})]}),n.enabled?W(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`Cloud 전송을 끄면 아직 전송하지 않은 대기 기록이 삭제됩니다. 공유기의 로컬 기록은 유지됩니다.`}):null,W(`div`,{class:`mt-4 flex flex-wrap items-center gap-x-5 gap-y-1 border-t border-slate-200 pt-3 text-xs font-semibold text-slate-500`,children:[W(`span`,{children:[`마지막 동기화: `,n.lastSuccessAt>0?jo(n.lastSuccessAt):`아직 없음`]}),W(`span`,{children:[`Cloud 보관: `,$o(n)]}),n.pendingEvents>0?W(`span`,{children:[`전송 대기: `,Y(n.pendingEvents),`건`]}):null]}),n.enabled&&n.phase===`error`&&n.lastErrorCode?W(`p`,{class:`mt-3 mb-0 text-xs font-semibold text-amber-700`,children:[`오류 코드: `,W(`code`,{children:n.lastErrorCode})]}):null,W(`details`,{class:`mt-3 border-t border-slate-200 pt-3`,children:[W(`summary`,{class:`cursor-pointer text-xs font-extrabold text-teal-700 hover:text-teal-800 focus-visible:rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-teal-500`,children:`Cloud 동기화 상세 정보`}),W(`dl`,{class:`mt-3 grid gap-3 sm:grid-cols-4`,children:[W(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[W(`dt`,{class:`text-xs font-bold text-slate-500`,children:`멤버십`}),W(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:s??(n.eligible===!1?`Free`:`확인 중`)})]}),W(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[W(`dt`,{class:`text-xs font-bold text-slate-500`,children:`전송 대기`}),W(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:[Y(n.pendingEvents),`건`]})]}),W(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[W(`dt`,{class:`text-xs font-bold text-slate-500`,children:`마지막 동기화`}),W(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:n.lastSuccessAt>0?jo(n.lastSuccessAt):`아직 없음`})]}),W(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[W(`dt`,{class:`text-xs font-bold text-slate-500`,children:`Cloud 보관`}),W(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:$o(n)})]})]}),W(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`SmartSafeHub 계정에 연결된 Free는 최근 7일, Pro와 Ultimate는 최근 90일 동안 Cloud 활동 기록을 보관합니다. 끄면 전송 대기 데이터와 임시 인증 정보를 삭제하며, 다시 켠 뒤 새로 발생한 활동부터 Cloud에 전송합니다. 로컬 최근 활동은 항상 유지됩니다.`})]})]})}function ts({actionError:e,actionMessage:t,data:n,error:r,loading:i,onDismissActionFeedback:a,onRetry:o,onSetCloudSync:s,savingCloudSync:c}){let[l,u]=L(20),d=n?.events??[],f=d.slice(0,l),p=d.length-f.length;return W(`div`,{class:`min-w-0 space-y-6`,children:[W(`section`,{"aria-labelledby":`activity-cloud-title`,class:`min-w-0`,children:[W(J,{id:`activity-cloud-title`,eyebrow:`Cloud history`,title:`Cloud 활동 기록`,description:`활동 기록을 Cloud에 자동으로 전송할지 설정합니다. 공유기 내 최근 활동은 항상 기록됩니다.`}),W(es,{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})]}),W(`section`,{"aria-labelledby":`activity-history-title`,class:`min-w-0`,children:[W(J,{id:`activity-history-title`,eyebrow:`Local history`,title:`현재 부팅 이후의 최근 활동`,description:`인터넷, SafeShield, 업데이트, 계정 권한과 주요 설정에서 실제 상태가 변경된 시점만 기록합니다.`,action:W(`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,`건`]})}),W(`div`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:i||r&&!n?W(Zo,{error:r,loading:i,onRetry:o}):W(b,{children:[W(Xo,{events:f}),d.length>20?W(`div`,{class:`mt-5 flex flex-wrap items-center justify-between gap-3 border-t border-slate-200 pt-4`,children:[W(`p`,{class:`m-0 text-xs font-semibold text-slate-500`,children:[Y(f.length),` / `,Y(d.length),`건 표시 중`]}),W(`div`,{class:`flex flex-wrap items-center gap-2`,children:[l>20?W(`button`,{class:`inline-flex min-h-10 items-center justify-center rounded-xl border border-slate-200 bg-white px-4 py-2 text-xs font-extrabold text-slate-700 transition hover:bg-slate-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-teal-500`,onClick:()=>u(20),type:`button`,children:`최신 20건만 보기`}):null,p>0?W(`button`,{class:`inline-flex min-h-10 items-center justify-center rounded-xl border border-teal-200 bg-teal-50 px-4 py-2 text-xs font-extrabold text-teal-800 transition hover:bg-teal-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-teal-500`,onClick:()=>u(e=>Math.min(e+20,d.length)),type:`button`,children:[`이전 활동 더 보기 (`,Y(Math.min(20,p)),`건)`]}):null]})]}):null]})})]}),W(`aside`,{class:`rounded-2xl border border-slate-200 bg-slate-50 px-5 py-4`,children:[W(`p`,{class:`m-0 text-sm font-extrabold text-slate-800`,children:`로컬 활동 기록 안내`}),W(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`이 기록은 공유기의 메모리 영역에만 보관되어 재부팅하면 초기화됩니다. 직접 설정을 변경한 작업은 성공 시점에 즉시 기록하고, 인터넷 및 장치 진단처럼 외부 상태를 확인해야 하는 항목은 실제 상태 변화가 관찰된 경우에만 추가합니다.`})]})]})}function ns(e){return!Number.isFinite(e)||e<0?`—`:e<1024?`${Math.round(e)} B`:e<1024**2?`${(e/1024).toFixed(1)} KB`:e<1024**3?`${(e/1024**2).toFixed(1)} MB`:`${(e/1024**3).toFixed(1)} GB`}function rs(e){if(!e||!Number.isSafeInteger(e.totalBytes)||!Number.isSafeInteger(e.availableBytes)||e.totalBytes<=0||e.availableBytes<0||e.availableBytes>e.totalBytes||e.mount!==`/overlay`&&e.mount!==`/`)return null;let t=e.availableBytes/e.totalBytes;return{availablePercent:Math.round(t*100),availableRatio:t,availableText:ns(e.availableBytes),usedText:ns(e.totalBytes-e.availableBytes),totalText:ns(e.totalBytes),level:t<=.1?`critical`:t<=.2?`warning`:`normal`}}var is=2*Math.PI*39;function as({storage:e}){let t=rs(e),n=t?(t.availableRatio*is).toFixed(2):`0`;return W(`aside`,{"aria-label":`앱 설치 저장 공간`,"data-level":t?.level??`unavailable`,class:`ssh-apps-storage-card flex min-w-0 items-center gap-3 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5`,children:[W(`div`,{class:`ssh-apps-storage-ring relative grid size-24 shrink-0 place-items-center`,"data-level":t?.level??`unavailable`,children:[W(`svg`,{"aria-hidden":`true`,class:`size-24`,viewBox:`0 0 96 96`,children:[W(`circle`,{class:`ssh-apps-storage-track`,cx:`48`,cy:`48`,r:`39`}),t&&t.availableRatio>0?W(`circle`,{class:`ssh-apps-storage-free`,cx:`48`,cy:`48`,r:`39`,"stroke-dasharray":`${n} ${is.toFixed(2)}`}):null]}),W(`div`,{class:`absolute inset-0 flex flex-col items-center justify-center text-center`,children:[W(`strong`,{class:`text-lg leading-6 font-black text-slate-950`,children:t?`${t.availablePercent}%`:`—`}),W(`span`,{class:`text-[10px] font-bold text-slate-500`,children:`여유`})]})]}),W(`div`,{"aria-live":`polite`,class:`min-w-0 flex-1`,children:[W(`h3`,{class:`m-0 text-sm font-black text-slate-950`,children:`저장 공간`}),t?W(b,{children:[W(`p`,{class:`ssh-apps-storage-available mb-0 mt-1 text-base font-extrabold text-teal-700`,children:[t.availableText,` 남음`]}),W(`p`,{class:`mb-0 mt-1 text-[11px] leading-4 text-slate-500`,title:`앱 설치 가능 용량을 기준으로 계산하며, 파일시스템 예약 공간이 사용량에 포함될 수 있습니다.`,children:[`사용 `,t.usedText,` / 전체 `,t.totalText]})]}):W(`p`,{class:`mb-0 mt-1 text-xs leading-5 text-slate-500`,children:`용량 정보를 확인할 수 없습니다.`})]})]})}function os(e){switch(e){case`network`:return`network`;case`security`:return`security`;case`entertainment`:return`entertainment`;case`utilities`:return`utilities`;case`other`:return`other`;case`smart_home`:case`iot`:return`smart_home`;default:return`other`}}function ss({category:e,class:t}){switch(e){case`all`:return W(Qt,{class:t});case`network`:return W($t,{class:t});case`security`:return W(qt,{class:t});case`smart_home`:return W(Xt,{class:t});case`entertainment`:return W(en,{class:t});case`utilities`:return W(tn,{class:t});case`other`:return W(nn,{class:t});default:return W(nn,{class:t})}}function cs(e){return!e||typeof e.path!=`string`||e.path.length>160?null:e.type===`static`?e.path===`/retro-arcade/`?e.path:null:e.type===`luci`&&/^\/cgi-bin\/luci\/admin\/[a-z0-9_-]+(?:\/[a-z0-9_-]+)*$/.test(e.path)?e.path:null}function ls(e){return e===`first_party`}var us=`smartsafehub:apps:dismissed-operation:v1`;function ds(e){return!e||e.phase===`idle`?``:e.operationId?`operation:${e.operationId}`:`legacy:${e.phase}:${e.action}:${e.appId}:${e.code}`}function fs(){try{return typeof window>`u`?null:window.localStorage.getItem(us)}catch{return null}}function ps(e){if(e)try{typeof window<`u`&&window.localStorage.setItem(us,e)}catch{}}var ms={E21:`서버에서 승인 정보를 확인하지 못했습니다. 인터넷 연결을 확인해 주세요.`,E22:`이 펌웨어에서 승인되지 않은 앱입니다.`,E24:`공유기의 패키지 아키텍처와 앱이 일치하지 않습니다.`,E25:`배포 저장소가 승인 당시와 달라졌거나 접근할 수 없습니다. 관리자 확인 후 다시 시도해 주세요.`,E26:`앱 설치에 필요한 메모리 또는 저장 공간이 부족합니다.`,E27:`승인된 패키지 정보가 올바르지 않습니다.`,E28:`이전에 설치한 앱의 구성이 현재 승인 정보와 다릅니다.`,E29:`동일한 패키지가 이미 다른 방법으로 설치되어 있습니다.`,E31:`앱 파일을 다운로드하지 못했습니다.`,E32:`다운로드된 앱 파일이 올바르지 않습니다.`,E33:`앱 파일의 검증 값이 일치하지 않습니다.`,E34:`패키지 설치 사전 확인에 실패했습니다.`,E35:`패키지를 설치하지 못했습니다.`,E37:`공식 OpenWrt LuCI 저장소가 승인 당시와 다릅니다. 관리자에게 확인해 주세요.`,E38:`기존 공유 라이브러리의 버전이 승인된 버전과 달라 설치할 수 없습니다.`,E51:`SmartSafeHub Apps에서 관리하는 앱만 업데이트할 수 있습니다.`,E52:`현재 설치 버전과 승인된 업데이트가 일치하지 않습니다. 앱 구성과 업데이트 버전을 확인해 주세요.`,E53:`다른 시스템 패키지 변경이 필요하여 앱 업데이트를 중단했습니다.`,E54:`앱 업데이트에 실패했습니다. 일부 패키지가 변경되었을 수 있으니 관리자에게 확인해 주세요.`,E55:`업데이트 후 설치 기록을 저장하지 못했습니다. 관리자에게 확인해 주세요.`,E41:`SmartSafeHub Apps에서 설치한 앱만 여기서 제거할 수 있습니다.`,E44:`기존 시스템에 영향을 줄 수 있어 앱 제거가 차단됐습니다.`,E45:`앱 제거에 실패했습니다.`},hs=[{id:`all`,label:`전체`},{id:`network`,label:`네트워크`},{id:`security`,label:`보안`},{id:`smart_home`,label:`스마트홈`},{id:`entertainment`,label:`엔터테인먼트`},{id:`utilities`,label:`도구`},{id:`other`,label:`기타`}];function gs({action:e}){let t=e===`remove`;return W(`span`,{"aria-live":`polite`,class:`inline-flex items-center gap-1.5 text-xs font-bold ${t?`text-red-700`:`text-teal-700`}`,role:`status`,children:[W(`svg`,{"aria-hidden":`true`,class:`size-4 animate-spin`,fill:`none`,viewBox:`0 0 24 24`,children:[W(`circle`,{cx:`12`,cy:`12`,r:`9`,stroke:`currentColor`,"stroke-width":`3`,opacity:`0.25`}),W(`path`,{d:`M21 12a9 9 0 0 0-9-9`,stroke:`currentColor`,"stroke-width":`3`,"stroke-linecap":`round`})]}),t?`제거 중…`:e===`update`?`업데이트 중…`:`설치 중…`]})}function _s({data:e,error:t,loading:n,refreshing:r,onRetry:i,installStatus:a,metrics:o,statusError:s,onRefreshStatus:c,onInstall:l,onUpdate:u,onRemove:d,demo:f=!1,onDemoAction:p}){let[m,h]=L(`all`),[g,_]=L(``),[v,y]=L(null),[x,S]=L(null),C=z(null),w=z(null),[T,E]=L(null),[D,O]=L(fs),k=e,A=k?.apps??[],j=k&&!t&&!n&&!r&&!s?Object.entries(a?.installed??{}).filter(([e,t])=>t.retired===!0&&t.managed&&!A.some(t=>t.id===e)):[],M=a?.restore,N=a?.job,P=N?.phase===`starting`||N?.phase===`running`,F=M?.phase===`starting`||M?.phase===`running`,I=ds(N),ee=e=>{O(e),ps(e)},te=N&&N.phase!==`idle`&&I!==D;R(()=>{if(N?.phase!==`success`||!te)return;let e=window.setTimeout(()=>ee(I),7e3);return()=>window.clearTimeout(e)},[I,N?.phase,te]);let ne=N?.action===`install`||N?.action===`update`||N?.action===`remove`?N.action:null,re=Xe(()=>A.filter(e=>(m===`all`||os(e.category)===m)&&`${e.name} ${e.summary}`.toLocaleLowerCase().includes(g.trim().toLocaleLowerCase())),[A,m,g]);R(()=>{if(!x)return;C.current?.focus();let e=e=>{e.key===`Escape`&&(e.preventDefault(),S(null),w.current?.focus())};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[x]);let ie=()=>{S(null),w.current?.focus()},ae=(e,t)=>{!a||s||v!==null||P||F||(w.current=t,S(e))},oe=async(e,t)=>{if(!(!a||s||v!==null||P||F)){if(f)t===`remove`&&ie(),p?.();else if((t!==`update`||window.confirm(`${e.name} 앱을 최신 승인 버전으로 업데이트하시겠습니까?\n\n업데이트 중 공유기를 재부팅하지 마세요.`))&&(t!==`install`||window.confirm(`${e.name} 앱을 설치하시겠습니까?\n\n앱을 변경하는 동안 공유기를 재부팅하지 마세요.`))){t===`remove`&&ie(),E(null),y({appId:e.id,action:t});try{t===`install`?await l(e.id):t===`update`?await u(e.id):await d(e.id),c()}catch(t){E(q(t,`${e.name} 앱 작업을 시작하지 못했습니다.`))}finally{y(null)}}}};return W(`section`,{class:`min-w-0 space-y-6`,children:[x?W(`div`,{class:`fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70 p-4`,onClick:e=>{e.target===e.currentTarget&&ie()},role:`presentation`,children:W(`div`,{"aria-describedby":`apps-remove-description`,"aria-labelledby":`apps-remove-title`,"aria-modal":`true`,class:`w-full max-w-md rounded-2xl border border-slate-200 bg-white p-5 shadow-xl`,onKeyDown:e=>{e.key===`Tab`&&(e.shiftKey&&e.target===C.current?(e.preventDefault(),e.currentTarget.querySelector(`[data-confirm-remove]`)?.focus()):!e.shiftKey&&e.target instanceof HTMLButtonElement&&e.target.hasAttribute(`data-confirm-remove`)&&(e.preventDefault(),C.current?.focus()))},role:`dialog`,children:[W(`h2`,{class:`m-0 text-lg font-extrabold text-slate-950`,id:`apps-remove-title`,children:`앱 제거 확인`}),W(`p`,{class:`mt-3 break-words text-sm text-slate-700`,id:`apps-remove-description`,children:[W(`strong`,{children:x.name}),` 앱을 이 공유기에서 제거하시겠습니까?`]}),W(`p`,{class:`mt-2 text-xs leading-5 text-slate-600`,children:`앱에서 제공하던 기능을 더 이상 사용할 수 없습니다. 제거하는 동안 공유기를 재부팅하지 마세요.`}),x.id===`travelmate`?W(`p`,{class:`mt-2 text-xs leading-5 text-slate-600`,children:`다른 공유기 기능에서도 사용하는 공용 패키지는 안전을 위해 유지됩니다.`}):null,W(`div`,{class:`mt-5 flex justify-end gap-2`,children:[W(`button`,{class:`min-h-10 rounded-lg border border-slate-200 bg-white px-4 text-sm font-bold text-slate-700 transition hover:bg-slate-50`,onClick:ie,ref:C,type:`button`,children:`취소`}),W(`button`,{class:`min-h-10 rounded-lg bg-red-700 px-4 text-sm font-bold text-white transition hover:bg-red-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-100 disabled:opacity-50`,"data-confirm-remove":!0,disabled:!a||!!s||v!==null||P||F,onClick:()=>void oe(x,`remove`),type:`button`,children:`앱 제거`})]})]})}):null,W(`div`,{class:`min-w-0`,children:[W(J,{eyebrow:`SmartSafeHub Apps`,title:`Apps`,description:`SmartSafeHub에서 직접 검증한 앱으로 공유기의 기능을 확장하세요.`}),W(`div`,{class:`grid min-w-0 gap-3 lg:grid-cols-[minmax(0,1fr)_minmax(0,18.5rem)]`,children:[W(`div`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:[W(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[W(`div`,{class:`flex min-w-0 items-center gap-3`,children:[W(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-teal-50 text-teal-700`,children:W(Qt,{class:`size-6`})}),W(`div`,{class:`min-w-0`,children:[W(`h3`,{class:`m-0 text-base font-black text-slate-950`,children:`검증된 앱`}),W(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`현재 펌웨어에서 확인된 앱만 표시합니다.`})]})]}),W(`div`,{class:`flex flex-wrap items-center gap-2`,children:[W(`span`,{class:`rounded-full border border-teal-200 bg-teal-50 px-3 py-1 text-xs font-bold text-teal-800`,children:k?`${A.length}개`:`확인 전`}),t&&k?W(`span`,{class:`rounded-full border border-amber-200 bg-amber-50 px-2 py-1 text-[11px] font-semibold text-amber-800`,title:t,role:`status`,children:`이전 목록`}):null,W(`button`,{class:`inline-flex min-h-10 items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 text-xs font-bold text-slate-700 transition hover:border-teal-300 hover:text-teal-700 disabled:opacity-50`,disabled:n||r,onClick:i,type:`button`,children:[W(pn,{class:`size-4 ${r?`animate-spin`:``}`}),r?`확인 중`:`목록 새로고침`]})]})]}),W(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`검증된 APK를 설치·업데이트·제거할 수 있습니다. 작업 중에는 다른 앱 작업을 시작할 수 없습니다.`})]}),W(as,{storage:o?.storage})]})]}),M&&(M.phase===`starting`||M.phase===`running`||M.phase===`failed`)?W(`div`,{"aria-live":`polite`,class:`rounded-2xl border p-4 text-sm ${M.phase===`failed`?`border-amber-300 bg-amber-50 text-amber-900`:`border-teal-200 bg-teal-50 text-teal-900`}`,role:`status`,children:[W(`strong`,{children:M.phase===`failed`?`Apps 자동 복원을 완료하지 못했습니다.`:`펌웨어 업데이트 후 Apps를 복원하는 중입니다.`}),M.appId?W(`span`,{children:[` · `,M.appId]}):null,M.phase===`failed`?W(`p`,{class:`mb-0 mt-1 text-xs`,children:[ms[M.code]??`승인·연결·설치 상태를 확인해 주세요.`,` 필요한 경우 앱 카드에서 설치 마무리를 진행할 수 있습니다.`]}):W(`p`,{class:`mb-0 mt-1 text-xs`,children:`검증된 패키지만 다시 설치합니다. 완료될 때까지 앱 설치·제거를 시작하지 마세요.`})]}):null,te&&N?W(`div`,{"aria-live":`polite`,class:`rounded-2xl border p-4 text-sm ${N.phase===`failed`?`border-rose-200 bg-rose-50 text-rose-800`:P&&N.action===`remove`?`border-red-200 bg-red-50 text-red-800`:`border-teal-200 bg-teal-50 text-teal-900`}`,role:`status`,children:W(`div`,{class:`flex items-start justify-between gap-3`,children:[W(`div`,{class:`min-w-0`,children:[P?N.action===`remove`?`앱 제거 중`:N.action===`update`?`앱 업데이트 중`:`앱 설치 중`:N.phase===`success`?`마지막 앱 작업 완료`:`마지막 앱 작업 실패`,N.appId?` · ${N.appId}`:``,N.code&&N.code!==`NONE`?` · 오류 코드 ${N.code}`:``,N.phase===`failed`&&N.code&&ms[N.code]?W(`p`,{class:`mb-0 mt-1 text-xs`,children:ms[N.code]}):null,P?W(`p`,{class:`mb-0 mt-1 text-xs`,children:[N.action===`remove`?`앱 제거와 패키지 정리가 진행 중입니다.`:N.action===`update`?`승인된 버전으로 앱을 업데이트하고 있습니다.`:`설치 확인과 패키지 처리가 진행 중입니다.`,` 화면을 닫아도 작업은 계속됩니다.`]}):null]}),P?null:W(`button`,{"aria-label":`앱 작업 알림 닫기`,class:`shrink-0 rounded-lg px-2 py-1 text-xs font-bold hover:bg-black/5 focus-visible:outline-none focus-visible:ring-2`,onClick:()=>ee(I),type:`button`,children:`닫기 ×`})]})}):null,T?W(`p`,{class:`rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-800`,role:`alert`,children:T}):null,s?W(`div`,{class:`rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900`,role:`alert`,children:[`설치 상태를 확인하지 못했습니다. 설치 및 제거 버튼이 비활성화됩니다. `,W(`button`,{class:`underline`,onClick:c,type:`button`,children:`다시 확인`})]}):null,t&&!k?W(`div`,{class:`rounded-2xl border border-rose-200 bg-rose-50 p-5`,role:`alert`,children:[W(`p`,{class:`m-0 text-sm font-bold text-rose-800`,children:`앱 목록을 불러오지 못했습니다.`}),W(`p`,{class:`mt-2 mb-0 text-sm text-rose-700`,children:t}),W(`button`,{class:`mt-3 rounded-lg border border-rose-300 px-3 py-2 text-xs font-bold text-rose-800`,onClick:i,type:`button`,children:`다시 시도`})]}):null,n&&!k?W(`div`,{"aria-live":`polite`,class:`rounded-2xl border border-slate-200 bg-white p-8 text-center text-sm text-slate-500`,role:`status`,children:`검증된 앱 목록을 확인하는 중입니다.`}):null,k?W(b,{children:[A.length>0?W(`div`,{class:`space-y-4`,children:[W(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[W(`div`,{class:`flex flex-wrap items-center gap-2`,role:`group`,"aria-label":`앱 카테고리`,children:hs.map(e=>W(`button`,{class:`inline-flex min-h-10 items-center gap-1.5 rounded-xl border px-3 text-xs font-bold ${m===e.id?`border-teal-400 bg-teal-50 text-teal-800`:`border-slate-200 bg-white text-slate-600`}`,onClick:()=>h(e.id),type:`button`,children:[W(ss,{category:e.id,class:`size-4 shrink-0`}),e.label]},e.id))}),W(`input`,{"aria-label":`앱 검색`,class:`min-h-10 w-full rounded-xl border border-slate-200 bg-white px-3 text-sm text-slate-900 outline-none focus:border-teal-500 sm:w-52`,onInput:e=>_(e.currentTarget.value),placeholder:`앱 검색`,type:`search`,value:g})]}),W(`div`,{class:`grid min-w-0 gap-3 sm:grid-cols-2 xl:grid-cols-3`,children:re.map(e=>{let t=a?.installed[e.id],n=t?.installed??!1,r=o?.installedSizes?.[e.id],i=n&&typeof r==`number`&&Number.isSafeInteger(r)&&r>=0?ns(r):null,c=t?.managed??!1,l=!!(n&&c&&t?.updateAvailable),u=n?t?.installedVersion:null,d=!!(n&&u&&(l||u!==e.version)),m=v?.appId===e.id?v.action:P&&N?.appId===e.id?ne:null,h=!a||!!s||v!==null||P||F,g=c&&(n||!!t?.partial),_=n&&t?.webUiReady?cs(e.webUi):null;return W(`article`,{class:`flex min-w-0 flex-col rounded-2xl border border-slate-200 bg-white p-5 shadow-sm`,children:[W(`div`,{class:`flex min-w-0 items-start gap-3`,children:[W(`span`,{class:`grid size-14 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-600`,children:W(ss,{category:os(e.category),class:`size-7`})}),W(`div`,{class:`min-w-0 flex-1`,children:[W(`div`,{class:`flex min-w-0 flex-wrap items-start justify-between gap-x-3 gap-y-2`,children:[W(`h3`,{class:`m-0 min-w-0 flex-1 break-words text-sm font-black text-slate-950`,children:e.name}),n&&c?W(`span`,{class:`ssh-apps-installed-badge shrink-0 rounded-full border px-2 py-0.5 text-[11px] font-bold`,children:`✓ 설치됨`}):null]}),W(`div`,{class:`mt-1 flex flex-wrap items-center gap-x-2 gap-y-1`,children:[W(`span`,{class:`text-xs text-slate-500`,title:n?`공유기에 설치된 대표 APK 패키지 버전입니다.`:`현재 제공 중인 승인 버전입니다.`,children:n?u?`설치 v${u}`:`설치 버전 확인 불가`:`v${e.version}`}),l?W(`span`,{class:`ssh-apps-update-badge rounded-full border px-2 py-0.5 text-[11px] font-bold`,children:`업데이트 가능`}):null]}),d||i!==null?W(`p`,{class:`mb-0 mt-1 flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-0.5 text-[11px] text-slate-500`,children:[d?W(`span`,{title:`현재 서버가 승인한 앱 릴리스 버전입니다. 설치된 APK 패키지의 버전과 다를 수 있습니다.`,children:[`제공 v`,e.version]}):null,d&&i!==null?W(`span`,{"aria-hidden":`true`,children:`·`}):null,i===null?null:W(`span`,{title:`설치된 APK 패키지의 용량 합계입니다. 앱 데이터 및 캐시는 포함되지 않습니다.`,children:[`설치 용량 약 `,i]})]}):null]})]}),W(`p`,{class:`mt-4 mb-4 min-h-10 break-words text-sm leading-5 text-slate-600`,children:e.summary}),W(`div`,{class:`mt-auto flex flex-wrap items-center justify-between gap-2 border-t border-slate-100 pt-3`,children:[ls(e.publisherType)?W(`span`,{class:`ssh-apps-official-badge inline-flex items-center gap-1 rounded-lg border px-2 py-1 text-xs font-bold`,title:`SmartSafeHub가 직접 개발하고 제공하는 앱입니다.`,children:[W(`span`,{"aria-hidden":`true`,children:`✦`}),` SmartSafeHub 공식`]}):W(`span`,{class:`text-xs font-bold text-slate-500`,children:`✓ 검증된 앱`}),m?W(gs,{action:m}):W(`span`,{class:`flex flex-wrap items-center gap-2`,children:[c&&t?.partial&&!n?W(`button`,{class:`min-h-9 rounded-lg border border-teal-300 bg-white px-3 text-xs font-bold text-teal-800 hover:bg-teal-50 disabled:opacity-50`,disabled:h,onClick:()=>void oe(e,`install`),type:`button`,children:`설치 마무리`}):null,l?W(`button`,{class:`ssh-apps-update-button min-h-9 rounded-lg border px-3 text-xs font-bold transition disabled:opacity-50`,disabled:h,onClick:()=>void oe(e,`update`),type:`button`,children:`업데이트`}):null,_?f?W(`button`,{"aria-label":`${e.name} 데모 실행 안내`,class:`inline-flex min-h-9 items-center rounded-lg border border-teal-700 bg-teal-700 px-3 text-xs font-bold text-white transition hover:bg-teal-800`,onClick:p,type:`button`,children:`열기 ↗`}):W(`a`,{"aria-label":`${e.name} 새 탭에서 열기`,class:`inline-flex min-h-9 items-center rounded-lg border border-teal-700 bg-teal-700 px-3 text-xs font-bold text-white transition hover:bg-teal-800`,href:_,rel:`noopener noreferrer`,target:`_blank`,children:`열기 ↗`}):n&&e.webUi?W(`span`,{class:`text-xs text-amber-700`,title:`이 앱의 웹 UI 패키지를 설치해야 열 수 있습니다.`,children:`웹 UI 미설치`}):null,n&&!c?W(`span`,{class:`text-xs text-slate-500`,title:`공유기 소프트웨어 메뉴 또는 SSH로 설치한 앱은 Apps에서 제거하지 않습니다.`,children:`기존에 설치됨`}):t?.partial&&!c?W(`span`,{class:`text-xs text-amber-700`,children:`일부 패키지 설치됨`}):W(`button`,{class:`min-h-9 rounded-lg border bg-white px-3 text-xs font-bold transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50 ${g?`border-red-300 text-red-700 hover:bg-red-50 focus-visible:ring-red-100`:`border-teal-300 text-teal-800 hover:bg-teal-50 focus-visible:ring-teal-200`}`,disabled:h||!!t?.partial&&!c,onClick:t=>g?ae(e,t.currentTarget):void oe(e,`install`),type:`button`,children:g?`제거`:c?`재설치`:`설치`})]})]})]},e.id)})}),re.length===0?W(`p`,{class:`rounded-xl bg-slate-100 p-5 text-center text-sm text-slate-600`,children:`검색 조건에 맞는 앱이 없습니다.`}):null]}):W(`div`,{class:`rounded-2xl border border-dashed border-slate-300 bg-white px-5 py-12 text-center`,children:[W(Qt,{class:`mx-auto size-10 text-slate-400`}),W(`h3`,{class:`mt-4 mb-2 text-base font-black text-slate-900`,children:`아직 등록된 앱이 없습니다.`}),W(`p`,{class:`m-0 text-sm leading-6 text-slate-500`,children:`현재 기기와 펌웨어에 대해 승인된 앱이 준비되면 이곳에서 확인할 수 있습니다.`})]}),W(`p`,{class:`text-xs text-slate-400`,children:[`기기 `,k.deviceCode,` · 펌웨어 `,k.firmwareBuildId,` · `,k.channel,` 채널`]})]}):null,j.length>0?W(`div`,{class:`rounded-2xl border border-amber-200 bg-amber-50 p-5`,children:[W(`h3`,{class:`m-0 text-sm font-black text-amber-900`,children:`제공이 중단된 앱`}),W(`p`,{class:`mb-3 mt-1 text-xs text-amber-800`,children:`현재 승인 목록에서 제외되었지만 기기에 남아 있는 앱입니다. 설치는 불가능하며 제거할 수 있습니다.`}),W(`div`,{class:`space-y-2`,children:j.map(([e])=>{let t=v?.appId===e?v.action:P&&N?.appId===e?ne:null;return W(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[W(`span`,{class:`break-all text-sm text-amber-900`,children:e}),t?W(gs,{action:t}):W(`button`,{class:`min-h-9 rounded-lg border border-red-300 bg-white px-3 text-xs font-bold text-red-700 transition hover:bg-red-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-100 disabled:cursor-not-allowed disabled:opacity-50`,disabled:!a||P||F||v!==null||!!s,onClick:t=>ae({id:e,name:e,summary:``,description:``,category:``,version:``},t.currentTarget),type:`button`,children:`제거`})]},e)})})]}):null]})}function vs(){return W(`section`,{"aria-busy":`true`,"aria-live":`polite`,class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:W(`div`,{class:`flex items-center gap-4`,children:[W(pn,{class:`size-8 shrink-0 animate-spin text-teal-700`}),W(`div`,{class:`min-w-0 text-left`,children:W(`h2`,{class:`m-0 text-lg font-extrabold text-slate-950`,children:`장치 상태를 확인하고 있습니다`})})]})})}function ys({message:e,onRetry:t}){return W(`section`,{"aria-live":`assertive`,class:`rounded-2xl border border-red-200 bg-red-50 p-5 sm:p-6`,children:[W(`p`,{class:`m-0 text-xs font-bold uppercase tracking-[0.16em] text-red-700`,children:`연결 오류`}),W(`h2`,{class:`mt-2 mb-0 text-lg font-extrabold text-red-950`,children:`장치 상태를 불러오지 못했습니다`}),W(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-red-800`,children:e}),W(`button`,{class:`mt-5 inline-flex min-h-10 items-center justify-center gap-2 rounded-xl border-0 bg-red-700 px-4 py-2 text-sm font-bold text-white shadow-sm transition hover:bg-red-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-red-200`,onClick:t,type:`button`,children:[W(pn,{class:`size-4`}),`다시 시도`]})]})}var bs=[{value:`all`,label:`전체`},{value:`online`,label:`현재 연결`},{value:`wifi`,label:`Wi-Fi`},{value:`ethernet`,label:`유선/기타`},{value:`offline`,label:`최근 기기`}],xs=new Intl.Collator(`ko`),Ss=new Intl.DateTimeFormat(`ko-KR`,{month:`numeric`,day:`numeric`,hour:`2-digit`,minute:`2-digit`});function Cs(e){switch(e){case`wifi`:return`Wi-Fi`;case`ethernet`:return`유선/기타`;default:return`연결 방식 미확인`}}function ws(e){return e.hostname??e.ipv4Address??`이름 없는 기기`}function Ts(e){return e===null?null:e>=-50?`매우 좋음`:e>=-60?`좋음`:e>=-70?`보통`:`약함`}function Es(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 Ds(e){return e.leaseActive?e.leaseExpiresAt===null?`DHCP 임대 활성`:`DHCP ${Ss.format(new Date(e.leaseExpiresAt*1e3))}까지`:null}function Os(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 ks({device:e}){return W(`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:[W(`span`,{class:`size-2 rounded-full ${e.online?`bg-emerald-500`:`bg-slate-400`}`}),e.online?`현재 연결`:`최근 임대`]})}function As({device:e}){let t=Ts(e.signalDbm),n=Es(e.connectedSeconds),r=Ds(e);return W(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:[W(`div`,{class:`flex min-w-0 flex-col items-start gap-4 sm:flex-row sm:justify-between`,children:[W(`div`,{class:`flex min-w-0 items-start gap-3`,children:[W(`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`?W(on,{class:`size-6`}):e.connection===`ethernet`?W(cn,{class:`size-6`}):W(Yt,{class:`size-6`})}),W(`div`,{class:`min-w-0`,children:[W(`h3`,{class:`m-0 break-words text-base font-extrabold text-slate-950`,children:ws(e)}),W(`p`,{class:`mt-1 mb-0 break-all font-mono text-xs leading-5 text-slate-500`,children:e.mac})]})]}),W(`span`,{class:`shrink-0 self-start`,children:W(ks,{device:e})})]}),W(`dl`,{class:`mt-5 grid gap-3 text-sm sm:grid-cols-2`,children:[W(`div`,{class:`rounded-xl bg-slate-50 p-3.5`,children:[W(`dt`,{class:`text-xs font-bold text-slate-500`,children:`IP 주소`}),W(`dd`,{class:`mt-1.5 mb-0 ml-0 font-extrabold text-slate-900`,children:e.ipv4Address??`확인되지 않음`})]}),W(`div`,{class:`rounded-xl bg-slate-50 p-3.5`,children:[W(`dt`,{class:`text-xs font-bold text-slate-500`,children:`연결 방식`}),W(`dd`,{class:`mt-1.5 mb-0 ml-0 font-extrabold text-slate-900`,children:Cs(e.connection)})]})]}),W(`div`,{class:`mt-4 flex flex-wrap gap-2 text-xs font-bold text-slate-600`,children:[e.ssid?W(`span`,{class:`rounded-full bg-teal-50 px-3 py-1.5 text-teal-800`,children:[e.ssid,e.bandLabel?` · ${e.bandLabel}`:``]}):null,t?W(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:[`신호 `,t,` · `,e.signalDbm,` dBm`]}):null,n?W(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:[`연결 `,n]}):null,r?W(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:r}):null,e.interface?W(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:e.interface}):null]})]})}function js({data:e,error:t,loading:n,onRetry:r}){let[i,a]=L(``),[o,s]=L(`all`),c=Xe(()=>{if(!e)return[];let t=i.trim().toLocaleLowerCase();return[...e.devices].filter(e=>Os(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?xs.compare(ws(e),ws(t)):e.connection===`wifi`?-1:1:e.online?-1:1)},[e,o,i]);return n?W(vs,{}):t?W(ys,{message:t,onRetry:r}):e?W(b,{children:[W(J,{eyebrow:`Overview`,title:`연결 기기 현황`,description:`네트워크에 연결되었거나 최근 확인된 기기를 요약합니다.`}),W(`section`,{"aria-label":`연결 기기 요약`,class:`grid gap-4 sm:grid-cols-2 xl:grid-cols-4`,children:[W(Ms,{description:`${e.totals.offline}대는 최근 DHCP 임대에서 확인`,icon:W(Yt,{class:`size-6`}),label:`확인된 기기`,value:e.totals.known}),W(Ms,{description:`Wi-Fi 또는 ARP에서 현재 확인됨`,icon:W(dn,{class:`size-6`}),label:`현재 연결`,value:e.totals.online}),W(Ms,{description:`무선 AP에 연결된 기기`,icon:W(on,{class:`size-6`}),label:`Wi-Fi`,value:e.totals.wireless}),W(Ms,{description:`LAN ARP에서 확인된 비무선 기기`,icon:W(cn,{class:`size-6`}),label:`유선/기타`,value:e.totals.ethernet})]}),W(`section`,{class:`mt-7`,"aria-label":`기기 목록 및 필터`,children:[W(J,{eyebrow:`Device inventory`,title:`기기 목록`,description:`연결된 기기를 검색하고 무선·유선 상태별로 확인합니다.`}),W(`div`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:W(`div`,{class:`flex min-w-0 flex-col gap-3 lg:flex-row lg:items-center lg:justify-between`,children:[W(`div`,{class:`ssh-touch-scroll flex min-w-0 gap-2 overflow-x-auto pb-1 lg:flex-1`,role:`group`,"aria-label":`기기 필터`,children:bs.map(e=>W(`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))}),W(`label`,{class:`relative block w-full lg:w-72 lg:shrink-0 xl:w-80`,children:[W(`span`,{class:`sr-only`,children:`기기 검색`}),W(`span`,{class:`pointer-events-none absolute inset-y-0 left-0 flex w-11 items-center justify-center text-slate-400`,children:W(En,{class:`size-5`})}),W(`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})]})]})})]}),c.length?W(`section`,{"aria-live":`polite`,class:`mt-5 grid gap-4 lg:grid-cols-2`,children:c.map(e=>W(As,{device:e},e.id))}):W(`section`,{class:`mt-5 rounded-2xl border border-dashed border-slate-300 bg-white p-5 text-center sm:p-8`,children:[W(Yt,{class:`mx-auto size-10 text-slate-400`}),W(`h2`,{class:`mt-4 mb-0 text-lg font-extrabold text-slate-950`,children:`조건에 맞는 기기가 없습니다`}),W(`p`,{class:`mt-2 mb-0 text-sm text-slate-600`,children:`검색어 또는 필터를 변경해 주세요.`})]}),W(`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 Ms({description:e,icon:t,label:n,value:r}){return W(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:[W(`div`,{class:`flex items-start justify-between gap-4`,children:[W(`div`,{children:[W(`p`,{class:`m-0 text-xs font-bold uppercase tracking-[0.14em] text-slate-500`,children:n}),W(`p`,{class:`mt-3 mb-0 text-3xl font-black tracking-tight text-slate-950`,children:r})]}),W(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:t})]}),W(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:e})]})}function Ns(e){return e+.5|0}var Ps=(e,t,n)=>Math.max(Math.min(e,n),t);function Fs(e){return Ps(Ns(e*2.55),0,255)}function Is(e){return Ps(Ns(e*255),0,255)}function Ls(e){return Ps(Ns(e/2.55)/100,0,1)}function Rs(e){return Ps(Ns(e*100),0,100)}var zs={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},Bs=[...`0123456789ABCDEF`],Vs=e=>Bs[e&15],Hs=e=>Bs[(e&240)>>4]+Bs[e&15],Us=e=>(e&240)>>4==(e&15),Ws=e=>Us(e.r)&&Us(e.g)&&Us(e.b)&&Us(e.a);function Gs(e){var t=e.length,n;return e[0]===`#`&&(t===4||t===5?n={r:255&zs[e[1]]*17,g:255&zs[e[2]]*17,b:255&zs[e[3]]*17,a:t===5?zs[e[4]]*17:255}:(t===7||t===9)&&(n={r:zs[e[1]]<<4|zs[e[2]],g:zs[e[3]]<<4|zs[e[4]],b:zs[e[5]]<<4|zs[e[6]],a:t===9?zs[e[7]]<<4|zs[e[8]]:255})),n}var Ks=(e,t)=>e<255?t(e):``;function qs(e){var t=Ws(e)?Vs:Hs;return e?`#`+t(e.r)+t(e.g)+t(e.b)+Ks(e.a,t):void 0}var Js=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Ys(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 Xs(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 Zs(e,t,n){let r=Ys(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 Qs(e,t,n,r,i){return e===i?(t-n)/r+(t.5?l/(2-i-a):l/(i+a),s=Qs(t,n,r,l,i),s=s*60+.5),[s|0,c||0,o]}function ec(e,t,n,r){return(Array.isArray(t)?e(t[0],t[1],t[2]):e(t,n,r)).map(Is)}function tc(e,t,n){return ec(Ys,e,t,n)}function nc(e,t,n){return ec(Zs,e,t,n)}function rc(e,t,n){return ec(Xs,e,t,n)}function ic(e){return(e%360+360)%360}function ac(e){let t=Js.exec(e),n=255,r;if(!t)return;t[5]!==r&&(n=t[6]?Fs(+t[5]):Is(+t[5]));let i=ic(+t[2]),a=t[3]/100,o=t[4]/100;return r=t[1]===`hwb`?nc(i,a,o):t[1]===`hsv`?rc(i,a,o):tc(i,a,o),{r:r[0],g:r[1],b:r[2],a:n}}function oc(e,t){var n=$s(e);n[0]=ic(n[0]+t),n=tc(n),e.r=n[0],e.g=n[1],e.b=n[2]}function sc(e){if(!e)return;let t=$s(e),n=t[0],r=Rs(t[1]),i=Rs(t[2]);return e.a<255?`hsla(${n}, ${r}%, ${i}%, ${Ls(e.a)})`:`hsl(${n}, ${r}%, ${i}%)`}var cc={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`},lc={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 uc(){let e={},t=Object.keys(lc),n=Object.keys(cc),r,i,a,o,s;for(r=0;r>16&255,a>>8&255,a&255]}return e}var dc;function fc(e){dc||(dc=uc(),dc.transparent=[0,0,0,0]);let t=dc[e.toLowerCase()];return t&&{r:t[0],g:t[1],b:t[2],a:t.length===4?t[3]:255}}var pc=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;function mc(e){let t=pc.exec(e),n=255,r,i,a;if(t){if(t[7]!==r){let e=+t[7];n=t[8]?Fs(e):Ps(e*255,0,255)}return r=+t[1],i=+t[3],a=+t[5],r=255&(t[2]?Fs(r):Ps(r,0,255)),i=255&(t[4]?Fs(i):Ps(i,0,255)),a=255&(t[6]?Fs(a):Ps(a,0,255)),{r,g:i,b:a,a:n}}}function hc(e){return e&&(e.a<255?`rgba(${e.r}, ${e.g}, ${e.b}, ${Ls(e.a)})`:`rgb(${e.r}, ${e.g}, ${e.b})`)}var gc=e=>e<=.0031308?e*12.92:e**(1/2.4)*1.055-.055,_c=e=>e<=.04045?e/12.92:((e+.055)/1.055)**2.4;function vc(e,t,n){let r=_c(Ls(e.r)),i=_c(Ls(e.g)),a=_c(Ls(e.b));return{r:Is(gc(r+n*(_c(Ls(t.r))-r))),g:Is(gc(i+n*(_c(Ls(t.g))-i))),b:Is(gc(a+n*(_c(Ls(t.b))-a))),a:e.a+n*(t.a-e.a)}}function yc(e,t,n){if(e){let r=$s(e);r[t]=Math.max(0,Math.min(r[t]+r[t]*n,t===0?360:1)),r=tc(r),e.r=r[0],e.g=r[1],e.b=r[2]}}function bc(e,t){return e&&Object.assign(t||{},e)}function xc(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=Is(e[3]))):(t=bc(e,{r:0,g:0,b:0,a:1}),t.a=Is(t.a)),t}function Sc(e){return e.charAt(0)===`r`?mc(e):ac(e)}var Cc=class e{constructor(t){if(t instanceof e)return t;let n=typeof t,r;n===`object`?r=xc(t):n===`string`&&(r=Gs(t)||fc(t)||Sc(t)),this._rgb=r,this._valid=!!r}get valid(){return this._valid}get rgb(){var e=bc(this._rgb);return e&&(e.a=Ls(e.a)),e}set rgb(e){this._rgb=xc(e)}rgbString(){return this._valid?hc(this._rgb):void 0}hexString(){return this._valid?qs(this._rgb):void 0}hslString(){return this._valid?sc(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=vc(this._rgb,e._rgb,t)),this}clone(){return new e(this.rgb)}alpha(e){return this._rgb.a=Is(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=Ns(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 yc(this._rgb,2,e),this}darken(e){return yc(this._rgb,2,-e),this}saturate(e){return yc(this._rgb,1,e),this}desaturate(e){return yc(this._rgb,1,-e),this}rotate(e){return oc(this._rgb,e),this}};function wc(){}var Tc=(()=>{let e=0;return()=>e++})();function X(e){return e==null}function Ec(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 Z(e){return e!==null&&Object.prototype.toString.call(e)===`[object Object]`}function Dc(e){return(typeof e==`number`||e instanceof Number)&&isFinite(+e)}function Oc(e,t){return Dc(e)?e:t}function Q(e,t){return e===void 0?t:e}var kc=(e,t)=>typeof e==`string`&&e.endsWith(`%`)?parseFloat(e)/100:+e/t,Ac=(e,t)=>typeof e==`string`&&e.endsWith(`%`)?parseFloat(e)/100*t:+e;function jc(e,t,n){if(e&&typeof e.call==`function`)return e.apply(n,t)}function Mc(e,t,n,r){let i,a,o;if(Ec(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 Vc(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 Hc(e){let t=Vc(e);return e=>{for(let n of t){if(n===``)break;e=e&&e[n]}return e}}function Uc(e,t){return(Bc[t]||(Bc[t]=Hc(t)))(e)}function Wc(e){return e.charAt(0).toUpperCase()+e.slice(1)}var Gc=e=>e!==void 0,Kc=e=>typeof e==`function`,qc=(e,t)=>{if(e.size!==t.size)return!1;for(let n of e)if(!t.has(n))return!1;return!0};function Jc(e){return e.type===`mouseup`||e.type===`click`||e.type===`contextmenu`}var $=Math.PI,Yc=2*$,Xc=Yc+$,Zc=1/0,Qc=$/180,$c=$/2,el=$/4,tl=$*2/3,nl=Math.log10,rl=Math.sign;function il(e,t,n){return Math.abs(e-t)e-t).pop(),t}function sl(e){return typeof e==`symbol`||typeof e==`object`&&!!e&&!(Symbol.toPrimitive in e||`toString`in e||`valueOf`in e)}function cl(e){return!sl(e)&&!isNaN(parseFloat(e))&&isFinite(e)}function ll(e,t){let n=Math.round(e);return n-t<=e&&n+t>=e}function ul(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 Sl(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 Cl=(e,t,n,r)=>Sl(e,n,r?r=>{let i=e[r][t];return ie[r][t]Sl(e,n,r=>e[r][t]>=n);function Tl(e,t,n){let r=0,i=e.length;for(;rr&&e[i-1]>n;)i--;return r>0||i{let n=`_onData`+Wc(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 Ol(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)&&(El.forEach(t=>{delete e[t]}),delete e._chartjs)}function kl(e){let t=new Set(e);return t.size===e.length?e:Array.from(t)}var Al=function(){return typeof window>`u`?function(e){return e()}:window.requestAnimationFrame}();function jl(e,t){let n=[],r=!1;return function(...i){n=i,r||(r=!0,Al.call(window,()=>{r=!1,e.apply(t,n)}))}}function Ml(e,t){let n;return function(...r){return t?(clearTimeout(n),n=setTimeout(e,t,r)):e.apply(this,r),t}}var Nl=e=>e===`start`?`left`:e===`end`?`right`:`center`,Pl=(e,t,n)=>e===`start`?t:e===`end`?n:(t+n)/2;function Fl(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(Cl(c,u,d).lo,n?r:Cl(t,u,o.getPixelForValue(d)).lo),l){let e=c.slice(0,i+1).reverse().findIndex(e=>!X(e[s.axis]));i-=Math.max(0,e)}i=yl(i,0,r-1)}if(m){let e=Math.max(Cl(c,o.axis,f,!0).hi+1,n?0:Cl(t,u,o.getPixelForValue(f),!0).hi+1);if(l){let t=c.slice(e-1).findIndex(e=>!X(e[s.axis]));e+=Math.max(0,t)}a=yl(e,i,r)-i}else a=r-i}return{start:i,count:a}}function Il(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 Ll=e=>e===0||e===1,Rl=(e,t,n)=>-(2**(10*--e)*Math.sin((e-t)*Yc/n)),zl=(e,t,n)=>2**(-10*e)*Math.sin((e-t)*Yc/n)+1,Bl={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*$c)+1,easeOutSine:e=>Math.sin(e*$c),easeInOutSine:e=>-.5*(Math.cos($*e)-1),easeInExpo:e=>e===0?0:2**(10*(e-1)),easeOutExpo:e=>e===1?1:-(2**(-10*e))+1,easeInOutExpo:e=>Ll(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=>Ll(e)?e:Rl(e,.075,.3),easeOutElastic:e=>Ll(e)?e:zl(e,.075,.3),easeInOutElastic(e){let t=.1125,n=.45;return Ll(e)?e:e<.5?.5*Rl(e*2,t,n):.5+.5*zl(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-Bl.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?Bl.easeInBounce(e*2)*.5:Bl.easeOutBounce(e*2-1)*.5+.5};function Vl(e){if(e&&typeof e==`object`){let t=e.toString();return t===`[object CanvasPattern]`||t===`[object CanvasGradient]`}return!1}function Hl(e){return Vl(e)?e:new Cc(e)}function Ul(e){return Vl(e)?e:new Cc(e).saturate(.5).darken(.1).hexString()}var Wl=[`x`,`y`,`borderWidth`,`radius`,`tension`],Gl=[`color`,`borderColor`,`backgroundColor`];function Kl(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:Gl},numbers:{type:`number`,properties:Wl}}),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 ql(e){e.set(`layout`,{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})}var Jl=new Map;function Yl(e,t){t=t||{};let n=e+JSON.stringify(t),r=Jl.get(n);return r||(r=new Intl.NumberFormat(e,t),Jl.set(n,r)),r}function Xl(e,t,n){return Yl(t,n).format(e)}var Zl={values(e){return Ec(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=Ql(e,n)}let o=nl(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),Xl(e,r,c)},logarithmic(e,t,n){if(e===0)return`0`;let r=n[t].significand||e/10**Math.floor(nl(e));return[1,2,3,5,10,15].includes(r)||t>.8*n.length?Zl.numeric.call(this,e,t,n):``}};function Ql(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 $l={formatters:Zl};function eu(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:$l.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 tu=Object.create(null),nu=Object.create(null);function ru(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)=>Ul(t.backgroundColor),this.hoverBorderColor=(e,t)=>Ul(t.borderColor),this.hoverColor=(e,t)=>Ul(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 iu(this,e,t)}get(e){return ru(this,e)}describe(e,t){return iu(nu,e,t)}override(e,t){return iu(tu,e,t)}route(e,t,n,r){let i=ru(this,e),a=ru(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 Z(e)?Object.assign({},t,e):Q(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}},[Kl,ql,eu]);function ou(e){return!e||X(e.size)||X(e.family)?null:(e.style?e.style+` `:``)+(e.weight?e.weight+` `:``)+e.size+`px `+e.family}function su(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 cu(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 pu(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,vu(e,a),c=0;c+e||0;function Du(e,t){let n={},r=Z(t),i=r?Object.keys(t):t,a=Z(e)?r?n=>Q(e[n],e[t[n]]):t=>e[t]:()=>e;for(let e of i)n[e]=Eu(a(e));return n}function Ou(e){return Du(e,{top:`y`,right:`x`,bottom:`y`,left:`x`})}function ku(e){return Du(e,[`topLeft`,`topRight`,`bottomLeft`,`bottomRight`])}function Au(e){let t=Ou(e);return t.width=t.left+t.right,t.height=t.top+t.bottom,t}function ju(e,t){e=e||{},t=t||au.font;let n=Q(e.size,t.size);typeof n==`string`&&(n=parseInt(n,10));let r=Q(e.style,t.style);r&&!(``+r).match(wu)&&(console.warn(`Invalid font style specified: "`+r+`"`),r=void 0);let i={family:Q(e.family,t.family),lineHeight:Tu(Q(e.lineHeight,t.lineHeight),n),size:n,style:r,weight:Q(e.weight,t.weight),string:``};return i.string=ou(i),i}function Mu(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 Pu(e,t){return Object.assign(Object.create(e),t)}function Fu(e,t=[``],n,r,i=()=>e[0]){let a=n||e;return r===void 0&&(r=Zu(`_fallback`,e)),new Proxy({[Symbol.toStringTag]:`Object`,_cacheable:!0,_scopes:e,_rootScopes:a,_fallback:r,_getTarget:i,override:n=>Fu([n,...e],t,a,r)},{deleteProperty(t,n){return delete t[n],delete t._keys,delete e[0][n],!0},get(n,r){return Bu(n,r,()=>Xu(r,t,e,n))},getOwnPropertyDescriptor(e,t){return Reflect.getOwnPropertyDescriptor(e._scopes[0],t)},getPrototypeOf(){return Reflect.getPrototypeOf(e[0])},has(e,t){return Qu(e).includes(t)},ownKeys(e){return Qu(e)},set(e,t,n){let r=e._storage||(e._storage=i());return e[t]=r[t]=n,delete e._keys,!0}})}function Iu(e,t,n,r){let i={_cacheable:!1,_proxy:e,_context:t,_subProxy:n,_stack:new Set,_descriptors:Lu(e,r),setContext:t=>Iu(e,t,n,r),override:i=>Iu(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 Bu(e,t,()=>Vu(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 Lu(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:Kc(n)?n:()=>n,isIndexable:Kc(r)?r:()=>r}}var Ru=(e,t)=>e?e+Wc(t):t,zu=(e,t)=>Z(t)&&e!==`adapters`&&(Object.getPrototypeOf(t)===null||t.constructor===Object);function Bu(e,t,n){if(Object.prototype.hasOwnProperty.call(e,t)||t===`constructor`)return e[t];let r=n();return e[t]=r,r}function Vu(e,t,n){let{_proxy:r,_context:i,_subProxy:a,_descriptors:o}=e,s=r[t];return Kc(s)&&o.isScriptable(t)&&(s=Hu(t,s,e,n)),Ec(s)&&s.length&&(s=Uu(t,s,e,o.isIndexable)),zu(t,s)&&(s=Iu(s,i,a&&a[t],o)),s}function Hu(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),zu(e,c)&&(c=qu(i._scopes,i,e,c)),c}function Uu(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(Z(t[0])){let n=t,r=i._scopes.filter(e=>e!==n);t=[];for(let c of n){let n=qu(r,i,e,c);t.push(Iu(n,a,o&&o[e],s))}}return t}function Wu(e,t,n){return Kc(e)?e(t,n):e}var Gu=(e,t)=>e===!0?t:typeof e==`string`?Uc(t,e):void 0;function Ku(e,t,n,r,i){for(let a of t){let t=Gu(n,a);if(t){e.add(t);let a=Wu(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 qu(e,t,n,r){let i=t._rootScopes,a=Wu(t._fallback,n,r),o=[...e,...i],s=new Set;s.add(r);let c=Ju(s,o,n,a||n,r);return c===null||a!==void 0&&a!==n&&(c=Ju(s,o,a,c,r),c===null)?!1:Fu(Array.from(s),[``],i,a,()=>Yu(t,n,r))}function Ju(e,t,n,r,i){for(;n;)n=Ku(e,t,n,r,i);return n}function Yu(e,t,n){let r=e._getTarget();t in r||(r[t]={});let i=r[t];return Ec(i)&&Z(n)?n:i||{}}function Xu(e,t,n,r){let i;for(let a of t)if(i=Zu(Ru(a,e),n),i!==void 0)return zu(e,i)?qu(n,r,e,i):i}function Zu(e,t){for(let n of t){if(!n)continue;let t=n[e];if(t!==void 0)return t}}function Qu(e){let t=e._keys;return t||(t=e._keys=$u(e._scopes)),t}function $u(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 ed(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 id(e,t,n,r){let i=e.skip?t:e,a=t,o=n.skip?t:n,s=hl(a,i),c=hl(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 ad(e,t,n){let r=e.length,i,a,o,s,c,l=nd(e,0);for(let u=0;u!e.skip)),t.cubicInterpolationMode===`monotone`)sd(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 hd(e,t){return md(e).getPropertyValue(t)}var gd=[`top`,`right`,`bottom`,`left`];function _d(e,t,n){let r={};n=n?`-`+n:``;for(let i=0;i<4;i++){let a=gd[i];r[a]=parseFloat(e[t+`-`+a+n])||0}return r.width=r.left+r.right,r.height=r.top+r.bottom,r}var vd=(e,t,n)=>(e>0||t>0)&&(!n||!n.shadowRoot);function yd(e,t){let n=e.touches,r=n&&n.length?n[0]:e,{offsetX:i,offsetY:a}=r,o=!1,s,c;if(vd(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 bd(e,t){if(`native`in e)return e;let{canvas:n,currentDevicePixelRatio:r}=t,i=md(n),a=i.boxSizing===`border-box`,o=_d(i,`padding`),s=_d(i,`border`,`width`),{x:c,y:l,box:u}=yd(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 xd(e,t,n){let r,i;if(t===void 0||n===void 0){let a=e&&fd(e);if(!a)t=e.clientWidth,n=e.clientHeight;else{let e=a.getBoundingClientRect(),o=md(a),s=_d(o,`border`,`width`),c=_d(o,`padding`);t=e.width-c.width-s.width,n=e.height-c.height-s.height,r=pd(o.maxWidth,a,`clientWidth`),i=pd(o.maxHeight,a,`clientHeight`)}}return{width:t,height:n,maxWidth:r||Zc,maxHeight:i||Zc}}var Sd=e=>Math.round(e*10)/10;function Cd(e,t,n,r){let i=md(e),a=_d(i,`margin`),o=pd(i.maxWidth,e,`clientWidth`)||Zc,s=pd(i.maxHeight,e,`clientHeight`)||Zc,c=xd(e,t,n),{width:l,height:u}=c;if(i.boxSizing===`content-box`){let e=_d(i,`border`,`width`),t=_d(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=Sd(Math.min(l,o,c.maxWidth)),u=Sd(Math.min(u,s,c.maxHeight)),l&&!u&&(u=Sd(l/2)),(t!==void 0||n!==void 0)&&r&&c.height&&u>c.height&&(u=c.height,l=Sd(Math.floor(u*r))),{width:l,height:u}}function wd(e,t,n){let r=t||1,i=Sd(e.height*r),a=Sd(e.width*r);e.height=Sd(e.height),e.width=Sd(e.width);let o=e.canvas;o.style&&(n||!o.style.height&&!o.style.width)&&(o.style.height=`${e.height}px`,o.style.width=`${e.width}px`);let s=Math.floor(i),c=Math.floor(a);return e.currentDevicePixelRatio!==r||o.height!==s||o.width!==c?(e.currentDevicePixelRatio=r,o.height=s,o.width=c,e.ctx.setTransform(r,0,0,r,0,0),!0):!1}var Td=function(){let e=!1;try{let t={get passive(){return e=!0,!1}};dd()&&(window.addEventListener(`test`,null,t),window.removeEventListener(`test`,null,t))}catch{}return e}();function Ed(e,t){let n=hd(e,t),r=n&&n.match(/^(\d+)(\.\d+)?px$/);return r?+r[1]:void 0}function Dd(e,t,n,r){return{x:e.x+n*(t.x-e.x),y:e.y+n*(t.y-e.y)}}function Od(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 kd(e,t,n,r){let i={x:e.cp2x,y:e.cp2y},a={x:t.cp1x,y:t.cp1y},o=Dd(e,i,n),s=Dd(i,a,n),c=Dd(a,t,n);return Dd(Dd(o,s,n),Dd(s,c,n),n)}var Ad=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}}},jd=function(){return{x(e){return e},setWidth(e){},textAlign(e){return e},xPlus(e,t){return e+t},leftForLtr(e,t){return e}}};function Md(e,t,n){return e?Ad(t,n):jd()}function Nd(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 Pd(e,t){t!==void 0&&(delete e.prevTextDirection,e.canvas.style.setProperty(`direction`,t[0],t[1]))}function Fd(e){return e===`angle`?{between:vl,compare:gl,normalize:_l}:{between:xl,compare:(e,t)=>e-t,normalize:e=>e}}function Id({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 Ld(e,t,n){let{property:r,start:i,end:a}=n,{between:o,normalize:s}=Fd(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(Id({start:g,end:e,loop:f,count:o,style:p})),g=null),n=e,y=_));return g!==null&&m.push(Id({start:g,end:d,loop:f,count:o,style:p})),m}function zd(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 Vd(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 Hd(e,t){let n=e.points,r=e.options.spanGaps,i=n.length;if(!i)return[];let a=!!e._loop,{start:o,end:s}=Bd(n,i,a,r);return r===!0?Ud(e,[{start:o,end:s,loop:a}],n,t):Ud(e,Vd(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=Al.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)}},Zd=`transparent`,Qd={boolean(e,t,n){return n>.5?t:e},color(e,t,n){let r=Hl(e||Zd),i=r.valid&&Hl(t||Zd);return i&&i.valid?i.mix(r,n).hexString():t},number(e,t,n){return e+(t-e)*n}},$d=class{constructor(e,t,n,r){let i=t[n];r=Mu([e.to,r,i,e.from]);let a=Mu([e.from,i,r]);this._active=!0,this._fn=e.fn||Qd[e.type||typeof a],this._easing=Bl[e.easing]||Bl.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=Mu([e.to,t,r,e.from]),this._from=Mu([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(!Z(i))return;let a={};for(let e of t)a[e]=i[e];(Ec(i.properties)&&i.properties||[r]).forEach(e=>{e!==r&&n.has(e)||n.set(e,a)})})}_animateOptions(e,t){let n=t.options,r=nf(e,n);if(!r)return[];let i=this._createAnimations(r,n);return n.$shared&&tf(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 $d(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 Xd.add(this._chart,n),!0}};function tf(e,t){let n=[],r=Object.keys(t);for(let t=0;t0||!n&&t<0)return i.index}return null}function hf(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=df(a,o,r),d=t.length,f;for(let e=0;en[e].axis===t).shift()}function _f(e,t){return Pu(e,{active:!1,dataset:void 0,datasetIndex:t,index:t,mode:`default`,type:`dataset`})}function vf(e,t,n){return Pu(e,{active:!1,dataIndex:t,parsed:void 0,raw:void 0,element:n,index:t,mode:`default`,type:`data`})}function yf(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;t&&t[r]!==void 0&&t[r][n]!==void 0&&(delete t[r][n],t[r]._visualValues!==void 0&&t[r]._visualValues[n]!==void 0&&delete t[r]._visualValues[n])}}}var bf=e=>e===`reset`||e===`none`,xf=(e,t)=>t?e:Object.assign({},e),Sf=(e,t,n)=>e&&!t.hidden&&t._stacked&&{keys:sf(n,!0),values:null},Cf=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=uf(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&&yf(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=Q(n.xAxisID,gf(e,`x`)),a=t.yAxisID=Q(n.yAxisID,gf(e,`y`)),o=t.rAxisID=Q(n.rAxisID,gf(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&&Ol(this._data,this),e._stacked&&yf(e)}_dataCheck(){let e=this.getDataset(),t=e.data||(e.data=[]),n=this._data;if(Z(t)){let e=this._cachedMeta;this._data=lf(t,e)}else if(n!==t){if(n){Ol(n,this);let e=this._cachedMeta;yf(e),e._parsed=[]}t&&Object.isExtensible(t)&&Dl(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=uf(t.vScale,t),t.stack!==n.stack&&(r=!0,yf(t),t.stack=n.stack),this._resyncElements(e),(r||i!==t._stacked)&&(hf(this,t._parsed),t._stacked=uf(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=Ec(r[e])?this.parseArrayData(n,r,e,t):Z(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(xf(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 ef(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||bf(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){bf(r)?Object.assign(e,n):this._resolveAnimations(t,r).update(e,n)}updateSharedOptions(e,t,n){e&&!bf(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 Tf(e){let t=e.iScale,n=wf(t,e.type),r=t._length,i,a,o,s,c=()=>{o!==32767&&o!==-32768&&(Gc(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 kf(e,t,n,r){return Ec(e)?Of(e,t,n,r):t[n.axis]=n.parse(e,r),t}function Af(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):rl(e)}function Nf(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(X(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[Q(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;dvl(e,s,c,!0)?1:Math.max(t,t*n,r,r*n),m=(e,t,r)=>vl(e,s,c,!0)?-1:Math.min(t,t*n,r,r*n),h=p(0,l,d),g=p($c,u,f),_=m($,l,d),v=m($+$c,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 Hf=class extends Cf{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(Z(n[e])){let{key:e=`value`}=this._parsing;i=t=>+Uc(n[t],e)}let a,o;for(a=e,o=e+t;a0&&!isNaN(e)?Math.abs(e)/t*Yc:0}getLabelAndValue(e){let t=this._cachedMeta,n=this.chart,r=n.data.labels||[],i=Xl(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`&&e!==`spacingMode`,_indexable:e=>e!==`spacing`&&e!==`spacingMode`&&!e.startsWith(`borderDash`)&&!e.startsWith(`hoverBorderDash`)}),V(Hf,`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 Uf=class extends Cf{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}=Fl(t,r,a);this._drawStart=o,this._drawCount=s,Il(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=cl(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=X(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()}};V(Uf,`id`,`line`),V(Uf,`defaults`,{datasetElementType:`line`,dataElementType:`point`,showLine:!0,spanGaps:!1}),V(Uf,`overrides`,{scales:{_index_:{type:`category`},_value_:{type:`linear`}}});var Wf=class extends Cf{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=Xl(t._parsed[e].r,n.options.locale);return{label:r[e]||``,value:i}}parseObjectData(e,t,n,r){return ed.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*$,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)?dl(this.resolveDataElementOptions(e,t).angle||n):0}};V(Wf,`id`,`polarArea`),V(Wf,`defaults`,{dataElementType:`arc`,animation:{animateRotate:!0,animateScale:!0},animations:{numbers:{type:`number`,properties:[`x`,`y`,`startAngle`,`endAngle`,`innerRadius`,`outerRadius`]}},indexAxis:`r`,startAngle:0,spacing:0,spacingMode:`proportional`}),V(Wf,`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 Gf=class extends Hf{};V(Gf,`id`,`pie`),V(Gf,`defaults`,{cutout:0,rotation:0,circumference:360,radius:`100%`});var Kf=class extends Cf{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 ed.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}};V(qf,`id`,`scatter`),V(qf,`defaults`,{datasetElementType:!1,dataElementType:`point`,showLine:!1,fill:!1}),V(qf,`overrides`,{interaction:{mode:`point`},scales:{x:{type:`linear`},y:{type:`linear`}}});function Jf(){throw Error(`This method is not implemented: Check that a complete date adapter is provided.`)}var Yf={_date:class e{static override(t){Object.assign(e.prototype,t)}constructor(e){V(this,`options`,void 0),this.options=e||{}}init(){}formats(){return Jf()}parse(){return Jf()}format(){return Jf()}add(){return Jf()}diff(){return Jf()}startOf(){return Jf()}endOf(){return Jf()}}};function Xf(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?wl:Cl;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=>!X(e[t.axis]));r.lo-=Math.max(0,a);let o=n.slice(r.hi).findIndex(e=>!X(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 Zf(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 ip={evaluateInteractionItems:Zf,modes:{index(e,t,n,r){let i=bd(t,e),a=n.axis||`x`,o=n.includeInvisible||!1,s=n.intersect?$f(e,i,a,r,o):np(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=bd(t,e),a=n.axis||`xy`,o=n.includeInvisible||!1,s=n.intersect?$f(e,i,a,r,o):np(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 sp(e,t){return e.filter(e=>ap.indexOf(e.pos)===-1&&e.box.axis===t)}function cp(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 lp(e){let t=[],n,r,i,a,o,s;for(n=0,r=(e||[]).length;ne.box.fullSize),!0),r=cp(op(t,`left`),!0),i=cp(op(t,`right`)),a=cp(op(t,`top`),!0),o=cp(op(t,`bottom`)),s=sp(t,`x`),c=sp(t,`y`);return{fullSize:n,leftAndTop:r.concat(a),rightAndBottom:i.concat(c).concat(o).concat(s),chartArea:op(t,`chartArea`),vertical:r.concat(i).concat(c),horizontal:a.concat(o).concat(s)}}function pp(e,t,n,r){return Math.max(e[n],t[n])+Math.max(e[r],t[r])}function mp(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 hp(e,t,n,r){let{pos:i,box:a}=n,o=e.maxPadding;if(!Z(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&&mp(o,a.getPadding());let s=Math.max(0,t.outerWidth-pp(o,e,`left`,`right`)),c=Math.max(0,t.outerHeight-pp(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 gp(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 _p(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 vp(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);mp(f,Au(r));let p=Object.assign({maxPadding:f,w:a,h:o,x:i.left,y:i.top},i),m=dp(c.concat(l),d);vp(s.fullSize,p,d,m),vp(c,p,d,m),vp(l,p,d,m)&&vp(c,p,d,m),gp(p),bp(s.leftAndTop,p,d,m),p.x+=p.w,p.y+=p.h,bp(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},Mc(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})})}},Sp=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){}},Cp=class extends Sp{acquireContext(e){return e&&e.getContext&&e.getContext(`2d`)||null}updateConfig(e){e.options.animation=!1}},wp=`$chartjs`,Tp={touchstart:`mousedown`,touchmove:`mousemove`,touchend:`mouseup`,pointerenter:`mouseenter`,pointerdown:`mousedown`,pointermove:`mousemove`,pointerup:`mouseup`,pointerleave:`mouseout`,pointerout:`mouseout`},Ep=e=>e===null||e===``;function Dp(e,t){let n=e.style,r=e.getAttribute(`height`),i=e.getAttribute(`width`);if(e[wp]={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`,Ep(i)){let t=Ed(e,`width`);t!==void 0&&(e.width=t)}if(Ep(r)){if(e.style.height===``)e.height=e.width/(t||2);else{let t=Ed(e,`height`);t!==void 0&&(e.height=t)}}return e}var Op=Td?{passive:!0}:!1;function kp(e,t,n){e&&e.addEventListener(t,n,Op)}function Ap(e,t,n){e&&e.canvas&&e.canvas.removeEventListener(t,n,Op)}function jp(e,t){let n=Tp[e.type]||e.type,{x:r,y:i}=bd(e,t);return{type:n,chart:t,native:e,x:r===void 0?null:r,y:i===void 0?null:i}}function Mp(e,t){for(let n of e)if(n===t||n.contains(t))return!0}function Np(e,t,n){let r=e.canvas,i=new MutationObserver(e=>{let t=!1;for(let n of e)t=t||Mp(n.addedNodes,r),t=t&&!Mp(n.removedNodes,r);t&&n()});return i.observe(document,{childList:!0,subtree:!0}),i}function Pp(e,t,n){let r=e.canvas,i=new MutationObserver(e=>{let t=!1;for(let n of e)t=t||Mp(n.removedNodes,r),t=t&&!Mp(n.addedNodes,r);t&&n()});return i.observe(document,{childList:!0,subtree:!0}),i}var Fp=new Map,Ip=0;function Lp(){let e=window.devicePixelRatio;e!==Ip&&(Ip=e,Fp.forEach((t,n)=>{n.currentDevicePixelRatio!==e&&t()}))}function Rp(e,t){Fp.size||window.addEventListener(`resize`,Lp),Fp.set(e,t)}function zp(e){Fp.delete(e),Fp.size||window.removeEventListener(`resize`,Lp)}function Bp(e,t,n){let r=e.canvas,i=r&&fd(r);if(!i)return;let a=jl((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),Rp(e,a),o}function Vp(e,t,n){n&&n.disconnect(),t===`resize`&&zp(e)}function Hp(e,t,n){let r=e.canvas,i=jl(t=>{e.ctx!==null&&n(jp(t,e))},e);return kp(r,t,i),i}var Up=class extends Sp{acquireContext(e,t){let n=e&&e.getContext&&e.getContext(`2d`);return n&&n.canvas===e?(Dp(e,t),n):null}releaseContext(e){let t=e.canvas;if(!t[wp])return!1;let n=t[wp].initial;[`height`,`width`].forEach(e=>{let r=n[e];X(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[wp],!0}addEventListener(e,t,n){this.removeEventListener(e,t);let r=e.$proxies||(e.$proxies={});r[t]=({attach:Np,detach:Pp,resize:Bp}[t]||Hp)(e,t,n)}removeEventListener(e,t){let n=e.$proxies||(e.$proxies={}),r=n[t];r&&(({attach:Vp,detach:Vp,resize:Vp}[t]||Ap)(e,t,r),n[t]=void 0)}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(e,t,n,r){return Cd(e,t,n,r)}isAttached(e){let t=e&&fd(e);return!!(t&&t.isConnected)}};function Wp(e){return!dd()||typeof OffscreenCanvas<`u`&&e instanceof OffscreenCanvas?Cp:Up}var Gp=class{constructor(){V(this,`x`,void 0),V(this,`y`,void 0),V(this,`active`,!1),V(this,`options`,void 0),V(this,`$animations`,void 0)}tooltipPosition(e){let{x:t,y:n}=this.getProps([`x`,`y`],e);return{x:t,y:n}}hasValue(){return cl(this.x)&&cl(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}};V(Gp,`defaults`,{}),V(Gp,`defaultRoutes`,void 0);function Kp(e,t){let n=e.options.ticks,r=qp(e),i=Math.min(n.maxTicksLimit||r,r),a=n.major.enabled?Yp(t):[],o=a.length,s=a[0],c=a[o-1],l=[];if(o>i)return Xp(t,l,a,o/i),l;let u=Jp(a,t,i);if(o>0){let e,n,r=o>1?Math.round((c-s)/(o-1)):null;for(Zp(t,l,u,X(r)?0:s-r,s),e=0,n=o-1;ei)return t}return Math.max(i,1)}function Yp(e){let t=[],n,r;for(n=0,r=e.length;ne===`left`?`right`:e===`right`?`left`:e,em=(e,t,n)=>t===`top`||t===`left`?e[t]+n:e[t]-n,tm=(e,t)=>Math.min(t||e,e);function nm(e,t){let n=[],r=e.length/t,i=e.length,a=0;for(;ao+s)))return c}function im(e,t){Mc(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:Oc(t,Oc(n,t)),max:Oc(n,Oc(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(){jc(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=Nu(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=yl(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-am(e.grid)-t.padding-om(e.title,this.chart.options.font),c=Math.sqrt(u*u+d*d),a=fl(Math.min(Math.asin(yl((l.highest.height+6)/o,-1,1)),Math.asin(yl(s/c,-1,1))-Math.asin(yl(d/c,-1,1)))),a=Math.max(r,Math.min(i,a))),this.labelRotation=a}afterCalculateLabelRotation(){jc(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){jc(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=om(r,t.options.font);if(o?(e.width=this.maxWidth,e.height=am(i)+a):(e.height=this.maxHeight,e.width=am(i)+a),n.display&&this.ticks.length){let{first:t,last:r,widest:i,highest:a}=this._getLabelSizes(),s=n.padding*2,c=dl(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(){jc(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 bl(this._alignToPixels?lu(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=am(i),d=[],f=o.setContext(this.getContext()),p=f.display?f.width:0,m=p/2,h=function(e){return lu(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(Z(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(Z(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=Q(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(-dl(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(`.`);au.route(a,i,c,s)})}function hm(e){return`id`in e&&`defaults`in e}var gm=new class{constructor(){this.controllers=new fm(Cf,`datasets`,!0),this.elements=new fm(Gp,`elements`),this.plugins=new fm(Object,`plugins`),this.scales=new fm(dm,`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):Mc(t,t=>{let r=n||this._getRegistryForType(t);this._exec(e,r,t)})})}_exec(e,t,n){let r=Wc(e);jc(n[`before`+r],[],n),t[e](n),jc(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 vm(e){let t={},n=[],r=Object.keys(gm.plugins.items);for(let e=0;e1&&Tm(e[0].toLowerCase());if(t)return t}throw Error(`Cannot determine type of '${e}' axis. Please provide 'axis' or 'position' option.`)}function Om(e,t,n){if(n[t+`AxisID`]===e)return{axis:t}}function km(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 Om(e,`x`,n[0])||Om(e,`y`,n[0])}return{}}function Am(e,t){let n=tu[e.type]||{scales:{}},r=t.scales||{},i=Sm(e.type,t),a=Object.create(null);return Object.keys(r).forEach(t=>{let o=r[t];if(!Z(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=Dm(t,o,km(t,e),au.scales[o.type]),c=wm(s,i),l=n.scales||{};a[t]=Rc(Object.create(null),[{axis:s},o,l[s],l[c]])}),e.data.datasets.forEach(n=>{let i=n.type||e.type,o=n.indexAxis||Sm(i,t),s=(tu[i]||{}).scales||{};Object.keys(s).forEach(e=>{let t=Cm(e,o),i=n[t+`AxisID`]||t;a[i]=a[i]||Object.create(null),Rc(a[i],[{axis:t},r[i],s[e]])})}),Object.keys(a).forEach(e=>{let t=a[e];Rc(t,[au.scales[t.type],au.scale])}),a}function jm(e){let t=e.options||(e.options={});t.plugins=Q(t.plugins,{}),t.scales=Am(e,t)}function Mm(e){return e=e||{},e.datasets=e.datasets||[],e.labels=e.labels||[],e}function Nm(e){return e=e||{},e.data=Mm(e.data),jm(e),e}var Pm=new Map,Fm=new Set;function Im(e,t){let n=Pm.get(e);return n||(n=t(),Pm.set(e,n),Fm.add(n)),n}var Lm=(e,t,n)=>{let r=Uc(t,n);r!==void 0&&e.add(r)},Rm=class{constructor(e){this._config=Nm(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=Mm(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(),jm(e)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(e){return Im(e,()=>[[`datasets.${e}`,``]])}datasetAnimationScopeKeys(e,t){return Im(`${e}.transition.${t}`,()=>[[`datasets.${e}.transitions.${t}`,`transitions.${t}`],[`datasets.${e}`,``]])}datasetElementScopeKeys(e,t){return Im(`${e}-${t}`,()=>[[`datasets.${e}.elements.${t}`,`datasets.${e}`,`elements.${t}`,``]])}pluginScopeKeys(e){let t=e.id,n=this.type;return Im(`${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=>Lm(s,e,t))),t.forEach(e=>Lm(s,r,e)),t.forEach(e=>Lm(s,tu[i]||{},e)),t.forEach(e=>Lm(s,au,e)),t.forEach(e=>Lm(s,nu,e))});let c=Array.from(s);return c.length===0&&c.push(Object.create(null)),Fm.has(t)&&a.set(t,c),c}chartOptionScopes(){let{options:e,type:t}=this;return[e,tu[t]||{},au.datasets[t]||{},{type:t},au,nu]}resolveNamedOptions(e,t,n,r=[``]){let i={$shared:!0},{resolver:a,subPrefixes:o}=zm(this._resolverCache,e,r),s=a;if(Vm(a,t)){i.$shared=!1,n=Kc(n)?n():n;let t=this.createResolver(e,n,o);s=Iu(a,n,t)}for(let e of t)i[e]=s[e];return i}createResolver(e,t,n=[``],r){let{resolver:i}=zm(this._resolverCache,e,n);return Z(t)?Iu(i,t,void 0,r):i}};function zm(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:Fu(t,n),subPrefixes:n.filter(e=>!e.toLowerCase().includes(`hover`))},r.set(i,a)),a}var Bm=e=>Z(e)&&Object.getOwnPropertyNames(e).some(t=>Kc(e[t]));function Vm(e,t){let{isScriptable:n,isIndexable:r}=Lu(e);for(let i of t){let t=n(i),a=r(i),o=(a||t)&&e[i];if(t&&(Kc(o)||Bm(o))||a&&Ec(o))return!0}return!1}var Hm=`4.6.1`,Um=[`top`,`bottom`,`left`,`right`,`chartArea`];function Wm(e,t){return e===`top`||e===`bottom`||Um.indexOf(e)===-1&&t===`x`}function Gm(e,t){return function(n,r){return n[e]===r[e]?n[t]-r[t]:n[e]-r[e]}}function Km(e){let t=e.chart,n=t.options.animation;t.notifyPlugins(`afterRender`),jc(n&&n.onComplete,[e],t)}function qm(e){let t=e.chart,n=t.options.animation;jc(n&&n.onProgress,[e],t)}function Jm(e){return dd()&&typeof e==`string`?e=document.getElementById(e):e&&e.length&&(e=e[0]),e&&e.canvas&&(e=e.canvas),e}var Ym={},Xm=e=>{let t=Jm(e);return Object.values(Ym).filter(e=>e.canvas===t).pop()};function Zm(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 Qm(e,t,n,r){return!n||e.type===`mouseout`?null:r?t:e}var $m=class{static register(...e){gm.add(...e),eh()}static unregister(...e){gm.remove(...e),eh()}constructor(e,t){let n=this.config=new Rm(t),r=Jm(e),i=Xm(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||(Wp(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=Tc(),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 _m,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=Ml(e=>this.update(e),a.resizeDelay||0),this._dataChanges=[],Ym[this.id]=this,!o||!s?console.error(`Failed to create chart: can't acquire context from the given item`):(Xd.listen(this,`complete`,Km),Xd.listen(this,`progress`,qm),this._initialize(),this.attached&&this.update())}get aspectRatio(){let{options:{aspectRatio:e,maintainAspectRatio:t},width:n,height:r,_aspectRatio:i}=this;return X(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 gm}_initialize(){return this.notifyPlugins(`beforeInit`),this.options.responsive?this.resize():wd(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins(`afterInit`),this}clear(){return uu(this.canvas,this.ctx),this}stop(){return Xd.stop(this),this}resize(e,t){Xd.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,wd(this,o,!0)&&(this.notifyPlugins(`resize`,{size:a}),jc(n.onResize,[this,a],this),this.attached&&this._doResize(s)&&this.render())}ensureScalesHaveIDs(){Mc(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=Dm(e,n),i=r===`r`,a=r===`x`;return{options:n,dposition:i?`chartArea`:a?`bottom`:`left`,dtype:i?`radialLinear`:a?`category`:`linear`}}))),Mc(i,t=>{let i=t.options,a=i.id,o=Dm(a,i),s=Q(i.type,t.dtype);(i.position===void 0||Wm(i.position,o)!==Wm(t.dposition))&&(i.position=t.dposition),r[a]=!0;let c=null;a in n&&n[a].type===s?c=n[a]:(c=new(gm.getScale(s))({id:a,type:s,ctx:this.ctx,chart:this}),n[c.id]=c),c.init(i,e)}),Mc(r,(e,t)=>{e||delete n[t]}),Mc(n,e=>{xp.configure(this,e,e.options),xp.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(Gm(`z`,`_idx`));let{_active:o,_lastEvent:s}=this;s?this._eventHandler(s,!0):o.length&&this._updateHoverStyles(o,o,!0),this.render()}_updateScales(){Mc(this.scales,e=>{xp.removeBox(this,e)}),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){let e=this.options;qc(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)Zm(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;xp.update(this,this.width,this.height,e);let t=this.chartArea,n=t.width<=0||t.height<=0;this._layers=[],Mc(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=Yd(this,e);this.notifyPlugins(`beforeDatasetDraw`,n)!==!1&&(r&&mu(t,r),e.controller.draw(),r&&hu(t),n.cancelable=!1,this.notifyPlugins(`afterDatasetDraw`,n))}isPointInArea(e){return pu(e,this.chartArea,this._minPadding)}getElementsAtEventForMode(e,t,n,r){let i=ip.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=Pu(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);Gc(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(),Xd.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)};Mc(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(){Mc(this._listeners,(e,t)=>{this.platform.removeEventListener(this,t,e)}),this._listeners={},Mc(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}});Nc(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=Jc(e),c=Qm(e,this._lastEvent,n,s);n&&(this._lastEvent=null,jc(i.onHover,[e,o,this],this),s&&jc(i.onClick,[e,o,this],this));let l=!Nc(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)}};V($m,`defaults`,au),V($m,`instances`,Ym),V($m,`overrides`,tu),V($m,`registry`,gm),V($m,`version`,Hm),V($m,`getChart`,Xm);function eh(){return Mc($m.instances,e=>e._plugins.invalidate())}function th(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,_l(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,_l(r-n));e.arc(i,a,s+l/2,n-t/2,r+t/2,!0)}else{let t=Math.min(l/2,o*_l(r-n));if(u===`round`)e.arc(i,a,t,n-$/2,r+$/2,!0);else if(u===`bevel`){let o=2*t*t,s=-o*Math.cos(n+$/2)+i,c=-o*Math.sin(n+$/2)+a,l=o*Math.cos(r+$/2)+i,u=o*Math.sin(r+$/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 nh(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+$c,r-$c),e.closePath(),e.clip()}function rh(e){return Du(e,[`outerStart`,`outerEnd`,`innerStart`,`innerEnd`])}function ih(e,t,n,r){let i=rh(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 yl(e,0,Math.min(a,t))};return{outerStart:s(i.outerStart),outerEnd:s(i.outerEnd),innerStart:yl(i.innerStart,0,o),innerEnd:yl(i.innerEnd,0,o)}}function ah(e,t,n,r){return{x:n+e*Math.cos(t),y:r+e*Math.sin(t)}}function oh(e,t,n,r){let{x:i,y:a,startAngle:o,pixelMargin:s,innerRadius:c}=t,l=Math.max(t.outerRadius+r+n-s,0),u=c>0?c+r+n+s:0;e.beginPath(),e.arc(i,a,l,o,o+Yc),u>0&&(e.moveTo(i+Math.cos(o)*u,a+Math.sin(o)*u),e.arc(i,a,u,o+Yc,o,!0)),e.closePath()}function sh(e,t,n,r,i,a){let{x:o,y:s,startAngle:c,pixelMargin:l,innerRadius:u}=t,{spacingMode:d=`angular`}=t.options,f=i-c;if(a&&t.options.selfJoin&&Math.abs(f)>=Yc-1e-4){oh(e,t,n,r);return}let p=Math.max(t.outerRadius+r+n-l,0),m=u>0?u+r+n+l:0,h=0,g=0,_=p>0?Math.max(.001,f*p-n/$)/p:.001,v=(f-_)/2;if(r){let e=((u>0?u-r:0)+(p>0?p-r:0))/2,t=(()=>{let t=e===0?f:f*e/(e+r);return(f-t)/2})(),n=e>0?Math.asin(Math.min(1,r/e)):0,i=Math.max(0,_/2-.001),a=Math.sin(i);if(d===`parallel`){if(m===0&&a>0){let e=r/a,t=Math.max(0,p-.001);m=Math.min(e,t)}let e=Math.min(p>0?p*a:1/0,m>0?m*a:1/0),t=Math.min(r,e);h=p>0?Math.asin(Math.min(1,t/p)):Math.min(i,n),g=m>0?Math.asin(Math.min(1,t/m)):h}else d===`proportional`?(h=Math.min(i,t),g=Math.min(i,t)):(h=Math.min(i,n),g=Math.min(i,n))}let y=c+v+h,b=i-v-h,x=c+v+g,S=i-v-g,C=Math.min(b-y,S-x),{outerStart:w,outerEnd:T,innerStart:E,innerEnd:D}=ih(t,m,p,C),O=p-w,k=p-T,A=y+w/O,j=b-T/k,M=m+E,N=m+D,P=x+E/M,F=S-D/N;if(e.beginPath(),a){let t=(A+j)/2;if(e.arc(o,s,p,A,t),e.arc(o,s,p,t,j),T>0){let t=ah(k,j,o,s);e.arc(t.x,t.y,T,j,b+$c)}let n=ah(N,S,o,s);if(e.lineTo(n.x,n.y),D>0){let t=ah(N,F,o,s);e.arc(t.x,t.y,D,S+$c,F+Math.PI)}let r=(S-D/m+(x+E/m))/2;if(e.arc(o,s,m,S-D/m,r,!0),e.arc(o,s,m,r,x+E/m,!0),E>0){let t=ah(M,P,o,s);e.arc(t.x,t.y,E,P+Math.PI,x-$c)}let i=ah(O,y,o,s);if(e.lineTo(i.x,i.y),w>0){let t=ah(O,A,o,s);e.arc(t.x,t.y,w,y-$c,A)}}else{e.moveTo(o,s);let t=Math.cos(A)*p+o,n=Math.sin(A)*p+s;e.lineTo(t,n);let r=Math.cos(j)*p+o,i=Math.sin(j)*p+s;e.lineTo(r,i)}e.closePath()}function ch(e,t,n,r,i){let{fullCircles:a,startAngle:o,circumference:s}=t,c=t.endAngle;if(a){sh(e,t,n,r,c,i);for(let t=0;t=Yc-1e-4;if(a){sh(e,t,n,r,h,i);for(let t=0;t0)&&c.selfJoin&&h-o>=$&&p===0&&u!==`miter`&&th(e,t,h),a||(sh(e,t,n,r,h,i),e.stroke())}var uh=class extends Gp{constructor(e){super(),V(this,`circumference`,void 0),V(this,`endAngle`,void 0),V(this,`fullCircles`,void 0),V(this,`innerRadius`,void 0),V(this,`outerRadius`,void 0),V(this,`pixelMargin`,void 0),V(this,`startAngle`,void 0),V(this,`circular`,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,this.circular=!1,e&&Object.assign(this,e)}inRange(e,t,n){let{angle:r,distance:i}=ml(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=Q(l,o-a),f=vl(r,a,o)&&a!==o,p=d>=Yc||f,m=xl(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>Yc?Math.floor(n/Yc):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($,n||0)));e.fillStyle=t.backgroundColor,e.strokeStyle=t.borderColor,ch(e,this,s,i,a),lh(e,this,s,i,a),e.restore()}};V(uh,`id`,`arc`),V(uh,`defaults`,{borderAlign:`center`,borderColor:`#fff`,borderDash:[],borderDashOffset:0,borderJoinStyle:void 0,borderRadius:0,borderWidth:2,offset:0,spacing:0,spacingMode:`angular`,angle:void 0,circular:!0,selfJoin:!1}),V(uh,`defaultRoutes`,{backgroundColor:`backgroundColor`}),V(uh,`descriptors`,{_scriptable:!0,_indexable:e=>e!==`borderDash`});function dh(e,t,n=t){e.lineCap=Q(n.borderCapStyle,t.borderCapStyle),e.setLineDash(Q(n.borderDash,t.borderDash)),e.lineDashOffset=Q(n.borderDashOffset,t.borderDashOffset),e.lineJoin=Q(n.borderJoinStyle,t.borderJoinStyle),e.lineWidth=Q(n.borderWidth,t.borderWidth),e.strokeStyle=Q(n.borderColor,t.borderColor)}function fh(e,t,n){e.lineTo(n.x,n.y)}function ph(e){return e.stepped?gu:e.tension||e.cubicInterpolationMode===`monotone`?_u:fh}function mh(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 _h(e){let t=e.options,n=t.borderDash&&t.borderDash.length;return!e._decimated&&!e._loop&&!t.tension&&t.cubicInterpolationMode!==`monotone`&&!t.stepped&&!n?gh:hh}function vh(e){return e.stepped?Od:e.tension||e.cubicInterpolationMode===`monotone`?kd:Dd}function yh(e,t,n,r){let i=t._path;i||(i=t._path=new Path2D,t.path(i,n,r)&&i.closePath()),dh(e,t.options),e.stroke(i)}function bh(e,t,n,r){let{segments:i,options:a}=t,o=_h(t);for(let s of i)dh(e,a,s.style),e.beginPath(),o(e,t,s,{start:n,end:n+r-1})&&e.closePath(),e.stroke()}var xh=typeof Path2D==`function`;function Sh(e,t,n,r){xh&&!t.options.segment?yh(e,t,n,r):bh(e,t,n,r)}var Ch=class extends Gp{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;ud(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=Hd(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=zd(this,{property:t,start:r,end:r});if(!a.length)return;let o=[],s=vh(n),c,l;for(c=0,l=a.length;ce!==`borderDash`&&e!==`fill`});function wh(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 Rh(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 zh(e,t){let n=e.chart.ctx,{body:r,footer:i,title:a}=e,{boxWidth:o,boxHeight:s}=t,c=Au(t.bodyFont),l=Au(t.titleFont),u=Au(t.footerFont),d=a.length,f=i.length,p=r.length,m=ku(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,Q(e.title,y),n.font=c.string,Q(e.beforeBody.concat(e.afterBody),y),v=t.displayColors?o+2+t.boxPadding:0,Q(r,e=>{Q(e.before,y),Q(e.lines,y),Q(e.after,y)}),v=0,n.font=u.string,Q(e.footer,y),n.restore(),g+=m.width,{width:g,height:h}}function Bh(e,t){let{y:n,height:r}=t;return ne.height-r/2?`bottom`:`center`}function Vh(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 Hh(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`),Vh(l,e,t,n)&&(l=`center`),l}function Uh(e,t,n){let r=n.yAlign||t.yAlign||Bh(e,n);return{xAlign:n.xAlign||t.xAlign||Hh(e,t,n,r),yAlign:r}}function Wh(e,t){let{x:n,width:r}=e;return t===`right`?n-=r:t===`center`&&(n-=r/2),n}function Gh(e,t,n){let{y:r,height:i}=e;return t===`top`?r+=n:r-=t===`bottom`?i+n:i/2,r}function Kh(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}=Ou(o),m=Wh(t,s),h=Gh(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:vl(m,0,r.width-t.width),y:vl(h,0,r.height-t.height)}}function qh(e,t,n){let r=ku(n.padding);return t===`center`?e.x+e.width/2:t===`right`?e.x+e.width-r.right:e.x+r.left}function Jh(e){return Ih([],Lh(e))}function Yh(e,t,n){return Nu(e,{tooltip:t,tooltipItems:n,type:`tooltip`})}function Xh(e,t){let n=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return n?e.override(n):e}var Zh={beforeTitle:wc,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=Xh(n,e);Ih(t.before,Lh(Qh(i,`beforeLabel`,this,e))),Ih(t.lines,Qh(i,`label`,this,e)),Ih(t.after,Lh(Qh(i,`afterLabel`,this,e))),r.push(t)}),r}getAfterBody(e,t){return Jh(Qh(t.callbacks,`afterBody`,this,e))}getFooter(e,t){let{callbacks:n}=t,r=Qh(n,`beforeFooter`,this,e),i=Qh(n,`footer`,this,e),a=Qh(n,`afterFooter`,this,e),o=[];return o=Ih(o,Lh(r)),o=Ih(o,Lh(i)),o=Ih(o,Lh(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))),Q(o,t=>{let n=Xh(e.callbacks,t);r.push(Qh(n,`labelColor`,this,t)),i.push(Qh(n,`labelPointStyle`,this,t)),a.push(Qh(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=Fh[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=zh(this,n),o=Object.assign({},e,t),s=Uh(this.chart,n,o),c=Kh(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}=Ou(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=jd(n.rtl,this.x,this.width);for(e.x=qh(this,n.titleAlign,n),t.textAlign=c.textAlign(n.titleAlign),t.textBaseline=`middle`,a=Au(n.titleFont),o=n.titleSpacing,t.fillStyle=n.titleColor,t.font=a.string,s=0;se!==0)?(e.beginPath(),e.fillStyle=i.multiKeyBackground,xu(e,{x:t,y:p,w:c,h:s,radius:o}),e.fill(),e.stroke(),e.fillStyle=a.backgroundColor,e.beginPath(),xu(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=Au(n.bodyFont),d=u.lineHeight,f=0,p=jd(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=qh(this,h,n),t.fillStyle=n.bodyColor,Q(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=Fh[e.position].call(this,this._active,this._eventPosition);if(!n)return;let a=this._size=zh(this,e),o=Object.assign({},n,this._size),s=Uh(t,e,o),c=Kh(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=ku(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),Md(e,t.textDirection),i.y+=a.top,this.drawTitle(i,e,t),this.drawBody(i,e,t),this.drawFooter(i,e,t),Nd(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=!Mc(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||!Mc(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=Fh[i.position].call(this,e,t);return a!==!1&&(n!==a.x||r!==a.y)}};z($h,`positioners`,Fh);var eg={id:`tooltip`,_element:$h,positioners:Fh,afterInit(e,t,n){n&&(e.tooltip=new $h({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:Zh},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`]},tg=(e,t,n,r)=>(typeof t==`string`?(n=e.push(t)-1,r.unshift({index:n,label:t})):isNaN(t)&&(n=null),n);function ng(e,t,n,r){let i=e.indexOf(t);return i===-1?tg(e,t,n,r):i===e.lastIndexOf(t)?i:n}var rg=(e,t)=>e===null?null:vl(Math.round(e),0,t);function ig(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}};z(ag,`id`,`category`),z(ag,`defaults`,{ticks:{callback:ig}});function og(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=!Y(a),_=!Y(o),v=!Y(c),y=(h-m)/(u+1),b=il((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=il(w*b/p/f)*f),Y(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&&cl((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=rl(w,Math.round(w),b/1e3)?Math.round(w):Math.ceil(w));let T=Math.max(fl(b),fl(S));x=10**(Y(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&&rl(n[n.length-1].value,o,sg(o,y,e))?n[n.length-1].value=o:n.push({value:o}):_&&C!==o||n.push({value:C}),n}function sg(e,t,{horizontal:n,minRotation:r}){let i=ul(r),a=(n?Math.sin(i):Math.cos(i))||.001,o=.75*t*(``+e).length;return Math.min(t/a,o)}var cg=class extends um{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 Y(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=nl(r),t=nl(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=og({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`&&ll(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 Yl(e,this.chart.options.locale,this.options.ticks.format)}},lg=class extends cg{determineDataLimits(){let{min:e,max:t}=this.getMinMax(!0);this.min=Dc(e)?e:0,this.max=Dc(t)?t:1,this.handleTickRangeOptions()}computeTickLimit(){let e=this.isHorizontal(),t=e?this.width:this.height,n=ul(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}};z(lg,`id`,`linear`),z(lg,`defaults`,{ticks:{callback:Ql.formatters.numeric}});var ug=e=>Math.floor(tl(e)),dg=(e,t)=>10**(ug(e)+t);function fg(e){return e/10**ug(e)==1}function pg(e,t,n){let r=10**n,i=Math.floor(e/r);return Math.ceil(t/r)-i}function mg(e,t){let n=ug(t-e);for(;pg(e,t,n)>10;)n++;for(;pg(e,t,n)<10;)n--;return Math.min(n,ug(e))}function hg(e,{min:t,max:n}){t=Oc(e.min,t);let r=[],i=ug(t),a=mg(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=Oc(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=Oc(e.max,f);return r.push({value:p,major:fg(p),significand:d}),r}var gg=class extends um{constructor(e){super(e),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(e,t){let n=cg.prototype.parse.apply(this,[e,t]);if(n===0)this._zero=!0;else return Dc(n)&&n>0?n:null}determineDataLimits(){let{min:e,max:t}=this.getMinMax(!0);this.min=Dc(e)?Math.max(0,e):null,this.max=Dc(t)?Math.max(0,t):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!Dc(this._userMin)&&(this.min=e===dg(this.min,0)?dg(this.min,-1):dg(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(dg(n,-1)),a(dg(r,1)))),n<=0&&i(dg(r,-1)),r<=0&&a(dg(n,1)),this.min=n,this.max=r}buildTicks(){let e=this.options,t=hg({min:this._userMin,max:this._userMax},this);return e.bounds===`ticks`&&ll(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`:Yl(e,this.chart.options.locale,this.options.ticks.format)}configure(){let e=this.min;super.configure(),this._startValue=tl(e),this._valueRange=tl(this.max)-tl(e)}getPixelForValue(e){return(e===void 0||e===0)&&(e=this.min),e===null||isNaN(e)?NaN:this.getPixelForDecimal(e===this.min?0:(tl(e)-this._startValue)/this._valueRange)}getValueForPixel(e){let t=this.getDecimalForPixel(e);return 10**(this._startValue+t*this._valueRange)}};z(gg,`id`,`logarithmic`),z(gg,`defaults`,{ticks:{callback:Ql.formatters.logarithmic,major:{enabled:!0}}});function _g(e){let t=e.ticks;if(t.display&&e.display){let e=ku(t.backdropPadding);return Z(t.font&&t.font.size,iu.font.size)+e.height}return 0}function vg(e,t,n){return n=Ec(n)?n:[n],{w:su(e,t.string,n),h:n.length*t.lineHeight}}function yg(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 bg(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?$/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 Sg(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(dl(gl(c.angle+Qc))),u=Dg(c.y,s.h,l),d=Tg(l),f=Eg(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 Cg(e,t){if(!t)return!0;let{left:n,top:r,right:i,bottom:a}=e;return!(fu({x:n,y:r},t)||fu({x:n,y:a},t)||fu({x:i,y:r},t)||fu({x:i,y:a},t))}function wg(e,t,n){let r=[],i=e._pointLabels.length,a=e.options,{centerPointLabels:o,display:s}=a.pointLabels,c={extra:_g(a)/2,additionalAngle:o?$/i:0},l;for(let a=0;a270||n<90)&&(e-=t),e}function Og(e,t,n){let{left:r,top:i,right:a,bottom:o}=n,{backdropColor:s}=t;if(!Y(s)){let n=Ou(t.borderRadius),c=ku(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(),xu(e,{x:l,y:u,w:d,h:f,radius:n}),e.fill()):e.fillRect(l,u,d,f)}}function kg(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));Og(n,a,t);let o=Au(a.font),{x:s,y:c,textAlign:l}=t;bu(n,e._pointLabels[i],s,c+o.lineHeight/2,o,{color:a.color,textAlign:l,textBaseline:`middle`})}}function Ag(e,t,n,r){let{ctx:i}=e;if(n)i.arc(e.xCenter,e.yCenter,t,0,Jc);else{let n=e.getPointPosition(0,t);i.moveTo(n.x,n.y);for(let a=1;a{let n=jc(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?bg(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=Jc/(this._pointLabels.length||1),n=this.options.startAngle||0;return gl(e*t+ul(n))}getDistanceFromCenterForValue(e){if(Y(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(Y(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);jg(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=Au(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=ku(s.backdropPadding);e.fillRect(-a/2-t.left,-i-c.size/2-t.top,a+t.width,c.size+t.height)}bu(e,r.label,0,-i,c,{color:s.color,strokeColor:s.textStrokeColor,strokeWidth:s.textStrokeWidth})}),e.restore()}drawTitle(){}};z(Ng,`id`,`radialLinear`),z(Ng,`defaults`,{display:!0,animate:!0,position:`chartArea`,angleLines:{display:!0,lineWidth:1,borderDash:[],borderDashOffset:0},grid:{circular:!1},startAngle:0,ticks:{showLabelBackdrop:!0,callback:Ql.formatters.numeric},pointLabels:{backdropColor:void 0,backdropPadding:2,display:!0,font:{size:10},callback(e){return e},padding:5,centerPointLabels:!1}}),z(Ng,`defaultRoutes`,{"angleLines.color":`borderColor`,"pointLabels.color":`color`,"ticks.color":`color`}),z(Ng,`descriptors`,{angleLines:{_fallback:`grid`}});var Pg={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}},Fg=Object.keys(Pg);function Ig(e,t){return e-t}function Lg(e,t){if(Y(t))return null;let n=e._adapter,{parser:r,round:i,isoWeekday:a}=e._parseOpts,o=t;return typeof r==`function`&&(o=r(o)),Dc(o)||(o=typeof r==`string`?n.parse(o,r):n.parse(o)),o===null?null:(i&&(o=i===`week`&&(sl(a)||a===!0)?n.startOf(o,`isoWeek`,a):n.startOf(o,i)),+o)}function Rg(e,t,n,r){let i=Fg.length;for(let a=Fg.indexOf(e);a=Fg.indexOf(n);a--){let n=Fg[a];if(Pg[n].common&&e._adapter.diff(i,r,n)>=t-1)return n}return Fg[n?Fg.indexOf(n):0]}function Bg(e){for(let t=Fg.indexOf(e)+1,n=Fg.length;t=t?n[r]:n[i];e[a]=!0}}function Hg(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 Ug(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=vl(t,0,a),n=vl(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||Rg(i.minUnit,t,n,this._getLabelCapacity(t)),o=Z(r.ticks.stepSize,1),s=a===`week`&&i.isoWeekday,c=sl(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 jc(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}=Sl(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}=Sl(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 Kg=class extends Wg{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=Gg(t,this.min),this._tableRange=Gg(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(Gg(this._table,e)-this._minPos)/this._tableRange}getValueForPixel(e){let t=this._offsets,n=this.getDecimalForPixel(e)/t.factor-t.end;return Gg(this._table,n*this._tableRange+this._minPos,!0)}};z(Kg,`id`,`timeseries`),z(Kg,`defaults`,Wg.defaults),Qm.register(Rf,Ph,ag,lg,eg);var qg=new Intl.DateTimeFormat(`ko-KR`,{hour:`2-digit`,hour12:!1}),Jg=`#94a3b8`,Yg=`rgba(148, 163, 184, 0.18)`,Xg=`#0d9488`,Zg=`#0f766e`;function Qg(e){return qg.format(new Date(e*1e3))}function $g(e){return e.map((t,n)=>n%3==0||n===e.length-1?Qg(t.bucketStart):``)}function e_({buckets:e}){let t=L(null),n=L(null),r=L(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?Qg(n.bucketStart):``},label:e=>`차단 ${J(Number(e.raw??0))}`,afterLabel:e=>{let t=r.current[e.dataIndex];return t?`DNS 요청 ${J(t.queries)}`:``}}}},scales:{x:{border:{display:!1},grid:{display:!1},ticks:{autoSkip:!1,color:Jg,font:{family:a,size:11,weight:700},maxRotation:0,minRotation:0}},y:{beginAtZero:!0,border:{display:!1},grid:{color:Yg},ticks:{color:Jg,font:{family:a,size:11,weight:700},maxTicksLimit:4,precision:0}}}};return n.current=new Qm(e,{type:`bar`,data:{labels:$g(r.current),datasets:[{data:r.current.map(e=>e.blocked),backgroundColor:Xg,hoverBackgroundColor:Zg,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=$g(e),t.data.datasets[0].data=e.map(e=>e.blocked),t.update())},[e]),H(`div`,{class:`mt-5 h-44 sm:h-52`,children:H(`canvas`,{"aria-label":`최근 24시간 시간대별 SafeShield 차단 요청`,ref:t,role:`img`,children:`최근 24시간 시간대별 SafeShield 차단 요청 차트`})})}var t_=3600,n_=24;function r_(e,t){return e<=0?`0.0%`:`${(t/e*100).toFixed(1)}%`}function i_(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/t_)*t_}function a_(e){let t=new Map(e.hourly.map(e=>[e.bucketStart,e])),n=i_(e)-23*t_;return Array.from({length:n_},(e,r)=>{let i=n+r*t_;return t.get(i)??{bucketStart:i,queries:0,blocked:0}})}function o_({children:e}){return H(`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:H(`p`,{class:`m-0 max-w-lg text-sm font-semibold leading-6 text-slate-500`,children:e})})}function s_({data:e,error:t,loading:n,nowTimestamp:r,refreshing:i}){let a;if(n&&!e)a=H(o_,{children:`SafeShield 보호 활동을 불러오는 중입니다.`});else if(t&&!e)a=H(o_,{children:`SafeShield 보호 활동을 불러오지 못했습니다. SafeShield 페이지에서 상태를 확인해 주세요.`});else if(!e||!e.available)a=H(o_,{children:`현재 SafeShield에서 차단 통계를 제공하지 않습니다. 통계 기능을 확인해 주세요.`});else if(!e.enabled)a=H(o_,{children:`통계 수집이 꺼져 있습니다. SafeShield 페이지에서 통계를 켜면 최근 24시간 활동이 표시됩니다.`});else{let t=a_(e),n=t.reduce((e,t)=>({queries:e.queries+t.queries,blocked:e.blocked+t.blocked}),{queries:0,blocked:0});a=H(b,{children:[H(`dl`,{class:`mt-5 grid gap-3 sm:grid-cols-3`,children:[H(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3`,children:[H(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:`DNS 요청`}),H(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-slate-950`,children:J(n.queries)})]}),H(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3`,children:[H(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:`차단`}),H(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-teal-700`,children:J(n.blocked)})]}),H(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3`,children:[H(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:`차단율`}),H(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-slate-950`,children:r_(n.queries,n.blocked)})]})]}),H(e_,{buckets:t})]})}return H(`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:[H(`div`,{class:`min-w-0`,children:[H(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`SafeShield activity`}),H(`div`,{class:`mt-2 flex min-w-0 items-center justify-between gap-3`,children:[H(`h3`,{class:`m-0 min-w-0 text-xl font-black text-slate-950`,children:`최근 24시간 보호 활동`}),H(`a`,{class:`shrink-0 whitespace-nowrap text-xs font-extrabold text-teal-700 no-underline hover:text-teal-800`,href:`#safeshield`,children:`상세 통계 →`})]}),e?.updatedAt?H(`p`,{class:`mt-2 mb-0 text-[0.68rem] font-semibold text-slate-400`,title:jo(e.updatedAt),children:i?`갱신 중…`:`집계: ${Mo(e.updatedAt,r)}`}):null]}),a,t&&e?H(`p`,{class:`mt-3 mb-0 text-xs font-bold text-amber-700`,children:`최근 통계 갱신에 실패해 마지막 정상 데이터를 표시하고 있습니다.`}):null]})}var c_=7200;function l_(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,c_)}var u_=2*Math.PI*39;function d_(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 f_({detail:e,eyebrow:t,href:n,icon:r,linkLabel:i=`자세히 보기 →`,meta:a,metaState:o,metaWarning:s=!1,state:c=`neutral`,value:l}){let u=H(b,{children:[H(`div`,{class:`flex items-start justify-between gap-4`,children:[H(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.16em] text-slate-500`,children:t}),H(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl ring-1 ring-inset ${d_(c)}`,children:r})]}),H(`p`,{class:`mt-4 mb-0 break-words text-2xl font-black tracking-tight text-slate-950`,children:l}),H(`p`,{class:`mt-2 mb-0 min-h-10 text-sm font-medium leading-5 text-slate-500`,children:e}),a?H(`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?H(`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?H(`a`,{class:`${d} no-underline hover:border-slate-300 hover:shadow-md`,href:n,children:u}):H(`article`,{class:d,children:u})}var p_=6e4;function m_(e,t){return!e||!Number.isFinite(e)||e<=0?null:Math.max(0,t-e)}function h_(e,t,n,r){return!t||t<=0?`${e}: ${r}`:H(`span`,{title:jo(t),children:[e,`: `,Mo(t,n)]})}function g_(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 __(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 v_({label:e,value:t}){return H(`div`,{class:`flex min-w-0 items-start justify-between gap-4 border-b border-slate-100 py-3 last:border-b-0`,children:[H(`dt`,{class:`text-sm font-semibold text-slate-500`,children:e}),H(`dd`,{class:`m-0 min-w-0 max-w-[68%] break-words text-right text-sm font-extrabold text-slate-950`,children:t})]})}function y_(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 b_({data:e,error:t,loading:n,nowTimestamp:r}){let i=!!(e&&e.generatedAt>0),a=y_(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 H(`div`,{class:`mt-5 border-t border-slate-200 pt-5`,children:[H(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[H(`div`,{class:`flex items-center gap-2`,children:[H(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:`장치 진단`}),i?H(`span`,{class:`rounded-full px-2.5 py-1 text-[11px] font-extrabold ${a.badge}`,children:a.label}):null]}),H(`a`,{class:`text-xs font-extrabold text-teal-700 no-underline hover:text-teal-900`,href:`#settings`,children:`상세 보기 →`})]}),t&&!i?H(`div`,{class:`mt-3 rounded-xl border border-amber-200 bg-amber-50/70 px-4 py-3`,children:H(`div`,{class:`flex items-start gap-2`,children:[H(W,{class:`mt-0.5 size-4 shrink-0 text-amber-700`}),H(`div`,{class:`min-w-0`,children:[H(`p`,{class:`m-0 text-sm font-extrabold text-slate-900`,children:`진단 상태를 확인하지 못했습니다.`}),H(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`설정에서 장치 진단 상태를 다시 확인해 주세요.`})]})]})}):i&&e?H(`div`,{class:`mt-3 rounded-xl border px-4 py-3 ${a.panel}`,children:[H(`div`,{class:`flex min-w-0 items-start gap-3`,children:[e.overall===`ok`?H(wn,{class:`mt-0.5 size-5 shrink-0 ${a.icon}`}):e.overall===`initializing`?H(pn,{class:`mt-0.5 size-5 shrink-0 ${a.icon}`}):H(W,{class:`mt-0.5 size-5 shrink-0 ${a.icon}`}),H(`div`,{class:`min-w-0 flex-1`,children:[H(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:e.summary.message}),H(`p`,{class:`mt-1 mb-0 text-xs font-semibold leading-5 text-slate-500`,children:[`마지막 진단: `,Mo(e.generatedAt,r),` · 검사 항목:`,` `,e.summary.total,`개`]})]})]}),s.length>0?H(`div`,{class:`mt-3 grid gap-2 sm:grid-cols-2`,children:s.map(e=>H(`div`,{class:`min-w-0 rounded-lg border border-white/80 bg-white/70 px-3 py-2`,children:[H(`p`,{class:`m-0 text-xs font-extrabold text-slate-900`,children:e.label}),H(`p`,{class:`mt-1 mb-0 line-clamp-2 text-xs leading-5 text-slate-500`,children:e.message})]},e.id))}):null,c>0?H(`p`,{class:`mt-2 mb-0 text-xs font-extrabold text-slate-600`,children:[`외 `,c,`건의 확인 항목이 있습니다.`]}):null]}):H(`div`,{class:`mt-3 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3`,children:[H(`p`,{class:`m-0 text-sm font-extrabold text-slate-900`,children:n?`진단 상태를 확인하고 있습니다.`:`아직 생성된 진단 결과가 없습니다.`}),H(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`진단 결과가 생성되면 현재 장치 상태를 이곳에 요약해서 표시합니다.`})]})]})}function x_(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?`${J(e.blocklist.validLineCount)}개 차단 규칙`:`차단 목록 적용됨`;return{detail:r?`${i} · ${r.toUpperCase()}`:i,state:`healthy`,value:`보호 중`}}function S_({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=Qe(()=>i?Lo(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 H(vs,{});if(c&&!i)return H(ys,{message:c,onRetry:y});if(!i)return null;let N=A?Math.round(A.percent):0,P=is(i.runtime.storage),ee=P?(P.availableRatio*u_).toFixed(2):`0`,te=x_(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?`${J(a.totals.online)}대`:s?`확인 중`:`확인 필요`,se=a?`Wi-Fi ${J(a.totals.wireless)}대 · 유선/기타 ${J(a.totals.ethernet)}대`:o||`연결 기기 요약을 불러오고 있습니다.`,ce=g_(i.network.protocol),le=i.network.available?i.network.ipv4Address?`WAN ${i.network.ipv4Address}${ce?` · ${ce}`:``}`:ce?`${ce} · WAN 주소를 받지 못했습니다.`:`WAN 주소를 받지 못했습니다.`:`WAN 인터페이스를 찾을 수 없습니다.`,ue=!!h?.conflict.detected,de=!i.network.up||ue||g!==null&&!h?`warning`:h?`healthy`:`neutral`,fe=ue?`네트워크 충돌`:i.network.up?`정상 연결`:`연결 확인`,pe=i.network.up?ue?`⚠ LAN 대역과 충돌합니다`:_&&!h?`네트워크 대역 확인 중`:g&&!h?`네트워크 대역을 확인하지 못했습니다.`:h?`✓ 네트워크 충돌 없음`:`네트워크 대역 확인 필요`:`WAN 연결 상태를 확인해 주세요.`,me=!i.network.up||ue||g!==null&&!h?`warning`:h?`healthy`:`neutral`,he=i.network.ipv4Address?__(i.network.ipv4Address)?`${i.network.ipv4Address} · 사설 네트워크`:i.network.ipv4Address:`할당되지 않음`,ge=m_(b?.timestamps.lastSuccess,j),_e=b?Math.max(b.timestamps.refreshIntervalS*2,86400):null,ve=!!(b?.enabled&&b.status!==`running`&&ge!==null&&_e!==null&&ge>_e),ye=l_(D,j),be=!!l?.current.metadataAvailable,xe=d&&!l?`확인 중`:u&&!l?`확인 필요`:be?l?.current.releaseVersion?`SmartSafeHub ${l.current.releaseVersion}`:`SmartSafeHub 펌웨어`:`${i.software.distribution} ${i.software.version}`,Se=d&&!l?`확인 중`:u&&!l?`확인 필요`:be&&l?.current.buildId||i.software.revision,Ce=be?`빌드 ID`:`리비전`;return H(`div`,{class:`min-w-0 space-y-6`,children:[H(`section`,{"aria-labelledby":`dashboard-overview-title`,children:[H(q,{description:`인터넷, SafeShield, 연결 기기와 소프트웨어 상태를 한눈에 확인합니다.`,eyebrow:`Overview`,id:`dashboard-overview-title`,title:`시스템 개요`}),H(`div`,{class:`grid min-w-0 gap-4 sm:grid-cols-2 xl:grid-cols-4`,children:[H(f_,{detail:le,eyebrow:`Internet`,href:`#network`,icon:H(dn,{class:`size-5`}),linkLabel:ue?`해결하기 →`:`자세히 보기 →`,meta:pe,metaState:me,state:de,value:fe}),H(f_,{detail:te.detail,eyebrow:`SafeShield`,href:`#safeshield`,icon:H(Yt,{class:`size-5`}),meta:h_(ve?`차단 목록 갱신 지연`:`차단 목록 갱신`,b?.timestamps.lastSuccess,j,S?`확인 중`:`기록 없음`),metaWarning:ve,state:te.state===`healthy`&&ve?`warning`:te.state,value:te.value}),H(f_,{detail:se,eyebrow:`Connected devices`,href:`#devices`,icon:H(Zt,{class:`size-5`}),meta:h_(`목록 확인`,a?.generatedAt,j,s?`확인 중`:`기록 없음`),state:a?`healthy`:o?`warning`:`neutral`,value:oe}),H(f_,{detail:ae,eyebrow:`Software update`,href:`#system`,icon:H(on,{class:`size-5`}),meta:D&&!D.settings.checkEnabled?h_(`자동 확인 꺼짐 · 마지막 확인`,D.lastCheckAt,j,`확인 기록 없음`):h_(ye?`업데이트 확인 지연`:`마지막 확인`,D?.lastCheckAt,j,k?`확인 중`:`아직 없음`),metaWarning:ye,state:O||ye||re?`warning`:D?.lastCheckAt?`healthy`:`neutral`,value:ie})]}),e===!1?H(`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:[H(`div`,{class:`flex min-w-0 items-start gap-3`,children:[H(`span`,{class:`ssh-dashboard-account-banner-icon grid size-10 shrink-0 place-items-center rounded-xl`,"aria-hidden":`true`,children:H($t,{class:`size-5`})}),H(`div`,{class:`min-w-0`,children:[H(`div`,{class:`flex flex-wrap items-center gap-2`,children:[H(`p`,{class:`ssh-dashboard-account-banner-title m-0 font-bold`,children:`SmartSafeHub 계정을 연결해 주세요`}),H(`span`,{class:`ssh-dashboard-account-banner-badge rounded-full px-2 py-0.5 text-[0.7rem] font-bold`,children:`연결 필요`})]}),H(`p`,{class:`ssh-dashboard-account-banner-description mt-1 mb-0 text-sm leading-relaxed`,children:`계정을 연결하면 웹사이트에서 기기를 관리하고 구독 정보를 확인할 수 있습니다. DNS 보호는 연결하지 않아도 계속 작동합니다.`})]})]}),H(`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]}),H(`section`,{"aria-labelledby":`dashboard-activity-title`,children:[H(q,{description:`최근 SafeShield 차단 활동과 현재 네트워크 연결 구성을 함께 확인합니다.`,eyebrow:`Activity`,id:`dashboard-activity-title`,title:`네트워크 보호 활동`}),H(`div`,{class:`grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1.55fr)_minmax(18rem,0.65fr)]`,children:[H(s_,{data:C,error:w,loading:T,nowTimestamp:j,refreshing:E}),H(`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:[H(`div`,{class:`flex items-start justify-between gap-4`,children:[H(`div`,{children:[H(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Network`}),H(`h3`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`연결 상태`})]}),H(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl ${i.network.up&&!ue?`bg-emerald-50 text-emerald-700`:`bg-amber-50 text-amber-800`}`,children:H(dn,{class:`size-5`})})]}),H(`dl`,{class:`mt-4 mb-0`,children:[H(v_,{label:`인터넷`,value:ue?`네트워크 충돌`:i.network.up?`정상 연결`:`연결 확인`}),H(v_,{label:`WAN IP`,value:he}),H(v_,{label:`프로토콜`,value:ce||`확인되지 않음`}),H(v_,{label:`상위 네트워크`,value:h?.wan.subnet||(_?`확인 중`:g?`확인 필요`:`확인되지 않음`)}),H(v_,{label:`LAN 네트워크`,value:h?.lan.subnet||(_?`확인 중`:g?`확인 필요`:`확인되지 않음`)}),H(v_,{label:`대역 충돌`,value:h?H(`span`,{class:h.conflict.detected?`text-amber-700`:`text-emerald-700`,children:h.conflict.detected?`충돌 감지`:`없음`}):_?`확인 중`:`확인 필요`})]}),H(`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:`네트워크 보기`})]})]})]}),H(`section`,{"aria-labelledby":`dashboard-recent-activity-title`,children:[H(q,{action:H(`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:`최근 활동`}),H(`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?H(Qo,{error:n,loading:r}):H(Zo,{compact:!0,events:(t?.events??[]).slice(0,3)})})]}),H(`section`,{"aria-labelledby":`dashboard-system-title`,children:[H(q,{description:`메모리, 디스크, 시스템 부하와 장치 정보를 빠르게 점검합니다.`,eyebrow:`System`,id:`dashboard-system-title`,title:`시스템 상태`}),H(`div`,{class:`grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1.45fr)_minmax(20rem,0.75fr)]`,children:[H(`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:[H(`div`,{class:`flex flex-wrap items-start justify-between gap-4`,children:[H(`div`,{children:[H(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Runtime`}),H(`h3`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`리소스 사용량`})]}),H(`span`,{class:`inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-black ring-1 ring-inset ${N>=90?`bg-amber-50 text-amber-800 ring-amber-200`:`bg-emerald-50 text-emerald-700 ring-emerald-200`}`,children:[H(`span`,{class:`size-2 rounded-full ${N>=90?`bg-amber-500`:`bg-emerald-500`}`}),N>=90?`메모리 확인`:`정상 실행`]})]}),H(`div`,{class:`mt-5 grid min-w-0 grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4`,children:[H(`div`,{class:`flex min-w-0 flex-col rounded-xl border border-slate-200 bg-slate-50 p-3 sm:p-4`,children:[H(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[H(mn,{class:`size-4`}),H(`span`,{class:`text-xs font-extrabold`,children:`메모리`})]}),H(`div`,{class:`flex min-h-0 flex-1 flex-col justify-center py-1`,children:[H(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:[N,`%`]}),H(`p`,{class:`mt-1 mb-0 text-xs font-semibold text-slate-500`,children:[A?Oo(A.used):`-`,` /`,` `,Oo(i.runtime.memory.total)]}),H(`div`,{"aria-label":`메모리 사용률 ${N}%`,class:`mt-3 h-1.5 overflow-hidden rounded-full bg-slate-200`,role:`progressbar`,"aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":N,children:H(`div`,{class:`h-full rounded-full transition-[width] duration-300 ${N>=90?`bg-amber-500`:`bg-teal-600`}`,style:{width:`${Math.min(100,N)}%`}})})]})]}),H(`div`,{class:`ssh-dashboard-storage-card flex min-w-0 flex-col rounded-xl border border-slate-200 bg-slate-50 p-3 sm:p-4`,"data-level":P?.level??`unavailable`,children:[H(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[H(vn,{class:`size-4`}),H(`span`,{class:`text-xs font-extrabold`,children:`디스크`})]}),H(`div`,{class:`mt-1 flex justify-center`,role:`img`,"aria-label":P?`디스크 여유 공간 ${P.availablePercent}%, ${P.availableText} 남음, 사용 ${P.usedText} / 전체 ${P.totalText}`:`디스크 저장 공간 확인 불가`,children:H(`div`,{class:`ssh-apps-storage-ring relative grid size-[76px] shrink-0 place-items-center`,"data-level":P?.level??`unavailable`,children:[H(`svg`,{"aria-hidden":`true`,class:`size-[76px]`,viewBox:`0 0 96 96`,children:[H(`circle`,{class:`ssh-apps-storage-track`,cx:`48`,cy:`48`,r:`39`}),P&&P.availableRatio>0?H(`circle`,{class:`ssh-apps-storage-free`,cx:`48`,cy:`48`,r:`39`,"stroke-dasharray":`${ee} ${u_.toFixed(2)}`}):null]}),H(`div`,{class:`absolute inset-0 flex items-center justify-center text-center`,children:H(`strong`,{class:`text-base leading-none font-black text-slate-950`,children:P?`${P.availablePercent}%`:`—`})})]})}),H(`p`,{class:`ssh-apps-storage-available mt-1 mb-0 text-center text-xs font-extrabold text-teal-700`,children:P?`${P.availableText} 남음`:`저장 공간 확인 불가`}),P?H(`p`,{class:`mt-1 mb-0 text-center text-[11px] leading-4 font-semibold text-slate-500`,title:`앱 설치 가능 공간을 기준으로 계산하며 파일시스템 예약 공간이 사용량에 포함될 수 있습니다.`,children:[`사용 `,P.usedText,` / 전체 `,P.totalText]}):null]}),H(`div`,{class:`flex min-w-0 flex-col rounded-xl border border-slate-200 bg-slate-50 p-3 sm:p-4`,children:[H(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[H(Xt,{class:`size-4`}),H(`span`,{class:`text-xs font-extrabold`,children:`시스템 부하`})]}),H(`div`,{class:`flex min-h-0 flex-1 flex-col justify-center py-1`,children:[H(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:ko(i.runtime.load[0])}),H(`p`,{class:`mt-1 mb-0 text-xs font-semibold text-slate-500`,children:[`5분 `,ko(i.runtime.load[1]),` · 15분`,` `,ko(i.runtime.load[2])]})]})]}),H(`div`,{class:`flex min-w-0 flex-col rounded-xl border border-slate-200 bg-slate-50 p-3 sm:p-4`,children:[H(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[H(pn,{class:`size-4`}),H(`span`,{class:`text-xs font-extrabold`,children:`실행 시간`})]}),H(`div`,{class:`flex min-h-0 flex-1 flex-col justify-center py-1`,children:[H(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:Do(i.runtime.uptime)}),H(`p`,{class:`mt-1 mb-0 text-xs font-semibold text-slate-500`,children:[`부팅 `,Ao(i.runtime.localtime,i.runtime.uptime)]})]})]})]}),H(b_,{data:f,error:p,loading:m,nowTimestamp:j})]}),H(`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:[H(`div`,{class:`flex items-start justify-between gap-4`,children:[H(`div`,{children:[H(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Device`}),H(`h3`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`장치 정보`})]}),H(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:H(Xt,{class:`size-5`})})]}),H(`dl`,{class:`mt-4 mb-0`,children:[H(v_,{label:`모델`,value:i.device.model}),H(v_,{label:`호스트 이름`,value:i.device.hostname}),H(v_,{label:`펌웨어`,value:H(`span`,{title:be?l?.current.releaseVersion?`SmartSafeHub 펌웨어 ${l.current.releaseVersion}`:`SmartSafeHub 커스텀 펌웨어`:u||void 0,children:xe})}),H(v_,{label:Ce,value:Se}),H(v_,{label:`커널`,value:i.software.kernel}),H(v_,{label:`WAN IP`,value:i.network.ipv4Address||`할당되지 않음`})]})]})]})]})]})}var C_=Object.assign;function w_(e,t){for(var n in e)if(n!=`__source`&&e[n]!==t[n])return!0;for(var r in t)if(r!=`__source`&&!(r in e))return!0;return!1}var T_=/^(-|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|asp|fill|st)/;function E_(e,t){function n(e){var n=this.props.ref;return n!=e.ref&&n&&(typeof n==`function`?n(null):n.current=null),t?!t(this.props,e)||n!=e.ref:w_(this.props,e)}function r(t){return this.shouldComponentUpdate=n,v(e,t)}return r.displayName=`Memo(`+(e.displayName||e.name)+`)`,r.prototype.isReactComponent=!0,r.type=e,r}function D_(e,t){var n=le(e,t);return n.containerInfo=t,n}var O_=Symbol.for(`react.element`),k_=/^(?: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]/,A_=/[A-Z0-9]/g,j_=typeof document<`u`,M_=function(e){return/fil|che|rad/.test(e)};x.prototype.isReactComponent=!0,[`componentWillMount`,`componentWillReceiveProps`,`componentWillUpdate`].forEach(function(e){Object.defineProperty(x.prototype,e,{configurable:!0,get:function(){return this[`UNSAFE_`+e]},set:function(t){Object.defineProperty(this,e,{configurable:!0,writable:!0,value:t})}})});var N_=e.event;e.event=function(e){return N_&&(e=N_(e)),e.persist=function(){},e.isPropagationStopped=function(){return this.cancelBubble},e.isDefaultPrevented=function(){return this.defaultPrevented},e.nativeEvent=e};var P_={configurable:!0,get:function(){return this.class}},F_=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||j_&&a==`children`&&n==`noscript`||a==`class`||a==`className`)){if(a==`style`&&o&&o.constructor==Object){var s=void 0;for(var c in o)typeof o[c]!=`number`||T_.test(c)||(s||(s=o=C_({},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`||M_(t.type)?l==`onfocus`?a=`onfocusin`:l==`onblur`&&(a=`onfocusout`):l=a=`oninput`,l==`oninput`&&r[a=l]&&(a=`oninputCapture`)}else i&&k_.test(a)?a=a.replace(A_,`-$&`).toLowerCase():o===null&&(o=void 0);r[a]=o}}n==`select`&&(r.multiple&&Array.isArray(r.value)&&(r.value=k(t.children).forEach(function(e){e.props.selected=r.value.indexOf(e.props.value)!=-1})),r.defaultValue!=null&&(r.value=k(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",P_)):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=C_({},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=O_,F_&&F_(e)};var I_=8,L_=256,R_=8;function z_(e){return e.findIndex(e=>!e.disabled)}function B_(e){for(let t=e.length-1;t>=0;--t)if(!e[t]?.disabled)return t;return-1}function V_(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:z_(e)}function U_({ariaLabel:e,className:t=``,disabled:n=!1,id:r,onChange:i,options:a,value:o,variant:s=`default`}){let c=$e(),l=r??`ssh-custom-select-${c}`,u=`${l}-menu`,d=L(null),f=L(null),p=L(null),m=L([]),[h,g]=F(!1),[_,v]=F(()=>H_(a,o)),[y,b]=F(`bottom`),[x,S]=F(L_),[C,w]=F({}),[T,E]=F(null),D=Qe(()=>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(L_,Math.max(44,a.length*42+12)),r=window.innerHeight-t.bottom-R_,i=t.top-R_,o=rr?`top`:`bottom`,s=Math.min(t.width,Math.max(0,window.innerWidth-16)),c=Math.max(R_,Math.min(t.left,window.innerWidth-s-R_)),l=o===`bottom`?r:i;b(o),S(Math.max(44,Math.min(L_,Math.floor(l)))),w({left:`${c}px`,width:`${s}px`,...o===`bottom`?{top:`${t.bottom+I_}px`}:{bottom:`${window.innerHeight-t.top+I_}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??H_(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(H_(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 N=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`,P=h?H(`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:H(`div`,{class:`overflow-y-auto overscroll-contain`,style:{maxHeight:`${x}px`},children:a.map((e,t)=>{let n=e.value===o;return H(`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(V_(a,t,1));break;case`ArrowUp`:e.preventDefault(),A(V_(a,t,-1));break;case`Home`:e.preventDefault(),A(z_(a));break;case`End`:e.preventDefault(),A(B_(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:[H(`span`,{class:`min-w-0 truncate`,children:e.label}),n?H(`span`,{"aria-hidden":`true`,class:`size-2 shrink-0 rounded-full bg-teal-500`}):null]},e.value)})})}):null;return H(`div`,{class:`relative ${t}`,ref:d,children:[H(`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 ${N}`,disabled:n,id:l,onClick:()=>{h?k():(O(),g(!0))},onKeyDown:e=>{switch(e.key){case`ArrowDown`:e.preventDefault(),M(h?V_(a,_,1):H_(a,o));break;case`ArrowUp`:e.preventDefault(),M(h?V_(a,_,-1):H_(a,o));break;case`Home`:e.preventDefault(),M(z_(a));break;case`End`:e.preventDefault(),M(B_(a));break;case`Enter`:case` `:e.preventDefault(),h?k():M();break;case`Escape`:h&&(e.preventDefault(),k())}},ref:f,type:`button`,children:[H(`span`,{class:`min-w-0 truncate`,children:D?.label??`선택하세요`}),H(jn,{"aria-hidden":`true`,class:`size-4 shrink-0 text-slate-400 transition-transform ${h?`rotate-180`:``}`})]}),P&&T?D_(P,T):null,H(`span`,{class:`sr-only`,"aria-live":`polite`,children:y===`top`?`선택 목록이 위쪽에 열렸습니다.`:``})]})}var W_=[{value:`skb`,label:`SK Broadband`},{value:`lgu`,label:`LG U+`}];function G_(e){return e===`lgu`?`LG U+`:`SK Broadband`}function K_({active:e,label:t,value:n}){return H(`div`,{class:`rounded-xl border border-slate-200 bg-white p-4`,children:[H(`div`,{class:`flex items-center justify-between gap-3`,children:[H(`p`,{class:`m-0 text-xs font-extrabold text-slate-500`,children:t}),H(`span`,{class:`size-2.5 shrink-0 rounded-full ${e?`bg-emerald-500`:`bg-slate-300`}`,"aria-hidden":`true`})]}),H(`p`,{class:`mt-2 mb-0 text-sm font-black text-slate-950`,children:n})]})}function q_({data:e,error:t,feedback:n,loading:r,onDismissFeedback:i,onRetry:a,onSave:o,saving:s}){let[c,l]=F(!1),[u,d]=F(`skb`);if(I(()=>{e&&(l(e.enabled),d(e.provider))},[e]),r&&!e)return H(vs,{});if(t&&!e)return H(ys,{message:t,onRetry:a});if(!e)return H(ys,{message:`IPTV 설정을 확인할 수 없습니다.`,onRetry:a});let f=c!==e.enabled||u!==e.provider,p=!e.enabled||e.running&&e.igmpSnooping;return H(`div`,{class:`space-y-5`,children:[H(`section`,{class:`rounded-2xl border border-amber-200 bg-amber-50 p-5 sm:p-6`,children:H(`div`,{class:`flex items-start gap-4`,children:[H(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-amber-100 text-amber-800`,children:H(W,{class:`size-5`})}),H(`div`,{class:`min-w-0 flex-1`,children:[H(`div`,{class:`flex flex-wrap items-center gap-2`,children:[H(`h2`,{class:`m-0 text-base font-black text-amber-950`,children:`IPTV 실험 기능`}),H(`span`,{class:`rounded-full border border-amber-300 bg-amber-100 px-2 py-0.5 text-[10px] font-black uppercase tracking-[0.12em] text-amber-800`,children:`Beta`})]}),H(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-amber-900/80`,children:`현재 SK Broadband와 LG U+를 지원하며 두 통신사에는 동일한 멀티캐스트 IPTV 프로파일을 적용합니다. KT IPTV는 별도 네트워크 방식이 필요해 아직 지원하지 않습니다.`})]})]})}),n?H(`section`,{"aria-live":`polite`,class:`rounded-2xl border p-4 ${n.kind===`success`?`border-emerald-200 bg-emerald-50 text-emerald-900`:`border-red-200 bg-red-50 text-red-900`}`,children:H(`div`,{class:`flex items-start justify-between gap-3`,children:[H(`p`,{class:`m-0 text-sm font-bold leading-6`,children:n.message}),H(`button`,{class:`shrink-0 text-xs font-extrabold underline underline-offset-2`,onClick:i,type:`button`,children:`닫기`})]})}):null,H(q,{eyebrow:`IPTV`,title:`IPTV 설정`,description:`통신사 환경에 맞는 멀티캐스트 중계 기능을 관리합니다.`}),H(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:[H(`div`,{class:`flex min-w-0 items-start gap-3`,children:[H(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:H(fn,{class:`size-5`})}),H(`div`,{class:`min-w-0 flex-1`,children:[H(`div`,{class:`flex min-w-0 flex-wrap items-center justify-between gap-2`,children:[H(`h2`,{class:`m-0 text-lg font-black text-slate-950`,children:`IPTV`}),H(`div`,{class:`flex shrink-0 items-center gap-2`,children:[f?H(`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:[H(W,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}):null,H(`button`,{"aria-checked":c,"aria-label":`IPTV 사용`,class:`ssh-switch-control relative inline-flex shrink-0 rounded-full border-0 transition focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 ${c?`bg-teal-700`:`bg-slate-300`} disabled:cursor-not-allowed disabled:opacity-60`,disabled:s||!e.available,onClick:()=>l(e=>!e),role:`switch`,type:`button`,children:H(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${c?`left-6`:`left-1`}`})})]})]}),H(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`WAN의 멀티캐스트 방송을 LAN 셋톱박스로 전달합니다.`})]})]}),H(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]`,children:[H(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[H(`div`,{class:`flex flex-wrap items-start justify-between gap-2`,children:[H(`div`,{children:[H(`label`,{class:`block text-xs font-extrabold text-slate-600`,htmlFor:`iptv-provider`,children:`통신사`}),H(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`IPTV를 제공하는 회선 사업자를 선택하세요.`})]}),H(`span`,{class:`rounded-full border border-teal-200 bg-teal-50 px-2.5 py-1 text-[10px] font-black text-teal-700`,children:`멀티캐스트 방식`})]}),H(U_,{ariaLabel:`IPTV 통신사`,className:`mt-3`,disabled:s||!c,id:`iptv-provider`,onChange:e=>d(e),options:W_,value:u}),H(`div`,{class:`mt-3 flex items-start gap-2 rounded-lg border border-slate-200 bg-white p-3`,children:[H(wn,{"aria-hidden":`true`,class:`mt-0.5 size-4 shrink-0 text-teal-700`}),H(`p`,{class:`m-0 text-xs leading-5 text-slate-600`,children:[G_(u),`에는 현재 SK Broadband/LG U+ 공통 IGMP Proxy 프로파일을 적용합니다. KT는 추후 별도 IPTV 프로파일로 지원할 예정입니다.`]})]})]}),H(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[H(`div`,{class:`flex items-center justify-between gap-3`,children:[H(`p`,{class:`m-0 text-xs font-extrabold text-slate-500`,children:`현재 지원 프로파일`}),H(`span`,{class:`text-xs font-extrabold text-slate-500`,children:`SKB · LG U+`})]}),H(`p`,{class:`mt-2 mb-0 text-sm font-black text-slate-950`,children:`WAN → IGMP Proxy → LAN`}),H(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-600`,children:`WAN을 upstream, LAN을 downstream으로 사용하고 LAN bridge에는 IGMP snooping을 함께 활성화해 필요하지 않은 포트와 Wi-Fi로 멀티캐스트가 불필요하게 퍼지는 것을 줄입니다.`})]})]}),e.available?null:H(`div`,{class:`mt-5 rounded-xl border border-red-200 bg-red-50 p-4 text-sm font-bold leading-6 text-red-900`,children:`IGMP Proxy 패키지를 찾지 못했습니다. 현재 펌웨어에 igmpproxy 패키지가 포함되어 있는지 확인해 주세요.`}),H(`div`,{class:`mt-6 flex flex-wrap items-center justify-between gap-3 border-t border-slate-200 pt-5`,children:[H(`p`,{class:`m-0 text-xs leading-5 text-slate-500`,children:`설정을 적용하면 네트워크 서비스가 잠시 다시 로드될 수 있습니다.`}),H(`button`,{class:`inline-flex min-h-11 items-center justify-center rounded-xl border-0 bg-teal-700 px-5 py-2.5 text-sm font-black text-white shadow-sm transition hover:bg-teal-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-not-allowed disabled:opacity-50`,disabled:s||!e.available||!f&&!c,onClick:()=>void o({enabled:c,provider:u}),type:`button`,children:s?`적용 중…`:f?`변경사항 적용`:`설정 다시 적용`})]})]}),H(q,{eyebrow:`Runtime`,title:`IPTV 동작 상태`,description:`마지막으로 적용한 IPTV 설정과 중계 서비스의 실행 상태를 확인합니다.`}),H(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:[H(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[H(`p`,{class:`m-0 text-xs leading-5 text-slate-500`,children:`적용된 IPTV 중계 서비스와 멀티캐스트 전달 상태입니다.`}),H(`span`,{class:`inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-black ${p?`bg-emerald-100 text-emerald-800`:`bg-amber-100 text-amber-800`}`,children:[H(wn,{class:`size-4`}),p?e.enabled?`정상`:`꺼짐`:`확인 필요`]})]}),f?H(`div`,{class:`mt-4 flex items-start gap-2 rounded-xl border border-amber-200 bg-amber-50 p-3 text-xs font-bold leading-5 text-amber-900`,children:[H(W,{"aria-hidden":`true`,class:`mt-0.5 size-4 shrink-0`}),`아래 동작 상태는 마지막으로 적용된 설정 기준입니다. 변경사항을 적용하면 새 설정으로 갱신됩니다.`]}):null,H(`div`,{class:`mt-5 grid gap-3 sm:grid-cols-2 lg:grid-cols-4`,children:[H(K_,{active:e.enabled&&e.running,label:`IGMP Proxy`,value:e.enabled?e.running?`동작 중`:`중지됨`:`꺼짐`}),H(K_,{active:e.enabled&&e.igmpSnooping,label:`IGMP Snooping`,value:e.igmpSnooping?`켜짐`:`꺼짐`}),H(K_,{active:e.enabled,label:`Upstream`,value:e.upstreamNetwork.toUpperCase()}),H(K_,{active:e.enabled,label:`Downstream`,value:e.lanBridge??e.downstreamNetwork.toUpperCase()})]})]})]})}function J_(e){let t=e.split(`.`);return[t[0]??``,t[1]??``,t[2]??``,t[3]??``]}function Y_(e){return e.replace(/\D/g,``).slice(0,3)}function X_(e){return e.every(e=>e===``)?``:e.join(`.`)}function Z_({disabled:e=!1,label:t,value:n,onChange:r}){let i=J_(n),a=(e,t)=>{let n=[...i];n[e]=Y_(t),r(X_(n))};return H(`div`,{class:`ssh-ipv4-segments mt-2 min-h-11`,"aria-label":`${t} 입력`,children:i.map((n,r)=>H(`div`,{class:`ssh-ipv4-segment`,children:[H(`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}),r{if(!/^(0|[1-9][0-9]{0,2})$/.test(e))return!1;let t=Number(e);return t>=0&&t<=255})}function tv(e){let t=J_(e);return t.slice(0,3).some(e=>e===``||Number(e)>255)?null:t.slice(0,3).join(`.`)}function nv(e){return J_(e)[3]}function rv(e){let t=tv(e.lan.address),n=nv(e.dhcp.start??``),r=nv(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 iv({disabled:e,label:t,lanAddress:n,value:r,onChange:i}){let a=tv(n),o=a?.split(`.`)??[``,``,``],s=nv(r),c=e=>{let t=Y_(e);i(a&&t!==``?`${a}.${t}`:t)};return H(`label`,{class:`block`,children:[H(`span`,{class:`text-sm font-extrabold text-slate-800`,children:t}),H(`div`,{class:`ssh-ipv4-segments mt-2 min-h-11`,"aria-label":`${t} 입력`,children:[o.map((e,t)=>H(`div`,{class:`ssh-ipv4-segment`,children:[H(`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||`—`}),H(`span`,{"aria-hidden":`true`,class:`ssh-ipv4-separator text-sm font-black text-slate-400`,children:`.`})]},t)),H(`div`,{class:`ssh-ipv4-segment`,children:H(`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})})]}),H(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`앞 3개 주소는 공유기 IP 주소와 동일하게 유지됩니다.`})]})}function av(e){return`${window.location.protocol}//${e}${window.location.pathname}${window.location.search}${window.location.hash}`}function ov({feedback:e,onDismiss:t}){return H(`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:[H(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[H(`p`,{class:`m-0 font-bold leading-6`,children:e.message}),H(`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?H(`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:av(e.newAddress),children:[`새 공유기 주소 `,e.newAddress,`로 연결`]}):null]})}function sv({data:e}){let t=e.conflict.detected;return H(`section`,{class:`grid gap-4 lg:grid-cols-3`,"aria-label":`내부 네트워크 상태`,children:[H(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[H(`span`,{class:`grid size-10 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:H(Xt,{class:`size-5`})}),H(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`SmartSafeHub LAN`}),H(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:e.lan.subnet}),H(`p`,{class:`mt-1 mb-0 text-sm font-semibold text-slate-600`,children:[`공유기 주소 `,e.lan.address]})]}),H(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[H(`span`,{class:`grid size-10 place-items-center rounded-xl bg-slate-100 text-slate-700`,children:H(dn,{class:`size-5`})}),H(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`상위 네트워크`}),H(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:e.wan.subnet??`확인 전`}),H(`p`,{class:`mt-1 mb-0 text-sm font-semibold text-slate-600`,children:e.wan.connected?`WAN ${e.wan.address??`주소 확인 중`}`:`WAN 연결 후 자동으로 확인합니다.`})]}),H(`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:[H(`span`,{class:`grid size-10 place-items-center rounded-xl ${t?`bg-red-100 text-red-700`:`bg-emerald-100 text-emerald-700`}`,children:H(t?W:wn,{class:`size-5`})}),H(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] opacity-70`,children:`대역 충돌`}),H(`p`,{class:`mt-2 mb-0 text-lg font-black`,children:t?`주소 대역이 겹칩니다`:`충돌이 감지되지 않았습니다`}),H(`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 cv({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(``),[_,v]=F(``),[y,x]=F(`12h`),[S,C]=F(!1),[w,T]=F(null),E=(e={})=>{if(!t)return!1;let n=rv(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??y)!==n.leaseTime},D=e=>{let t=tv(e);if(u(e),!t){C(E({ipAddress:e}));return}let n=nv(h),r=nv(_),i=n?`${t}.${n}`:``,a=r?`${t}.${r}`:``;g(i),v(a),C(E({ipAddress:e,dhcpStart:i,dhcpEnd:a}))},O=e=>{g(e),C(E({dhcpStart:e}))},k=e=>{v(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=rv(t);u(e.ipAddress),f(e.prefixLength),m(e.dhcpEnabled),g(e.dhcpStart),v(e.dhcpEnd),x(e.leaseTime),T(null)},[t,S]);let N=Qe(()=>{let e=[...Q_];return e.includes(d)||(e.push(d),e.sort((e,t)=>e-t)),e},[d]),P=Qe(()=>{let e=[...$_];return e.some(([e])=>e===y)||e.push([y,`현재 설정 (${y})`]),e},[y]);if(i)return H(vs,{});if(n)return H(ys,{message:n,onRetry:s});if(!t)return null;let ee=e!==null,te=async e=>{e.preventDefault(),ev(l)?!ev(h)||!ev(_)?T(`DHCP 시작/종료 주소를 올바른 IPv4 형식으로 입력해 주세요.`):window.confirm(`내부 네트워크 설정을 적용하면 연결된 기기가 새 DHCP 정보를 받는 동안 잠시 연결이 끊길 수 있습니다. 계속하시겠습니까?`)&&(T(null),await c({ipAddress:l.trim(),prefixLength:d,dhcpEnabled:p,dhcpStart:h.trim(),dhcpEnd:_.trim(),leaseTime:y})&&C(!1)):T(`공유기 주소를 올바른 IPv4 형식으로 입력해 주세요.`)},ne=async()=>{t.recommendation&&window.confirm(`상위 네트워크와 겹치지 않는 ${t.recommendation.subnet} 대역으로 자동 변경합니다. 현재 연결이 잠시 끊길 수 있습니다. 계속하시겠습니까?`)&&(T(null),await a()&&C(!1))};return H(b,{children:[r?H(ov,{feedback:r,onDismiss:o}):null,H(q,{eyebrow:`Overview`,title:`내부 네트워크 상태`,description:`LAN 주소와 DHCP 할당 상태를 확인합니다.`}),H(sv,{data:t}),t.conflict.detected?H(`section`,{class:`mt-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-amber-950 sm:p-6`,children:H(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between`,children:[H(`div`,{class:`min-w-0`,children:[H(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-amber-700`,children:`자동 충돌 해결`}),H(`h2`,{class:`mt-2 mb-0 text-lg font-black`,children:`안전한 LAN 대역으로 변경할 수 있습니다`}),H(`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?H(`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:ee,onClick:()=>void ne(),type:`button`,children:[H(wn,{class:`size-5 ${e===`auto`?`animate-pulse`:``}`}),e===`auto`?`변경 중`:`추천 대역으로 자동 변경`]}):null]})}):null,H(`div`,{class:`mt-7`,children:H(q,{eyebrow:`Local network`,title:`LAN 및 DHCP 설정`,description:`상위 공유기와 다른 주소 대역을 사용해야 안정적으로 라우팅할 수 있습니다.`})}),H(`form`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,onSubmit:e=>void te(e),children:[S?H(`div`,{class:`mb-5 flex justify-end`,children:H(`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:[H(W,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]})}):null,H(`div`,{class:`grid gap-5 lg:grid-cols-2`,children:[H(`div`,{class:`block`,children:[H(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`공유기 IP 주소`}),H(Z_,{disabled:ee,label:`공유기 IP 주소`,onChange:D,value:l})]}),H(`div`,{class:`block`,children:[H(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`현재 LAN 대역`}),H(`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}),H(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`서브넷 마스크는 아래 고급 설정에서 변경할 수 있습니다.`})]}),H(iv,{disabled:ee,label:`DHCP 시작 주소`,lanAddress:l,onChange:O,value:h}),H(iv,{disabled:ee,label:`DHCP 종료 주소`,lanAddress:l,onChange:k,value:_})]}),H(`details`,{class:`mt-6 rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[H(`summary`,{class:`cursor-pointer text-sm font-black text-slate-900`,children:`고급 DHCP 설정`}),H(`div`,{class:`mt-5 grid gap-5 lg:grid-cols-2`,children:[H(`div`,{class:`block`,children:[H(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`서브넷 마스크`}),H(U_,{ariaLabel:`서브넷 마스크`,className:`mt-2`,disabled:ee,id:`lan-prefix-length`,onChange:e=>A(Number(e)),options:N.map(e=>({value:String(e),label:`/${e} · ${e===t.lan.prefixLength?t.lan.netmask:`CIDR /${e}`}`})),value:String(d),variant:`emphasized`}),H(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`일반 가정용 네트워크는 /24 사용을 권장합니다.`})]}),H(`div`,{class:`block`,children:[H(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`DHCP 임대 시간`}),H(U_,{ariaLabel:`DHCP 임대 시간`,className:`mt-2`,disabled:ee,id:`lan-dhcp-lease-time`,onChange:j,options:P.map(([e,t])=>({value:e,label:t})),value:y,variant:`emphasized`})]}),H(`label`,{class:`inline-flex min-h-11 items-center gap-3 text-sm font-extrabold text-slate-800 lg:col-span-2`,children:[H(`input`,{checked:p,class:`size-5 accent-teal-700`,disabled:ee,onChange:e=>M(e.currentTarget.checked),type:`checkbox`}),`SmartSafeHub DHCP 서버 사용`]}),H(`p`,{class:`m-0 text-xs font-semibold leading-5 text-slate-500 lg:col-span-2`,children:`DHCP를 끄면 SmartSafeHub에 연결되는 장치에 IP 주소를 자동으로 할당하지 않습니다. 별도의 DHCP 서버를 사용하는 고급 구성에서만 비활성화하세요.`})]})]}),w?H(`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,H(`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:[H(`p`,{class:`m-0 text-xs font-semibold leading-5 text-slate-500`,children:`주소가 바뀌면 현재 관리 페이지 연결이 종료되며 새 IP를 받아 다시 접속해야 합니다.`}),H(`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:ee||!S,type:`submit`,children:[H(wn,{class:`size-5 ${e===`saving`?`animate-pulse`:``}`}),e===`saving`?`저장 중`:`설정 저장`]})]})]})]})}var lv=[`dhcp`,`pppoe`,`static`],uv=`1.1.1.1`,dv=Array.from({length:33},(e,t)=>t);function fv(e){return lv.includes(e)}function pv(e){switch(e){case`dhcp`:return`자동 IP (DHCP)`;case`pppoe`:return`PPPoE`;case`static`:return`고정 IPv4`;default:return e?e.toUpperCase():`확인 전`}}function mv(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 hv(e){return e.trim()}function gv(e,t){let n=mv(t);return e.protocol===n.protocol?e.protocol===`dhcp`||!fv(e.protocol)?!1:e.protocol===`pppoe`?hv(e.pppoeUsername)!==n.pppoeUsername||e.pppoePassword.length>0:hv(e.staticAddress)!==n.staticAddress||e.staticPrefixLength!==n.staticPrefixLength||hv(e.staticGateway)!==n.staticGateway||hv(e.dnsPrimary)!==n.dnsPrimary||hv(e.dnsSecondary)!==n.dnsSecondary:!0}function _v(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 vv(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 yv(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 bv({feedback:e,onDismiss:t}){return H(`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:H(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[H(`p`,{class:`m-0 font-bold leading-6`,children:e.message}),H(`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 xv({data:e,busy:t,onReconnect:n}){let r=e.status,i=r.connected;return H(`section`,{class:`grid gap-4 lg:grid-cols-3`,"aria-label":`인터넷 연결 상태`,children:[H(`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:[H(`div`,{class:`flex items-start justify-between gap-3`,children:[H(`span`,{class:`grid size-10 place-items-center rounded-xl ${i?`bg-emerald-100 text-emerald-700`:`bg-amber-100 text-amber-700`}`,children:H(i?wn:W,{class:`size-5`})}),H(`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:[H(hn,{class:`size-4 ${t?`animate-spin`:``}`}),`재연결`]})]}),H(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] opacity-70`,children:`인터넷 상태`}),H(`p`,{class:`mt-2 mb-0 text-xl font-black`,children:r.pending?`연결 중`:i?`연결됨`:`연결 안 됨`}),H(`p`,{class:`mt-1 mb-0 text-sm font-semibold leading-6 opacity-80`,children:[pv(r.protocol??e.configuration.protocol),` · `,yv(r.uptimeSeconds)]})]}),H(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[H(`span`,{class:`grid size-10 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:H(dn,{class:`size-5`})}),H(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`WAN IPv4`}),H(`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}`}`:`주소 없음`}),H(`p`,{class:`mt-1 mb-0 text-sm font-semibold text-slate-600`,children:r.device?`인터페이스 ${r.device}`:`인터페이스 확인 전`})]}),H(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[H(`span`,{class:`grid size-10 place-items-center rounded-xl bg-slate-100 text-slate-700`,children:H(Xt,{class:`size-5`})}),H(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`Gateway · DNS`}),H(`p`,{class:`mt-2 mb-0 break-all text-base font-black text-slate-950`,children:r.gateway??`Gateway 확인 전`}),H(`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 Sv({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(mv(t)),g(null))},[t,d]);let _=Qe(()=>{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 H(vs,{});if(n)return H(ys,{message:n,onRetry:s});if(!t)return null;let v=e!==null,y=e=>{u(n=>{let r={...n,...e};return f(gv(r,t)),r}),g(null)},x=async e=>{if(e.preventDefault(),!fv(l.protocol))g(`DHCP, PPPoE 또는 고정 IPv4 중 하나를 선택해 주세요.`);else{if(l.protocol===`pppoe`){if(!hv(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(!_v(l.staticAddress)){g(`고정 IPv4 주소를 올바르게 입력해 주세요.`);return}if(!_v(l.staticGateway)){g(`기본 게이트웨이를 올바르게 입력해 주세요.`);return}if(!_v(l.dnsPrimary)){g(`기본 DNS 서버를 올바르게 입력해 주세요.`);return}if(hv(l.dnsSecondary)&&!_v(l.dnsSecondary)){g(`보조 DNS 서버를 올바르게 입력해 주세요.`);return}}window.confirm(`인터넷 연결 설정을 적용하면 WAN 연결이 다시 시작되어 인터넷이 잠시 끊길 수 있습니다. 계속하시겠습니까?`)&&await c({protocol:l.protocol,pppoeUsername:hv(l.pppoeUsername),pppoePassword:l.pppoePassword,pppoePasswordChanged:l.pppoePassword.length>0,staticAddress:hv(l.staticAddress),staticPrefixLength:l.staticPrefixLength,staticGateway:hv(l.staticGateway),dnsPrimary:hv(l.dnsPrimary),dnsSecondary:hv(l.dnsSecondary)})&&(u(e=>({...e,pppoePassword:``})),f(!1))}};return H(b,{children:[r?H(bv,{feedback:r,onDismiss:a}):null,H(q,{eyebrow:`Overview`,title:`인터넷 연결 상태`,description:`WAN 연결 상태와 네트워크 정보를 확인합니다.`}),H(xv,{data:t,busy:v,onReconnect:()=>{window.confirm(`WAN 연결을 다시 시작합니다. 인터넷이 잠시 끊길 수 있습니다. 계속하시겠습니까?`)&&o()}}),t.configuration.supported?null:H(`section`,{class:`mt-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-amber-950 sm:p-6`,children:H(`div`,{class:`flex items-start gap-3`,children:[H(W,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),H(`div`,{children:[H(`h2`,{class:`m-0 text-base font-black`,children:`현재 WAN 방식은 직접 관리하지 않습니다`}),H(`p`,{class:`mt-2 mb-0 text-sm font-semibold leading-6 text-amber-900/80`,children:[`현재 설정은 `,t.configuration.protocol,`입니다. 아래에서 DHCP, PPPoE 또는 고정 IPv4를 선택해 저장하면 해당 방식으로 전환합니다.`]})]})]})}),H(`div`,{class:`mt-7`,children:H(q,{eyebrow:`Internet`,title:`인터넷 연결 설정`,description:`통신사 또는 상위 네트워크 환경에 맞는 WAN 연결 방식을 선택합니다.`})}),H(`form`,{class:`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:[d?H(`div`,{class:`mb-5 flex justify-end`,children:H(`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:[H(W,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]})}):null,H(`div`,{children:[H(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`연결 방식`}),H(U_,{ariaLabel:`WAN 연결 방식`,className:`mt-2 max-w-xl`,disabled:v,id:`wan-protocol`,onChange:e=>y(e===`static`&&!hv(l.dnsSecondary)?{protocol:e,dnsSecondary:uv}:{protocol:e}),options:_,value:l.protocol,variant:`emphasized`}),H(`span`,{class:`mt-2 block text-xs font-semibold leading-5 text-slate-500`,children:`대부분의 가정용 인터넷과 상위 공유기 연결은 자동 IP(DHCP)를 사용합니다.`})]}),l.protocol===`pppoe`?H(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[H(`label`,{class:`block`,children:[H(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`PPPoE 사용자명`}),H(`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:v,onInput:e=>y({pppoeUsername:e.currentTarget.value}),spellcheck:!1,value:l.pppoeUsername})]}),H(`label`,{class:`block`,children:[H(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`PPPoE 비밀번호`}),H(`span`,{class:`relative mt-2 block`,children:[H(`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:v,onInput:e=>y({pppoePassword:e.currentTarget.value}),placeholder:t.configuration.protocol===`pppoe`&&t.configuration.pppoe.passwordConfigured?`변경할 때만 입력`:`PPPoE 비밀번호`,...p?{type:`text`}:{type:`password`},value:l.pppoePassword}),H(`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:v,onClick:()=>m(e=>!e),type:`button`,children:H(p?Sn:xn,{class:`size-5`})})]}),H(`span`,{class:`mt-2 block text-xs text-slate-500`,children:t.configuration.protocol===`pppoe`&&t.configuration.pppoe.passwordConfigured?`비워 두면 현재 저장된 비밀번호를 그대로 사용합니다.`:`통신사에서 제공한 PPPoE 비밀번호를 입력해 주세요.`})]})]}):null,l.protocol===`static`?H(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[H(`label`,{class:`block`,children:[H(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`IPv4 주소`}),H(Z_,{disabled:v,label:`IPv4 주소`,onChange:e=>y({staticAddress:e}),value:l.staticAddress})]}),H(`div`,{class:`block`,children:[H(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`서브넷 마스크`}),H(U_,{ariaLabel:`WAN 서브넷 마스크`,className:`mt-2`,disabled:v,id:`wan-static-prefix-length`,onChange:e=>y({staticPrefixLength:Number(e)}),options:dv.map(e=>({value:String(e),label:`/${e} · ${vv(e)}`})),value:String(l.staticPrefixLength),variant:`emphasized`})]}),H(`label`,{class:`block`,children:[H(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`기본 게이트웨이`}),H(Z_,{disabled:v,label:`기본 게이트웨이`,onChange:e=>y({staticGateway:e}),value:l.staticGateway})]}),H(`label`,{class:`block`,children:[H(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`기본 DNS`}),H(Z_,{disabled:v,label:`기본 DNS`,onChange:e=>y({dnsPrimary:e}),value:l.dnsPrimary})]}),H(`label`,{class:`block lg:col-start-2`,children:[H(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`보조 DNS (선택)`}),H(Z_,{disabled:v,label:`보조 DNS`,onChange:e=>y({dnsSecondary:e}),value:l.dnsSecondary})]})]}):null,l.protocol===`dhcp`?H(`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?H(`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,H(`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:[H(`p`,{class:`m-0 text-xs font-semibold leading-5 text-slate-500`,children:`설정 저장 후 WAN만 다시 연결하며 LAN 관리 페이지와 내부 네트워크 설정은 유지됩니다.`}),H(`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:v||!d,type:`submit`,children:[H(wn,{class:`size-5 ${e===`saving`?`animate-pulse`:``}`}),e===`saving`?`저장 중`:`설정 저장`]})]})]})]})}var Cv=3,wv=10;function Tv(e,t){return e<=0?`0.0%`:`${(t/e*100).toFixed(1)}%`}function Ev(e){return e.id===`other`?`기타 기기`:e.hostname?e.hostname:e.identified?`이름 없는 기기`:`미식별 기기`}function Dv(e){return[...e].sort((e,t)=>e.id===`other`?1:t.id===`other`?-1:t.blocked-e.blocked||t.queries-e.queries||Ev(e).localeCompare(Ev(t),`ko`))}function Ov({devices:e,deviceLimit:t,truncated:n}){let r=Dv(e),[i,a]=F(!1),[o,s]=F(1),c=Math.max(1,Math.ceil(r.length/wv)),l=Math.min(o,c),u=(l-1)*wv,d=Math.min(u+wv,r.length),f=i?r.slice(u,d):r.slice(0,Cv),p=r.length>Cv;return I(()=>{s(e=>Math.min(e,c))},[c]),H(`div`,{class:`mt-6 rounded-2xl border border-slate-100 bg-slate-50/70 p-4 sm:p-5`,children:[H(`div`,{class:`flex flex-wrap items-start justify-between gap-3`,children:[H(`div`,{children:[H(`div`,{class:`flex flex-wrap items-center gap-2`,children:[H(`p`,{class:`m-0 text-xs font-bold text-slate-500`,children:`기기별 통계`}),p&&!i?H(`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]}),H(`h3`,{class:`mt-1 mb-0 text-base font-extrabold text-slate-950`,children:`어떤 기기에서 차단되었는지 확인`}),H(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-500`,children:`DHCP 정보가 있는 기기는 MAC 주소를 기준으로 식별하며, 기기별 숫자만 현재 부팅 세션 동안 메모리에 집계합니다.`})]}),H(`div`,{class:`flex flex-wrap items-center justify-end gap-2`,children:[H(`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:[H(Zt,{class:`size-4 text-teal-700`}),J(r.length),`개 항목`]}),p?H(`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?`간단히 보기 ↑`:`전체 ${J(r.length)}개 기기 보기 ↓`}):null]})]}),r.length===0?H(`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 요청이 발생하면 다음 통계 갱신 시 표시됩니다.`}):H(`div`,{class:`mt-4 grid gap-3`,id:`safeshield-device-statistics-list`,children:f.map(e=>H(`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:[H(`div`,{class:`min-w-0`,children:H(`div`,{class:`flex min-w-0 flex-wrap items-center gap-2`,children:[H(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg bg-teal-50 text-teal-700`,children:H(Zt,{class:`size-4`})}),H(`div`,{class:`min-w-0`,children:[H(`h4`,{class:`m-0 truncate text-sm font-extrabold text-slate-950`,children:Ev(e)}),H(`p`,{class:`mt-1 mb-0 break-all text-xs text-slate-500`,children:[e.ip||`IP 미확인`,e.mac?` · ${e.mac}`:``]})]}),H(`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 임시 식별`})]})}),H(`dl`,{class:`m-0 grid grid-cols-3 gap-2 text-right`,children:[H(`div`,{class:`min-w-20`,children:[H(`dt`,{class:`text-[11px] font-bold text-slate-400`,children:`DNS 요청`}),H(`dd`,{class:`mt-1 mb-0 ml-0 text-sm font-extrabold text-slate-900`,children:J(e.queries)})]}),H(`div`,{class:`min-w-20`,children:[H(`dt`,{class:`text-[11px] font-bold text-slate-400`,children:`차단`}),H(`dd`,{class:`mt-1 mb-0 ml-0 text-sm font-extrabold text-teal-700`,children:J(e.blocked)})]}),H(`div`,{class:`min-w-20`,children:[H(`dt`,{class:`text-[11px] font-bold text-slate-400`,children:`차단율`}),H(`dd`,{class:`mt-1 mb-0 ml-0 text-sm font-extrabold text-slate-900`,children:Tv(e.queries,e.blocked)})]})]})]},e.id))}),p&&!i?H(`p`,{class:`mt-3 mb-0 text-xs font-semibold leading-5 text-slate-500`,children:[`전체 `,J(r.length),`개 기기 중 차단 수 기준 상위 `,Cv,`개를 표시하고 있습니다.`]}):null,i&&r.length>wv?H(`nav`,{"aria-label":`기기별 통계 페이지`,class:`mt-4 flex flex-wrap items-center justify-between gap-3`,children:[H(`p`,{class:`m-0 text-xs font-semibold text-slate-500`,children:[J(u+1),`–`,J(d),` /`,` `,J(r.length),`개`]}),H(`div`,{class:`flex items-center gap-2`,children:[H(`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:`이전`}),H(`span`,{"aria-current":`page`,class:`min-w-20 text-center text-xs font-extrabold text-slate-600`,children:[J(l),` / `,J(c)]}),H(`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?H(`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:[`개별 통계는 최대 `,J(t),`개 기기까지 추적합니다. 한도를 초과한 기기의 요청은 기타 기기로 합산됩니다.`]}):null]})}var kv=3600,Av=24;function jv(e,t){return e<=0?`0.0%`:`${(t/e*100).toFixed(1)}%`}function Mv(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/kv)*kv}function Nv(e){let t=new Map(e.hourly.map(e=>[e.bucketStart,e])),n=Mv(e)-23*kv;return Array.from({length:Av},(e,r)=>{let i=n+r*kv;return t.get(i)??{bucketStart:i,queries:0,blocked:0}})}function Pv({emphasized:e=!1,label:t,value:n}){return H(`div`,{class:`rounded-xl bg-slate-50 p-4 sm:p-5`,children:[H(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:t}),H(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight ${e?`text-teal-700`:`text-slate-950`}`,children:n})]})}function Fv({action:e,data:t,error:n,loading:r,onRetry:i,onSetEnabled:a,refreshing:o}){if(r&&t===null)return H(b,{children:[H(q,{eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청과 차단 현황을 확인합니다.`}),H(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:H(`p`,{class:`m-0 text-sm font-bold text-slate-500`,children:`차단 통계를 불러오는 중입니다…`})})]});if(n&&t===null)return H(b,{children:[H(q,{eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청과 차단 현황을 확인합니다.`}),H(`section`,{class:`rounded-2xl border border-amber-200 bg-amber-50 p-5 sm:p-6`,children:[H(`p`,{class:`m-0 text-sm font-extrabold text-amber-950`,children:`차단 통계를 불러오지 못했습니다.`}),H(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-amber-800`,children:n}),H(`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 H(b,{children:[H(q,{eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청과 차단 현황을 확인합니다.`}),H(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:H(`p`,{class:`m-0 text-sm leading-6 text-slate-600`,children:`현재 SafeShield에서 통계 데이터를 제공하지 않습니다. SafeShield 0.3.15 버전 이상이 설치되어 있고, 통계 수집이 활성화되어 있는지 확인해 주세요.`})})]});let s=Nv(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 H(b,{children:[H(q,{action:H(`div`,{class:`flex items-center gap-3 rounded-xl border border-slate-100 bg-slate-50 px-3 py-2.5`,children:[H(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-500`,children:H(vn,{class:`size-5`})}),H(`div`,{class:`text-right`,children:[H(`p`,{class:`m-0 text-[0.68rem] font-bold text-slate-400`,children:`통계 수집`}),H(`p`,{class:`mt-1 mb-0 text-xs font-extrabold ${u||d?`text-emerald-700`:`text-slate-500`}`,role:`status`,children:f})]}),H(`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:H(`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?H(hn,{"aria-hidden":`true`,class:`size-3 animate-spin text-teal-600`}):null})})]}),eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청 원본은 저장하지 않고 숫자만 로컬 메모리에 집계합니다.`}),H(`section`,{"aria-busy":u,class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6 ${u?`cursor-wait`:``}`,children:[t.enabled?null:H(`div`,{class:`mb-4 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm leading-6 text-slate-600`,children:`통계 수집이 꺼져 있습니다. 활성화하면 DNS 요청 원본은 저장하지 않고 요청 수와 차단 수만 로컬 메모리에 집계합니다.`}),H(`dl`,{class:`mt-2 grid gap-3 sm:grid-cols-2 xl:grid-cols-4`,children:[H(Pv,{label:`최근 24시간 DNS 요청`,value:J(l.queries)}),H(Pv,{emphasized:!0,label:`최근 24시간 차단`,value:J(l.blocked)}),H(Pv,{label:`최근 24시간 차단율`,value:jv(l.queries,l.blocked)}),H(Pv,{label:`현재 시간 차단`,value:J(c?.blocked??0)})]}),H(`div`,{class:`mt-5 rounded-xl bg-slate-50/70 p-4 sm:p-5`,children:[H(`div`,{class:`flex flex-wrap items-end justify-between gap-3`,children:[H(`div`,{children:[H(`p`,{class:`m-0 text-xs font-bold text-slate-500`,children:`최근 24시간`}),H(`h3`,{class:`mt-1 mb-0 text-base font-extrabold text-slate-950`,children:`시간대별 차단 요청`})]}),H(`span`,{class:`text-xs font-bold text-slate-500`,children:o?`갱신 중…`:`마지막 집계 ${jo(t.updatedAt)}`})]}),H(e_,{buckets:s})]}),H(Ov,{deviceLimit:t.deviceLimit,devices:t.devices,truncated:t.devicesTruncated}),H(`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:[H(`span`,{children:[`수집 누적 DNS 요청 `,J(t.totals.queries)]}),H(`span`,{children:[`수집 누적 차단 `,J(t.totals.blocked)]}),H(`span`,{children:[`수집 시작 `,jo(t.startedAt)]}),H(`span`,{children:[`보존 `,J(t.retentionHours),`시간`]}),t.volatile?H(`span`,{children:`재부팅 시 초기화되는 메모리 통계`}):null]}),n?H(`p`,{class:`mt-3 mb-0 text-xs font-bold text-amber-700`,children:`최근 자동 갱신에 실패했습니다. 현재 표시된 마지막 정상 통계를 유지합니다.`}):null]})]})}function Iv({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 H(`span`,{children:t});let a=jo(e);return H(`span`,{class:`inline-flex max-w-full flex-wrap items-center gap-1`,children:[H(`time`,{class:`min-w-0`,dateTime:new Date(e*1e3).toISOString(),title:a,children:No(e,n)}),H(`details`,{class:`group inline-flex max-w-full flex-wrap items-center gap-1 align-middle`,children:[H(`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:H(`svg`,{"aria-hidden":`true`,class:`size-3.5`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.8`,viewBox:`0 0 24 24`,children:[H(`circle`,{cx:`12`,cy:`12`,r:`9`}),H(`path`,{d:`M12 11v5m0-8h.01`,strokeLinecap:`round`})]})}),H(`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})]})]})}var Lv=`https://www.smartsafehub.com/pricing/`;function Rv({value:e,trueLabel:t=`정상`,falseLabel:n=`확인 필요`}){return H(`span`,{class:`inline-flex items-center gap-2 text-sm font-extrabold ${e?`text-emerald-700`:`text-slate-500`}`,children:[H(`span`,{class:`size-2 rounded-full ${e?`bg-emerald-500`:`bg-slate-300`}`}),e?t:n]})}function zv(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 Bv(e){return!e.enabled||e.status===`disabled`?`비활성화`:e.active&&e.runtime.dnsmasqRunning&&e.runtime.dnsRuntimeOk?`보호 중`:e.status===`paused`?`일시 중지`:`확인 필요`}function Vv(e){let t=zv(e);return t===`error`&&Bv(e)===`보호 중`&&e.runtime.lastErrorCode?`갱신 실패`:{disabled:`비활성화`,refreshing:`갱신 중`,protecting:`보호 중`,paused:`일시 중지`,error:`오류`,attention:`확인 필요`}[t]}function Hv(e){return e?`자동 최적화`:`확인되지 않음`}function Uv(e){let t=Po(e);return t===null?`확인되지 않음`:H(Iv,{timestamp:t})}function Wv(e){let t=zv(e);return t===`protecting`?e.blocklist.validLineCount>0?`${J(e.blocklist.validLineCount)}개 도메인 차단 규칙으로 DNS를 보호하고 있습니다.`:e.blocklist.installed?`차단 목록이 설치되어 있으며 DNS 보호가 정상적으로 동작하고 있습니다.`:`SafeShield 서비스와 DNS 런타임이 정상적으로 동작하고 있습니다.`:t===`refreshing`?`차단 목록을 최신 상태로 갱신하고 있습니다.`:t===`disabled`?`SafeShield DNS 보호가 비활성화되어 있습니다.`:t===`paused`?`SafeShield 작업이 일시 중지되어 있습니다.`:t===`error`?Bv(e)===`보호 중`?`기존 DNS 보호는 유지되고 있지만 최근 차단 목록 갱신을 완료하지 못했습니다.`:`SafeShield 보호 상태에 문제가 발생했습니다. 아래 오류 내용을 확인해 주세요.`:e.active?e.runtime.dnsmasqRunning?e.runtime.dnsRuntimeOk?e.summary.message:`SafeShield DNS 런타임 상태를 확인해야 합니다.`:`dnsmasq가 실행 중이 아니어서 DNS 보호 상태를 확인해야 합니다.`:`SafeShield가 활성화되어 있지만 서비스가 실행 중이 아닙니다.`}function Gv(e){return{ok:`정상`,warning:`경고`,error:`오류`,unavailable:`사용 불가`}[e]??e}function Kv({data:e}){let t=zv(e);return H(`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:Vv(e)})}function qv({failed:e,step:t,total:n}){let r=2*Math.PI*17,i=r*.22,a=r-i;return H(`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:[H(`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:[H(`circle`,{class:`ssh-safeshield-refresh-loader-track`,cx:`24`,cy:`24`,fill:`none`,r:17,stroke:`currentColor`,strokeWidth:2.5}),e?null:H(`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})]}),H(`span`,{class:`ssh-safeshield-refresh-donut-label`,children:e?`!`:`${t}/${n}`})]})}function Jv({data:e}){let t=Ga(e.status,e.stage),n=zv(e)===`error`;if(!t&&!n)return null;let r=Ka(e.stage),i=n?Ja(e.runtime.lastErrorCode,e.stage):null;return H(`div`,{"aria-live":n?`assertive`:`polite`,class:`ssh-safeshield-refresh`,"data-state":n?`error`:`active`,role:n?`alert`:`status`,children:[H(qv,{failed:n,step:r.number,total:r.total}),H(`div`,{class:`ssh-safeshield-refresh-copy`,children:[H(`p`,{class:`ssh-safeshield-refresh-kicker`,children:n?`갱신 실패 · ${r.number}/${r.total}`:`갱신 ${r.number}/${r.total}`}),H(`p`,{class:`ssh-safeshield-refresh-title`,children:n?`${r.label} 단계에서 문제가 발생했습니다.`:r.label}),H(`p`,{class:`ssh-safeshield-refresh-description`,children:i??r.description}),n&&e.runtime.lastErrorCode?H(`p`,{class:`ssh-safeshield-refresh-error-code`,children:[`오류 코드: `,H(`code`,{children:e.runtime.lastErrorCode})]}):null]})]})}function Yv({label:e,value:t}){return H(`div`,{class:`bg-white px-5 py-4 sm:px-6`,children:[H(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.14em] text-slate-400`,children:e}),H(`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 Xv(e){return e?.trim().toUpperCase()||`FREE`}function Zv(e){return e===`FREE`?`free`:e===`PRO`?`pro`:e===`ULTIMATE`?`ultimate`:`paid`}function Qv({compact:e=!1,plan:t}){let n=Zv(t),r=n!==`free`;return H(`span`,{class:`ssh-safeshield-plan-badge`,"data-compact":e?`true`:`false`,"data-tier":n,title:r?`${t} 멤버십`:`무료 플랜`,children:[H(`span`,{"aria-hidden":`true`,class:`ssh-safeshield-plan-badge-mark`,children:r?`✦`:`•`}),H(`span`,{children:t})]})}function $v({plan:e}){let t=Zv(e)===`free`;return H(`div`,{class:`bg-white px-5 py-4 sm:px-6`,children:[H(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.14em] text-slate-400`,children:`Plan`}),H(`dd`,{class:`mt-2 mb-0 ml-0 flex min-w-0 flex-wrap items-center gap-2`,children:[H(Qv,{plan:e}),t?H(`span`,{class:`ssh-safeshield-plan-caption`,children:`기본 플랜`}):null]})]})}function ey(){return H(`div`,{class:`ssh-safeshield-upgrade-card`,children:[H(`span`,{"aria-hidden":`true`,class:`ssh-safeshield-upgrade-mark`,children:`✦`}),H(`div`,{class:`ssh-safeshield-upgrade-copy`,children:[H(`div`,{class:`ssh-safeshield-upgrade-meta`,children:[H(`span`,{children:`SmartSafeHub Membership`}),H(`span`,{class:`ssh-safeshield-upgrade-soon`,children:`Coming soon`})]}),H(`strong`,{children:`더 강력한 보호 기능을 준비하고 있습니다.`}),H(`span`,{children:`프리미엄 보호와 확장된 관리 기능을 미리 확인해 보세요.`})]}),H(`a`,{class:`ssh-safeshield-upgrade-action`,href:Lv,rel:`noopener noreferrer`,target:`_blank`,children:[`요금제 보기`,H(`span`,{"aria-hidden":`true`,children:`→`})]})]})}function ty({children:e,eyebrow:t,icon:n,title:r}){return H(`article`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6 xl:p-4 2xl:p-5`,children:[H(`div`,{class:`flex min-w-0 items-start justify-between gap-4 xl:gap-2`,children:[H(`div`,{children:[H(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.16em] text-slate-400`,children:t}),H(`h3`,{class:`mt-2 mb-0 text-lg font-black tracking-tight text-slate-950 xl:text-base 2xl:text-lg`,children:r})]}),H(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-500 xl:size-9 2xl:size-10`,children:n})]}),H(`dl`,{class:`mt-5 mb-0 grid gap-3`,children:e})]})}function ny({label:e,value:t}){return H(`div`,{class:`flex min-w-0 items-center justify-between gap-2 border-b border-slate-100 pb-3 last:border-b-0 last:pb-0`,children:[H(`dt`,{class:`text-sm font-semibold text-slate-500`,children:e}),H(`dd`,{class:`m-0 min-w-0 break-words text-right text-sm font-extrabold text-slate-950`,children:t})]})}function ry({error:e,message:t,onDismiss:n}){if(!e&&!t)return null;let r=e!==null;return H(`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:[H(`div`,{class:`flex min-w-0 items-start gap-3`,children:[H(r?W:wn,{class:`mt-0.5 size-5 shrink-0`}),H(`p`,{class:`m-0 font-semibold leading-6`,children:e??t})]}),H(`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 iy({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&&!Ga(r.status,r.stage)&&_(r.blocklist.validLineCount)},[r?.available,r?.blocklist.validLineCount,r?.stage,r?.status]),a)return H(vs,{});if(i)return H(ys,{message:i,onRetry:f});if(!r)return H(vs,{});if(!r.available)return H(`section`,{class:`rounded-2xl border border-amber-200 bg-amber-50 p-5 sm:p-6`,children:[H(`span`,{class:`grid size-11 place-items-center rounded-xl bg-amber-100 text-amber-800`,children:H(W,{class:`size-6`})}),H(`h2`,{class:`mt-5 mb-0 text-xl font-extrabold text-amber-950`,children:`SafeShield 상태 API를 찾을 수 없습니다`}),H(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-amber-900`,children:`safeshield 패키지와 rpcd ucode 플러그인이 설치되어 있는지 확인해 주세요.`})]});let v=r.enabled,y=Xv(r.entitlement.plan),x=r.status===`running`,S=e!==null,C=(Ga(r.status,r.stage)||zv(r)===`error`)&&r.blocklist.validLineCount===0&&g!==null?g:r.blocklist.validLineCount,w=v?e===`disable`?`끄는 중…`:`보호 끄기`:e===`enable`?`켜는 중…`:`보호 켜기`;function T(){(!v||window.confirm(`보호를 끄면, 현재 차단 목록이 제거되어 위험에 노출될 수 있습니다. 그래도 계속하시겠습니까?`))&&m(!v)}return H(b,{children:[H(q,{eyebrow:`Protection`,title:`SafeShield 보호 상태`,description:`DNS 보호 상태와 적용 중인 차단 목록을 확인합니다.`}),H(`section`,{class:`mb-8 grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1.08fr)_minmax(0,0.92fr)] xl:items-stretch`,children:[H(`article`,{"aria-label":`보호 상태 요약`,class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6 xl:h-full`,children:[H(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between xl:flex-col 2xl:flex-row`,children:[H(`div`,{class:`flex min-w-0 items-start gap-3`,children:[H(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:H(Yt,{class:`size-5`})}),H(`div`,{class:`min-w-0`,children:[H(`div`,{class:`flex flex-wrap items-center gap-2`,children:[H(`h3`,{class:`m-0 text-base font-black text-slate-950`,children:`현재 보호 상태`}),H(Kv,{data:r})]}),H(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:Wv(r)})]})]}),H(`div`,{class:`flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap xl:justify-end`,children:[H(`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:[H(gn,{class:`size-4 ${e===`enable`||e===`disable`?`animate-pulse`:``}`}),w]}),H(`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:[H(_n,{class:`size-4 ${e===`refresh`||x?`animate-bounce`:``}`}),e===`refresh`?`시작 중…`:x?`갱신 중…`:r.status===`error`?`다시 갱신`:`지금 갱신`]})]})]}),H(Jv,{data:r}),H(`dl`,{class:`mt-5 mb-0 grid min-w-0 grid-cols-2 overflow-hidden rounded-xl border border-slate-200 bg-slate-200`,children:[H(Yv,{label:`SafeShield`,value:r.version??`확인되지 않음`}),H(Yv,{label:`Blocklist`,value:`${J(C)}개 도메인`}),H(Yv,{label:`Last refresh`,value:H(Iv,{timestamp:r.timestamps.lastSuccess})}),H($v,{plan:y})]}),y===`FREE`?H(ey,{}):null]}),H(`section`,{"aria-label":`보호 데이터 요약`,class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6 xl:h-full`,children:[H(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[H(`div`,{class:`flex min-w-0 items-center gap-3`,children:[H(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-500`,children:H(vn,{class:`size-5`})}),H(`div`,{children:[H(`p`,{class:`m-0 text-[0.65rem] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Protection data`}),H(`h3`,{class:`mt-1 mb-0 text-sm font-black text-slate-950`,children:`보호 데이터`})]})]}),H(`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?H(wn,{class:`size-3.5 shrink-0`}):H(W,{class:`size-3.5 shrink-0`}),r.artifact.resolved&&r.blocklist.installed&&r.blocklist.verificationOk?`최신`:`확인 필요`]})]}),H(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-500`,children:`SafeShield가 기기 성능에 맞춰 사용하는 보호 목록의 구성과 최신 상태를 확인합니다.`}),H(`dl`,{class:`mt-3 mb-0 grid grid-cols-1 gap-2 sm:grid-cols-2`,children:[H(`div`,{class:`min-w-0 rounded-lg border border-slate-200 bg-white px-3 py-2.5`,children:[H(`dt`,{class:`text-xs font-bold text-slate-500`,children:`기기 최적화`}),H(`dd`,{class:`mt-1 mb-0 text-sm font-extrabold text-slate-950`,children:Hv(r.artifact.tier)})]}),H(`div`,{class:`min-w-0 rounded-lg border border-slate-200 bg-white px-3 py-2.5`,children:[H(`dt`,{class:`text-xs font-bold text-slate-500`,children:`마지막 업데이트`}),H(`dd`,{class:`mt-1 mb-0 text-sm font-extrabold leading-5 text-slate-950`,children:Uv(r.artifact.version)})]})]}),H(`section`,{"aria-label":`보호 데이터 세부 정보`,class:`mt-3 border-t border-slate-200 pt-3`,children:[H(`h4`,{class:`m-0 text-xs font-extrabold text-slate-600`,children:`세부 정보`}),H(`dl`,{class:`mt-3 mb-0 grid gap-2`,children:[H(ny,{label:`데이터 프로필`,value:r.artifact.tier||`확인되지 않음`}),H(ny,{label:`데이터 버전`,value:r.artifact.version||`확인되지 않음`})]})]})]})]}),H(ry,{error:t,message:n,onDismiss:u}),H(Fv,{action:e,data:o,error:s,loading:c,onRetry:p,onSetEnabled:h,refreshing:l}),H(`section`,{class:`mt-7`,children:[H(q,{description:`DNS 보호 런타임, 차단 목록, 갱신 일정과 상태 점검 결과를 한곳에서 확인합니다.`,eyebrow:`Protection details`,title:`보호 구성`}),H(`div`,{class:`grid min-w-0 grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4`,children:[H(ty,{eyebrow:`Runtime`,icon:H(wn,{class:`size-5`}),title:`DNS 보호 런타임`,children:[H(ny,{label:`SafeShield 서비스`,value:H(Rv,{falseLabel:`중지됨`,trueLabel:`동작 중`,value:r.active})}),H(ny,{label:`DNS 서비스`,value:H(Rv,{falseLabel:`중지됨`,value:r.runtime.dnsmasqRunning})}),H(ny,{label:`SafeShield DNS 연동`,value:H(Rv,{value:r.runtime.dnsRuntimeOk})}),H(ny,{label:`차단 목록 갱신`,value:H(Rv,{falseLabel:`중지됨`,value:r.runtime.refreshdRunning})})]}),H(ty,{eyebrow:`Blocklist`,icon:H(vn,{class:`size-5`}),title:`차단 목록`,children:[H(ny,{label:`적용 규칙`,value:`${J(r.blocklist.validLineCount)}개 도메인`}),H(ny,{label:`파일 크기`,value:Oo(r.blocklist.fileSizeKb*1024)}),H(ny,{label:`설치 상태`,value:H(Rv,{falseLabel:`미설치`,trueLabel:`설치됨`,value:r.blocklist.installed})}),H(ny,{label:`검증 상태`,value:H(Rv,{falseLabel:`확인 필요`,value:r.blocklist.verificationOk})})]}),H(ty,{eyebrow:`Refresh schedule`,icon:H(Cn,{class:`size-5`}),title:`차단 목록 갱신`,children:[H(ny,{label:`마지막 성공`,value:H(Iv,{timestamp:r.timestamps.lastSuccess})}),H(ny,{label:`다음 갱신`,value:jo(r.timestamps.nextRefreshAt)}),H(ny,{label:`갱신 주기`,value:Io(r.timestamps.refreshIntervalS)}),H(ny,{label:`최근 결과`,value:r.runtime.lastResult||(x?`갱신 중`:`확인되지 않음`)})]}),H(ty,{eyebrow:`Health`,icon:H(Yt,{class:`size-5`}),title:`상태 점검`,children:[H(ny,{label:`전체 상태`,value:Gv(r.health.overall)}),H(ny,{label:`경고`,value:H(`span`,{class:`text-amber-700`,children:J(r.issueCounts.warnings)})}),H(ny,{label:`오류`,value:H(`span`,{class:`text-red-700`,children:J(r.issueCounts.errors)})}),H(ny,{label:`최근 오류 코드`,value:r.runtime.lastErrorCode||`없음`})]})]})]})]})}function ay(e,t,n,r){let i=t?e.filter(e=>e.includes(t)):e;if(!n)return{filteredRules:i,manualRules:i,managedRules:[]};let a=[],o=[];for(let e of i)(r.has(e)?o:a).push(e);return{filteredRules:i,manualRules:a,managedRules:o}}function oy(e,t){return t?e.filter(e=>e.title.toLowerCase().includes(t)||e.domains.some(e=>e.includes(t))):e}function sy(e,t){return e===t||e.length===t.length&&e.every((e,n)=>e===t[n])}function cy(e,t){return e===t||e.length===t.length&&e.every((e,n)=>{let r=t[n];return r!==void 0&&e.id===r.id&&e.title===r.title&&sy(e.domains,r.domains)})}function ly(e,t){return{domains:sy(e.domains,t.domains)?e.domains:t.domains,rules:cy(e.rules,t.rules)?e.rules:t.rules}}var uy=[],dy=[];function fy(e){return e.trim().toLowerCase().replace(/\.+$/,``)}function py(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))}var my=E_(function({action:e,kind:t,onAddRule:n,onDeleteRule:r,rules:i,recommendedDomainSet:a,recommendedKnown:o,recommendedRules:s,onRequestRevoke:c,revokeBusy:l}){let[u,d]=F(``),[f,p]=F(``),[m,h]=F(null),[g,_]=F(!0),[v,y]=F(null),b=t===`allow`,x=e!==null,S=Qe(()=>f.trim().toLowerCase(),[f]),C=S.length>0,{filteredRules:w,manualRules:T,managedRules:E}=Qe(()=>ay(i,S,b&&o,a),[i,S,b,o,a]),D=Qe(()=>b?oy(s,S):[],[b,s,S]),O=C||(v??(s.length||E.length)<=3),k=C||g;async function A(e){e.preventDefault();let r=fy(u);py(r)?(h(null),await n(t,r,()=>{d(e=>fy(e)===r?``:e)})):h(`example.com 형식의 도메인을 입력해 주세요. URL, IP 주소, 와일드카드는 사용할 수 없습니다.`)}let j=b?{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:`등록된 차단 도메인이 없습니다.`};function M(n){let i=b&&o&&a.has(n),s=!b||o?i?`추천 규칙`:`직접 추가`:null,c=e?.operation===`delete`&&e.action===t&&e.domain===n;return H(`li`,{class:`flex min-w-0 items-center justify-between gap-3 px-3.5 py-3`,children:[H(`div`,{class:`flex min-w-0 flex-1 flex-wrap items-center gap-2`,children:H(`code`,{class:`min-w-0 break-all font-mono text-sm font-semibold text-slate-800`,children:n})}),i?H(`span`,{class:`shrink-0 rounded-full bg-teal-50 px-2.5 py-1 text-[11px] font-bold text-teal-800`,title:`추천 규칙의 적용과 해제는 웹사이트에서 관리합니다.`,children:`웹사이트 관리`}):H(`button`,{"aria-label":`${n} ${j.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:x||i,onClick:()=>void r(t,n),title:s===`직접 추가`?`직접 추가한 규칙 삭제`:`삭제`,type:`button`,children:H(En,{class:`size-4 ${c?`animate-pulse`:``}`})})]},n)}return H(`article`,{class:`min-w-0 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm shadow-slate-900/5`,children:[H(`div`,{class:`border-b border-slate-200 p-4 sm:p-6`,children:[H(`div`,{class:`flex min-w-0 items-start gap-3`,children:[H(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl ${j.icon}`,children:H(b?wn:Yt,{class:`size-6`})}),H(`div`,{class:`min-w-0`,children:[H(`div`,{class:`flex flex-wrap items-center gap-2`,children:[H(`h2`,{class:`m-0 text-xl font-extrabold tracking-tight text-slate-950`,children:j.label}),H(`span`,{class:`rounded-full px-2.5 py-1 text-xs font-extrabold ${j.badge}`,children:i.length})]}),H(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:j.description})]})]}),H(`form`,{class:`mt-5`,onSubmit:A,children:[H(`label`,{class:`block text-xs font-extrabold text-slate-600`,for:`${t}-domain`,children:`새 도메인`}),H(`div`,{class:`mt-2 flex flex-col gap-2 sm:flex-row`,children:[H(`input`,{"aria-describedby":m?`${t}-domain-error`:void 0,"aria-invalid":m?!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:x,id:`${t}-domain`,onInput:e=>d(e.currentTarget.value),placeholder:`example.com`,spellcheck:!1,type:`text`,value:u}),H(`button`,{class:`inline-flex min-h-11 w-full shrink-0 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 sm:w-auto ${j.button}`,disabled:x||u.trim().length===0,type:`submit`,children:[H(Tn,{class:`size-4`}),e?.operation===`add`&&e.action===t?`추가 중…`:`추가`]})]}),m?H(`p`,{class:`mt-2 mb-0 text-xs font-semibold leading-5 text-red-700`,id:`${t}-domain-error`,children:m}):null]})]}),H(`div`,{class:`p-4 sm:p-6`,children:[H(`label`,{class:`relative block`,for:`${t}-search`,children:[H(`span`,{class:`sr-only`,children:[j.label,` 검색`]}),H(`span`,{class:`pointer-events-none absolute inset-y-0 left-0 flex w-11 items-center justify-center text-slate-400`,children:H(Dn,{class:`size-4`})}),H(`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=>p(e.currentTarget.value),placeholder:`${j.label} 검색`,type:`search`,value:f})]}),b&&o?H(`div`,{class:`mt-4 overflow-hidden rounded-xl border border-slate-200`,children:[H(`section`,{"aria-label":`직접 추가한 허용 규칙`,children:[H(`button`,{"aria-controls":`allow-manual-domains`,"aria-expanded":k,class:`flex min-h-14 w-full items-center justify-between gap-3 ssh-rule-group-toggle bg-slate-50 px-4 py-3 text-left focus-visible:outline-2 focus-visible:outline-teal-500`,onClick:()=>_(!k),type:`button`,children:[H(`span`,{class:`min-w-0 text-sm font-extrabold text-slate-800`,children:[`직접 추가한 규칙 `,H(`span`,{class:`ml-1 rounded-full bg-white px-2 py-0.5 text-xs font-bold text-slate-600`,children:[T.length,C?`개 검색됨`:`개`]})]}),H(`span`,{"aria-hidden":`true`,class:`shrink-0 text-sm font-bold text-slate-500`,children:k?`접기 −`:`펼치기 +`})]}),H(`div`,{hidden:!k,id:`allow-manual-domains`,children:T.length?H(`ul`,{class:`m-0 max-h-[20rem] list-none divide-y divide-slate-200 overflow-y-auto p-0`,children:T.map(M)}):H(`p`,{class:`m-0 px-4 py-5 text-sm text-slate-500`,children:C?`검색 결과가 없습니다.`:`직접 추가한 허용 도메인이 없습니다.`})})]}),H(`section`,{"aria-label":`웹사이트 추천 규칙`,class:`border-t border-slate-200`,children:[H(`button`,{"aria-controls":`allow-recommended-domains`,"aria-expanded":O,class:`flex min-h-14 w-full items-center justify-between gap-3 ssh-rule-group-toggle bg-slate-50 px-4 py-3 text-left focus-visible:outline-2 focus-visible:outline-teal-500`,onClick:()=>y(!O),type:`button`,children:[H(`span`,{class:`min-w-0 text-sm font-extrabold text-slate-800`,children:[`추천 규칙 `,H(`span`,{class:`ml-1 rounded-full bg-white px-2 py-0.5 text-xs font-bold text-teal-800`,children:[!C&&s.length?s.length:E.length,C?`개 검색됨`:`개`]})]}),H(`span`,{"aria-hidden":`true`,class:`shrink-0 text-sm font-bold text-slate-500`,children:O?`접기 −`:`펼치기 +`})]}),H(`div`,{hidden:!O,id:`allow-recommended-domains`,children:[D.length?H(`ul`,{class:`m-0 max-h-[22rem] list-none divide-y divide-slate-200 overflow-y-auto p-0`,children:D.map(e=>H(`li`,{class:`flex min-w-0 flex-wrap items-center justify-between gap-3 px-3.5 py-3`,children:[H(`div`,{class:`min-w-0 flex-1`,children:[H(`strong`,{class:`block break-words text-sm text-slate-900`,children:e.title}),H(`div`,{class:`mt-1 flex flex-wrap gap-1.5`,children:e.domains.map(e=>H(`code`,{class:`break-all text-xs text-slate-600`,children:e},e))})]}),c?H(`button`,{class:`shrink-0 rounded-lg border border-red-200 px-3 py-2 text-xs font-bold text-red-700 hover:bg-red-50 disabled:opacity-50`,disabled:l||x,onClick:()=>c(e),type:`button`,children:`적용 해제`}):H(`span`,{class:`text-xs text-slate-500`,children:`웹사이트 관리`})]},e.id))}):E.length?H(`ul`,{class:`m-0 max-h-[20rem] list-none divide-y divide-slate-200 overflow-y-auto p-0`,children:E.map(M)}):H(`p`,{class:`m-0 px-4 py-5 text-sm text-slate-500`,children:C?`검색 결과가 없습니다.`:`추천으로 추가된 허용 도메인이 없습니다.`}),C?null:H(`p`,{class:`m-0 border-t border-slate-100 px-4 py-3 text-xs leading-5 text-slate-500`,children:`추천 규칙은 이 공유기에서 적용 해제할 수 있습니다. 새 규칙의 적용은 SmartSafeHub 웹사이트에서 관리해 주세요.`})]})]})]}):H(`div`,{class:`mt-4 max-h-[28rem] overflow-y-auto rounded-xl border border-slate-200`,children:w.length?H(`ul`,{class:`m-0 list-none divide-y divide-slate-200 p-0`,children:w.map(M)}):H(`div`,{class:`px-5 py-10 text-center`,children:[H(On,{class:`mx-auto size-8 text-slate-300`}),H(`p`,{class:`mt-3 mb-0 text-sm font-bold text-slate-600`,children:C?`검색 결과가 없습니다.`:j.empty})]})})]})]})});function hy({action:e,data:t,error:n,feedback:r,loading:i,onAddRule:a,onDeleteRule:o,onDismissFeedback:s,onRetry:c,onLoadRecommendedRulesStatus:l,onSyncRecommendedRules:u,onRevokeRecommendedRule:d}){let[f,p]=F(null),[m,h]=F(!1),[g,_]=F(!1),[v,y]=F(null),[x,S]=F(!1),[C,w]=F(!1),[T,E]=F(!1),D=L(null),O=L(null),k=L(null),A=L(c);A.current=c;let[j,M]=F(null),[N,P]=F(null),ee=f?.domains??uy,te=f?.rules??dy,ne=Qe(()=>new Set(ee),[ee]),re=R(e=>{p(t=>{if(!t)return e;let{domains:n,rules:r}=ly(t,e);return{...e,domains:n,rules:r}}),h(!0);let t=JSON.stringify([e.revision,e.domains]),n=!1;return e.syncStatus===`confirmed`&&k.current!==t&&(O.current!==null&&(A.current(),n=!0),k.current=t),O.current=t,D.current!==null&&e.revokeOperation?.phase===`failed`&&e.revokeOperation.argument===String(D.current)&&(D.current=null,w(!1),E(!1),P(`추천 규칙 적용 해제를 완료하지 못했습니다. 연결 상태를 확인하고 다시 시도해 주세요.`)),e.syncStatus===`confirmed`&&D.current===null&&e.revokeOperation?.phase!==`failed`&&P(null),D.current!==null&&e.syncStatus===`confirmed`&&!e.rules.some(e=>e.id===D.current)&&(D.current=null,w(!1),E(!1),P(null),M(`추천 규칙 적용 해제를 확인했습니다.`)),n},[]);I(()=>{if(!l)return;let e=l,t=!0,n=!1;function r(){n||g||x||(n=!0,e().then(e=>{t&&re(e)}).catch(()=>{}).finally(()=>{n=!1}))}r();let i=window.setInterval(r,15e3),a=()=>r(),o=()=>{document.hidden||r()};return window.addEventListener(`focus`,a),document.addEventListener(`visibilitychange`,o),()=>{t=!1,window.clearInterval(i),window.removeEventListener(`focus`,a),document.removeEventListener(`visibilitychange`,o)}},[l,g,x,re]),I(()=>{if(!C)return;let e=window.setTimeout(()=>E(!0),6e4);return()=>window.clearTimeout(e)},[C]);async function ie(){if(!(!u||g||x)){_(!0),P(null),M(null);try{let e=await u(),t=re(e);M(e.syncStatus===`confirmed`?`서버에서 규칙을 가져와 SafeShield 허용 목록을 확인했습니다.`:`추천 규칙 상태를 확인 중입니다. 진행 결과는 자동으로 갱신됩니다.`),e.syncStatus===`confirmed`&&!t&&c()}catch{P(`추천 규칙을 동기화하지 못했습니다. 서버 연결 및 기기 등록 상태를 확인해 주세요.`)}finally{_(!1)}}}async function ae(){if(!v||x||!d)return;let e=v.id;D.current=e,S(!0),w(!0),E(!1),P(null),M(null);try{let t=await d(e);re(t),y(null),D.current!==null&&M(`적용 해제 요청을 처리했습니다. 공유기의 허용 목록을 확인하고 있습니다.`)}catch{y(null),P(`적용 해제 결과를 아직 확인하지 못했습니다. 공유기의 상태를 자동으로 다시 확인합니다.`);try{let e=await l?.();e&&re(e)}catch{}}finally{S(!1)}}let oe=f?.syncStage,se=f?.syncStatus===`pending`,ce=f?.syncStatus===`delayed`,le=f?.syncStatus===`failed`,ue=oe===`server_sync`?`서버 동기화 중`:oe===`safeshield_refreshing`?`SafeShield 갱신 중`:oe===`applying`?`추천 규칙 적용 중`:oe===`verifying`?`허용 목록 확인 중`:ce?`재확인 지연`:le?`동기화 실패`:se?`동기화 대기`:`허용 목록 확인`,de=oe===`safeshield_refreshing`&&se?`SafeShield가 보호 목록을 갱신하고 있습니다. 갱신이 끝나면 추천 규칙을 자동으로 다시 확인합니다.`:oe===`applying`&&se?`추천 규칙을 SafeShield 허용 목록에 반영하고 있습니다.`:oe===`verifying`&&se?`SafeShield 허용 목록에서 추천 규칙 반영 결과를 확인하고 있습니다.`:oe===`server_sync`&&se?`SmartSafeHub 서버에서 최신 추천 규칙을 확인하고 있습니다.`:ce?`서버 응답이 지연되고 있습니다. 이미 확인한 허용 규칙은 유지되며 자동으로 다시 시도합니다.`:le?f?.syncReason===`auth_rejected`?`SmartSafeHub 기기 인증이 거부되었습니다. 공유기 계정 연결 상태를 확인해 주세요.`:f?.syncReason===`api_rejected`?`서버가 추천 규칙 요청을 거부했습니다. Hub API 배포 상태를 확인해 주세요.`:f?.syncReason===`invalid_response`?`서버 응답을 확인할 수 없습니다. 규칙은 유지되며 다음 동기화에서 다시 확인합니다.`:`추천 규칙 반영을 완료하지 못했습니다. 다음 자동 동기화에서 다시 시도합니다.`:se?`추천 규칙의 적용 결과를 기다리고 있습니다.`:null,fe=C&&D.current!==null?{tone:`amber`,label:T?`적용 해제 처리 지연`:`적용 해제 확인 중`,description:T?`적용 해제 확인이 지연되고 있습니다. SafeShield 갱신 중이라면 완료 후 자동으로 재확인합니다.`:de??`적용 해제 상태를 확인하고 있습니다. 확인되면 목록과 상태가 자동 갱신됩니다.`}:se||ce||le?{tone:le?`red`:`amber`,label:ue,description:de??``}:N?{tone:`red`,label:`상태 확인 필요`,description:N}:f?.syncWarning?{tone:`amber`,label:`서버 재확인 지연`,description:`이미 확인한 허용 규칙은 유지되며 자동으로 다시 시도합니다.`}:j?{tone:`green`,label:`허용 목록 확인`,description:j}:null;return i&&!t?H(vs,{}):n&&!t?H(ys,{message:n,onRetry:c}):t?H(b,{children:[v?H(`div`,{class:`fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70 p-4`,role:`presentation`,onClick:()=>{x||y(null)},children:H(`section`,{"aria-labelledby":`recommended-revoke-title`,"aria-modal":`true`,class:`w-full max-w-md rounded-2xl border border-slate-200 bg-white p-5 shadow-xl`,onClick:e=>e.stopPropagation(),role:`dialog`,children:[H(`h2`,{class:`m-0 text-lg font-extrabold text-slate-950`,id:`recommended-revoke-title`,children:`추천 규칙 적용 해제`}),H(`p`,{class:`mt-3 text-sm text-slate-700`,children:[H(`strong`,{children:v.title}),` 규칙을 이 공유기에서 해제할까요?`]}),H(`p`,{class:`mt-2 text-xs leading-5 text-slate-600`,children:[`포함된 도메인 `,v.domains.length,`개를 다시 확인합니다. 직접 추가한 허용 규칙과 다른 추천 규칙에 필요한 도메인은 유지됩니다.`]}),H(`div`,{class:`mt-5 flex justify-end gap-2`,children:[H(`button`,{class:`rounded-lg border border-slate-200 px-4 py-2 text-sm font-bold text-slate-700 disabled:cursor-wait disabled:opacity-50`,disabled:x,onClick:()=>y(null),type:`button`,children:`취소`}),H(`button`,{class:`inline-flex min-w-24 items-center justify-center gap-2 rounded-lg bg-red-700 px-4 py-2 text-sm font-bold text-white transition hover:bg-red-800 disabled:cursor-progress disabled:bg-red-700 disabled:opacity-80`,"aria-busy":x,disabled:x,onClick:()=>void ae(),type:`button`,children:[x?H(`svg`,{class:`size-4 animate-spin`,"aria-hidden":`true`,viewBox:`0 0 24 24`,fill:`none`,children:[H(`circle`,{cx:`12`,cy:`12`,r:`9`,stroke:`currentColor`,"stroke-width":`3`,opacity:`0.25`}),H(`path`,{d:`M21 12a9 9 0 0 0-9-9`,stroke:`currentColor`,"stroke-width":`3`,"stroke-linecap":`round`})]}):null,H(`span`,{role:`status`,children:x?`해제 중…`:`적용 해제`})]})]})]})}):null,H(q,{eyebrow:`Local overrides`,title:`사용자 도메인 규칙`,description:`허용 목록은 오탐을 해제하고, 차단 목록은 원하는 도메인을 SafeShield 보호 목록에 추가합니다.`}),H(`section`,{"aria-label":`규칙 개수 요약`,class:`min-w-0`,children:H(`dl`,{class:`m-0 grid grid-cols-1 gap-3 sm:grid-cols-3`,children:[H(`div`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white px-5 py-4 shadow-sm shadow-slate-900/5`,children:[H(`dt`,{class:`text-sm font-extrabold text-emerald-700`,children:`허용 도메인`}),H(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight text-slate-950`,children:[t.counts.allow,H(`span`,{class:`ml-1 text-sm font-bold text-slate-500`,children:`개`})]}),H(`p`,{class:`mt-1 mb-0 text-xs text-slate-500`,children:`차단하지 않도록 등록한 도메인`})]}),H(`div`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white px-5 py-4 shadow-sm shadow-slate-900/5`,children:[H(`dt`,{class:`text-sm font-extrabold text-red-700`,children:`차단 도메인`}),H(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight text-slate-950`,children:[t.counts.block,H(`span`,{class:`ml-1 text-sm font-bold text-slate-500`,children:`개`})]}),H(`p`,{class:`mt-1 mb-0 text-xs text-slate-500`,children:`추가로 차단하도록 등록한 도메인`})]}),H(`div`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white px-5 py-4 shadow-sm shadow-slate-900/5`,children:[H(`dt`,{class:`text-sm font-extrabold text-slate-600`,children:`전체 등록 규칙`}),H(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight text-slate-950`,children:[t.counts.total,H(`span`,{class:`ml-1 text-sm font-bold text-slate-500`,children:`개`})]}),H(`p`,{class:`mt-1 mb-0 text-xs text-slate-500`,children:`허용 및 차단 규칙의 합계`})]})]})}),H(`div`,{class:`mt-7`,children:H(q,{eyebrow:`Recommendations`,title:`추천 사용자 규칙`,description:`웹사이트에서 선택한 권장 허용 규칙의 동기화 상태를 확인하고 관리합니다.`})}),H(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,"aria-label":`추천 사용자 규칙`,children:[H(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between`,children:[H(`div`,{class:`min-w-0 flex-1`,children:[H(`div`,{class:`flex flex-wrap items-center gap-2`,children:[H(`span`,{class:`grid size-8 place-items-center rounded-lg bg-teal-50 text-teal-700`,children:H(wn,{class:`size-4`})}),H(`h3`,{class:`m-0 text-base font-extrabold text-slate-950`,children:`동기화 상태`}),l?H(`span`,{"aria-live":`polite`,class:`rounded-full px-2.5 py-1 text-xs font-bold ${f?.syncStatus===`confirmed`?`bg-emerald-100 text-emerald-800`:f?.syncStatus===`failed`?`bg-red-100 text-red-800`:`bg-amber-100 text-amber-800`}`,children:f?.syncStatus===`confirmed`?`허용 목록 확인`:ue}):null]}),H(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:`웹사이트에서 선택한 허용 규칙을 약 30초마다 확인합니다. 적용 해제는 이 공유기에서도 할 수 있습니다.`}),H(`div`,{class:`mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs font-semibold text-slate-600`,children:[H(`span`,{children:[`추천으로 추가 `,H(`strong`,{class:`text-teal-800`,children:[m?ee.length:`—`,`개 도메인`]})]}),f?.syncStatus===`confirmed`?H(`span`,{children:[`허용 확인 `,H(`strong`,{class:`text-emerald-800`,children:[f.confirmedDomains.length,`개`]})]}):null,f?.lastCheckedAt?H(`span`,{children:[f.syncWarning?`최근 재확인 시도`:`최근 확인`,`: `,H(Iv,{timestamp:f.lastCheckedAt})]}):null]})]}),H(`div`,{class:`flex w-full flex-wrap gap-2 sm:w-auto lg:shrink-0`,children:[u?H(`button`,{class:`inline-flex min-h-10 flex-1 items-center justify-center 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 sm:flex-none`,disabled:g,onClick:()=>void ie(),type:`button`,children:g?`동기화 중…`:`지금 동기화`}):null,H(`a`,{class:`inline-flex min-h-10 flex-1 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:flex-none`,href:`https://www.smartsafehub.com/dashboard/recommended-rules/`,target:`_blank`,rel:`noopener noreferrer`,children:`추천 규칙 보기 ↗`})]})]}),fe?H(`div`,{"aria-live":`polite`,class:`mt-3 flex items-start gap-2 rounded-xl px-3 py-2.5 text-sm ${fe.tone===`red`?`bg-red-50 text-red-800`:fe.tone===`green`?`bg-emerald-50 text-emerald-800`:`bg-amber-50 text-amber-800`}`,role:`status`,children:[H(`span`,{"aria-hidden":`true`,class:`mt-1 size-2 shrink-0 rounded-full ${fe.tone===`red`?`bg-red-500`:fe.tone===`green`?`bg-emerald-500`:`bg-amber-500`}`}),H(`div`,{class:`min-w-0`,children:[H(`strong`,{children:fe.label}),H(`p`,{class:`mt-1 mb-0 leading-5`,children:fe.description})]})]}):null,H(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`추천 규칙 동기화는 SafeShield 차단 목록 갱신과 별개입니다. 허용 목록 확인은 DNS 처리나 서비스 재생 성공을 보장하지 않습니다.`})]}),t.applyLocalOverrides?null:H(`section`,{class:`mt-4 flex items-start gap-3 rounded-2xl border border-amber-200 bg-amber-50 p-4`,role:`alert`,children:[H(W,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),H(`div`,{children:[H(`p`,{class:`m-0 text-sm font-extrabold text-amber-950`,children:`로컬 규칙 적용이 꺼져 있습니다`}),H(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:`규칙은 저장되지만 SafeShield의 apply_local_overrides 설정을 켜기 전까지 DNS에 반영되지 않습니다.`})]})]}),t.safeshieldEnabled?null:H(`section`,{class:`mt-4 flex items-start gap-3 rounded-2xl border border-slate-200 bg-slate-100 p-4`,role:`status`,children:[H(Yt,{class:`mt-0.5 size-5 shrink-0 text-slate-600`}),H(`div`,{children:[H(`p`,{class:`m-0 text-sm font-extrabold text-slate-950`,children:`SafeShield가 비활성화되어 있습니다`}),H(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`규칙을 미리 저장할 수 있으며, SafeShield를 켜고 차단 목록을 갱신하면 적용됩니다.`})]})]}),r?H(`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:[H(`div`,{class:`flex min-w-0 items-start gap-3`,children:[H(wn,{class:`mt-0.5 size-5 shrink-0 text-teal-700`}),H(`p`,{class:`m-0 text-sm font-semibold leading-6 text-teal-950`,children:r})]}),H(`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?H(`section`,{"aria-live":`assertive`,class:`mt-4 flex items-start gap-3 rounded-2xl border border-red-200 bg-red-50 p-4`,children:[H(W,{class:`mt-0.5 size-5 shrink-0 text-red-700`}),H(`p`,{class:`m-0 text-sm font-semibold leading-6 text-red-900`,children:n})]}):null,H(`section`,{class:`mt-5 grid gap-5 xl:grid-cols-2`,children:[H(my,{action:e,kind:`allow`,onAddRule:a,onDeleteRule:o,rules:t.allow,recommendedDomainSet:ne,recommendedKnown:m,recommendedRules:te,...d?{onRequestRevoke:y}:{},revokeBusy:x}),H(my,{action:e,kind:`block`,onAddRule:a,onDeleteRule:o,rules:t.block,recommendedDomainSet:ne,recommendedKnown:m,recommendedRules:te,revokeBusy:x})]})]}):H(ys,{message:`사용자 규칙 응답이 비어 있습니다.`,onRetry:c})}function gy(e,t){return V(`smartsafehub`,`system_root_password_change`,{current_password:e,new_password:t})}var _y=Array.from({length:24},(e,t)=>{let n=String(t).padStart(2,`0`);return{value:n,label:`${n}시`}}),vy=Array.from({length:60},(e,t)=>{let n=String(t).padStart(2,`0`);return{value:n,label:`${n}분`}});function yy(e){let t=/^([01]\d|2[0-3]):([0-5]\d)$/.exec(e);return{hour:t?.[1]??`03`,minute:t?.[2]??`00`}}function by({ariaLabel:e,className:t=``,disabled:n=!1,id:r,onChange:i,value:a,variant:o=`default`}){let{hour:s,minute:c}=yy(a),l=(e,t)=>{i(`${e}:${t}`)};return H(`div`,{"aria-label":e,class:`ssh-time-select ${t}`.trim(),"data-time-select":r,role:`group`,children:[H(U_,{ariaLabel:`${e} 시`,disabled:n,id:`${r}-hour`,onChange:e=>l(e,c),options:_y,value:s,variant:o}),H(U_,{ariaLabel:`${e} 분`,disabled:n,id:`${r}-minute`,onChange:e=>l(s,e),options:vy,value:c,variant:o})]})}function xy(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone||null}catch{return null}}function Sy(e){return H(`article`,{class:`h-full min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6 ${e.className??``}`,children:[H(`div`,{class:`flex min-w-0 items-start justify-between gap-3`,children:[H(`div`,{class:`flex min-w-0 items-start gap-4`,children:[H(`div`,{class:`flex size-11 shrink-0 items-center justify-center rounded-xl bg-slate-100 text-slate-600`,children:e.icon}),H(`div`,{class:`min-w-0`,children:[H(`div`,{class:`flex flex-wrap items-center gap-2`,children:[H(`h2`,{class:`m-0 text-lg font-black text-slate-950`,children:e.title}),e.titleActions]}),H(`p`,{class:`mt-1.5 mb-0 text-sm leading-6 text-slate-500`,children:e.description})]})]}),e.headerActions?H(`div`,{class:`shrink-0`,children:e.headerActions}):null]}),H(`div`,{class:`mt-5`,children:e.children})]})}function Cy(e){let[t,n]=F(``),[r,i]=F(e.data?.localtime??0),a=xy();I(()=>{e.data?.zonename&&n(e.data.zonename)},[e.data?.zonename]),I(()=>{let t=e.data?.localtime??0,n=Date.now();if(i(t),!Number.isFinite(t)||t<=0)return;let r=window.setInterval(()=>{i(t+Math.floor((Date.now()-n)/1e3))},1e3);return()=>window.clearInterval(r)},[e.data?.localtime]);let o=e.data?Object.keys(e.data.timezones).sort((e,t)=>e.localeCompare(t)):[],s=Fo(r,e.data?.zonename??`UTC`),c=a!==null&&e.data?.timezones[a]!==void 0,l=e.data!==null&&t.length>0&&t!==e.data.zonename;return H(b,{children:[H(Sy,{description:`공유기의 기준 시간대를 설정합니다. 로그, 통계, 자동 설치와 예약 재부팅 시각도 이 시간대를 기준으로 동작합니다.`,icon:H(pn,{class:`size-5`}),title:`시간 및 시간대`,children:[(e.saveError||e.saveMessage)&&H(`div`,{class:`mb-5 flex min-w-0 items-start justify-between gap-3 rounded-xl border px-4 py-3 text-sm font-bold ${e.saveError?`border-rose-200 bg-rose-50 text-rose-800`:`border-emerald-200 bg-emerald-50 text-emerald-800`}`,children:[H(`span`,{class:`min-w-0`,children:e.saveError||e.saveMessage}),H(`button`,{class:`shrink-0 rounded-lg px-2 py-1 text-xs font-extrabold hover:bg-black/5`,onClick:e.onDismissFeedback,type:`button`,children:`닫기`})]}),e.error&&!e.data?H(`div`,{class:`rounded-xl border border-rose-200 bg-rose-50 p-4`,children:[H(`p`,{class:`m-0 text-sm font-bold text-rose-800`,children:e.error}),H(`button`,{class:`mt-3 inline-flex min-h-10 items-center rounded-xl border border-rose-300 bg-white px-3 py-2 text-xs font-extrabold text-rose-700 transition hover:bg-rose-100`,onClick:e.onRetry,type:`button`,children:`다시 불러오기`})]}):H(b,{children:[H(`div`,{class:`grid min-w-0 grid-cols-1 gap-3 sm:grid-cols-2`,children:[H(`div`,{class:`rounded-xl bg-slate-50 p-4`,children:[H(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.12em] text-slate-500`,children:`현재 시간`}),H(`p`,{class:`mt-2 mb-0 text-sm font-extrabold leading-6 text-slate-900`,children:s}),H(`p`,{class:`mt-1 mb-0 break-words text-xs leading-5 text-slate-500`,children:e.data?.zonename??`시간대 확인 중`})]}),H(`div`,{class:`rounded-xl bg-slate-50 p-4`,children:[H(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.12em] text-slate-500`,children:`시간 동기화`}),H(`p`,{class:`mt-2 mb-0 text-sm font-extrabold ${e.data?.ntpEnabled?`text-emerald-700`:`text-amber-700`}`,children:e.data?.ntpEnabled?`자동 동기화 설정됨`:`자동 동기화 꺼짐`}),H(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`장치의 기본 NTP 설정 상태입니다.`}),H(`button`,{class:`mt-3 inline-flex min-h-9 items-center justify-center gap-2 rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-extrabold text-slate-700 transition hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-50`,disabled:!e.data?.ntpEnabled||e.saving||e.syncing,onClick:()=>void e.onSync(),type:`button`,children:e.syncing?`동기화 요청 중`:`지금 동기화`})]})]}),H(`div`,{class:`mt-5`,children:[H(`label`,{class:`mb-2 block text-sm font-extrabold text-slate-800`,for:`smartsafehub-timezone`,children:`시간대`}),H(U_,{ariaLabel:`시간대`,disabled:e.loading||e.saving||e.syncing||!e.data,id:`smartsafehub-timezone`,onChange:n,options:e.data?o.map(e=>({value:e,label:e})):[{value:``,label:`시간대 불러오는 중`}],value:t}),H(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-500`,children:`시간대를 변경하면 시스템에 즉시 적용되며 자동 설치와 예약 재부팅 일정도 새 기준 시간으로 다시 계산합니다.`})]}),H(`div`,{class:`mt-4 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:justify-end`,children:[c&&a!==t&&H(`button`,{class:`inline-flex min-h-11 w-full items-center justify-center rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-extrabold text-slate-700 transition hover:bg-slate-50 disabled:opacity-60 sm:w-auto`,disabled:e.saving||e.syncing,onClick:()=>n(a),type:`button`,children:`브라우저 시간대 사용`}),H(`button`,{class:`inline-flex min-h-11 w-full items-center justify-center rounded-xl bg-teal-700 px-4 py-2.5 text-sm font-extrabold text-white transition hover:bg-teal-800 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto`,disabled:!l||e.saving||e.syncing,onClick:()=>void e.onSave(t),type:`button`,children:e.saving?`시간대 저장 중`:`시간대 저장`})]})]})]}),H(`div`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:H(Fy,{data:e.scheduledRebootData,error:e.scheduledRebootError,loading:e.scheduledRebootLoading,onDismissFeedback:e.onDismissScheduledRebootFeedback,onRetry:e.onRetry,onSave:e.onSaveScheduledReboot,saveError:e.scheduledRebootSaveError,saveMessage:e.scheduledRebootSaveMessage,saving:e.scheduledRebootSaving})})]})}function wy(e){return e===`initializing`?{badge:`bg-teal-100 text-teal-800`,panel:`border-teal-200 bg-teal-50`,label:`준비 중`}:e===`critical`?{badge:`bg-rose-100 text-rose-800`,panel:`border-rose-200 bg-rose-50`,label:`이상`}:e===`warning`?{badge:`bg-amber-100 text-amber-800`,panel:`border-amber-200 bg-amber-50`,label:`주의`}:e===`unknown`?{badge:`bg-slate-200 text-slate-700`,panel:`border-slate-200 bg-slate-50`,label:`확인 불가`}:{badge:`bg-emerald-100 text-emerald-800`,panel:`border-emerald-200 bg-emerald-50`,label:`정상`}}function Ty(e){if(!e.enabled)return`보고 중지됨`;switch(e.lastResult){case`reported`:return`정상 보고`;case`initializing`:return e.lastReportAt>0?`상태 갱신 중`:`첫 보고 진행 중`;case`failed`:return`최근 보고 실패`;case`idle`:return`다음 보고 대기`;case`ineligible`:return`멤버십 확인 필요`;case`disabled`:case`never`:return`첫 보고 준비 중`;default:return`보고 준비 중`}}function Ey(e){return e.lastReportAt>0?Mo(e.lastReportAt):e.enabled?`첫 보고 대기 중`:`보고 기록 없음`}function Dy(e){let t=e?.plan?.trim().toUpperCase(),n=e?.entitlementStatus?.trim().toLowerCase();return t&&(n===`trial`||n===`trialing`)?`${t} · 체험`:t||`유료 · Trial`}function Oy(e){let t=e.data,n=wy(t?.overall??`unknown`),r=t?.checks.filter(e=>e.status!==`ok`)??[],i=r.length>0?r.slice(0,4):t?.checks.filter(e=>[`system.memory`,`network.wan`,`service.dnsmasq`,`safeshield.runtime`].includes(e.id))??[],a=t?.reporter,o=a?.enabled===!0,s=a?.eligible===!0||o,c=e.savingReporter?o?`원격 상태 보고를 켜고 있습니다.`:`원격 상태 보고를 끄고 있습니다.`:o?`원격 상태 보고가 켜져 있습니다.`:`원격 상태 보고가 꺼져 있습니다.`,l=o?`장치 상태가 바뀌면 즉시 보고하고, 정상 상태에서도 주기적으로 SmartSafeHub 서버에 보고합니다.`:`로컬 진단은 계속 동작하지만 장치 상태는 SmartSafeHub 서버에 자동으로 전송되지 않습니다.`;return H(b,{children:[H(Sy,{description:`SmartSafeHub가 장치 상태를 직접 점검하고 이상 항목을 알려줍니다.`,icon:H(_n,{class:`size-5`}),title:`장치 진단`,children:[(e.actionError||e.actionMessage)&&H(`div`,{class:`mb-4 flex items-start justify-between gap-3 rounded-xl border px-4 py-3 text-sm font-bold ${e.actionError?`border-rose-200 bg-rose-50 text-rose-800`:`border-emerald-200 bg-emerald-50 text-emerald-800`}`,children:[H(`span`,{children:e.actionError||e.actionMessage}),H(`button`,{class:`shrink-0 rounded-lg px-2 py-1 text-xs font-extrabold hover:bg-black/5`,onClick:e.onDismissFeedback,type:`button`,children:`닫기`})]}),e.error&&!t?H(`div`,{class:`rounded-xl border border-rose-200 bg-rose-50 p-4`,children:[H(`p`,{class:`m-0 text-sm font-bold text-rose-800`,children:e.error}),H(`button`,{class:`mt-3 inline-flex min-h-10 items-center rounded-xl border border-rose-300 bg-white px-3 py-2 text-xs font-extrabold text-rose-700 transition hover:bg-rose-100`,onClick:e.onRetry,type:`button`,children:`다시 불러오기`})]}):H(b,{children:[H(`div`,{class:`rounded-xl border p-4 ${n.panel}`,children:H(`div`,{class:`flex flex-wrap items-start justify-between gap-3`,children:[H(`div`,{class:`flex min-w-0 items-start gap-3`,children:[H(`span`,{class:`mt-0.5 shrink-0`,children:t?.overall===`ok`?H(wn,{class:`size-5 text-emerald-700`}):t?.overall===`initializing`?H(pn,{class:`size-5 text-teal-700`}):H(W,{class:`size-5 text-amber-700`})}),H(`div`,{class:`min-w-0`,children:[H(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:t?.summary.message??`장치 상태를 확인하고 있습니다.`}),H(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-600`,children:[`마지막 진단 : `,t?Mo(t.generatedAt):`확인 중`]})]})]}),H(`span`,{class:`rounded-full px-2.5 py-1 text-xs font-extrabold ${n.badge}`,children:n.label})]})}),t&&H(`div`,{class:`mt-4 grid grid-cols-3 gap-2`,children:[H(`div`,{class:`rounded-xl bg-slate-50 p-3`,children:[H(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`진단 항목`}),H(`p`,{class:`mt-1 mb-0 text-lg font-black text-slate-950`,children:t.summary.total})]}),H(`div`,{class:`rounded-xl bg-slate-50 p-3`,children:[H(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`주의`}),H(`p`,{class:`mt-1 mb-0 text-lg font-black text-amber-700`,children:t.summary.warning})]}),H(`div`,{class:`rounded-xl bg-slate-50 p-3`,children:[H(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`이상`}),H(`p`,{class:`mt-1 mb-0 text-lg font-black text-rose-700`,children:t.summary.critical})]})]}),i.length>0&&H(`div`,{class:`mt-4 space-y-2`,children:i.map(e=>{let t=wy(e.status);return H(`div`,{class:`flex min-w-0 items-start justify-between gap-3 rounded-xl border border-slate-200 px-3 py-2.5`,children:[H(`div`,{class:`min-w-0`,children:[H(`p`,{class:`m-0 text-xs font-extrabold text-slate-900`,children:e.label}),H(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:e.message})]}),H(`span`,{class:`shrink-0 rounded-full px-2 py-1 text-[11px] font-extrabold ${t.badge}`,children:t.label})]},e.id)})}),H(`div`,{class:`mt-4 flex flex-col gap-2 sm:flex-row`,children:[H(`button`,{class:`inline-flex min-h-11 w-full items-center justify-center 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 sm:w-auto`,disabled:e.running||e.loading,onClick:e.onRun,type:`button`,children:e.running?`진단 중`:`지금 진단`}),H(`button`,{class:`inline-flex min-h-11 w-full items-center justify-center 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 sm:w-auto`,disabled:e.downloadBusy,onClick:e.onDownload,type:`button`,children:e.downloadBusy?`진단 정보 생성 중`:`진단 정보 다운로드`})]}),H(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`다운로드 파일에는 호스트명, WAN IP와 Wi-Fi SSID 같은 네트워크 식별 정보가 포함될 수 있으므로 외부 전달 전에 내용을 확인해 주세요.`})]})]}),H(`div`,{class:`grid min-w-0 content-start gap-4`,children:[H(Sy,{description:`장치의 최소 상태 정보와 이상 코드만 서버로 전송합니다. 직접 켜고 언제든지 끌 수 있습니다.`,icon:H(Yt,{class:`size-5`}),title:`원격 상태 보고`,titleActions:H(`span`,{class:`inline-flex rounded-full bg-slate-100 px-2.5 py-1 text-[11px] font-extrabold uppercase tracking-[0.08em] text-slate-600`,children:Dy(a)}),headerActions:H(`button`,{"aria-checked":a?.enabled===!0,"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 ${a?.enabled?`border-teal-600 bg-teal-600`:`border-slate-300 bg-slate-200`}`,disabled:!t||e.savingReporter||!s,onClick:()=>e.onSetReporter(!(a?.enabled??!1)),role:`switch`,type:`button`,children:H(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${a?.enabled?`left-6`:`left-1`}`})}),children:H(`div`,{class:`min-w-0`,children:a?.eligible?H(`div`,{class:`rounded-xl bg-slate-50 p-4`,children:[H(`div`,{class:`mb-4 flex items-start gap-3 rounded-xl border p-3 ${o?`border-emerald-200 bg-emerald-50`:`border-slate-200 bg-white`}`,children:[H(`span`,{class:`mt-0.5 shrink-0`,children:o?H(wn,{class:`size-5 text-emerald-700`}):H(gn,{class:`size-5 text-slate-500`})}),H(`div`,{class:`min-w-0 flex-1`,children:[H(`div`,{class:`flex flex-wrap items-center gap-2`,children:[H(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:c}),H(`span`,{class:`rounded-full px-2 py-0.5 text-[10px] font-extrabold ${e.savingReporter?`bg-slate-200 text-slate-700`:o?`bg-emerald-100 text-emerald-800`:`bg-slate-200 text-slate-700`}`,children:e.savingReporter?`설정 중`:o?`켜짐`:`꺼짐`})]}),H(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-600`,children:l})]})]}),H(`div`,{class:`grid grid-cols-1 gap-3 sm:grid-cols-2`,children:[H(`div`,{children:[H(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`최근 전송 상태`}),H(`p`,{class:`mt-1 mb-0 text-sm font-black text-slate-900`,children:e.savingReporter?`설정 반영 중`:Ty(a)})]}),H(`div`,{children:[H(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`마지막 서버 보고`}),H(`p`,{class:`mt-1 mb-0 text-sm font-black text-slate-900`,children:Ey(a)})]})]}),a.enabled&&a.lastErrorCode&&H(`p`,{class:`mt-3 mb-0 text-xs font-bold text-rose-700`,children:[`최근 보고 오류 : `,a.lastErrorCode]})]}):H(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[H(`p`,{class:`m-0 text-sm font-extrabold text-slate-900`,children:`원격 상태 보고는 유료 멤버십 또는 체험 기간에 사용할 수 있습니다.`}),H(`a`,{class:`mt-3 inline-flex text-xs font-extrabold text-teal-700 hover:text-teal-800`,href:`https://www.smartsafehub.com/pricing/`,rel:`noopener noreferrer`,target:`_blank`,children:`멤버십 알아보기`})]})})}),H(Sy,{description:`진단에 필요한 최소한의 상태 정보만 전송하며, 네트워크 식별 정보와 이용 내용은 보내지 않습니다.`,icon:H(Yt,{class:`size-5`}),title:`개인정보 보호`,children:H(`div`,{class:`grid grid-cols-1 gap-3 sm:grid-cols-2`,children:[H(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-3.5`,children:[H(`div`,{class:`flex items-center gap-2`,children:[H(`span`,{class:`flex size-7 shrink-0 items-center justify-center rounded-lg bg-white text-slate-600`,children:H(vn,{class:`size-4`})}),H(`p`,{class:`m-0 text-sm font-black text-slate-900`,children:`전송되는 정보`})]}),H(`div`,{class:`mt-3 space-y-3`,children:[H(`div`,{children:[H(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`기기 상태 수치`}),H(`p`,{class:`mt-1 mb-0 text-xs font-semibold leading-5 text-slate-700`,children:`메모리 · 부하 · 저장 공간`})]}),H(`div`,{children:[H(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`진단 정보`}),H(`p`,{class:`mt-1 mb-0 text-xs font-semibold leading-5 text-slate-700`,children:`진단 상태 · 이상 코드 · 보고 시각`})]})]})]}),H(`div`,{class:`rounded-xl border border-teal-200 bg-teal-50 p-3.5`,children:[H(`div`,{class:`flex items-center gap-2`,children:[H(`span`,{class:`flex size-7 shrink-0 items-center justify-center rounded-lg bg-white text-teal-700`,children:H(Yt,{class:`size-4`})}),H(`p`,{class:`m-0 text-sm font-black text-slate-900`,children:`전송하지 않는 정보`})]}),H(`div`,{class:`mt-3 space-y-3`,children:[H(`div`,{children:[H(`p`,{class:`m-0 text-[11px] font-extrabold text-teal-800`,children:`개인 식별 · 네트워크 정보`}),H(`p`,{class:`mt-1 mb-0 text-xs font-semibold leading-5 text-slate-700`,children:`호스트명 · WAN IP · Wi-Fi SSID/MAC`})]}),H(`div`,{children:[H(`p`,{class:`m-0 text-[11px] font-extrabold text-teal-800`,children:`이용 내용 및 원문 데이터`}),H(`p`,{class:`mt-1 mb-0 text-xs font-semibold leading-5 text-slate-700`,children:`DNS 요청 내용 · 시스템 로그 원문`})]})]})]})]})})]})]})}function ky(e){let[t,n]=F(!1);return H(`label`,{class:`block min-w-0`,for:e.id,children:[H(`span`,{class:`mb-2 block text-sm font-extrabold text-slate-800`,children:e.label}),H(`span`,{class:`relative block min-w-0`,children:[H(`input`,{autoComplete:e.autoComplete,class:`ssh-password-input min-h-11 w-full rounded-xl border border-slate-300 bg-white px-3 py-2.5 pr-12 text-sm font-bold text-slate-900 outline-none transition focus:border-teal-500 focus:ring-2 focus:ring-teal-100 disabled:cursor-wait disabled:opacity-60`,disabled:e.disabled,id:e.id,name:e.name,onInput:t=>e.onInput(t.currentTarget.value),placeholder:e.placeholder,...t?{type:`text`}:{type:`password`},value:e.value}),H(`button`,{"aria-label":t?`${e.label} 숨기기`:`${e.label} 표시`,"aria-pressed":t,class:`absolute inset-y-0 right-1 my-auto flex size-9 items-center justify-center rounded-lg text-slate-500 transition hover:bg-slate-100 hover:text-slate-800 disabled:opacity-50`,disabled:e.disabled,onClick:()=>n(e=>!e),title:t?`비밀번호 숨기기`:`비밀번호 표시`,type:`button`,children:H(t?Sn:xn,{class:`size-4`})})]})]})}function Ay(e){return H(`li`,{class:`flex min-w-0 items-center gap-1.5 text-xs font-bold ${e.met?`text-emerald-700`:`text-slate-500`}`,children:[H(wn,{class:`size-3.5 shrink-0`,"aria-hidden":`true`}),H(`span`,{children:e.label})]})}function jy(e){let[t,n]=F(``),[r,i]=F(``),[a,o]=F(``),[s,c]=F(!1),[l,u]=F(null),d=Vn(r),f=Hn(d),p=a.length>0&&r===a,m=t.length>0&&r.length>0&&t!==r,h=!s&&m&&f&&p,g=async()=>{if(u(null),!t)u(`현재 관리자 비밀번호를 입력해 주세요.`);else if(!f)u(`새 비밀번호는 8자 이상이며 영문과 숫자를 각각 하나 이상 포함해야 합니다.`);else if(t===r)u(`새 관리자 비밀번호는 현재 비밀번호와 다르게 설정해 주세요.`);else if(r!==a)u(`새 비밀번호 확인 값이 일치하지 않습니다.`);else{c(!0);try{await gy(t,r),n(``),i(``),o(``);try{await Oe()}catch{}c(!1),e.onChanged()}catch(e){u(K(e,`관리자 비밀번호를 변경하지 못했습니다. 잠시 후 다시 시도해 주세요.`)),c(!1)}}};return H(Sy,{description:`현재 비밀번호를 확인한 뒤 공유기 관리자 비밀번호를 변경합니다. 변경이 완료되면 새 비밀번호로 다시 로그인해야 합니다.`,icon:H(yn,{class:`size-5`}),title:`관리자 비밀번호`,children:H(`form`,{class:`space-y-4`,noValidate:!0,onSubmit:e=>{e.preventDefault(),g()},children:[l&&H(`div`,{"aria-live":`polite`,class:`rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm font-bold text-rose-800`,role:`alert`,children:l}),H(ky,{autoComplete:`current-password`,disabled:s,id:`smartsafehub-current-admin-password`,label:`현재 비밀번호`,name:`current-password`,onInput:n,placeholder:`현재 관리자 비밀번호`,value:t}),H(ky,{autoComplete:`new-password`,disabled:s,id:`smartsafehub-new-admin-password`,label:`새 비밀번호`,name:`new-password`,onInput:i,placeholder:`새 관리자 비밀번호`,value:r}),H(`ul`,{"aria-label":`새 비밀번호 요구 사항`,class:`m-0 grid list-none grid-cols-1 gap-2 p-0 sm:grid-cols-3`,children:[H(Ay,{label:`8자 이상`,met:d.length}),H(Ay,{label:`영문자 포함`,met:d.letter}),H(Ay,{label:`숫자 포함`,met:d.number})]}),H(`div`,{class:`pt-2`,children:H(ky,{autoComplete:`new-password`,disabled:s,id:`smartsafehub-confirm-admin-password`,label:`새 비밀번호 확인`,name:`confirm-password`,onInput:o,placeholder:`새 관리자 비밀번호 다시 입력`,value:a})}),a.length>0&&H(`p`,{"aria-live":`polite`,class:`m-0 text-xs font-bold ${p?`text-emerald-700`:`text-rose-700`}`,role:`status`,children:p?`새 비밀번호가 일치합니다.`:`새 비밀번호 확인 값이 일치하지 않습니다.`}),r.length>0&&t===r&&H(`p`,{class:`m-0 text-xs font-bold text-amber-700`,children:`새 비밀번호는 현재 비밀번호와 다르게 설정해 주세요.`}),H(`div`,{class:`pt-2`,children:H(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-3 text-xs leading-5 text-slate-600`,children:`비밀번호는 현재 공유기에 직접 적용되며 외부 서버로 전송되지 않습니다. 변경 후 현재 로그인 세션은 종료됩니다.`})}),H(`div`,{class:`rounded-xl border border-amber-200 bg-amber-50 p-3 text-xs leading-5 text-amber-900`,children:[H(`strong`,{class:`block font-extrabold`,children:`비밀번호를 잊었을 때`}),`전원이 켜진 상태에서 Reset 버튼을 5~9초 누른 뒤 놓으면 관리자 비밀번호만 복구할 수 있습니다. 네트워크, Wi-Fi와 SafeShield 설정은 유지됩니다. 10초 이상 누르면 모든 사용자 설정을 초기화하는 기기 초기화가 실행됩니다.`]}),H(`div`,{class:`flex justify-end`,children:H(`button`,{class:`inline-flex min-h-11 w-full items-center justify-center rounded-xl bg-teal-700 px-4 py-2.5 text-sm font-extrabold text-white transition hover:bg-teal-800 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto`,disabled:!h,type:`submit`,children:s?`비밀번호 변경 중`:`비밀번호 변경`})})]})})}function My(e){let t=L(null),[n,r]=F(null),[i,a]=F(!1),o=e.action!==null;I(()=>{e.validated||a(!1)},[e.validated]);let s=()=>{!o&&!e.restoreAccepted&&t.current?.click()},c=async()=>{n&&await e.onUpload(n)&&(r(null),t.current&&(t.current.value=``))};return H(Sy,{description:`펌웨어 업데이트나 복구 전에 현재 OpenWrt 설정을 표준 백업 파일로 저장하고, 필요할 때 검증 후 복원합니다.`,icon:H(vn,{class:`size-5`}),title:`설정 백업 및 복원`,children:[(e.error||e.message)&&H(`div`,{class:`mb-5 flex min-w-0 items-start justify-between gap-3 rounded-xl border px-4 py-3 text-sm font-bold ${e.error?`border-rose-200 bg-rose-50 text-rose-800`:`border-emerald-200 bg-emerald-50 text-emerald-800`}`,children:[H(`span`,{class:`min-w-0`,children:e.error||e.message}),!e.restoreAccepted&&H(`button`,{class:`shrink-0 rounded-lg px-2 py-1 text-xs font-extrabold hover:bg-black/5`,onClick:e.onDismissFeedback,type:`button`,children:`닫기`})]}),H(`div`,{class:`grid min-w-0 grid-cols-1 gap-3`,children:[H(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4 sm:p-5`,children:[H(`p`,{class:`m-0 text-sm font-black text-slate-900`,children:`현재 설정 백업`}),H(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-600`,children:`네트워크, Wi-Fi, SmartSafeHub와 SafeShield 등 OpenWrt가 관리하는 설정을 표준 백업 파일로 저장합니다.`}),H(`button`,{class:`mt-4 inline-flex min-h-11 w-full 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 sm:w-auto`,disabled:o||e.restoreAccepted,onClick:e.onDownload,type:`button`,children:[H(_n,{class:`size-4`}),e.action===`download`?`백업 생성 중`:`설정 백업 다운로드`]}),H(`p`,{class:`mt-3 mb-0 text-xs font-bold leading-5 text-amber-700`,children:`백업에는 Wi-Fi 비밀번호, 관리자 설정, VPN 키나 기기 인증 정보 같은 민감한 값이 포함될 수 있으므로 안전한 위치에 보관해 주세요.`})]}),H(`div`,{class:`rounded-xl border border-slate-200 bg-white p-4 sm:p-5`,children:[H(`p`,{class:`m-0 text-sm font-black text-slate-900`,children:`설정 복원`}),H(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-600`,children:`SmartSafeHub 또는 OpenWrt 백업을 업로드하고 검증한 뒤 복원합니다. 최대 16MB까지 사용할 수 있습니다.`}),e.validated?H(`div`,{class:`mt-4 rounded-xl border border-emerald-200 bg-emerald-50 p-4`,children:[H(`div`,{class:`flex min-w-0 flex-col gap-1 sm:flex-row sm:items-center sm:justify-between`,children:H(`div`,{class:`min-w-0`,children:[H(`p`,{class:`m-0 break-words text-sm font-extrabold text-emerald-900`,children:e.validated.filename}),H(`p`,{class:`mt-1 mb-0 text-xs font-bold text-emerald-700`,children:[Oo(e.validated.sizeBytes),` · 검증 완료`]})]})}),i?H(`div`,{class:`mt-4 rounded-xl border border-rose-200 bg-white p-4`,children:[H(`p`,{class:`m-0 text-sm font-black text-rose-900`,children:`현재 설정을 백업 파일의 내용으로 덮어쓰시겠습니까?`}),H(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-rose-700`,children:`LAN 주소, Wi-Fi, 관리자 접속 정보가 바뀌어 현재 연결이 끊길 수 있습니다. 복원 직후 공유기가 자동으로 재부팅됩니다. 같은 장치와 호환되는 백업만 사용해 주세요.`}),H(`div`,{class:`mt-4 flex flex-col gap-3 sm:flex-row sm:flex-wrap`,children:[H(`button`,{class:`inline-flex min-h-10 w-full items-center justify-center rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-extrabold text-slate-700 transition hover:bg-slate-50 disabled:opacity-60 sm:w-auto`,disabled:o,onClick:()=>a(!1),type:`button`,children:`취소`}),H(`button`,{class:`inline-flex min-h-10 w-full items-center justify-center rounded-xl bg-rose-700 px-4 py-2 text-sm font-extrabold text-white transition hover:bg-rose-800 disabled:cursor-wait disabled:opacity-60 sm:w-auto`,disabled:o,onClick:()=>void e.onRestore(),type:`button`,children:e.action===`restore`?`설정 복원 중`:`설정 복원 및 재부팅`})]})]}):H(`div`,{class:`mt-4 flex flex-col gap-3 sm:flex-row sm:flex-wrap`,children:[H(`button`,{class:`inline-flex min-h-10 w-full items-center justify-center rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-extrabold text-slate-700 transition hover:bg-slate-50 disabled:opacity-60 sm:w-auto`,disabled:o||e.restoreAccepted,onClick:()=>void e.onDiscard(),type:`button`,children:e.action===`discard`?`파일 삭제 중`:`파일 삭제`}),H(`button`,{class:`inline-flex min-h-10 w-full items-center justify-center rounded-xl bg-amber-600 px-4 py-2 text-sm font-extrabold text-white transition hover:bg-amber-700 disabled:opacity-60 sm:w-auto`,disabled:o||e.restoreAccepted,onClick:()=>a(!0),type:`button`,children:`복원 준비`})]})]}):H(b,{children:[H(`input`,{accept:`.tar.gz,.tgz,application/gzip,application/x-gzip`,class:`sr-only`,disabled:o||e.restoreAccepted,onChange:e=>r(e.currentTarget.files?.[0]??null),ref:t,type:`file`}),H(`button`,{class:`mt-4 flex min-h-11 w-full items-center justify-between gap-3 rounded-xl border border-dashed border-slate-300 bg-slate-50 px-4 py-3 text-left text-sm font-bold text-slate-700 transition hover:border-teal-400 hover:bg-teal-50 disabled:cursor-wait disabled:opacity-60`,disabled:o||e.restoreAccepted,onClick:s,type:`button`,children:[H(`span`,{class:`min-w-0 truncate`,children:n?.name??`복원할 백업 파일 선택`}),H(`span`,{class:`shrink-0 text-xs font-extrabold text-teal-700`,children:`찾아보기`})]}),n&&H(`div`,{class:`mt-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[H(`span`,{class:`text-xs font-bold text-slate-500`,children:Oo(n.size)}),H(`button`,{class:`inline-flex min-h-10 w-full items-center justify-center rounded-xl border border-teal-300 bg-white px-4 py-2 text-sm font-extrabold text-teal-800 transition hover:bg-teal-50 disabled:cursor-wait disabled:opacity-60 sm:w-auto`,disabled:o||e.restoreAccepted,onClick:()=>void c(),type:`button`,children:e.action===`upload`?`업로드 중 ${Math.round(e.uploadProgress??0)}%`:e.action===`validate`?`백업 검증 중`:`업로드 및 검증`})]})]})]})]})]})}function Ny(e){return H(Sy,{description:`공유기 재부팅과 SmartSafeHub에서 제공하지 않는 고급 관리 도구를 한곳에서 제공합니다.`,...e.className===void 0?{}:{className:e.className},icon:H(sn,{class:`size-5`}),title:`시스템 도구`,children:H(`div`,{class:`space-y-3`,children:[H(`section`,{"aria-labelledby":`router-reboot-heading`,class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[H(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[H(`div`,{class:`flex min-w-0 items-start gap-3`,children:[H(`span`,{class:`flex size-9 shrink-0 items-center justify-center rounded-lg bg-rose-50 text-rose-700`,children:H(gn,{class:`size-4`})}),H(`div`,{class:`min-w-0`,children:[H(`p`,{class:`m-0 text-sm font-black text-rose-950`,id:`router-reboot-heading`,children:`공유기 재부팅`}),H(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`재부팅 중에는 인터넷과 Wi-Fi 연결이 잠시 중단됩니다.`})]})]}),!e.confirmingReboot&&H(`button`,{class:`inline-flex min-h-10 w-full shrink-0 items-center justify-center rounded-xl border border-rose-300 bg-white px-3 py-2 text-xs font-extrabold text-rose-700 transition hover:bg-rose-50 disabled:opacity-60 sm:w-auto`,disabled:e.action!==null||e.rebootAccepted,onClick:e.onConfirmReboot,type:`button`,children:`재부팅`})]}),e.confirmingReboot&&H(`div`,{class:`mt-3 rounded-xl border border-rose-200 bg-rose-50 p-3`,children:[H(`p`,{class:`m-0 text-xs font-extrabold leading-5 text-rose-900`,children:`지금 공유기를 재부팅하시겠습니까? 저장되지 않은 LuCI 설정이 있다면 먼저 저장해 주세요.`}),H(`div`,{class:`mt-3 flex flex-col gap-2 sm:flex-row sm:flex-wrap`,children:[H(`button`,{class:`inline-flex min-h-10 w-full items-center justify-center rounded-xl border border-slate-300 bg-white px-3 py-2 text-xs font-extrabold text-slate-700 transition hover:bg-slate-50 sm:w-auto`,disabled:e.action!==null,onClick:e.onCancelReboot,type:`button`,children:`취소`}),H(`button`,{class:`inline-flex min-h-10 w-full items-center justify-center rounded-xl bg-rose-700 px-3 py-2 text-xs font-extrabold text-white transition hover:bg-rose-800 disabled:cursor-wait disabled:opacity-60 sm:w-auto`,disabled:e.action!==null,onClick:e.onReboot,type:`button`,children:e.action===`reboot`?`재부팅 요청 중`:`지금 재부팅`})]})]})]}),H(`section`,{"aria-labelledby":`advanced-tools-heading`,class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[H(`div`,{class:`min-w-0`,children:[H(`p`,{class:`m-0 text-sm font-black text-slate-900`,id:`advanced-tools-heading`,children:`고급 도구`}),H(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`SmartSafeHub에서 아직 제공하지 않는 상세 시스템 설정이나 원본 로그가 필요할 때 사용합니다.`})]}),H(`div`,{class:`mt-3 flex flex-col gap-2 sm:flex-row sm:flex-wrap`,children:[H(`a`,{class:`inline-flex min-h-10 w-full items-center justify-center rounded-xl border border-slate-300 bg-white px-3 py-2 text-xs font-extrabold text-slate-800 no-underline transition hover:bg-slate-50 sm:w-auto`,href:e.advancedSystemUrl,children:`LuCI 고급 설정 열기`}),H(`a`,{class:`inline-flex min-h-10 w-full items-center justify-center rounded-xl border border-slate-300 bg-white px-3 py-2 text-xs font-extrabold text-slate-800 no-underline transition hover:bg-slate-50 sm:w-auto`,href:e.logsUrl,children:`시스템 로그 열기`})]})]})]})})}var Py=[{value:`mon`,label:`월요일`},{value:`tue`,label:`화요일`},{value:`wed`,label:`수요일`},{value:`thu`,label:`목요일`},{value:`fri`,label:`금요일`},{value:`sat`,label:`토요일`},{value:`sun`,label:`일요일`}];function Fy(e){let[t,n]=F(!1),[r,i]=F(`weekly`),[a,o]=F(`sun`),[s,c]=F(`04:00`);I(()=>{e.data&&(n(e.data.enabled),i(e.data.frequency),o(e.data.dayOfWeek),c(e.data.time))},[e.data?.dayOfWeek,e.data?.enabled,e.data?.frequency,e.data?.time]);let l=e.data!==null&&(t!==e.data.enabled||r!==e.data.frequency||a!==e.data.dayOfWeek||s!==e.data.time),u=Py.find(e=>e.value===a),d=t?r===`daily`?`매일 ${s}`:`매주 ${u?.label??`일요일`} ${s}`:`예약 재부팅 꺼짐`;return H(`section`,{"aria-labelledby":`scheduled-reboot-heading`,class:`min-w-0`,children:[H(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[H(`div`,{class:`flex min-w-0 items-start gap-3`,children:[H(`span`,{class:`flex size-11 shrink-0 items-center justify-center rounded-xl bg-slate-100 text-slate-600`,children:H(Cn,{class:`size-5`})}),H(`div`,{class:`min-w-0`,children:[H(`h2`,{class:`m-0 text-lg font-black text-slate-950`,id:`scheduled-reboot-heading`,children:`예약 재부팅`}),H(`p`,{class:`mt-1.5 mb-0 text-sm leading-6 text-slate-500`,children:`공유기를 정해진 시간에 자동으로 재부팅합니다. 기본값은 꺼짐이며 필요한 장치에서만 사용하세요.`})]})]}),H(`div`,{class:`flex shrink-0 flex-wrap items-center justify-between gap-3 sm:justify-end`,children:[l&&H(`span`,{"aria-live":`polite`,class:`inline-flex 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:[H(W,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}),H(`span`,{class:`text-xs font-extrabold text-slate-500`,children:t?`사용 중`:`꺼짐`}),H(`button`,{"aria-checked":t,"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 ${t?`border-teal-600 bg-teal-600`:`border-slate-300 bg-slate-200`}`,disabled:e.loading||e.saving||!e.data,onClick:()=>n(e=>!e),role:`switch`,type:`button`,children:H(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${t?`left-6`:`left-1`}`})})]})]}),(e.saveError||e.saveMessage)&&H(`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.saveError?`border-rose-200 bg-rose-50 text-rose-800`:`border-emerald-200 bg-emerald-50 text-emerald-800`}`,children:[H(`span`,{class:`min-w-0`,children:e.saveError||e.saveMessage}),H(`button`,{class:`shrink-0 rounded-lg px-2 py-1 text-xs font-extrabold hover:bg-black/5`,onClick:e.onDismissFeedback,type:`button`,children:`닫기`})]}),e.error&&!e.data?H(`div`,{class:`mt-4 rounded-xl border border-rose-200 bg-rose-50 p-4`,children:[H(`p`,{class:`m-0 text-sm font-bold text-rose-800`,children:e.error}),H(`button`,{class:`mt-3 inline-flex min-h-10 items-center rounded-xl border border-rose-300 bg-white px-3 py-2 text-xs font-extrabold text-rose-700 transition hover:bg-rose-100`,onClick:e.onRetry,type:`button`,children:`다시 불러오기`})]}):H(b,{children:[H(`div`,{class:`mt-4 grid min-w-0 grid-cols-1 gap-4 md:grid-cols-3`,children:[H(`div`,{children:[H(`label`,{class:`mb-2 block text-sm font-extrabold text-slate-800`,for:`scheduled-reboot-frequency`,children:`주기`}),H(U_,{ariaLabel:`예약 재부팅 주기`,disabled:!t||e.saving||!e.data,id:`scheduled-reboot-frequency`,onChange:e=>i(e),options:[{value:`weekly`,label:`매주`},{value:`daily`,label:`매일`}],value:r})]}),H(`div`,{children:[H(`label`,{class:`mb-2 block text-sm font-extrabold text-slate-800`,for:`scheduled-reboot-day`,children:`요일`}),H(U_,{ariaLabel:`예약 재부팅 요일`,disabled:!t||r!==`weekly`||e.saving||!e.data,id:`scheduled-reboot-day`,onChange:e=>o(e),options:Py.map(e=>({value:e.value,label:e.label})),value:a})]}),H(`div`,{children:[H(`label`,{class:`mb-2 block text-sm font-extrabold text-slate-800`,for:`scheduled-reboot-time-hour`,children:`재부팅 시간`}),H(by,{ariaLabel:`예약 재부팅 시간`,disabled:!t||e.saving||!e.data,id:`scheduled-reboot-time`,onChange:c,value:s})]})]}),H(`div`,{class:`mt-4 flex flex-col gap-3 rounded-xl bg-slate-50 p-4 sm:flex-row sm:items-end sm:justify-between`,children:[H(`div`,{class:`min-w-0`,children:[H(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.12em] text-slate-500`,children:`예약 일정`}),H(`p`,{class:`mt-1.5 mb-0 text-sm font-extrabold text-slate-900`,children:d}),H(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:[e.data?.timezone??`장치 시간대`,` 기준 · 업데이트 작업 중이면 15분 단위로 최대 2시간 연기합니다.`]})]}),H(`button`,{class:`inline-flex min-h-11 w-full shrink-0 items-center justify-center rounded-xl bg-teal-700 px-4 py-2.5 text-sm font-extrabold text-white transition hover:bg-teal-800 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto`,disabled:!l||e.saving||!e.data,onClick:()=>void e.onSave({enabled:t,frequency:r,dayOfWeek:a,time:s}),type:`button`,children:e.saving?`예약 저장 중`:`예약 저장`})]})]})]})}function Iy({activeTab:e,action:t,backupAction:n,backupError:r,backupMessage:i,backupRestoreAccepted:a,backupUploadProgress:o,backupValidated:s,data:c,error:l,feedbackError:u,feedbackMessage:d,health:f,healthActionError:p,healthActionMessage:m,healthError:h,healthLoading:g,healthRunning:_,healthSavingReporter:v,loading:y,rebootAccepted:b,scheduledRebootData:x,scheduledRebootError:S,scheduledRebootLoading:C,scheduledRebootSaveError:w,scheduledRebootSaveMessage:T,scheduledRebootSaving:E,timeData:D,timeError:O,timeLoading:k,timeSaveError:A,timeSaveMessage:j,timeSaving:M,timeSyncing:N,onBackupDiscard:P,onBackupDownload:ee,onBackupRestore:te,onBackupUpload:ne,onDismissBackupFeedback:re,onDismissFeedback:ie,onDismissHealthFeedback:ae,onDismissTimeFeedback:oe,onDownloadDiagnostics:se,onRunHealth:ce,onSetHealthReporter:le,onReboot:ue,onRetry:de,onDismissScheduledRebootFeedback:fe,onSaveScheduledReboot:pe,onSaveTimezone:me,onSyncTime:he,onAdministratorPasswordChanged:ge}){let[_e,ve]=F(!1),ye=be(`/admin/system`),xe=be(`/admin/status/logs`);return H(`section`,{class:`min-w-0 space-y-7`,children:[(u||d)&&H(`div`,{class:`flex min-w-0 flex-col gap-3 rounded-xl border px-4 py-3 text-sm font-bold sm:flex-row sm:items-center sm:justify-between ${u?`border-rose-200 bg-rose-50 text-rose-800`:`border-emerald-200 bg-emerald-50 text-emerald-800`}`,children:[H(`span`,{class:`min-w-0`,children:u||d}),!b&&H(`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:ie,type:`button`,children:`닫기`})]}),H(li,{active:e,label:`설정 메뉴`,tabs:[{id:`diagnostics`,label:`진단 및 지원`,href:`#settings`},{id:`time`,label:`시간 및 예약`,href:`#settings/time`},{id:`management`,label:`관리 및 복구`,href:`#settings/management`}]}),H(`section`,{"aria-labelledby":`diagnostics-tab`,class:`min-w-0`,hidden:e!==`diagnostics`,id:`diagnostics-panel`,role:`tabpanel`,children:[l&&!c?H(`div`,{class:`mb-4 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm font-semibold text-amber-900`,role:`status`,children:[H(`p`,{class:`m-0`,children:[`시스템 정보를 불러오지 못해 진단 파일 다운로드가 제한될 수 있습니다. `,l]}),H(`button`,{class:`mt-2 rounded-lg border border-current px-3 py-1.5 text-xs font-extrabold`,onClick:de,type:`button`,children:`다시 시도`})]}):y&&!c?H(`p`,{class:`mb-4 text-sm font-semibold text-slate-500`,role:`status`,children:`진단 파일에 필요한 시스템 정보를 확인하고 있습니다.`}):null,H(q,{eyebrow:`Device health`,title:`진단 및 지원`,description:`장치 진단과 원격 상태 보고를 확인하고 개인정보 보호 정책을 관리합니다.`}),H(`div`,{class:`grid min-w-0 grid-cols-1 gap-4 lg:grid-cols-2`,children:H(Oy,{actionError:p,actionMessage:m,data:f,downloadBusy:t===`diagnostics`,error:h,loading:g,onDismissFeedback:ae,onDownload:se,onRetry:de,onRun:ce,onSetReporter:le,running:_,savingReporter:v})})]}),H(`section`,{"aria-labelledby":`time-tab`,class:`min-w-0`,hidden:e!==`time`,id:`time-panel`,role:`tabpanel`,children:[H(q,{eyebrow:`Device settings`,title:`시간 및 예약`,description:`시간 기준과 예약 재부팅을 SmartSafeHub에서 직접 관리합니다.`}),H(`div`,{class:`grid min-w-0 grid-cols-1 gap-4 lg:grid-cols-2`,children:H(Cy,{data:D,error:O,loading:k,onDismissFeedback:oe,onDismissScheduledRebootFeedback:fe,onRetry:de,onSave:me,onSaveScheduledReboot:pe,onSync:he,saveError:A,saveMessage:j,saving:M,scheduledRebootData:x,scheduledRebootError:S,scheduledRebootLoading:C,scheduledRebootSaveError:w,scheduledRebootSaveMessage:T,scheduledRebootSaving:E,syncing:N})})]}),H(`section`,{"aria-labelledby":`management-tab`,class:`min-w-0`,hidden:e!==`management`,id:`management-panel`,role:`tabpanel`,children:[H(q,{eyebrow:`System management`,title:`시스템 관리`,description:`관리자 비밀번호, 설정 백업·복원과 공유기 시스템 도구를 한곳에서 관리합니다.`}),H(`div`,{class:`grid min-w-0 grid-cols-1 gap-4 lg:grid-cols-2`,children:[H(jy,{onChanged:ge}),H(My,{action:n,error:r,message:i,onDiscard:P,onDismissFeedback:re,onDownload:ee,onRestore:te,onUpload:ne,restoreAccepted:a,uploadProgress:o,validated:s}),H(Ny,{action:t,className:`lg:col-span-2`,advancedSystemUrl:ye,confirmingReboot:_e,logsUrl:xe,onCancelReboot:()=>ve(!1),onConfirmReboot:()=>ve(!0),onReboot:ue,rebootAccepted:b})]})]})]})}var Ly=`https://www.smartsafehub.com/dashboard/devices/`;async function Ry(e){if(navigator.clipboard?.writeText)try{await navigator.clipboard.writeText(e);return}catch{}let t=document.createElement(`textarea`);t.value=e,t.setAttribute(`readonly`,``),t.style.position=`fixed`,t.style.inset=`0 auto auto -9999px`,t.style.opacity=`0`,document.body.appendChild(t),t.select(),t.setSelectionRange(0,e.length);let n=document.execCommand(`copy`);if(t.remove(),!n)throw Error(`clipboard_copy_failed`)}function zy(e){return(e||`free`).toUpperCase()}function By(e){return e===`FREE`?`free`:e===`PRO`?`pro`:e===`ULTIMATE`?`ultimate`:`paid`}function Vy({plan:e}){let t=By(e),n=t!==`free`;return H(`span`,{class:`ssh-safeshield-plan-badge`,"data-compact":`false`,"data-tier":t,title:n?`${e} 멤버십`:`무료 플랜`,children:[H(`span`,{"aria-hidden":`true`,class:`ssh-safeshield-plan-badge-mark`,children:n?`✦`:`•`}),H(`span`,{children:e})]})}function Hy(e){if(!e.pairingCode||!e.pairingExpiresAt)return!1;let t=Date.parse(e.pairingExpiresAt);return Number.isFinite(t)&&t<=Date.now()}function Uy({data:e,error:t,loading:n,pairingBusy:r,refreshing:i,syncError:a,onRequestPairing:o,onRetry:s,demo:c=!1,onDemoAction:l}){let[u,d]=F(null),[f,p]=F(!1);if(I(()=>{if(!f)return;let e=window.setTimeout(()=>p(!1),1800);return()=>window.clearTimeout(e)},[f]),n&&!e)return H(vs,{});if(t&&!e)return H(ys,{message:t,onRetry:s});let m=e,h=m?.accountRegistered===!0,g=zy(m?.plan??null),_=m?Hy(m):!1,v=!h&&!_?m?.pairingCode:null,y=r||m?.pairingOperation?.phase===`starting`||m?.pairingOperation?.phase===`running`;async function b(){d(null),p(!1);try{await o()}catch{d(`계정 연결 코드를 발급하지 못했습니다. 잠시 후 다시 시도해 주세요.`)}}async function x(){if(v)try{await Ry(v),d(null),p(!0)}catch{p(!1),d(`연결 코드를 복사하지 못했습니다. 코드를 직접 선택해 복사해 주세요.`)}}return H(`div`,{class:`min-w-0`,children:[H(q,{eyebrow:`Account connection`,title:`기기 계정 연결`,description:`계정에 연결하면 웹사이트에서 기기를 관리하고 Cloud 기능과 구독 권한을 사용할 수 있습니다.`}),H(`div`,{class:`grid items-start gap-5 xl:items-stretch xl:grid-cols-[minmax(0,1.2fr)_minmax(18rem,0.8fr)]`,children:[H(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6 xl:h-full`,children:h?H(`div`,{children:[H(`div`,{class:`flex items-center gap-3`,children:[H(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-full bg-emerald-100 text-emerald-700`,children:H(wn,{class:`size-5`})}),H(`div`,{children:[H(`p`,{class:`m-0 text-base font-black text-slate-950`,children:`SmartSafeHub 계정에 연결됨`}),H(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`이 기기가 계정과 정상적으로 연결되어 있습니다. 연결 코드는 더 이상 필요하지 않습니다.`})]})]}),c?H(`button`,{class:`mt-5 inline-flex min-h-11 items-center justify-center rounded-xl border border-emerald-700 bg-emerald-700 px-4 py-2 text-sm font-extrabold text-white transition hover:bg-emerald-800`,onClick:l,type:`button`,children:`웹사이트에서 기기 관리`}):H(`a`,{class:`mt-5 inline-flex min-h-11 items-center justify-center rounded-xl border border-emerald-700 bg-emerald-700 px-4 py-2 text-sm font-extrabold text-white no-underline transition hover:bg-emerald-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-emerald-100`,href:Ly,rel:`noopener noreferrer`,target:`_blank`,children:`웹사이트에서 기기 관리`}),H(`p`,{class:`mt-4 mb-0 border-t border-slate-100 pt-4 text-xs font-semibold leading-5 text-slate-500`,children:`웹사이트에서 기기 등록을 해제하면 공유기도 다음 계정 상태 동기화에서 자동으로 연결 해제를 반영합니다.`})]}):H(`div`,{children:[H(`div`,{children:[H(`div`,{class:`flex flex-wrap items-center justify-between gap-2`,children:[H(`p`,{class:`m-0 text-sm font-black text-slate-900`,children:`계정 연결 코드`}),H(`span`,{class:`inline-flex items-center gap-1.5 rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-[0.7rem] font-black text-amber-800`,children:[H(pn,{class:`size-3.5`}),`10분 동안 유효`]})]}),H(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-500`,children:`코드를 발급한 뒤 smartsafehub.com에 로그인하여 기기를 연결하세요.`})]}),v?H(`div`,{class:`mt-4 flex flex-col gap-3 rounded-2xl border border-teal-200 bg-teal-50 p-4 sm:flex-row sm:items-center sm:justify-between`,role:`group`,"aria-label":`계정 연결 코드`,children:[H(`div`,{class:`min-w-0`,children:[H(`span`,{class:`block text-[0.7rem] font-black text-teal-700`,children:`연결 코드`}),H(`code`,{class:`mt-1 block select-all break-all font-mono text-2xl font-black tracking-[0.14em] text-slate-950`,children:v})]}),H(`button`,{"aria-label":`계정 연결 코드 복사`,class:`inline-flex min-h-11 shrink-0 items-center justify-center gap-2 rounded-xl border border-teal-300 bg-white px-4 text-sm font-black text-teal-800 transition hover:bg-teal-100 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100`,onClick:()=>void x(),type:`button`,children:[H(f?wn:bn,{class:`size-4`}),H(`span`,{"aria-live":`polite`,children:f?`복사됨`:`복사`})]})]}):_?H(`p`,{class:`mt-4 mb-0 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm font-bold text-amber-800`,children:`이전 연결 코드가 만료되었습니다. 새 코드를 발급해 주세요.`}):null,H(`button`,{class:`mt-4 inline-flex min-h-11 w-full items-center justify-center gap-2 rounded-xl border border-teal-700 bg-teal-700 px-4 py-2 text-sm font-black text-white transition hover:bg-teal-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-not-allowed disabled:opacity-50`,disabled:y,onClick:()=>void b(),type:`button`,children:[y?H(hn,{class:`size-4 animate-spin`}):null,y?`발급 중…`:v?`새 코드 발급`:`연결 코드 발급`]}),m?.pairingOperation?.phase===`failed`?H(`p`,{class:`mt-3 mb-0 text-sm font-semibold text-red-700`,children:`연결 코드 발급에 실패했습니다. 다시 시도해 주세요.`}):null,u?H(`p`,{class:`mt-3 mb-0 text-sm font-semibold text-red-700`,children:u}):null]})}),H(`aside`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6 xl:h-full`,children:[H(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Device status`}),H(`h2`,{class:`mt-2 mb-0 text-lg font-black tracking-tight text-slate-950`,children:`연결 상태`}),H(`dl`,{class:`mt-5 mb-0 grid gap-4`,children:[H(`div`,{class:`flex items-center justify-between gap-4 border-b border-slate-100 pb-4`,children:[H(`dt`,{class:`text-sm font-bold text-slate-500`,children:`계정`}),H(`dd`,{class:`m-0 text-sm font-black ${h?`text-emerald-700`:`text-slate-700`}`,children:h?`연결됨`:`연결 필요`})]}),H(`div`,{class:`flex items-center justify-between gap-4 border-b border-slate-100 pb-4`,children:[H(`dt`,{class:`text-sm font-bold text-slate-500`,children:`요금제`}),H(`dd`,{class:`m-0 flex min-w-0 flex-wrap items-center justify-end gap-2`,children:[H(Vy,{plan:g}),g===`FREE`?H(`span`,{class:`ssh-safeshield-plan-caption`,children:`기본 플랜`}):null]})]}),H(`div`,{class:`flex items-center justify-between gap-4`,children:[H(`dt`,{class:`text-sm font-bold text-slate-500`,children:`마지막 확인`}),H(`dd`,{class:`m-0 text-sm font-black text-slate-700`,children:Mo(m?.lastSuccessAt??0)})]})]}),i?H(`p`,{class:`mt-5 mb-0 text-xs font-bold text-teal-700`,children:`최신 상태를 확인하고 있습니다…`}):null,a?h?H(`p`,{class:`mt-5 mb-0 text-xs font-semibold leading-5 text-slate-500`,children:`최신 상태 확인이 지연되고 있습니다. 마지막으로 확인된 연결 상태를 표시합니다.`}):H(`p`,{class:`mt-5 mb-0 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs font-bold leading-5 text-amber-800`,children:[a,` 로컬에 저장된 마지막 상태를 표시합니다.`]}):null]})]})]})}function Wy(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 Ky(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 qy(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 Jy(e){return e.phase===`checking`||e.phase===`downloading`||e.phase===`verifying`||e.phase===`flashing`?H(hn,{class:`size-3.5 animate-spin`}):e.phase===`error`?H(W,{class:`size-3.5`}):e.phase===`ready`||e.updateAvailable?H(_n,{class:`size-3.5`}):e.lastCheckAt?H(wn,{class:`size-3.5`}):H(pn,{class:`size-3.5`})}function Yy({checked:e,disabled:t,onChange:n}){return H(`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:H(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${e?`left-6`:`left-1`}`})})}function Xy({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=L(null),[_,v]=F(null),[y,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=Wy(k,A),M=async()=>{_&&await h(_)&&(v(null),g.current&&(g.current.value=``))};return H(`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:[H(`div`,{class:`p-5 sm:p-6`,children:[H(`div`,{class:`flex flex-col gap-5 lg:flex-row lg:items-start lg:justify-between`,children:[H(`div`,{class:`flex min-w-0 gap-4`,children:[H(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-sky-50 text-sky-700`,children:H(Xt,{class:`size-6`})}),H(`div`,{class:`min-w-0`,children:[H(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-sky-700`,children:`Firmware update`}),H(`h2`,{class:`mt-2 mb-0 text-xl font-black text-slate-950 sm:text-2xl`,children:`펌웨어 업데이트`}),H(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500 lg:whitespace-nowrap`,children:`시스템 펌웨어를 확인하고 설치합니다. 온라인·수동 설치는 안전성 검증 후 진행됩니다.`})]})]}),n?H(`div`,{class:`flex shrink-0 flex-col gap-2 sm:flex-row lg:justify-end`,children:[H(`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 ${qy(n)}`,children:[Jy(n),Ky(n)]}),H(`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:[H(hn,{class:`size-4 ${E?`animate-spin`:``}`}),E?`확인 중...`:`펌웨어 확인`]}),n.updateAvailable&&!T?H(`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?H(hn,{class:`size-4 animate-spin`}):H(_n,{class:`size-4`}),D?`준비 중...`:`다운로드 및 검증`]}):null]}):null]}),t||o?H(`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:[H(`span`,{children:t||o}),H(`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`?H(`div`,{class:`mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4 sm:p-5`,role:`status`,children:H(`div`,{class:`flex gap-3`,children:[H(`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:H(hn,{class:`size-5 animate-spin`})}),H(`div`,{children:[H(`strong`,{class:`block text-sm font-black text-amber-950`,children:`펌웨어를 설치하고 재부팅하고 있습니다.`}),H(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:`전원을 끄지 마세요. 현재 주소에서 공유기가 다시 응답하면 SmartSafeHub가 자동으로 새로고침됩니다.`})]})]})}):null,i&&!n?H(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-800`,children:[H(`p`,{class:`m-0 font-bold`,children:i}),H(`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?H(`div`,{class:`mt-5 flex items-center gap-3 rounded-xl bg-slate-50 p-4 text-sm font-bold text-slate-500`,children:[H(hn,{class:`size-4 animate-spin`}),`펌웨어 상태를 확인하고 있습니다.`]}):n?H(b,{children:[n.lastError?H(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 sm:p-5`,children:H(`div`,{class:`flex gap-3`,children:[H(`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:H(W,{class:`size-5`})}),H(`div`,{class:`min-w-0 flex-1`,children:[H(`strong`,{class:`block text-sm font-black text-rose-950`,children:`펌웨어 작업을 완료하지 못했습니다.`}),H(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-rose-800`,children:n.lastError.message}),H(`details`,{class:`mt-3 rounded-lg border border-rose-200 bg-white p-3`,children:[H(`summary`,{class:`cursor-pointer text-xs font-extrabold text-rose-800`,children:`상세 정보 보기`}),H(`p`,{class:`mt-2 mb-0 break-all font-mono text-[11px] text-slate-600`,children:n.lastError.code})]})]})]})}):null,n.current.metadataAvailable?null:H(`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:[H(`strong`,{class:`block font-extrabold`,children:`현재 펌웨어의 버전 정보를 완전히 확인할 수 없습니다.`}),H(`span`,{class:`mt-1 block text-amber-800`,children:`새 펌웨어를 설치하면 이후부터 정확한 설치 버전을 자동으로 확인할 수 있습니다.`}),H(`details`,{class:`mt-2`,children:[H(`summary`,{class:`cursor-pointer font-extrabold text-amber-800`,children:`기술 상세 보기`}),H(`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.`})]})]}),H(`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:[H(`div`,{class:`bg-white p-4`,children:[H(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Device`}),H(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black text-slate-950`,children:n.current.deviceCode||n.current.boardName||`미확인`})]}),H(`div`,{class:`bg-white p-4`,children:[H(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Current version`}),H(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black text-slate-950`,children:n.current.releaseVersion||`미확인`}),n.current.buildId?H(`span`,{class:`mt-1 block break-all text-[11px] leading-5 text-slate-400`,children:[`빌드 ID `,n.current.buildId]}):n.current.openwrtVersion?H(`span`,{class:`mt-1 block text-[11px] leading-5 text-slate-400`,children:[`OpenWrt `,n.current.openwrtVersion]}):null]}),H(`div`,{class:`bg-white p-4`,children:[H(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Available version`}),H(`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?H(`span`,{class:`mt-1 block break-all text-[11px] leading-5 text-slate-400`,children:[`빌드 ID `,n.release.buildId]}):null]}),H(`div`,{class:`bg-white p-4`,children:[H(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Last check`}),H(`dd`,{class:`mt-2 mb-0 ml-0 text-sm font-black text-slate-950`,children:H(Iv,{fallback:`아직 없음`,timestamp:n.lastCheckAt})})]})]})]}):null,n?.updateAvailable&&n.release?H(`section`,{class:`mt-5 rounded-xl border border-sky-200 bg-sky-50/60 p-4 sm:p-5`,"data-section":`available-firmware`,children:[H(`div`,{class:`flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4`,children:[H(`div`,{children:[H(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-sky-700`,children:`Available firmware`}),H(`h3`,{class:`mt-2 mb-0 text-lg font-black text-slate-950`,children:`새 펌웨어가 있습니다.`}),H(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:[`펌웨어 `,n.release.version||`미확인`,` · `,Gy(n.release.sysupgrade.sizeBytes),` · `,(n.release.channel||n.settings.channel)===`beta`?`Beta`:`Stable`]})]}),H(`div`,{class:`flex shrink-0 flex-wrap items-center justify-start gap-2 sm:justify-end`,children:[H(`span`,{class:`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?H(`span`,{class:`inline-flex items-center gap-1.5 rounded-full bg-white px-3 py-1.5 text-xs font-extrabold text-slate-700 ring-1 ring-inset ring-sky-200`,"data-section":`firmware-bundled-software-chip`,children:[H(`span`,{class:`text-[11px] uppercase tracking-[0.12em] text-slate-500`,children:`관리 SW`}),H(`span`,{class:`text-slate-950`,children:k})]}):null]})]}),k?H(`div`,{class:`mt-4 rounded-xl border border-sky-100 bg-white p-4`,"data-section":`firmware-bundled-software`,children:[H(`div`,{class:`flex flex-wrap items-center justify-between gap-2`,children:[H(`span`,{class:`text-xs font-extrabold text-slate-800`,children:`포함된 관리 소프트웨어`}),H(`div`,{class:`flex flex-wrap items-center gap-2`,children:[H(`span`,{class:`text-sm font-black text-slate-950`,children:k}),j===`same`?H(`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]})]}),H(`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?H(`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)=>H(`li`,{children:e},`${t}-${e}`))}):null]}):null,T?H(`section`,{class:`mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4 sm:p-5`,"data-section":`prepared-firmware`,children:H(`div`,{class:`flex gap-4`,children:[H(`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:H(wn,{class:`size-5`})}),H(`div`,{class:`min-w-0 flex-1`,children:[H(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-amber-700`,children:`Verified image`}),H(`h3`,{class:`mt-2 mb-0 text-lg font-black text-amber-950`,children:`설치할 펌웨어가 준비되었습니다.`}),H(`p`,{class:`mt-2 mb-0 break-all text-sm leading-6 text-amber-900`,children:[T.filename||`firmware.bin`,` · `,Gy(T.sizeBytes)]}),T.sha256?H(`p`,{class:`mt-2 mb-0 break-all font-mono text-[11px] leading-5 text-amber-800`,children:[`SHA-256 `,T.sha256]}):null,H(`div`,{class:`mt-5 flex items-start justify-between gap-4 rounded-xl border border-amber-200 bg-white p-4`,children:[H(`div`,{children:[H(`strong`,{class:`block text-sm font-black text-slate-950`,children:`현재 설정 유지`}),H(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`Wi-Fi, SafeShield, SmartSafeHub 설정을 유지합니다.`}),T.allowBackup?null:H(`span`,{class:`mt-2 block text-xs font-bold text-rose-700`,children:`이 이미지는 설정 유지 업그레이드를 지원하지 않습니다.`})]}),H(Yy,{checked:y,disabled:!T.allowBackup,onChange:x})]}),S?H(`div`,{class:`mt-4 rounded-xl border border-rose-200 bg-rose-50 p-4`,children:[H(`strong`,{class:`block text-sm font-black text-rose-950`,children:`펌웨어 설치를 시작할까요?`}),H(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-rose-800`,children:`설치 중 전원을 끄면 장치가 복구 불가능한 상태가 될 수 있습니다. 이미지 검증은 설치 직전에 다시 수행됩니다.`}),H(`div`,{class:`mt-4 flex flex-wrap justify-end gap-2`,children:[H(`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:`취소`}),H(`button`,{class:`min-h-10 rounded-lg bg-rose-700 px-3 py-2 text-xs font-extrabold text-white`,onClick:()=>{C(!1),f(y)},type:`button`,children:`설치 시작`})]})]}):H(`div`,{class:`mt-4 flex flex-wrap justify-end gap-2`,children:[H(`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:`파일 삭제`}),H(`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:[H(on,{class:`size-4`}),`펌웨어 설치`]})]})]})]})}):null]}),H(`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:[H(`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:[H(`div`,{class:`flex min-w-0 items-center gap-3`,children:[H(`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:H(_n,{class:`size-5 rotate-180`})}),H(`div`,{class:`min-w-0`,children:[H(`strong`,{class:`block text-sm font-black text-slate-950`,children:`수동 펌웨어 설치`}),H(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`펌웨어 파일(.bin)을 직접 업로드하여 검증한 뒤 설치합니다.`})]})]}),H(`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:`열기`}),H(`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:`닫기`})]}),H(`div`,{class:`border-t border-slate-200 px-5 pb-5 pt-3 sm:px-6`,children:[H(`p`,{class:`mt-0 mb-4 text-xs leading-5 text-slate-500 lg:whitespace-nowrap`,children:"이 장치에 맞는 Sysupgrade 이미지(.bin)만 사용하세요. 업로드한 파일은 이미지 검증과 `sysupgrade --test`를 모두 통과해야 설치할 수 있으며 강제 설치는 제공하지 않습니다."}),H(`div`,{class:`rounded-xl border border-slate-200 bg-white p-4 sm:p-5`,"data-layout":`firmware-file-picker`,children:[H(`input`,{accept:`.bin,application/octet-stream`,"aria-label":`펌웨어 파일 선택`,class:`sr-only`,disabled:w||T!==null,onChange:e=>v(e.currentTarget.files?.[0]??null),ref:g,type:`file`}),H(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between`,children:[H(`div`,{class:`flex min-w-0 items-center gap-3`,children:[H(`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:H(_n,{class:`size-5 rotate-180`})}),H(`div`,{class:`min-w-0`,children:[H(`strong`,{class:`block truncate text-sm font-black text-slate-900`,children:_?_.name:`펌웨어 파일을 선택하세요`}),H(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:_?`${Gy(_.size)} · Sysupgrade 이미지 (.bin)`:`이 장치에 맞는 Sysupgrade 이미지 (.bin)를 선택합니다.`})]})]}),H(`div`,{class:`flex shrink-0 flex-wrap gap-2 sm:justify-end`,children:[H(`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:_?`다른 파일 선택`:`파일 선택`}),H(`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?H(hn,{class:`size-4 animate-spin`}):H(_n,{class:`size-4 rotate-180`}),O?`처리 중...`:`업로드 및 검증`]})]})]}),c===null?null:H(`div`,{class:`mt-4 border-t border-slate-100 pt-4`,children:[H(`div`,{class:`mb-2 flex items-center justify-between gap-3 text-xs font-bold text-slate-600`,children:[H(`span`,{children:`업로드 중`}),H(`span`,{children:[c,`%`]})]}),H(`div`,{class:`h-2 overflow-hidden rounded-full bg-slate-200`,children:H(`div`,{class:`h-full rounded-full bg-sky-600 transition-[width]`,style:{width:`${Math.max(0,Math.min(100,c))}%`}})})]})]})]})]})]})}var Zy=[{value:3600,label:`1시간마다`},{value:21600,label:`6시간마다`},{value:43200,label:`12시간마다`},{value:86400,label:`24시간마다`}],Qy=`luci-app-smartsafehub`;function $y(e){return e?new Date(e*1e3).toLocaleString(`ko-KR`,{dateStyle:`medium`,timeStyle:`short`}):`아직 없음`}function eb(e){return e===`stable`?`Stable`:e===`beta`?`Beta`:`미확인`}function tb(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 nb(e){return e.code===`UPDATES_INDEX_REFRESH_FAILED`?{title:`패키지 저장소를 확인하지 못했습니다.`,description:`OpenWrt 패키지 저장소 동기화 또는 네트워크 상태가 일시적으로 불안정할 수 있습니다. 잠시 후 업데이트 확인을 다시 실행해 주세요.`}:e.code===`UPDATES_INSTALL_FAILED`?{title:`관리 소프트웨어 업데이트 설치에 실패했습니다.`,description:`패키지 설치가 완료되지 않았습니다. 네트워크 연결과 저장 공간을 확인한 뒤 다시 시도해 주세요.`}:{title:`최근 업데이트 작업을 완료하지 못했습니다.`,description:`잠시 후 다시 시도해 주세요. 문제가 반복되면 상세 정보를 확인해 주세요.`}}function rb(e,t){return e.phase===`checking`?`업데이트 확인 중`:e.phase===`installing`?`업데이트 설치 중`:e.phase===`error`?`확인 필요`:e.updateCount>0?`업데이트 가능`:t?`업데이트 확인 지연`:e.lastCheckAt?`최신 상태`:`확인 전`}function ib(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 ab(e,t){return e.phase===`checking`||e.phase===`installing`?H(hn,{class:`size-3.5 shrink-0 animate-spin`}):e.phase===`error`?H(W,{class:`size-3.5 shrink-0`}):e.updateCount>0?H(_n,{class:`size-3.5 shrink-0`}):t?H(W,{class:`size-3.5 shrink-0`}):e.lastCheckAt?H(wn,{class:`size-3.5 shrink-0`}):H(pn,{class:`size-3.5 shrink-0`})}function ob({checked:e,disabled:t=!1,label:n,onChange:r}){return H(`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:H(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${e?`left-6`:`left-1`}`})})}function sb({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),[v,y]=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),y(n.settings.autoInstallTime))},[n,x]);let T=e===`check`||n?.phase===`checking`,E=e===`install`||n?.phase===`installing`,D=T||E,O=l_(n),k=n?.phase===`error`&&n.lastError?.code===`UPDATES_INDEX_REFRESH_FAILED`,A=n?.lastError?nb(n.lastError):null,j=n?.packages.find(e=>e.name===Qy)??n?.packages[0]??null,M=r?.updateAvailable?r.release?.bundledSoftware?.smartsafehub:null,N=!(!n?.lastCheckAt||n.updateCount!==0||O||j?.name!==Qy)&&Wy(M,j?.installedVersion)===`same`,P=(e={})=>n?(e.checkEnabled??f)!==n.settings.checkEnabled||(e.checkIntervalSeconds??m)!==n.settings.checkIntervalSeconds||(e.autoInstall??g)!==n.settings.autoInstall||(e.autoInstallTime??v)!==n.settings.autoInstallTime:!1,ee=async()=>{await d({checkEnabled:f,checkIntervalSeconds:m,autoInstall:g,autoInstallTime:v})&&S(!1)};return H(`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:[H(`section`,{class:`min-w-0 xl:col-start-1 xl:row-start-1`,"data-section":`software-update-status`,children:[H(`div`,{class:`p-5 sm:p-6`,children:[H(`div`,{class:`flex flex-col gap-5 lg:flex-row lg:items-start lg:justify-between`,children:[H(`div`,{class:`flex min-w-0 gap-4`,children:[H(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-teal-50 text-teal-700`,children:H(on,{class:`size-6`})}),H(`div`,{class:`min-w-0`,children:[H(`div`,{class:`flex flex-wrap items-center gap-2`,children:[H(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700`,children:`Management software`}),n?H(`span`,{class:`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-extrabold ring-1 ring-inset ${ib(n,O)}`,children:[H(`span`,{"aria-hidden":`true`,class:`flex items-center justify-center`,children:ab(n,O)}),rb(n,O)]}):null]}),H(`h2`,{class:`mt-2 mb-0 text-xl font-black text-slate-950 sm:text-2xl`,children:`관리 소프트웨어 업데이트`}),H(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500 xl:whitespace-nowrap`,children:`새 버전을 확인하고 안전하게 설치합니다.`})]})]}),n?H(`div`,{class:`flex shrink-0 flex-col gap-2 sm:flex-row lg:justify-end`,children:[H(`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:[H(hn,{class:`size-4 ${T?`animate-spin`:``}`}),T?`확인 중...`:`업데이트 확인`]}),n.updateCount>0&&!C?H(`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?H(hn,{class:`size-4 animate-spin`}):H(_n,{class:`size-4`}),E?`설치 중...`:`업데이트 설치`]}):null]}):null]}),(t||o)&&H(`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:[H(`span`,{children:t||o}),H(`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?H(`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:[H(`div`,{class:`flex items-start gap-3`,children:[H(`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:H(hn,{"aria-hidden":`true`,class:`size-5 animate-spin`})}),H(`div`,{class:`min-w-0`,children:[H(`h3`,{class:`m-0 text-sm font-black text-sky-950`,children:`관리 소프트웨어를 업데이트하고 있습니다.`}),H(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-sky-800`,children:`설치가 완료되면 이 화면이 자동으로 갱신됩니다. 작업 중에는 웹 연결이 잠시 끊길 수 있습니다.`})]})]}),H(`div`,{"aria-hidden":`true`,class:`ssh-update-progress-track mt-4`,children:H(`span`,{class:`ssh-update-progress-bar`})})]}):null,i&&!n?H(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-800`,children:[H(`p`,{class:`m-0 font-bold`,children:i}),H(`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?H(`div`,{class:`mt-5 flex items-center gap-3 rounded-xl bg-slate-50 p-4 text-sm font-bold text-slate-500`,children:[H(`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:H(hn,{"aria-hidden":`true`,class:`size-4 animate-spin`})}),H(`span`,{children:`관리 소프트웨어 업데이트 상태를 확인하고 있습니다.`})]}):n?H(b,{children:[n.lastError&&A?H(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 sm:p-5`,children:H(`div`,{class:`flex items-start gap-3`,children:[H(`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:H(W,{class:`size-5`})}),H(`div`,{class:`min-w-0 flex-1`,children:[H(`p`,{class:`m-0 text-sm font-extrabold text-rose-950`,children:A.title}),H(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-rose-800`,children:A.description}),n.lastError.at?H(`p`,{class:`mt-2 mb-0 text-xs font-bold text-rose-700`,children:[`발생 시각 `,$y(n.lastError.at)]}):null,H(`details`,{class:`mt-3 rounded-lg border border-rose-200 bg-white p-3`,children:[H(`summary`,{class:`cursor-pointer text-xs font-extrabold text-rose-800`,children:`상세 정보 보기`}),H(`div`,{class:`mt-3 grid gap-2 text-xs leading-5 text-slate-600`,children:[H(`p`,{class:`m-0`,children:[`오류 코드 `,H(`strong`,{class:`font-black text-slate-900`,children:n.lastError.code})]}),H(`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?H(`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:[H(`div`,{class:`min-w-0`,children:[H(`strong`,{class:`block text-sm font-black text-amber-900`,children:`관리 소프트웨어를 지금 업데이트할까요?`}),H(`span`,{class:`mt-1 block text-xs leading-5 text-amber-800`,children:`설치 중 SmartSafeHub 웹 화면 연결이 잠시 끊길 수 있습니다.`})]}),H(`div`,{class:`flex shrink-0 gap-2`,children:[H(`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:`취소`}),H(`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?H(`div`,{class:`px-5 pb-5 sm:px-6 sm:pb-6`,children:[H(`h3`,{class:`mb-3 text-sm font-black text-slate-950`,children:`현재 상태`}),H(`dl`,{class:`grid overflow-hidden rounded-xl border border-slate-200 bg-slate-200 sm:grid-cols-2`,children:[H(`div`,{class:`bg-white p-4`,children:[H(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Installed`}),H(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black text-slate-950`,children:j?.installedVersion||`미확인`})]}),H(`div`,{class:`bg-white p-4`,children:[H(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Available`}),H(`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?`확인 필요`:`최신 버전`})]}),H(`div`,{class:`bg-white p-4`,children:[H(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Last check`}),H(`dd`,{class:`mt-2 mb-0 ml-0 text-sm font-black text-slate-950`,children:H(Iv,{fallback:`아직 없음`,timestamp:n.lastCheckAt})})]}),H(`div`,{class:`bg-white p-4`,children:[H(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Auto install`}),H(`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?H(`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:H(`div`,{class:`flex gap-3`,children:[H(`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:H(W,{class:`size-5`})}),H(`div`,{children:[H(`h3`,{class:`m-0 text-base font-black text-amber-950`,children:`업데이트 확인이 지연되고 있습니다.`}),H(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:[`마지막 업데이트 확인: `,H(Iv,{timestamp:n.lastCheckAt}),`. 자동 확인 상태를 점검하거나 업데이트 확인을 실행해 주세요.`]})]})]})}):H(`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:H(`div`,{class:`flex gap-3`,children:[H(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-emerald-700`,children:H(wn,{class:`size-5`})}),H(`div`,{children:[H(`h3`,{class:`m-0 text-base font-black text-emerald-950`,children:`최신 버전을 사용 중입니다.`}),H(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-emerald-800`,children:[`마지막 업데이트 확인: `,H(Iv,{timestamp:n.lastCheckAt}),`.`]}),N?H(`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?H(`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:H(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between`,children:[H(`div`,{class:`flex min-w-0 gap-3`,children:[H(`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:H(hn,{class:`size-5`})}),H(`div`,{class:`min-w-0`,children:[H(`h3`,{class:`m-0 text-base font-black text-slate-900`,children:`업데이트 상태를 아직 확인하지 않았습니다.`}),H(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`업데이트 확인을 실행하면 현재 설치 버전과 사용 가능한 최신 버전을 확인합니다.`})]})]}),H(`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:[H(hn,{class:`size-4`}),`지금 확인`]})]})}):null]}),n?H(b,{children:[H(`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:[H(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[H(`div`,{class:`flex min-w-0 gap-4`,children:[H(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-600`,children:H(sn,{class:`size-5`})}),H(`div`,{class:`min-w-0`,children:[H(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Settings`}),H(`h3`,{class:`mt-2 mb-0 text-lg font-black text-slate-950`,children:`자동 업데이트`}),H(`p`,{class:`mt-2 mb-0 max-w-3xl text-sm leading-6 text-slate-500`,children:`관리 소프트웨어의 자동 확인과 자동 설치를 설정합니다.`})]})]}),x?H(`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:[H(W,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}):null]}),H(`div`,{class:`ssh-software-settings-grid mt-4`,children:[H(`div`,{class:`ssh-software-settings-channel rounded-xl border border-slate-200 p-4 sm:p-5`,children:H(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[H(`div`,{class:`min-w-0`,children:[H(`strong`,{class:`block text-sm font-black text-slate-950`,children:`업데이트 채널`}),H(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`현재 관리 소프트웨어 업데이트를 확인하는 배포 채널입니다.`})]}),H(`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 ${tb(n.settings.channel)}`,title:`현재 업데이트 확인 채널`,children:eb(n.settings.channel)})]})}),H(`div`,{class:`min-w-0 overflow-hidden rounded-xl border border-slate-200 p-4 sm:p-5`,children:[H(`div`,{class:`flex min-w-0 items-start justify-between gap-4`,children:[H(`div`,{class:`min-w-0`,children:H(`strong`,{class:`block text-sm font-black text-slate-950`,children:`자동으로 업데이트 확인`})}),H(ob,{checked:f,label:`자동 업데이트 확인`,onChange:e=>{p(e),S(P({checkEnabled:e}))}})]}),H(`div`,{class:`mt-4 block`,children:[H(`span`,{class:`text-xs font-extrabold text-slate-700`,children:`확인 주기`}),H(U_,{ariaLabel:`업데이트 확인 주기`,className:`mt-2`,disabled:!f,id:`software-update-check-interval`,onChange:e=>{let t=Number(e);h(t),S(P({checkIntervalSeconds:t}))},options:Zy.map(e=>({value:String(e.value),label:e.label})),value:String(m),variant:`emphasized`})]})]}),H(`div`,{class:`min-w-0 overflow-hidden rounded-xl border border-slate-200 p-4 sm:p-5`,children:[H(`div`,{class:`flex min-w-0 items-start justify-between gap-4`,children:[H(`div`,{class:`min-w-0`,children:H(`strong`,{class:`block text-sm font-black text-slate-950`,children:`자동으로 업데이트 설치`})}),H(ob,{checked:g,label:`자동 업데이트 설치`,onChange:e=>{_(e),S(P({autoInstall:e}))}})]}),H(`div`,{class:`mt-4 min-w-0`,children:[H(`span`,{class:`text-xs font-extrabold text-slate-700`,children:`설치 시각`}),H(by,{ariaLabel:`자동 업데이트 설치 시각`,className:`mt-2`,disabled:!g,id:`software-update-auto-install-time`,onChange:e=>{y(e),S(P({autoInstallTime:e}))},value:v,variant:`emphasized`})]})]})]}),g?H(`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,H(`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:[H(`p`,{class:`m-0 text-xs leading-5 text-slate-500`,children:[`마지막 설치: `,$y(n.lastInstallAt)]}),H(`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 ee(),type:`button`,children:e===`settings`?`저장 중`:`자동 업데이트 설정 저장`})]})]}),n.updateCount>0?H(`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:[H(`div`,{class:`flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4`,children:[H(`div`,{children:[H(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700`,children:`Release notes`}),H(`h3`,{class:`mt-2 mb-0 text-lg font-black text-slate-950`,children:`업데이트 내용`}),H(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500`,children:`현재 설치된 버전 이후 포함되는 주요 변경 사항입니다.`})]}),n.releaseNotes.length?H(`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?H(`div`,{class:`mt-5 grid gap-3`,children:[n.releaseNotes.map(e=>H(`article`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4 sm:p-5`,children:[H(`div`,{class:`flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-4`,children:[H(`strong`,{class:`break-all text-sm font-black text-slate-950`,children:e.version}),e.date?H(`span`,{class:`text-xs font-bold text-slate-500`,children:e.date}):null]}),e.summary?H(`p`,{class:`mt-3 mb-0 text-sm leading-6 text-slate-700`,children:e.summary}):null,e.sections.length?H(`details`,{class:`mt-4 rounded-lg border border-slate-200 bg-white p-3`,open:n.releaseNotes.length===1,children:[H(`summary`,{class:`cursor-pointer text-xs font-extrabold text-slate-800`,children:`자세한 변경 사항`}),H(`div`,{class:`mt-3 grid gap-4`,children:e.sections.map(t=>H(`section`,{children:[H(`h4`,{class:`m-0 text-xs font-extrabold text-slate-700`,children:t.title}),H(`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)=>H(`li`,{children:n},`${e.version}-${t.title}-${r}`))})]},`${e.version}-${t.title}`))})]}):null]},e.version)),n.releaseNotesComplete?null:H(`p`,{class:`m-0 rounded-xl bg-amber-50 px-4 py-3 text-xs font-bold leading-5 text-amber-800`,children:`일부 릴리즈 노트를 불러오지 못했습니다. 업데이트 확인과 설치는 계속 사용할 수 있습니다.`})]}):H(`p`,{class:`mt-4 mb-0 rounded-xl bg-slate-50 px-4 py-3 text-sm text-slate-500`,children:`릴리즈 노트를 불러오지 못했습니다. 업데이트 확인과 설치는 계속 사용할 수 있습니다.`})]}):null]}):null]})}function cb({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 H(`section`,{class:`min-w-0 space-y-5`,children:[H(`div`,{children:H(q,{eyebrow:`Firmware`,title:`시스템 펌웨어`,description:`공유기 운영체제의 새 버전을 확인하고 설치합니다.`})}),H(Xy,{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}),H(`div`,{class:`pt-2`,children:H(q,{eyebrow:`Software`,title:`관리 소프트웨어`,description:`SmartSafeHub 관리 화면 및 서비스의 업데이트를 확인합니다.`})}),H(sb,{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 lb={};lb.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},lb.QRErrorCorrectLevel=(e,t)=>{e.exports={L:1,M:0,Q:3,H:2}},lb.QRMaskPattern=(e,t)=>{e.exports={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7}},lb.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}},lb.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(db(`index`))(0,db(`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 pb(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 mb(e){return`${(e.replace(/[\\/:*?"<>|\x00-\x1f\x7f]/g,`_`).replace(/[. ]+$/g,``).trim()||`Wi-Fi`).slice(0,64)}-QR.png`}function hb(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 gb({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:fb(pb(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 H(`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:H(`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:[H(`h2`,{class:`m-0 text-xl font-extrabold`,children:`Wi-Fi QR 코드`}),H(`p`,{class:`mt-2 break-all text-sm font-semibold text-slate-600`,children:n?.ssid??`Wi-Fi 정보를 확인하는 중`}),i?H(`p`,{role:`alert`,class:`my-6 text-sm font-bold text-red-700`,children:i}):null,!n&&!i?H(`p`,{class:`my-6 text-sm`,children:`QR 코드를 만드는 중...`}):null,n?H(`div`,{class:`mx-auto my-5 w-full max-w-[288px] rounded-xl border border-slate-200 bg-white p-3`,children:H(`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:[H(`rect`,{width:c+8,height:c+8,fill:`white`}),n.matrix.map((e,t)=>e.map((e,n)=>e?H(`rect`,{x:n+4,y:t+4,width:`1`,height:`1`,fill:`black`},`${t}-${n}`):null))]})}):null,H(`p`,{class:`text-xs leading-5 text-slate-500`,children:[H(`span`,{class:`block`,children:`카메라로 스캔해 Wi-Fi에 연결하세요.`}),H(`span`,{class:`block`,children:`QR 코드에는 비밀번호가 포함됩니다.`})]}),o?H(`p`,{role:`alert`,class:`mt-3 text-sm font-bold text-red-700`,children:o}):null,n?H(`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`);hb(n.matrix,r,8);let i=document.createElement(`a`);i.href=t.toDataURL(`image/png`),i.download=mb(n.ssid),i.click()}catch{s(`QR 코드 이미지를 저장하지 못했습니다. 다시 시도해 주세요.`)}}},type:`button`,children:[H(_n,{class:`size-5`}),`QR 코드 다운로드`]}):null,H(`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 _b({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 H(`section`,{class:`mt-5 rounded-2xl border border-slate-200 bg-white p-5 sm:p-6`,children:[H(`h2`,{class:`m-0 text-lg font-extrabold text-slate-900`,children:`게스트 Wi-Fi`}),H(`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,v=t||_||!e.enabled||!e.passwordConfigured,y=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 H(b,{children:[H(`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 y(e),children:[H(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[H(`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:[H(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:H(Xt,{class:`size-6`})}),H(`span`,{class:`min-w-0 flex-1`,children:[H(`span`,{class:`block text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`2.4 GHz · 분리 네트워크`}),H(`span`,{class:`mt-2 block text-xl font-extrabold tracking-tight text-slate-950`,children:`게스트 Wi-Fi`}),H(`span`,{class:`mt-2 block break-all text-sm text-slate-600`,children:e.ssid})]}),H(jn,{class:`mt-3 size-5 shrink-0 text-slate-500 transition-transform ${a?`rotate-180`:``}`})]}),H(`div`,{class:`flex flex-wrap items-center gap-2 sm:justify-end`,children:[_&&H(`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:[H(W,{class:`size-3.5`}),`저장되지 않음`]}),H(`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:v,onClick:()=>c(!0),title:e.enabled?void 0:`게스트 Wi-Fi를 켜고 저장하면 QR 코드를 사용할 수 있습니다.`,type:`button`,children:[H(ln,{class:`size-4 shrink-0`}),`QR 코드 보기`]}),H(`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?`사용 중`:`꺼짐`})]})]}),H(`div`,{class:a?``:`hidden`,id:`ssh-guest-settings`,children:[H(`p`,{class:`mt-5 mb-0 text-sm leading-6 text-slate-600`,children:`방문자는 인터넷만 이용하며 내부 네트워크와 공유기 관리 화면에는 접근할 수 없습니다.`}),H(`div`,{class:`mt-5 grid gap-5 lg:grid-cols-2`,children:[H(`label`,{class:`block`,children:[H(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`게스트 Wi-Fi 이름 (SSID)`}),H(`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})]}),H(`label`,{class:`block`,children:[H(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`게스트 Wi-Fi 비밀번호`}),H(`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}),H(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`WPA2/WPA3 혼합 · 기본 Wi-Fi와 다른 비밀번호를 권장합니다.`})]})]}),H(`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&&H(`span`,{class:`block text-xs text-slate-500`,children:[`게스트 게이트웨이: `,e.subnet]})]}),H(`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:[H(`label`,{class:`inline-flex min-h-11 items-center gap-3 text-sm font-extrabold text-slate-800`,children:[H(`input`,{checked:p,class:`size-5 accent-teal-700`,disabled:t,onChange:e=>m(e.currentTarget.checked),type:`checkbox`}),`게스트 Wi-Fi 사용`]}),H(`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:[H(wn,{class:`size-5 ${n?`animate-pulse`:``}`}),n?`적용 중`:`설정 저장`]})]}),h&&H(`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&&H(gb,{load:i,onClose:()=>c(!1)})]})}var vb=[{value:`sae-mixed`,label:`WPA2/WPA3 혼합 (권장)`},{value:`sae`,label:`WPA3-Personal`},{value:`psk2`,label:`WPA2-Personal`},{value:`none`,label:`암호화 없음`}];function yb(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 bb(e){return e.length>=8&&e.length<=63||/^[0-9a-fA-F]{64}$/.test(e)}function xb({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=R(()=>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})`}]:[],...vb],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&&!bb(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 H(`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:[H(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[H(`div`,{class:`flex min-w-0 items-start gap-3`,children:[H(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:H(Xt,{class:`size-6`})}),H(`div`,{class:`min-w-0`,children:[H(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:e.bandLabel}),H(`h2`,{class:`mt-2 mb-0 break-words text-xl font-extrabold tracking-tight text-slate-950`,children:e.ssid||`이름 없는 Wi-Fi`}),H(`p`,{class:`mt-2 mb-0 text-sm text-slate-600`,children:[`채널 `,e.channel??`자동`,` · 연결 기기 `,e.clientCount,`대`]})]})]}),H(`div`,{class:`flex flex-wrap items-center gap-2 sm:justify-end`,children:[x?H(`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:[H(W,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}):null,H(`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:[H(ln,{class:`size-4 shrink-0`}),`QR 코드 보기`]}),H(`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:[H(`span`,{class:`size-2 rounded-full ${e.runtimeUp?`bg-emerald-500`:`bg-slate-400`}`}),e.runtimeUp?`동작 중`:e.enabled?`시작 대기`:`꺼짐`]})]})]}),H(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[H(`label`,{class:`block`,children:[H(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`Wi-Fi 이름 (SSID)`}),H(`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})]}),H(`div`,{class:`block`,children:[H(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`보안 방식`}),H(U_,{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`}),H(`span`,{class:`mt-2 block text-xs text-slate-500`,children:[`현재: `,yb(e)]})]}),H(`label`,{class:`block lg:col-span-2`,children:[H(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`새 비밀번호`}),H(`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}),H(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`저장된 비밀번호는 일반 설정 조회에 노출되지 않습니다. QR 코드에는 비밀번호가 포함됩니다.`})]})]}),H(`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:[H(`label`,{class:`inline-flex min-h-11 items-center gap-3 text-sm font-extrabold text-slate-800`,children:[H(`input`,{checked:s,class:`size-5 accent-teal-700`,disabled:t,onChange:e=>c(e.currentTarget.checked),type:`checkbox`}),`이 Wi-Fi 사용`]}),H(`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:[H(wn,{class:`size-5 ${n?`animate-pulse`:``}`}),n?`적용 중`:`설정 저장`]})]}),p?H(gb,{load:h,onClose:()=>m(!1)}):null,g?H(`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 Sb({feedback:e,onDismiss:t}){return H(`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:[H(`p`,{class:`m-0 font-bold leading-6`,children:e.message}),H(`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 Cb({data:e,error:t,feedback:n,loading:r,updatingSection:i,onDismissFeedback:a,onRetry:o,onUpdate:s,onUpdateGuest:c,onLoadQr:l,onLoadGuestQr:u}){return r?H(vs,{}):t?H(ys,{message:t,onRetry:o}):e?H(b,{children:[n?H(Sb,{feedback:n,onDismiss:a}):null,H(`section`,{class:`mb-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-amber-950 sm:p-6`,children:[H(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-amber-700`,children:`연결 주의`}),H(`h2`,{class:`mt-2 mb-0 text-lg font-extrabold`,children:`Wi-Fi 이름이나 비밀번호를 바꾸면 현재 연결이 끊어집니다`}),H(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-amber-900/80`,children:`변경한 Wi-Fi 이름과 비밀번호로 다시 연결하면 SmartSafeHub를 계속 사용할 수 있습니다. VLAN 등 고급 무선 옵션은 기존 LuCI에서 관리합니다.`})]}),H(q,{eyebrow:`Wireless`,title:`기본 Wi-Fi`,description:`무선 네트워크의 이름, 비밀번호, 보안 방식과 QR 코드를 관리합니다.`}),e.networks.length?H(`section`,{"aria-label":`기본 Wi-Fi 설정`,class:`grid gap-5 xl:grid-cols-2`,children:e.networks.map(e=>H(xb,{busy:i!==null,network:e,onUpdate:s,onLoadQr:l,saving:i===e.section},e.section))}):H(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 text-center shadow-sm shadow-slate-900/5 sm:p-8`,children:[H(Xt,{class:`mx-auto size-10 text-slate-400`}),H(`h2`,{class:`mt-4 mb-0 text-lg font-extrabold text-slate-950`,children:`관리할 기본 Wi-Fi를 찾지 못했습니다`}),H(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:`기존 LuCI에서 AP 모드의 무선 네트워크를 먼저 구성해 주세요.`})]}),e.guest&&H(b,{children:[H(`div`,{role:`separator`,"aria-label":`기본 Wi-Fi와 게스트 Wi-Fi 구분`,class:`mt-5 w-full border-t border-slate-200`}),H(`div`,{class:`mt-5`,children:H(q,{eyebrow:`Guest network`,title:`방문자용 네트워크`,description:`기본 네트워크와 분리된 게스트 Wi-Fi를 관리합니다.`})}),H(_b,{guest:e.guest,busy:i!==null,saving:i===`ssh_guest`,onUpdate:c,onLoadQr:u})]}),e.guestError&&H(`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 정보를 불러오지 못했습니다. 패키지를 설치한 직후라면 로그아웃 후 다시 로그인해 주세요.`,H(`button`,{class:`ml-3 underline font-bold`,onClick:o,type:`button`,children:`다시 시도`})]})]}):null}function wb({onAdministratorPasswordChanged:e}){let t=Bi(),n=Wi(),r=Si(),i=fi(t===`activity`),a=ui({active:t===`apps`,fallbackError:`검증된 앱 목록을 불러오지 못했습니다.`,loader:sr,pollInterval:e=>e?.refreshing?2e3:e?.ready?6e4:e?.refreshFailed?15e3:2e3,staleTimeMs:2e3}),o=ui({active:t===`apps`,fallbackError:`앱 설치 상태를 불러오지 못했습니다.`,loader:lr,pollInterval:e=>!e?.ready||e?.refreshing?2e3:e?.job.phase===`running`||e?.job.phase===`starting`||e?.restore?.phase===`running`||e?.restore?.phase===`starting`?3e3:12e3,staleTimeMs:3e3}),s=ui({active:t===`apps`,fallbackError:`저장 공간 정보를 확인하지 못했습니다.`,loader:dr,pollInterval:e=>e?.refreshing||!e?.ready?3e3:6e4,staleTimeMs:5e3}),c=io(t===`home`||t===`settings`),l=no(!0),u=Li(!0),d=ta(t===`network`),f=$i(t===`home`||t===`network`),p=Zi(t===`iptv`),m=wo(t===`wifi`),h=wi(t===`home`,!1),g=wi(t===`devices`),_=Za(t===`home`||t===`safeshield`),v=Ha(t===`home`||t===`safeshield`,t===`safeshield`),y=_,b=v,x=Ba(t===`rules`),S=yo(t===`account`),C=xo(t===`account`,S.data?.accountRegistered??null),w=mo(c.data),T=Xi(t===`home`||t===`settings`),E=ao(t===`settings`),D=ho(t===`settings`);I(()=>{let e=xy();e&&Dr(e).then(e=>{e.applied&&t===`settings`&&D.refresh()}).catch(()=>{})},[]);let O=o.data?.job.operationId,k=o.data?.job.phase,A=o.data?.restore?.phase,j=L(``);I(()=>{if(t!==`apps`||k!==`success`&&A!==`success`)return;let e=`${O??``}:${k}:${A??``}`;e!==j.current&&(j.current=e,Promise.allSettled([ur(),fr()]).then(()=>{o.refresh(),s.refresh()}))},[t,O,k,A]);let M=ja(_.refresh,v.refresh),N=t===`activity`?i:t===`network`?d:t===`wifi`?m:t===`iptv`?p:t===`devices`?g:t===`safeshield`?_:t===`rules`?x:t===`account`?S:t===`apps`?a:t===`system`?l:c,P;switch(t){case`apps`:P=H(_s,{data:a.data?.ready===!1?null:a.data,error:a.error??(a.data?.refreshFailed&&!a.data?.ready?`앱 목록 갱신에 실패했습니다. 서버 연결을 확인해 주세요.`:null),loading:a.loading||!!a.data&&!a.data.ready&&!a.data.refreshFailed,refreshing:a.refreshing||!!a.data?.refreshing,onRetry:()=>void cr().then(()=>a.refresh()).then(()=>Promise.all([o.refresh(),s.refresh()])).catch(()=>a.refresh()),installStatus:o.data?.ready===!1?null:o.data,metrics:s.data,statusError:o.error??(o.data?.ready===!1?`설치 상태를 확인하고 있습니다.`:null),onRefreshStatus:()=>void o.refresh(),onInstall:async e=>{await pr(e),await o.refresh()},onUpdate:async e=>{await mr(e),await o.refresh()},onRemove:async e=>{await hr(e),await o.refresh()}});break;case`network`:P=H(`div`,{class:`min-w-0`,children:[H(li,{active:n.network===`internet`?`internet`:`lan`,label:`네트워크 설정 메뉴`,tabs:[{id:`internet`,label:`인터넷 연결`,href:`#network`},{id:`lan`,label:`LAN 및 DHCP`,href:`#network/lan`}]}),H(`div`,{"aria-label":`인터넷 연결 설정`,"aria-labelledby":`internet-tab`,class:`min-w-0`,hidden:n.network!==`internet`,id:`internet-panel`,role:`tabpanel`,children:H(Sv,{action:d.action,data:d.data,error:d.error,feedback:d.feedback,loading:d.loading,onDismissFeedback:d.dismissFeedback,onReconnect:()=>void d.reconnect(),onRetry:()=>void d.refresh(),onSave:d.save})}),H(`div`,{"aria-label":`내부 네트워크 설정`,"aria-labelledby":`lan-tab`,class:`min-w-0`,hidden:n.network!==`lan`,id:`lan-panel`,role:`tabpanel`,children:H(cv,{action:f.action,data:f.data,error:f.error,feedback:f.feedback,loading:f.loading,onApplyRecommendation:f.applyRecommendation,onDismissFeedback:f.dismissFeedback,onRetry:()=>void f.refresh(),onSave:f.save})})]});break;case`activity`:P=H(ns,{actionError:i.actionError,actionMessage:i.actionMessage,data:i.data,error:i.error,loading:i.loading,onDismissActionFeedback:i.dismissActionFeedback,onRetry:()=>void i.refresh(),onSetCloudSync:e=>void i.setCloudSyncEnabled(e),savingCloudSync:i.savingCloudSync});break;case`wifi`:P=H(Cb,{data:m.data,error:m.error,feedback:m.feedback,loading:m.loading,onDismissFeedback:m.dismissFeedback,onRetry:()=>void m.refresh(),onUpdate:m.update,onUpdateGuest:m.updateGuest,onLoadQr:Wr,onLoadGuestQr:Gr,updatingSection:m.updatingSection});break;case`iptv`:P=H(q_,{data:p.data,error:p.error,feedback:p.feedback,loading:p.loading,onDismissFeedback:p.dismissFeedback,onRetry:()=>void p.refresh(),onSave:p.save,saving:p.saving});break;case`devices`:P=H(js,{data:g.data,error:g.error,loading:g.loading,onRetry:()=>void g.refresh()});break;case`account`:P=H(Uy,{data:S.data,error:S.error,loading:S.loading,pairingBusy:S.pairingBusy,refreshing:S.refreshing,syncError:S.syncError,onRequestPairing:S.requestPairing,onRetry:()=>void S.refresh()});break;case`system`:P=H(cb,{action:l.action,actionError:l.actionError,data:l.data,error:l.error,firmware:u,loading:l.loading,message:l.message,onCheck:()=>void l.check(),onDismissFeedback:l.dismissFeedback,onInstall:()=>void l.install(),onRetry:()=>void l.refresh(),onSaveSettings:l.saveSettings});break;case`settings`:P=H(Iy,{activeTab:n.settings,action:w.action,backupAction:r.action,backupError:r.error,backupMessage:r.message,backupRestoreAccepted:r.restoreAccepted,backupUploadProgress:r.uploadProgress,backupValidated:r.validated,data:c.data,error:c.error,feedbackError:w.error,feedbackMessage:w.message,health:T.data,healthActionError:T.actionError,healthActionMessage:T.actionMessage,healthError:T.error,healthLoading:T.loading,healthRunning:T.running,healthSavingReporter:T.savingReporter,loading:c.loading,onBackupDiscard:r.discard,onBackupDownload:()=>void r.download(),onBackupRestore:r.restore,onBackupUpload:r.upload,onDismissBackupFeedback:r.dismissFeedback,onDismissFeedback:w.dismissFeedback,onDismissHealthFeedback:T.dismissActionFeedback,onDismissTimeFeedback:D.dismissSaveFeedback,onDownloadDiagnostics:()=>void w.downloadDiagnostics(),onRunHealth:()=>void T.runDiagnostic(),onSetHealthReporter:e=>void T.setReporterEnabled(e),onReboot:()=>void w.reboot(),onRetry:()=>void Promise.all([c.refresh(),u.refresh(),D.refresh(),E.refresh(),T.refresh()]),onDismissScheduledRebootFeedback:E.dismissSaveFeedback,onSaveScheduledReboot:E.saveSettings,onSaveTimezone:D.saveTimezone,onSyncTime:D.syncTime,onAdministratorPasswordChanged:e,rebootAccepted:w.rebootAccepted,scheduledRebootData:E.data,scheduledRebootError:E.error,scheduledRebootLoading:E.loading,scheduledRebootSaveError:E.saveError,scheduledRebootSaveMessage:E.saveMessage,scheduledRebootSaving:E.saving,timeData:D.data,timeError:D.error,timeLoading:D.loading,timeSaveError:D.saveError,timeSaveMessage:D.saveMessage,timeSaving:D.saving,timeSyncing:D.syncing});break;case`safeshield`:P=H(iy,{action:M.action,actionError:M.error,actionMessage:M.message,data:_.data,error:_.error,loading:_.loading,statistics:v.data,statisticsError:v.error,statisticsLoading:v.loading,statisticsRefreshing:v.refreshing,onDismissFeedback:M.dismissFeedback,onRefreshBlocklist:()=>void M.refreshBlocklist(),onRetry:()=>void _.refresh(),onRetryStatistics:()=>void v.refresh(),onSetEnabled:e=>void M.setEnabled(e),onSetStatisticsEnabled:e=>void M.setStatisticsEnabled(e)});break;case`rules`:P=H(hy,{action:x.action,data:x.data,error:x.error,feedback:x.feedback,loading:x.loading,onAddRule:x.addRule,onDeleteRule:x.deleteRule,onLoadRecommendedRulesStatus:Qn,onSyncRecommendedRules:$n,onRevokeRecommendedRule:er,onDismissFeedback:x.dismissFeedback,onRetry:()=>void x.refresh()});break;default:P=H(S_,{accountRegistered:C,activity:c.data?.activityHistory??null,activityError:c.error,activityLoading:c.loading,data:c.data,devices:h.data,devicesError:h.error,devicesLoading:h.loading,error:c.error,firmware:u.data,firmwareError:u.error,firmwareLoading:u.loading,health:T.data,healthError:T.error,healthLoading:T.loading,lan:f.data,lanError:f.error,lanLoading:f.loading,loading:c.loading,onRetry:()=>void Promise.all([c.refresh(),h.refresh(),_.refresh(),v.refresh(),l.refresh(),u.refresh(),T.refresh(),f.refresh()]),safeshield:y.data,safeshieldError:y.error,safeshieldLoading:y.loading,statistics:b.data,statisticsError:b.error,statisticsLoading:b.loading,statisticsRefreshing:b.refreshing,updates:l.data,updatesError:l.error,updatesLoading:l.loading})}let ee=()=>{t===`home`?Promise.all([c.refresh(),h.refresh(),y.refresh(),b.refresh(),l.refresh(),u.refresh(),T.refresh(),f.refresh()]):t===`network`?Promise.all([d.refresh(),f.refresh()]):t===`system`?Promise.all([l.refresh(),u.refresh()]):t===`settings`?Promise.all([c.refresh(),u.refresh(),D.refresh(),E.refresh(),T.refresh()]):t===`safeshield`?Promise.all([_.refresh(),v.refresh()]):N.refresh()},te=+((l.data?.updateCount??0)>0)+ +!!u.data?.updateAvailable;return H(ci,{accountRegistered:C,loading:t===`network`?d.loading||f.loading:N.loading,onRefresh:ee,refreshing:N.refreshing||t===`network`&&f.refreshing||t===`home`&&(i.refreshing||h.refreshing||y.refreshing||b.refreshing||l.refreshing||u.refreshing||T.refreshing||f.refreshing)||t===`system`&&u.refreshing||t===`settings`&&(u.refreshing||D.refreshing||E.refreshing||T.refreshing)||t===`safeshield`&&v.refreshing,route:t,updateCount:te,children:P})}function Tb({onAdministratorPasswordChanged:e,onPasswordConfigured:t}){let[n,r]=F(`checking`),[i,a]=F(!1),o=R(async()=>{r(`checking`);try{let e=await Gt();a(e.recovery),r(e.configured?`ready`:`required`)}catch{r(`error`)}},[]);return I(()=>{o()},[o]),n===`ready`?H(wb,{onAdministratorPasswordChanged:e}):n===`required`?H(Gn,{onCompleted:()=>t(i),recovery:i}):H(`main`,{class:`ssh-initial-setup-probe`,children:H(`div`,{class:`ssh-initial-setup-probe-card`,role:n===`error`?`alert`:`status`,children:[n===`checking`?H(hn,{class:`ssh-login-probe-spinner`,"aria-hidden":`true`}):H(Yt,{"aria-hidden":`true`}),H(`strong`,{children:n===`checking`?`관리자 보안 상태 확인 중`:`관리자 보안 상태를 확인할 수 없습니다`}),H(`span`,{children:n===`checking`?`root 비밀번호 설정 여부를 확인하고 있습니다.`:`공유기 연결을 확인한 뒤 다시 시도해 주세요.`}),n===`error`?H(`button`,{onClick:()=>void o(),type:`button`,children:`다시 시도`}):null]})})}function Eb({icon:e,title:t,description:n}){return H(`div`,{class:`ssh-login-feature`,children:[H(`span`,{class:`ssh-login-feature-icon`,"aria-hidden":`true`,children:e}),H(`span`,{class:`ssh-login-feature-copy`,children:[H(`strong`,{children:t}),H(`span`,{children:n})]})]})}function Db({theme:e}){return H(e===`dark`?In:Fn,{})}function Ob({notice:e,onAuthenticated:t,probing:n=!1}){let r=Qe(Ce,[]),i=L(null),a=L(null),[o,s]=F(`root`),[c,l]=F(``),[u,d]=F(`ready`),[f,p]=F(null),[m,h]=F(e??null),[g,_]=F(!1),[v,y]=F(!1),[b,x]=F(!1),[S,C]=F(Rn),w=u===`submitting`,T=S===`dark`?`라이트 모드로 전환`:`다크 모드로 전환`;I(()=>{zn(S),Bn(S)},[S]),I(()=>{if(n)return;let e=window.requestAnimationFrame(()=>a.current?.focus());return()=>window.cancelAnimationFrame(e)},[n]),I(()=>{if(h(e??null),!e)return;let t=window.setTimeout(()=>h(null),7e3);return()=>window.clearTimeout(t)},[e]);let E=e=>{x(e.getModifierState?.(`CapsLock`)??!1)};return H(`main`,{class:`ssh-login-page`,"data-theme":S,children:[m?H(`div`,{"aria-atomic":`true`,"aria-live":`assertive`,class:`ssh-login-toast`,role:`alert`,children:[H(Yt,{"aria-hidden":`true`}),H(`span`,{children:m})]}):null,H(`section`,{class:`ssh-login-brand`,"aria-labelledby":`ssh-login-brand-title`,children:[H(`div`,{class:`ssh-login-brand-orb ssh-login-brand-orb-one`,"aria-hidden":`true`}),H(`div`,{class:`ssh-login-brand-orb ssh-login-brand-orb-two`,"aria-hidden":`true`}),H(`div`,{class:`ssh-login-brand-content`,children:[H(`span`,{class:`ssh-login-brand-mark`,"aria-hidden":`true`,children:H(Yt,{})}),H(`p`,{class:`ssh-login-eyebrow`,children:`SMART NETWORK PROTECTION`}),H(`h1`,{id:`ssh-login-brand-title`,children:`SmartSafeHub`}),H(`p`,{class:`ssh-login-brand-description`,children:`네트워크와 보안을 하나의 허브에서 편리하게 관리하세요.`}),H(`div`,{class:`ssh-login-features`,"aria-label":`SmartSafeHub 주요 기능`,children:[H(Eb,{description:`DNS 기반 광고·피싱·추적 차단과 사용자 보호 정책을 관리합니다.`,icon:H(Yt,{}),title:`SafeShield 보호`}),H(Eb,{description:`Wi-Fi 설정과 현재 연결된 기기의 상태를 한눈에 확인합니다.`,icon:H(cn,{}),title:`네트워크 관리`}),H(Eb,{description:`관리 기능과 인증은 현재 SmartSafeHub 장치에서 직접 처리됩니다.`,icon:H(yn,{}),title:`로컬 우선 관리`})]})]})]}),H(`section`,{class:`ssh-login-panel`,"aria-labelledby":`ssh-login-title`,children:[H(`button`,{"aria-label":T,class:`ssh-login-theme-toggle`,onClick:()=>C(e=>e===`dark`?`light`:`dark`),title:T,type:`button`,children:H(Db,{theme:S})}),H(`div`,{class:`ssh-login-panel-inner`,children:[n?H(`div`,{class:`ssh-login-probe`,role:`status`,"aria-live":`polite`,children:[H(hn,{class:`ssh-login-probe-spinner`,"aria-hidden":`true`}),H(`strong`,{children:`SmartSafeHub 연결 확인 중`}),H(`span`,{children:`로그인 세션을 확인하고 있습니다.`})]}):H(`div`,{class:`ssh-login-card`,children:[H(`div`,{class:`ssh-login-card-heading`,children:[H(`span`,{class:`ssh-login-mobile-mark`,"aria-hidden":`true`,children:H(Yt,{})}),H(`div`,{children:[H(`p`,{class:`ssh-login-kicker`,children:`SMARTSAFEHUB ACCESS`}),H(`h2`,{id:`ssh-login-title`,children:`로그인`}),H(`p`,{class:`ssh-login-subtitle`,children:`공유기 관리자 계정의 사용자 이름과 비밀번호를 입력해 주세요.`})]})]}),f?H(`div`,{class:`ssh-login-alert`,id:`ssh-login-error`,role:`alert`,"aria-live":`polite`,children:f}):null,H(`form`,{class:`ssh-login-form`,onSubmit:async e=>{e.preventDefault();let n=o.trim(),r=c;if(p(null),_(!1),!n||!r&&n!==`root`)p(n?`비밀번호를 입력해 주세요.`:`사용자 이름을 입력해 주세요.`),window.requestAnimationFrame(()=>{n?a.current?.focus():i.current?.focus()});else{d(`submitting`);try{let e=await ke(n,r);if(e){l(``),t(e);return}l(``),_(!0),p(`로그인하지 못했습니다. 계정 정보를 확인해 주세요. 추가 인증이 설정된 장치는 기본 LuCI 로그인에서 계속할 수 있습니다.`),window.requestAnimationFrame(()=>a.current?.focus())}catch{p(`공유기와 통신할 수 없습니다. 네트워크 연결을 확인한 뒤 다시 시도해 주세요.`)}finally{d(`ready`)}}},noValidate:!0,children:[H(`label`,{class:`ssh-login-field`,for:`smartsafehub-username`,children:[H(`span`,{class:`ssh-login-label`,children:`사용자 이름`}),H(`span`,{class:`ssh-login-input-wrap`,children:[H(`span`,{class:`ssh-login-field-icon`,"aria-hidden":`true`,children:H($t,{})}),H(`input`,{autoCapitalize:`none`,autoComplete:`username`,disabled:w,id:`smartsafehub-username`,name:`username`,onInput:e=>s(e.currentTarget.value),placeholder:`관리자 계정`,ref:i,required:!0,spellcheck:!1,type:`text`,value:o})]})]}),H(`label`,{class:`ssh-login-field`,for:`smartsafehub-password`,children:[H(`span`,{class:`ssh-login-label`,children:`비밀번호`}),H(`span`,{class:`ssh-login-input-wrap`,children:[H(`span`,{class:`ssh-login-field-icon`,"aria-hidden":`true`,children:H(yn,{})}),H(`input`,{autoComplete:`current-password`,disabled:w,id:`smartsafehub-password`,name:`password`,onBlur:()=>x(!1),onInput:e=>l(e.currentTarget.value),onKeyDown:E,onKeyUp:E,placeholder:`관리자 비밀번호`,ref:a,...v?{type:`text`}:{type:`password`},value:c}),H(`button`,{"aria-label":v?`비밀번호 숨기기`:`비밀번호 표시`,"aria-pressed":v,class:`ssh-login-password-toggle`,disabled:w,onClick:()=>{y(e=>!e),window.requestAnimationFrame(()=>a.current?.focus())},title:v?`비밀번호 숨기기`:`비밀번호 표시`,type:`button`,children:H(v?Sn:xn,{})})]})]}),o.trim()===`root`&&c.length===0?H(`p`,{class:`ssh-login-password-hint`,children:`초기 설정 전 root 계정은 비밀번호가 비어 있을 수 있습니다.`}):null,b?H(`p`,{class:`ssh-login-caps`,role:`status`,children:`Caps Lock이 켜져 있습니다.`}):null,H(`button`,{class:`ssh-login-submit`,disabled:w,type:`submit`,children:[w?H(hn,{class:`ssh-login-submit-spinner`,"aria-hidden":`true`}):null,H(`span`,{children:w?`로그인 중…`:`로그인`})]})]}),H(`div`,{class:`ssh-login-security-note`,children:[H(yn,{"aria-hidden":`true`}),H(`span`,{children:`관리자 비밀번호를 잊었다면 전원이 켜진 상태에서 Reset 버튼을 5~9초 누른 뒤 놓아 비밀번호만 복구할 수 있습니다. 10초 이상 누르면 모든 사용자 설정을 초기화하는 기기 초기화가 실행됩니다.`})]}),g?H(`a`,{class:`ssh-login-fallback`,href:r,children:`기본 LuCI 로그인으로 계속`}):null]}),H(`footer`,{class:`ssh-login-footer`,children:[H(`span`,{children:`SmartSafeHub`}),H(`span`,{"aria-hidden":`true`,children:`·`}),H(`span`,{children:window.location.host})]})]})]})]})}var kb=`smartsafehub-entry-root`,Ab=`data-smartsafehub-mount`,jb=`data-smartsafehub-shadow-style`;function Mb(){let e=ye(`/`);new Set([e,e.replace(/\/$/,``),ye(`/smartsafehub`),ye(`/admin/smartsafehub`)]).has(window.location.pathname)&&window.history.replaceState(null,``,`${xe()}${window.location.search}${window.location.hash}`)}function Nb(e,t){Ne(),window.__SMARTHUB_BOOTSTRAP__=Object.freeze({sessionId:e,rpcUrl:ye(`/admin/ubus`),assetBase:t.dataset.assetBase??`/luci-static/smartsafehub/`,assetVersion:t.dataset.assetVersion??`0.2.31-r1`,locale:document.documentElement.lang||`ko`})}function Pb(e,t){let n=window.__SMARTHUB_BOOTSTRAP__,r=n?.assetBase??t.dataset.assetBase??`/luci-static/smartsafehub/`,i=n?.assetVersion??t.dataset.assetVersion,a=`${r}${r.endsWith(`/`)?``:`/`}${e}`;return i?`${a}?v=${encodeURIComponent(i)}`:a}function Fb(e,t){let n=e.shadowRoot??e.attachShadow({mode:`open`}),r=n.querySelector(`link[${jb}]`),i=Pb(`app.css`,e);r?r.getAttribute(`href`)!==i&&(r.href=i):(r=document.createElement(`link`),r.rel=`stylesheet`,r.href=i,r.setAttribute(jb,``),n.appendChild(r));let a=n.querySelector(`[${Ab}]`);return a?a.className!==t&&(a.className=t):(a=document.createElement(`div`),a.className=t,a.setAttribute(Ab,``),n.appendChild(a)),a}function Ib(e,t,n){Nb(n,e),t.className=`smartsafehub-shadow-root`,se(H(Tb,{onAdministratorPasswordChanged:()=>{Lb(e,t,`관리자 비밀번호가 변경되었습니다. 새 비밀번호로 다시 로그인해 주세요.`)},onPasswordConfigured:n=>{Lb(e,t,n?`관리자 비밀번호 복구가 완료되었습니다. 새 비밀번호로 다시 로그인해 주세요.`:`관리자 비밀번호가 설정되었습니다. 새 비밀번호로 다시 로그인해 주세요.`)}}),t)}function Lb(e,t,n){delete window.__SMARTHUB_BOOTSTRAP__,t.className=`smartsafehub-login-shadow-root`,se(H(Ob,{notice:n,onAuthenticated:n=>{Ib(e,t,n)}}),t)}async function Rb(){Bn(Rn());let e=document.getElementById(kb);if(!e)return;Mb(),e.style.display=`block`,e.style.width=`100%`,e.style.minWidth=`0`,e.style.minHeight=`100dvh`,e.style.maxWidth=`100%`,e.style.isolation=`isolate`;let t=Fb(e,`smartsafehub-login-shadow-root`);window.addEventListener(Ae,()=>{Lb(e,t,je)}),se(H(Ob,{onAuthenticated:n=>{Ib(e,t,n)},probing:!0}),t);try{let n=await pe();if(n){Ib(e,t,n);return}}catch{}try{let n=await De();if(n){Ib(e,t,n);return}}catch{}Lb(e,t)}Rb(); \ No newline at end of file +`):e}function zh(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 Bh(e,t){let n=e.chart.ctx,{body:r,footer:i,title:a}=e,{boxWidth:o,boxHeight:s}=t,c=ju(t.bodyFont),l=ju(t.titleFont),u=ju(t.footerFont),d=a.length,f=i.length,p=r.length,m=Au(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,Mc(e.title,y),n.font=c.string,Mc(e.beforeBody.concat(e.afterBody),y),v=t.displayColors?o+2+t.boxPadding:0,Mc(r,e=>{Mc(e.before,y),Mc(e.lines,y),Mc(e.after,y)}),v=0,n.font=u.string,Mc(e.footer,y),n.restore(),g+=m.width,{width:g,height:h}}function Vh(e,t){let{y:n,height:r}=t;return ne.height-r/2?`bottom`:`center`}function Hh(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 Uh(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`),Hh(l,e,t,n)&&(l=`center`),l}function Wh(e,t,n){let r=n.yAlign||t.yAlign||Vh(e,n);return{xAlign:n.xAlign||t.xAlign||Uh(e,t,n,r),yAlign:r}}function Gh(e,t){let{x:n,width:r}=e;return t===`right`?n-=r:t===`center`&&(n-=r/2),n}function Kh(e,t,n){let{y:r,height:i}=e;return t===`top`?r+=n:r-=t===`bottom`?i+n:i/2,r}function qh(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}=ku(o),m=Gh(t,s),h=Kh(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:yl(m,0,r.width-t.width),y:yl(h,0,r.height-t.height)}}function Jh(e,t,n){let r=Au(n.padding);return t===`center`?e.x+e.width/2:t===`right`?e.x+e.width-r.right:e.x+r.left}function Yh(e){return Lh([],Rh(e))}function Xh(e,t,n){return Pu(e,{tooltip:t,tooltipItems:n,type:`tooltip`})}function Zh(e,t){let n=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return n?e.override(n):e}var Qh={beforeTitle:wc,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=Zh(n,e);Lh(t.before,Rh($h(i,`beforeLabel`,this,e))),Lh(t.lines,$h(i,`label`,this,e)),Lh(t.after,Rh($h(i,`afterLabel`,this,e))),r.push(t)}),r}getAfterBody(e,t){return Yh($h(t.callbacks,`afterBody`,this,e))}getFooter(e,t){let{callbacks:n}=t,r=$h(n,`beforeFooter`,this,e),i=$h(n,`footer`,this,e),a=$h(n,`afterFooter`,this,e),o=[];return o=Lh(o,Rh(r)),o=Lh(o,Rh(i)),o=Lh(o,Rh(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))),Mc(o,t=>{let n=Zh(e.callbacks,t);r.push($h(n,`labelColor`,this,t)),i.push($h(n,`labelPointStyle`,this,t)),a.push($h(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=Ih[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=Bh(this,n),o=Object.assign({},e,t),s=Wh(this.chart,n,o),c=qh(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}=ku(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=Md(n.rtl,this.x,this.width);for(e.x=Jh(this,n.titleAlign,n),t.textAlign=c.textAlign(n.titleAlign),t.textBaseline=`middle`,a=ju(n.titleFont),o=n.titleSpacing,t.fillStyle=n.titleColor,t.font=a.string,s=0;se!==0)?(e.beginPath(),e.fillStyle=i.multiKeyBackground,Su(e,{x:t,y:p,w:c,h:s,radius:o}),e.fill(),e.stroke(),e.fillStyle=a.backgroundColor,e.beginPath(),Su(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=ju(n.bodyFont),d=u.lineHeight,f=0,p=Md(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=Jh(this,h,n),t.fillStyle=n.bodyColor,Mc(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=Ih[e.position].call(this,this._active,this._eventPosition);if(!n)return;let a=this._size=Bh(this,e),o=Object.assign({},n,this._size),s=Wh(t,e,o),c=qh(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=Au(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),Nd(e,t.textDirection),i.y+=a.top,this.drawTitle(i,e,t),this.drawBody(i,e,t),this.drawFooter(i,e,t),Pd(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=!Nc(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||!Nc(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=Ih[i.position].call(this,e,t);return a!==!1&&(n!==a.x||r!==a.y)}};V(eg,`positioners`,Ih);var tg={id:`tooltip`,_element:eg,positioners:Ih,afterInit(e,t,n){n&&(e.tooltip=new eg({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:Qh},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`]},ng=(e,t,n,r)=>(typeof t==`string`?(n=e.push(t)-1,r.unshift({index:n,label:t})):isNaN(t)&&(n=null),n);function rg(e,t,n,r){let i=e.indexOf(t);return i===-1?ng(e,t,n,r):i===e.lastIndexOf(t)?i:n}var ig=(e,t)=>e===null?null:yl(Math.round(e),0,t);function ag(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}};V(og,`id`,`category`),V(og,`defaults`,{ticks:{callback:ag}});function sg(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=!X(a),_=!X(o),v=!X(c),y=(h-m)/(u+1),b=al((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=al(w*b/p/f)*f),X(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&&ll((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=il(w,Math.round(w),b/1e3)?Math.round(w):Math.ceil(w));let T=Math.max(pl(b),pl(S));x=10**(X(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&&il(n[n.length-1].value,o,cg(o,y,e))?n[n.length-1].value=o:n.push({value:o}):_&&C!==o||n.push({value:C}),n}function cg(e,t,{horizontal:n,minRotation:r}){let i=dl(r),a=(n?Math.sin(i):Math.cos(i))||.001,o=.75*t*(``+e).length;return Math.min(t/a,o)}var lg=class extends dm{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 X(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=rl(r),t=rl(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=sg({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`&&ul(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 Xl(e,this.chart.options.locale,this.options.ticks.format)}},ug=class extends lg{determineDataLimits(){let{min:e,max:t}=this.getMinMax(!0);this.min=Dc(e)?e:0,this.max=Dc(t)?t:1,this.handleTickRangeOptions()}computeTickLimit(){let e=this.isHorizontal(),t=e?this.width:this.height,n=dl(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}};V(ug,`id`,`linear`),V(ug,`defaults`,{ticks:{callback:$l.formatters.numeric}});var dg=e=>Math.floor(nl(e)),fg=(e,t)=>10**(dg(e)+t);function pg(e){return e/10**dg(e)==1}function mg(e,t,n){let r=10**n,i=Math.floor(e/r);return Math.ceil(t/r)-i}function hg(e,t){let n=dg(t-e);for(;mg(e,t,n)>10;)n++;for(;mg(e,t,n)<10;)n--;return Math.min(n,dg(e))}function gg(e,{min:t,max:n}){t=Oc(e.min,t);let r=[],i=dg(t),a=hg(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=Oc(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=Oc(e.max,f);return r.push({value:p,major:pg(p),significand:d}),r}var _g=class extends dm{constructor(e){super(e),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(e,t){let n=lg.prototype.parse.apply(this,[e,t]);if(n===0)this._zero=!0;else return Dc(n)&&n>0?n:null}determineDataLimits(){let{min:e,max:t}=this.getMinMax(!0);this.min=Dc(e)?Math.max(0,e):null,this.max=Dc(t)?Math.max(0,t):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!Dc(this._userMin)&&(this.min=e===fg(this.min,0)?fg(this.min,-1):fg(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(fg(n,-1)),a(fg(r,1)))),n<=0&&i(fg(r,-1)),r<=0&&a(fg(n,1)),this.min=n,this.max=r}buildTicks(){let e=this.options,t=gg({min:this._userMin,max:this._userMax},this);return e.bounds===`ticks`&&ul(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`:Xl(e,this.chart.options.locale,this.options.ticks.format)}configure(){let e=this.min;super.configure(),this._startValue=nl(e),this._valueRange=nl(this.max)-nl(e)}getPixelForValue(e){return(e===void 0||e===0)&&(e=this.min),e===null||isNaN(e)?NaN:this.getPixelForDecimal(e===this.min?0:(nl(e)-this._startValue)/this._valueRange)}getValueForPixel(e){let t=this.getDecimalForPixel(e);return 10**(this._startValue+t*this._valueRange)}};V(_g,`id`,`logarithmic`),V(_g,`defaults`,{ticks:{callback:$l.formatters.logarithmic,major:{enabled:!0}}});function vg(e){let t=e.ticks;if(t.display&&e.display){let e=Au(t.backdropPadding);return Q(t.font&&t.font.size,au.font.size)+e.height}return 0}function yg(e,t,n){return n=Ec(n)?n:[n],{w:cu(e,t.string,n),h:n.length*t.lineHeight}}function bg(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 xg(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?$/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 Cg(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(fl(_l(c.angle+$c))),u=Og(c.y,s.h,l),d=Eg(l),f=Dg(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 wg(e,t){if(!t)return!0;let{left:n,top:r,right:i,bottom:a}=e;return!(pu({x:n,y:r},t)||pu({x:n,y:a},t)||pu({x:i,y:r},t)||pu({x:i,y:a},t))}function Tg(e,t,n){let r=[],i=e._pointLabels.length,a=e.options,{centerPointLabels:o,display:s}=a.pointLabels,c={extra:vg(a)/2,additionalAngle:o?$/i:0},l;for(let a=0;a270||n<90)&&(e-=t),e}function kg(e,t,n){let{left:r,top:i,right:a,bottom:o}=n,{backdropColor:s}=t;if(!X(s)){let n=ku(t.borderRadius),c=Au(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(),Su(e,{x:l,y:u,w:d,h:f,radius:n}),e.fill()):e.fillRect(l,u,d,f)}}function Ag(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));kg(n,a,t);let o=ju(a.font),{x:s,y:c,textAlign:l}=t;xu(n,e._pointLabels[i],s,c+o.lineHeight/2,o,{color:a.color,textAlign:l,textBaseline:`middle`})}}function jg(e,t,n,r){let{ctx:i}=e;if(n)i.arc(e.xCenter,e.yCenter,t,0,Yc);else{let n=e.getPointPosition(0,t);i.moveTo(n.x,n.y);for(let a=1;a{let n=jc(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?xg(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=Yc/(this._pointLabels.length||1),n=this.options.startAngle||0;return _l(e*t+dl(n))}getDistanceFromCenterForValue(e){if(X(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(X(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);Mg(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=ju(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=Au(s.backdropPadding);e.fillRect(-a/2-t.left,-i-c.size/2-t.top,a+t.width,c.size+t.height)}xu(e,r.label,0,-i,c,{color:s.color,strokeColor:s.textStrokeColor,strokeWidth:s.textStrokeWidth})}),e.restore()}drawTitle(){}};V(Pg,`id`,`radialLinear`),V(Pg,`defaults`,{display:!0,animate:!0,position:`chartArea`,angleLines:{display:!0,lineWidth:1,borderDash:[],borderDashOffset:0},grid:{circular:!1},startAngle:0,ticks:{showLabelBackdrop:!0,callback:$l.formatters.numeric},pointLabels:{backdropColor:void 0,backdropPadding:2,display:!0,font:{size:10},callback(e){return e},padding:5,centerPointLabels:!1}}),V(Pg,`defaultRoutes`,{"angleLines.color":`borderColor`,"pointLabels.color":`color`,"ticks.color":`color`}),V(Pg,`descriptors`,{angleLines:{_fallback:`grid`}});var Fg={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}},Ig=Object.keys(Fg);function Lg(e,t){return e-t}function Rg(e,t){if(X(t))return null;let n=e._adapter,{parser:r,round:i,isoWeekday:a}=e._parseOpts,o=t;return typeof r==`function`&&(o=r(o)),Dc(o)||(o=typeof r==`string`?n.parse(o,r):n.parse(o)),o===null?null:(i&&(o=i===`week`&&(cl(a)||a===!0)?n.startOf(o,`isoWeek`,a):n.startOf(o,i)),+o)}function zg(e,t,n,r){let i=Ig.length;for(let a=Ig.indexOf(e);a=Ig.indexOf(n);a--){let n=Ig[a];if(Fg[n].common&&e._adapter.diff(i,r,n)>=t-1)return n}return Ig[n?Ig.indexOf(n):0]}function Vg(e){for(let t=Ig.indexOf(e)+1,n=Ig.length;t=t?n[r]:n[i];e[a]=!0}}function Ug(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 Wg(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=yl(t,0,a),n=yl(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||zg(i.minUnit,t,n,this._getLabelCapacity(t)),o=Q(r.ticks.stepSize,1),s=a===`week`&&i.isoWeekday,c=cl(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 jc(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}=Cl(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}=Cl(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 qg=class extends Gg{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=Kg(t,this.min),this._tableRange=Kg(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(Kg(this._table,e)-this._minPos)/this._tableRange}getValueForPixel(e){let t=this._offsets,n=this.getDecimalForPixel(e)/t.factor-t.end;return Kg(this._table,n*this._tableRange+this._minPos,!0)}};V(qg,`id`,`timeseries`),V(qg,`defaults`,Gg.defaults),$m.register(zf,Fh,og,ug,tg);var Jg=new Intl.DateTimeFormat(`ko-KR`,{hour:`2-digit`,hour12:!1}),Yg=`#94a3b8`,Xg=`rgba(148, 163, 184, 0.18)`,Zg=`#0d9488`,Qg=`#0f766e`;function $g(e){return Jg.format(new Date(e*1e3))}function e_(e){return e.map((t,n)=>n%3==0||n===e.length-1?$g(t.bucketStart):``)}function t_({buckets:e}){let t=z(null),n=z(null),r=z(e);return r.current=e,R(()=>{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?$g(n.bucketStart):``},label:e=>`차단 ${Y(Number(e.raw??0))}`,afterLabel:e=>{let t=r.current[e.dataIndex];return t?`DNS 요청 ${Y(t.queries)}`:``}}}},scales:{x:{border:{display:!1},grid:{display:!1},ticks:{autoSkip:!1,color:Yg,font:{family:a,size:11,weight:700},maxRotation:0,minRotation:0}},y:{beginAtZero:!0,border:{display:!1},grid:{color:Xg},ticks:{color:Yg,font:{family:a,size:11,weight:700},maxTicksLimit:4,precision:0}}}};return n.current=new $m(e,{type:`bar`,data:{labels:e_(r.current),datasets:[{data:r.current.map(e=>e.blocked),backgroundColor:Zg,hoverBackgroundColor:Qg,borderRadius:4,borderSkipped:!1,barPercentage:.82,categoryPercentage:.9,maxBarThickness:24}]},options:o}),()=>{n.current?.destroy(),n.current=null}},[]),R(()=>{let t=n.current;t&&(t.data.labels=e_(e),t.data.datasets[0].data=e.map(e=>e.blocked),t.update())},[e]),W(`div`,{class:`mt-5 h-44 sm:h-52`,children:W(`canvas`,{"aria-label":`최근 24시간 시간대별 SafeShield 차단 요청`,ref:t,role:`img`,children:`최근 24시간 시간대별 SafeShield 차단 요청 차트`})})}var n_=3600,r_=24;function i_(e,t){return e<=0?`0.0%`:`${(t/e*100).toFixed(1)}%`}function a_(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/n_)*n_}function o_(e){let t=new Map(e.hourly.map(e=>[e.bucketStart,e])),n=a_(e)-23*n_;return Array.from({length:r_},(e,r)=>{let i=n+r*n_;return t.get(i)??{bucketStart:i,queries:0,blocked:0}})}function s_({children:e}){return W(`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:W(`p`,{class:`m-0 max-w-lg text-sm font-semibold leading-6 text-slate-500`,children:e})})}function c_({data:e,error:t,loading:n,nowTimestamp:r,refreshing:i}){let a;if(n&&!e)a=W(s_,{children:`SafeShield 보호 활동을 불러오는 중입니다.`});else if(t&&!e)a=W(s_,{children:`SafeShield 보호 활동을 불러오지 못했습니다. SafeShield 페이지에서 상태를 확인해 주세요.`});else if(!e||!e.available)a=W(s_,{children:`현재 SafeShield에서 차단 통계를 제공하지 않습니다. 통계 기능을 확인해 주세요.`});else if(!e.enabled)a=W(s_,{children:`통계 수집이 꺼져 있습니다. SafeShield 페이지에서 통계를 켜면 최근 24시간 활동이 표시됩니다.`});else{let t=o_(e),n=t.reduce((e,t)=>({queries:e.queries+t.queries,blocked:e.blocked+t.blocked}),{queries:0,blocked:0});a=W(b,{children:[W(`dl`,{class:`mt-5 grid gap-3 sm:grid-cols-3`,children:[W(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3`,children:[W(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:`DNS 요청`}),W(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-slate-950`,children:Y(n.queries)})]}),W(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3`,children:[W(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:`차단`}),W(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-teal-700`,children:Y(n.blocked)})]}),W(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3`,children:[W(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:`차단율`}),W(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-slate-950`,children:i_(n.queries,n.blocked)})]})]}),W(t_,{buckets:t})]})}return W(`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:[W(`div`,{class:`min-w-0`,children:[W(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`SafeShield activity`}),W(`div`,{class:`mt-2 flex min-w-0 items-center justify-between gap-3`,children:[W(`h3`,{class:`m-0 min-w-0 text-xl font-black text-slate-950`,children:`최근 24시간 보호 활동`}),W(`a`,{class:`shrink-0 whitespace-nowrap text-xs font-extrabold text-teal-700 no-underline hover:text-teal-800`,href:`#safeshield`,children:`상세 통계 →`})]}),e?.updatedAt?W(`p`,{class:`mt-2 mb-0 text-[0.68rem] font-semibold text-slate-400`,title:Ao(e.updatedAt),children:i?`갱신 중…`:`집계: ${jo(e.updatedAt,r)}`}):null]}),a,t&&e?W(`p`,{class:`mt-3 mb-0 text-xs font-bold text-amber-700`,children:`최근 통계 갱신에 실패해 마지막 정상 데이터를 표시하고 있습니다.`}):null]})}var l_=7200;function u_(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,l_)}var d_=2*Math.PI*39;function f_(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 p_({detail:e,eyebrow:t,href:n,icon:r,linkLabel:i=`자세히 보기 →`,meta:a,metaState:o,metaWarning:s=!1,state:c=`neutral`,value:l}){let u=W(b,{children:[W(`div`,{class:`flex items-start justify-between gap-4`,children:[W(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.16em] text-slate-500`,children:t}),W(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl ring-1 ring-inset ${f_(c)}`,children:r})]}),W(`p`,{class:`mt-4 mb-0 break-words text-2xl font-black tracking-tight text-slate-950`,children:l}),W(`p`,{class:`mt-2 mb-0 min-h-10 text-sm font-medium leading-5 text-slate-500`,children:e}),a?W(`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?W(`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?W(`a`,{class:`${d} no-underline hover:border-slate-300 hover:shadow-md`,href:n,children:u}):W(`article`,{class:d,children:u})}var m_=6e4;function h_(e,t){return!e||!Number.isFinite(e)||e<=0?null:Math.max(0,t-e)}function g_(e,t,n,r){return!t||t<=0?`${e}: ${r}`:W(`span`,{title:Ao(t),children:[e,`: `,jo(t,n)]})}function __(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 v_(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 y_({label:e,value:t}){return W(`div`,{class:`flex min-w-0 items-start justify-between gap-4 border-b border-slate-100 py-3 last:border-b-0`,children:[W(`dt`,{class:`text-sm font-semibold text-slate-500`,children:e}),W(`dd`,{class:`m-0 min-w-0 max-w-[68%] break-words text-right text-sm font-extrabold text-slate-950`,children:t})]})}function b_(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 x_({data:e,error:t,loading:n,nowTimestamp:r}){let i=!!(e&&e.generatedAt>0),a=b_(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 W(`div`,{class:`mt-5 border-t border-slate-200 pt-5`,children:[W(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[W(`div`,{class:`flex items-center gap-2`,children:[W(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:`장치 진단`}),i?W(`span`,{class:`rounded-full px-2.5 py-1 text-[11px] font-extrabold ${a.badge}`,children:a.label}):null]}),W(`a`,{class:`text-xs font-extrabold text-teal-700 no-underline hover:text-teal-900`,href:`#settings`,children:`상세 보기 →`})]}),t&&!i?W(`div`,{class:`mt-3 rounded-xl border border-amber-200 bg-amber-50/70 px-4 py-3`,children:W(`div`,{class:`flex items-start gap-2`,children:[W(Cn,{class:`mt-0.5 size-4 shrink-0 text-amber-700`}),W(`div`,{class:`min-w-0`,children:[W(`p`,{class:`m-0 text-sm font-extrabold text-slate-900`,children:`진단 상태를 확인하지 못했습니다.`}),W(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`설정에서 장치 진단 상태를 다시 확인해 주세요.`})]})]})}):i&&e?W(`div`,{class:`mt-3 rounded-xl border px-4 py-3 ${a.panel}`,children:[W(`div`,{class:`flex min-w-0 items-start gap-3`,children:[e.overall===`ok`?W(Sn,{class:`mt-0.5 size-5 shrink-0 ${a.icon}`}):e.overall===`initializing`?W(dn,{class:`mt-0.5 size-5 shrink-0 ${a.icon}`}):W(Cn,{class:`mt-0.5 size-5 shrink-0 ${a.icon}`}),W(`div`,{class:`min-w-0 flex-1`,children:[W(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:e.summary.message}),W(`p`,{class:`mt-1 mb-0 text-xs font-semibold leading-5 text-slate-500`,children:[`마지막 진단: `,jo(e.generatedAt,r),` · 검사 항목:`,` `,e.summary.total,`개`]})]})]}),s.length>0?W(`div`,{class:`mt-3 grid gap-2 sm:grid-cols-2`,children:s.map(e=>W(`div`,{class:`min-w-0 rounded-lg border border-white/80 bg-white/70 px-3 py-2`,children:[W(`p`,{class:`m-0 text-xs font-extrabold text-slate-900`,children:e.label}),W(`p`,{class:`mt-1 mb-0 line-clamp-2 text-xs leading-5 text-slate-500`,children:e.message})]},e.id))}):null,c>0?W(`p`,{class:`mt-2 mb-0 text-xs font-extrabold text-slate-600`,children:[`외 `,c,`건의 확인 항목이 있습니다.`]}):null]}):W(`div`,{class:`mt-3 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3`,children:[W(`p`,{class:`m-0 text-sm font-extrabold text-slate-900`,children:n?`진단 상태를 확인하고 있습니다.`:`아직 생성된 진단 결과가 없습니다.`}),W(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`진단 결과가 생성되면 현재 장치 상태를 이곳에 요약해서 표시합니다.`})]})]})}function S_(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?`${Y(e.blocklist.validLineCount)}개 차단 규칙`:`차단 목록 적용됨`;return{detail:r?`${i} · ${r.toUpperCase()}`:i,state:`healthy`,value:`보호 중`}}function C_({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=Xe(()=>i?Io(i.runtime.memory):null,[i]),[j,M]=L(()=>Math.floor(Date.now()/1e3));if(R(()=>{let e=window.setInterval(()=>{M(Math.floor(Date.now()/1e3))},m_);return()=>window.clearInterval(e)},[]),v&&!i)return W(vs,{});if(c&&!i)return W(ys,{message:c,onRetry:y});if(!i)return null;let N=A?Math.round(A.percent):0,P=rs(i.runtime.storage),F=P?(P.availableRatio*d_).toFixed(2):`0`,I=S_(b,S,x),ee=D?.packages.find(e=>e.name===`luci-app-smartsafehub`)??D?.packages[0]??null,te=!!(D&&D.updateCount>0),ne=D?te?`${D.updateCount}개 업데이트`:D.lastCheckAt?`최신 상태`:`확인 전`:O?`확인 필요`:`확인 중`,re=ee?te&&ee.availableVersion?`${ee.installedVersion} → ${ee.availableVersion}`:`설치 버전 ${ee.installedVersion}`:O?`업데이트 상태를 불러오지 못했습니다.`:k?`업데이트 상태를 확인하고 있습니다.`:`업데이트 상태를 확인할 수 없습니다.`,ie=a?`${Y(a.totals.online)}대`:s?`확인 중`:`확인 필요`,ae=a?`Wi-Fi ${Y(a.totals.wireless)}대 · 유선/기타 ${Y(a.totals.ethernet)}대`:o||`연결 기기 요약을 불러오고 있습니다.`,oe=__(i.network.protocol),se=i.network.available?i.network.ipv4Address?`WAN ${i.network.ipv4Address}${oe?` · ${oe}`:``}`:oe?`${oe} · WAN 주소를 받지 못했습니다.`:`WAN 주소를 받지 못했습니다.`:`WAN 인터페이스를 찾을 수 없습니다.`,ce=!!h?.conflict.detected,le=!i.network.up||ce||g!==null&&!h?`warning`:h?`healthy`:`neutral`,ue=ce?`네트워크 충돌`:i.network.up?`정상 연결`:`연결 확인`,de=i.network.up?ce?`⚠ LAN 대역과 충돌합니다`:_&&!h?`네트워크 대역 확인 중`:g&&!h?`네트워크 대역을 확인하지 못했습니다.`:h?`✓ 네트워크 충돌 없음`:`네트워크 대역 확인 필요`:`WAN 연결 상태를 확인해 주세요.`,fe=!i.network.up||ce||g!==null&&!h?`warning`:h?`healthy`:`neutral`,pe=i.network.ipv4Address?v_(i.network.ipv4Address)?`${i.network.ipv4Address} · 사설 네트워크`:i.network.ipv4Address:`할당되지 않음`,me=h_(b?.timestamps.lastSuccess,j),he=b?Math.max(b.timestamps.refreshIntervalS*2,86400):null,ge=!!(b?.enabled&&b.status!==`running`&&me!==null&&he!==null&&me>he),_e=u_(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 W(`div`,{class:`min-w-0 space-y-6`,children:[W(`section`,{"aria-labelledby":`dashboard-overview-title`,children:[W(J,{description:`인터넷, SafeShield, 연결 기기와 소프트웨어 상태를 한눈에 확인합니다.`,eyebrow:`Overview`,id:`dashboard-overview-title`,title:`시스템 개요`}),W(`div`,{class:`grid min-w-0 gap-4 sm:grid-cols-2 xl:grid-cols-4`,children:[W(p_,{detail:se,eyebrow:`Internet`,href:`#network`,icon:W(ln,{class:`size-5`}),linkLabel:ce?`해결하기 →`:`자세히 보기 →`,meta:de,metaState:fe,state:le,value:ue}),W(p_,{detail:I.detail,eyebrow:`SafeShield`,href:`#safeshield`,icon:W(qt,{class:`size-5`}),meta:g_(ge?`차단 목록 갱신 지연`:`차단 목록 갱신`,b?.timestamps.lastSuccess,j,S?`확인 중`:`기록 없음`),metaWarning:ge,state:I.state===`healthy`&&ge?`warning`:I.state,value:I.value}),W(p_,{detail:ae,eyebrow:`Connected devices`,href:`#devices`,icon:W(Yt,{class:`size-5`}),meta:g_(`목록 확인`,a?.generatedAt,j,s?`확인 중`:`기록 없음`),state:a?`healthy`:o?`warning`:`neutral`,value:ie}),W(p_,{detail:re,eyebrow:`Software update`,href:`#system`,icon:W(rn,{class:`size-5`}),meta:D&&!D.settings.checkEnabled?g_(`자동 확인 꺼짐 · 마지막 확인`,D.lastCheckAt,j,`확인 기록 없음`):g_(_e?`업데이트 확인 지연`:`마지막 확인`,D?.lastCheckAt,j,k?`확인 중`:`아직 없음`),metaWarning:_e,state:O||_e||te?`warning`:D?.lastCheckAt?`healthy`:`neutral`,value:ne})]}),e===!1?W(`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:[W(`div`,{class:`flex min-w-0 items-start gap-3`,children:[W(`span`,{class:`ssh-dashboard-account-banner-icon grid size-10 shrink-0 place-items-center rounded-xl`,"aria-hidden":`true`,children:W(Zt,{class:`size-5`})}),W(`div`,{class:`min-w-0`,children:[W(`div`,{class:`flex flex-wrap items-center gap-2`,children:[W(`p`,{class:`ssh-dashboard-account-banner-title m-0 font-bold`,children:`SmartSafeHub 계정을 연결해 주세요`}),W(`span`,{class:`ssh-dashboard-account-banner-badge rounded-full px-2 py-0.5 text-[0.7rem] font-bold`,children:`연결 필요`})]}),W(`p`,{class:`ssh-dashboard-account-banner-description mt-1 mb-0 text-sm leading-relaxed`,children:`계정을 연결하면 웹사이트에서 기기를 관리하고 구독 정보를 확인할 수 있습니다. DNS 보호는 연결하지 않아도 계속 작동합니다.`})]})]}),W(`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]}),W(`section`,{"aria-labelledby":`dashboard-activity-title`,children:[W(J,{description:`최근 SafeShield 차단 활동과 현재 네트워크 연결 구성을 함께 확인합니다.`,eyebrow:`Activity`,id:`dashboard-activity-title`,title:`네트워크 보호 활동`}),W(`div`,{class:`grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1.55fr)_minmax(18rem,0.65fr)]`,children:[W(c_,{data:C,error:w,loading:T,nowTimestamp:j,refreshing:E}),W(`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:[W(`div`,{class:`flex items-start justify-between gap-4`,children:[W(`div`,{children:[W(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Network`}),W(`h3`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`연결 상태`})]}),W(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl ${i.network.up&&!ce?`bg-emerald-50 text-emerald-700`:`bg-amber-50 text-amber-800`}`,children:W(ln,{class:`size-5`})})]}),W(`dl`,{class:`mt-4 mb-0`,children:[W(y_,{label:`인터넷`,value:ce?`네트워크 충돌`:i.network.up?`정상 연결`:`연결 확인`}),W(y_,{label:`WAN IP`,value:pe}),W(y_,{label:`프로토콜`,value:oe||`확인되지 않음`}),W(y_,{label:`상위 네트워크`,value:h?.wan.subnet||(_?`확인 중`:g?`확인 필요`:`확인되지 않음`)}),W(y_,{label:`LAN 네트워크`,value:h?.lan.subnet||(_?`확인 중`:g?`확인 필요`:`확인되지 않음`)}),W(y_,{label:`대역 충돌`,value:h?W(`span`,{class:h.conflict.detected?`text-amber-700`:`text-emerald-700`,children:h.conflict.detected?`충돌 감지`:`없음`}):_?`확인 중`:`확인 필요`})]}),W(`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:`네트워크 보기`})]})]})]}),W(`section`,{"aria-labelledby":`dashboard-recent-activity-title`,children:[W(J,{action:W(`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:`최근 활동`}),W(`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?W(Zo,{error:n,loading:r}):W(Xo,{compact:!0,events:(t?.events??[]).slice(0,3)})})]}),W(`section`,{"aria-labelledby":`dashboard-system-title`,children:[W(J,{description:`메모리, 디스크, 시스템 부하와 장치 정보를 빠르게 점검합니다.`,eyebrow:`System`,id:`dashboard-system-title`,title:`시스템 상태`}),W(`div`,{class:`grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1.45fr)_minmax(20rem,0.75fr)]`,children:[W(`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:[W(`div`,{class:`flex flex-wrap items-start justify-between gap-4`,children:[W(`div`,{children:[W(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Runtime`}),W(`h3`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`리소스 사용량`})]}),W(`span`,{class:`inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-black ring-1 ring-inset ${N>=90?`bg-amber-50 text-amber-800 ring-amber-200`:`bg-emerald-50 text-emerald-700 ring-emerald-200`}`,children:[W(`span`,{class:`size-2 rounded-full ${N>=90?`bg-amber-500`:`bg-emerald-500`}`}),N>=90?`메모리 확인`:`정상 실행`]})]}),W(`div`,{class:`mt-5 grid min-w-0 grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4`,children:[W(`div`,{class:`flex min-w-0 flex-col rounded-xl border border-slate-200 bg-slate-50 p-3 sm:p-4`,children:[W(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[W(fn,{class:`size-4`}),W(`span`,{class:`text-xs font-extrabold`,children:`메모리`})]}),W(`div`,{class:`flex min-h-0 flex-1 flex-col justify-center py-1`,children:[W(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:[N,`%`]}),W(`p`,{class:`mt-1 mb-0 text-xs font-semibold text-slate-500`,children:[A?Do(A.used):`-`,` /`,` `,Do(i.runtime.memory.total)]}),W(`div`,{"aria-label":`메모리 사용률 ${N}%`,class:`mt-3 h-1.5 overflow-hidden rounded-full bg-slate-200`,role:`progressbar`,"aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":N,children:W(`div`,{class:`h-full rounded-full transition-[width] duration-300 ${N>=90?`bg-amber-500`:`bg-teal-600`}`,style:{width:`${Math.min(100,N)}%`}})})]})]}),W(`div`,{class:`ssh-dashboard-storage-card flex min-w-0 flex-col rounded-xl border border-slate-200 bg-slate-50 p-3 sm:p-4`,"data-level":P?.level??`unavailable`,children:[W(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[W(gn,{class:`size-4`}),W(`span`,{class:`text-xs font-extrabold`,children:`디스크`})]}),W(`div`,{class:`mt-1 flex justify-center`,role:`img`,"aria-label":P?`디스크 여유 공간 ${P.availablePercent}%, ${P.availableText} 남음, 사용 ${P.usedText} / 전체 ${P.totalText}`:`디스크 저장 공간 확인 불가`,children:W(`div`,{class:`ssh-apps-storage-ring relative grid size-[76px] shrink-0 place-items-center`,"data-level":P?.level??`unavailable`,children:[W(`svg`,{"aria-hidden":`true`,class:`size-[76px]`,viewBox:`0 0 96 96`,children:[W(`circle`,{class:`ssh-apps-storage-track`,cx:`48`,cy:`48`,r:`39`}),P&&P.availableRatio>0?W(`circle`,{class:`ssh-apps-storage-free`,cx:`48`,cy:`48`,r:`39`,"stroke-dasharray":`${F} ${d_.toFixed(2)}`}):null]}),W(`div`,{class:`absolute inset-0 flex items-center justify-center text-center`,children:W(`strong`,{class:`text-base leading-none font-black text-slate-950`,children:P?`${P.availablePercent}%`:`—`})})]})}),W(`p`,{class:`ssh-apps-storage-available mt-1 mb-0 text-center text-xs font-extrabold text-teal-700`,children:P?`${P.availableText} 남음`:`저장 공간 확인 불가`}),P?W(`p`,{class:`mt-1 mb-0 text-center text-[11px] leading-4 font-semibold text-slate-500`,title:`앱 설치 가능 공간을 기준으로 계산하며 파일시스템 예약 공간이 사용량에 포함될 수 있습니다.`,children:[`사용 `,P.usedText,` / 전체 `,P.totalText]}):null]}),W(`div`,{class:`flex min-w-0 flex-col rounded-xl border border-slate-200 bg-slate-50 p-3 sm:p-4`,children:[W(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[W(Jt,{class:`size-4`}),W(`span`,{class:`text-xs font-extrabold`,children:`시스템 부하`})]}),W(`div`,{class:`flex min-h-0 flex-1 flex-col justify-center py-1`,children:[W(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:Oo(i.runtime.load[0])}),W(`p`,{class:`mt-1 mb-0 text-xs font-semibold text-slate-500`,children:[`5분 `,Oo(i.runtime.load[1]),` · 15분`,` `,Oo(i.runtime.load[2])]})]})]}),W(`div`,{class:`flex min-w-0 flex-col rounded-xl border border-slate-200 bg-slate-50 p-3 sm:p-4`,children:[W(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[W(dn,{class:`size-4`}),W(`span`,{class:`text-xs font-extrabold`,children:`실행 시간`})]}),W(`div`,{class:`flex min-h-0 flex-1 flex-col justify-center py-1`,children:[W(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:Eo(i.runtime.uptime)}),W(`p`,{class:`mt-1 mb-0 text-xs font-semibold text-slate-500`,children:[`부팅 `,ko(i.runtime.localtime,i.runtime.uptime)]})]})]})]}),W(x_,{data:f,error:p,loading:m,nowTimestamp:j})]}),W(`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:[W(`div`,{class:`flex items-start justify-between gap-4`,children:[W(`div`,{children:[W(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Device`}),W(`h3`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`장치 정보`})]}),W(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:W(Jt,{class:`size-5`})})]}),W(`dl`,{class:`mt-4 mb-0`,children:[W(y_,{label:`모델`,value:i.device.model}),W(y_,{label:`호스트 이름`,value:i.device.hostname}),W(y_,{label:`펌웨어`,value:W(`span`,{title:ve?l?.current.releaseVersion?`SmartSafeHub 펌웨어 ${l.current.releaseVersion}`:`SmartSafeHub 커스텀 펌웨어`:u||void 0,children:ye})}),W(y_,{label:xe,value:be}),W(y_,{label:`커널`,value:i.software.kernel}),W(y_,{label:`WAN IP`,value:i.network.ipv4Address||`할당되지 않음`})]})]})]})]})]})}var w_=Object.assign;function T_(e,t){for(var n in e)if(n!=`__source`&&e[n]!==t[n])return!0;for(var r in t)if(r!=`__source`&&!(r in e))return!0;return!1}var E_=/^(-|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|asp|fill|st)/;function D_(e,t){function n(e){var n=this.props.ref;return n!=e.ref&&n&&(typeof n==`function`?n(null):n.current=null),t?!t(this.props,e)||n!=e.ref:T_(this.props,e)}function r(t){return this.shouldComponentUpdate=n,v(e,t)}return r.displayName=`Memo(`+(e.displayName||e.name)+`)`,r.prototype.isReactComponent=!0,r.type=e,r}function O_(e,t){var n=se(e,t);return n.containerInfo=t,n}var k_=Symbol.for(`react.element`),A_=/^(?: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]/,j_=/[A-Z0-9]/g,M_=typeof document<`u`,N_=function(e){return/fil|che|rad/.test(e)};x.prototype.isReactComponent=!0,[`componentWillMount`,`componentWillReceiveProps`,`componentWillUpdate`].forEach(function(e){Object.defineProperty(x.prototype,e,{configurable:!0,get:function(){return this[`UNSAFE_`+e]},set:function(t){Object.defineProperty(this,e,{configurable:!0,writable:!0,value:t})}})});var P_=e.event;e.event=function(e){return P_&&(e=P_(e)),e.persist=function(){},e.isPropagationStopped=function(){return this.cancelBubble},e.isDefaultPrevented=function(){return this.defaultPrevented},e.nativeEvent=e};var F_={configurable:!0,get:function(){return this.class}},I_=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||M_&&a==`children`&&n==`noscript`||a==`class`||a==`className`)){if(a==`style`&&o&&o.constructor==Object){var s=void 0;for(var c in o)typeof o[c]!=`number`||E_.test(c)||(s||(s=o=w_({},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`||N_(t.type)?l==`onfocus`?a=`onfocusin`:l==`onblur`&&(a=`onfocusout`):l=a=`oninput`,l==`oninput`&&r[a=l]&&(a=`oninputCapture`)}else i&&A_.test(a)?a=a.replace(j_,`-$&`).toLowerCase():o===null&&(o=void 0);r[a]=o}}n==`select`&&(r.multiple&&Array.isArray(r.value)&&(r.value=k(t.children).forEach(function(e){e.props.selected=r.value.indexOf(e.props.value)!=-1})),r.defaultValue!=null&&(r.value=k(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",F_)):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=w_({},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=k_,I_&&I_(e)};var L_=8,R_=256,z_=8;function B_(e){return e.findIndex(e=>!e.disabled)}function V_(e){for(let t=e.length-1;t>=0;--t)if(!e[t]?.disabled)return t;return-1}function H_(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:B_(e)}function W_({ariaLabel:e,className:t=``,disabled:n=!1,id:r,onChange:i,options:a,value:o,variant:s=`default`}){let c=Ze(),l=r??`ssh-custom-select-${c}`,u=`${l}-menu`,d=z(null),f=z(null),p=z(null),m=z([]),[h,g]=L(!1),[_,v]=L(()=>U_(a,o)),[y,b]=L(`bottom`),[x,S]=L(R_),[C,w]=L({}),[T,E]=L(null),D=Xe(()=>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(R_,Math.max(44,a.length*42+12)),r=window.innerHeight-t.bottom-z_,i=t.top-z_,o=rr?`top`:`bottom`,s=Math.min(t.width,Math.max(0,window.innerWidth-16)),c=Math.max(z_,Math.min(t.left,window.innerWidth-s-z_)),l=o===`bottom`?r:i;b(o),S(Math.max(44,Math.min(R_,Math.floor(l)))),w({left:`${c}px`,width:`${s}px`,...o===`bottom`?{top:`${t.bottom+L_}px`}:{bottom:`${window.innerHeight-t.top+L_}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??U_(a,o)))};R(()=>{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)},[]),R(()=>{v(U_(a,o)),m.current=m.current.slice(0,a.length)},[a,o]),R(()=>{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]),R(()=>{n&&h&&k()},[n,h]);let N=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`,P=h?W(`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:W(`div`,{class:`overflow-y-auto overscroll-contain`,style:{maxHeight:`${x}px`},children:a.map((e,t)=>{let n=e.value===o;return W(`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(H_(a,t,1));break;case`ArrowUp`:e.preventDefault(),A(H_(a,t,-1));break;case`Home`:e.preventDefault(),A(B_(a));break;case`End`:e.preventDefault(),A(V_(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:[W(`span`,{class:`min-w-0 truncate`,children:e.label}),n?W(`span`,{"aria-hidden":`true`,class:`size-2 shrink-0 rounded-full bg-teal-500`}):null]},e.value)})})}):null;return W(`div`,{class:`relative ${t}`,ref:d,children:[W(`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 ${N}`,disabled:n,id:l,onClick:()=>{h?k():(O(),g(!0))},onKeyDown:e=>{switch(e.key){case`ArrowDown`:e.preventDefault(),M(h?H_(a,_,1):U_(a,o));break;case`ArrowUp`:e.preventDefault(),M(h?H_(a,_,-1):U_(a,o));break;case`Home`:e.preventDefault(),M(B_(a));break;case`End`:e.preventDefault(),M(V_(a));break;case`Enter`:case` `:e.preventDefault(),h?k():M();break;case`Escape`:h&&(e.preventDefault(),k())}},ref:f,type:`button`,children:[W(`span`,{class:`min-w-0 truncate`,children:D?.label??`선택하세요`}),W(An,{"aria-hidden":`true`,class:`size-4 shrink-0 text-slate-400 transition-transform ${h?`rotate-180`:``}`})]}),P&&T?O_(P,T):null,W(`span`,{class:`sr-only`,"aria-live":`polite`,children:y===`top`?`선택 목록이 위쪽에 열렸습니다.`:``})]})}var G_=[{value:`skb`,label:`SK Broadband`},{value:`lgu`,label:`LG U+`}];function K_(e){return e===`lgu`?`LG U+`:`SK Broadband`}function q_({active:e,label:t,value:n}){return W(`div`,{class:`rounded-xl border border-slate-200 bg-white p-4`,children:[W(`div`,{class:`flex items-center justify-between gap-3`,children:[W(`p`,{class:`m-0 text-xs font-extrabold text-slate-500`,children:t}),W(`span`,{class:`size-2.5 shrink-0 rounded-full ${e?`bg-emerald-500`:`bg-slate-300`}`,"aria-hidden":`true`})]}),W(`p`,{class:`mt-2 mb-0 text-sm font-black text-slate-950`,children:n})]})}function J_({data:e,error:t,feedback:n,loading:r,onDismissFeedback:i,onRetry:a,onSave:o,saving:s}){let[c,l]=L(!1),[u,d]=L(`skb`);if(R(()=>{e&&(l(e.enabled),d(e.provider))},[e]),r&&!e)return W(vs,{});if(t&&!e)return W(ys,{message:t,onRetry:a});if(!e)return W(ys,{message:`IPTV 설정을 확인할 수 없습니다.`,onRetry:a});let f=c!==e.enabled||u!==e.provider,p=!e.enabled||e.running&&e.igmpSnooping;return W(`div`,{class:`space-y-5`,children:[W(`section`,{class:`rounded-2xl border border-amber-200 bg-amber-50 p-5 sm:p-6`,children:W(`div`,{class:`flex items-start gap-4`,children:[W(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-amber-100 text-amber-800`,children:W(Cn,{class:`size-5`})}),W(`div`,{class:`min-w-0 flex-1`,children:[W(`div`,{class:`flex flex-wrap items-center gap-2`,children:[W(`h2`,{class:`m-0 text-base font-black text-amber-950`,children:`IPTV 실험 기능`}),W(`span`,{class:`rounded-full border border-amber-300 bg-amber-100 px-2 py-0.5 text-[10px] font-black uppercase tracking-[0.12em] text-amber-800`,children:`Beta`})]}),W(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-amber-900/80`,children:`현재 SK Broadband와 LG U+를 지원하며 두 통신사에는 동일한 멀티캐스트 IPTV 프로파일을 적용합니다. KT IPTV는 별도 네트워크 방식이 필요해 아직 지원하지 않습니다.`})]})]})}),n?W(`section`,{"aria-live":`polite`,class:`rounded-2xl border p-4 ${n.kind===`success`?`border-emerald-200 bg-emerald-50 text-emerald-900`:`border-red-200 bg-red-50 text-red-900`}`,children:W(`div`,{class:`flex items-start justify-between gap-3`,children:[W(`p`,{class:`m-0 text-sm font-bold leading-6`,children:n.message}),W(`button`,{class:`shrink-0 text-xs font-extrabold underline underline-offset-2`,onClick:i,type:`button`,children:`닫기`})]})}):null,W(J,{eyebrow:`IPTV`,title:`IPTV 설정`,description:`통신사 환경에 맞는 멀티캐스트 중계 기능을 관리합니다.`}),W(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:[W(`div`,{class:`flex min-w-0 items-start gap-3`,children:[W(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:W(un,{class:`size-5`})}),W(`div`,{class:`min-w-0 flex-1`,children:[W(`div`,{class:`flex min-w-0 flex-wrap items-center justify-between gap-2`,children:[W(`h2`,{class:`m-0 text-lg font-black text-slate-950`,children:`IPTV`}),W(`div`,{class:`flex shrink-0 items-center gap-2`,children:[f?W(`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:[W(Cn,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}):null,W(`button`,{"aria-checked":c,"aria-label":`IPTV 사용`,class:`ssh-switch-control relative inline-flex shrink-0 rounded-full border-0 transition focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 ${c?`bg-teal-700`:`bg-slate-300`} disabled:cursor-not-allowed disabled:opacity-60`,disabled:s||!e.available,onClick:()=>l(e=>!e),role:`switch`,type:`button`,children:W(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${c?`left-6`:`left-1`}`})})]})]}),W(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`WAN의 멀티캐스트 방송을 LAN 셋톱박스로 전달합니다.`})]})]}),W(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]`,children:[W(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[W(`div`,{class:`flex flex-wrap items-start justify-between gap-2`,children:[W(`div`,{children:[W(`label`,{class:`block text-xs font-extrabold text-slate-600`,htmlFor:`iptv-provider`,children:`통신사`}),W(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`IPTV를 제공하는 회선 사업자를 선택하세요.`})]}),W(`span`,{class:`rounded-full border border-teal-200 bg-teal-50 px-2.5 py-1 text-[10px] font-black text-teal-700`,children:`멀티캐스트 방식`})]}),W(W_,{ariaLabel:`IPTV 통신사`,className:`mt-3`,disabled:s||!c,id:`iptv-provider`,onChange:e=>d(e),options:G_,value:u}),W(`div`,{class:`mt-3 flex items-start gap-2 rounded-lg border border-slate-200 bg-white p-3`,children:[W(Sn,{"aria-hidden":`true`,class:`mt-0.5 size-4 shrink-0 text-teal-700`}),W(`p`,{class:`m-0 text-xs leading-5 text-slate-600`,children:[K_(u),`에는 현재 SK Broadband/LG U+ 공통 IGMP Proxy 프로파일을 적용합니다. KT는 추후 별도 IPTV 프로파일로 지원할 예정입니다.`]})]})]}),W(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[W(`div`,{class:`flex items-center justify-between gap-3`,children:[W(`p`,{class:`m-0 text-xs font-extrabold text-slate-500`,children:`현재 지원 프로파일`}),W(`span`,{class:`text-xs font-extrabold text-slate-500`,children:`SKB · LG U+`})]}),W(`p`,{class:`mt-2 mb-0 text-sm font-black text-slate-950`,children:`WAN → IGMP Proxy → LAN`}),W(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-600`,children:`WAN을 upstream, LAN을 downstream으로 사용하고 LAN bridge에는 IGMP snooping을 함께 활성화해 필요하지 않은 포트와 Wi-Fi로 멀티캐스트가 불필요하게 퍼지는 것을 줄입니다.`})]})]}),e.available?null:W(`div`,{class:`mt-5 rounded-xl border border-red-200 bg-red-50 p-4 text-sm font-bold leading-6 text-red-900`,children:`IGMP Proxy 패키지를 찾지 못했습니다. 현재 펌웨어에 igmpproxy 패키지가 포함되어 있는지 확인해 주세요.`}),W(`div`,{class:`mt-6 flex flex-wrap items-center justify-between gap-3 border-t border-slate-200 pt-5`,children:[W(`p`,{class:`m-0 text-xs leading-5 text-slate-500`,children:`설정을 적용하면 네트워크 서비스가 잠시 다시 로드될 수 있습니다.`}),W(`button`,{class:`inline-flex min-h-11 items-center justify-center rounded-xl border-0 bg-teal-700 px-5 py-2.5 text-sm font-black text-white shadow-sm transition hover:bg-teal-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-not-allowed disabled:opacity-50`,disabled:s||!e.available||!f&&!c,onClick:()=>void o({enabled:c,provider:u}),type:`button`,children:s?`적용 중…`:f?`변경사항 적용`:`설정 다시 적용`})]})]}),W(J,{eyebrow:`Runtime`,title:`IPTV 동작 상태`,description:`마지막으로 적용한 IPTV 설정과 중계 서비스의 실행 상태를 확인합니다.`}),W(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:[W(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[W(`p`,{class:`m-0 text-xs leading-5 text-slate-500`,children:`적용된 IPTV 중계 서비스와 멀티캐스트 전달 상태입니다.`}),W(`span`,{class:`inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-black ${p?`bg-emerald-100 text-emerald-800`:`bg-amber-100 text-amber-800`}`,children:[W(Sn,{class:`size-4`}),p?e.enabled?`정상`:`꺼짐`:`확인 필요`]})]}),f?W(`div`,{class:`mt-4 flex items-start gap-2 rounded-xl border border-amber-200 bg-amber-50 p-3 text-xs font-bold leading-5 text-amber-900`,children:[W(Cn,{"aria-hidden":`true`,class:`mt-0.5 size-4 shrink-0`}),`아래 동작 상태는 마지막으로 적용된 설정 기준입니다. 변경사항을 적용하면 새 설정으로 갱신됩니다.`]}):null,W(`div`,{class:`mt-5 grid gap-3 sm:grid-cols-2 lg:grid-cols-4`,children:[W(q_,{active:e.enabled&&e.running,label:`IGMP Proxy`,value:e.enabled?e.running?`동작 중`:`중지됨`:`꺼짐`}),W(q_,{active:e.enabled&&e.igmpSnooping,label:`IGMP Snooping`,value:e.igmpSnooping?`켜짐`:`꺼짐`}),W(q_,{active:e.enabled,label:`Upstream`,value:e.upstreamNetwork.toUpperCase()}),W(q_,{active:e.enabled,label:`Downstream`,value:e.lanBridge??e.downstreamNetwork.toUpperCase()})]})]})]})}function Y_(e){return e?`disabled:cursor-wait`:`disabled:cursor-not-allowed`}function X_(e){let t=e.split(`.`);return[t[0]??``,t[1]??``,t[2]??``,t[3]??``]}function Z_(e){return e.replace(/\D/g,``).slice(0,3)}function Q_(e){return e.every(e=>e===``)?``:e.join(`.`)}function $_({disabled:e=!1,label:t,value:n,onChange:r}){let i=X_(n),a=(e,t)=>{let n=[...i];n[e]=Z_(t),r(Q_(n))};return W(`div`,{class:`ssh-ipv4-segments mt-2 min-h-11`,"aria-label":`${t} 입력`,children:i.map((n,r)=>W(`div`,{class:`ssh-ipv4-segment`,children:[W(`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}),r{if(!/^(0|[1-9][0-9]{0,2})$/.test(e))return!1;let t=Number(e);return t>=0&&t<=255})}function rv(e){let t=X_(e);return t.slice(0,3).some(e=>e===``||Number(e)>255)?null:t.slice(0,3).join(`.`)}function iv(e){return X_(e)[3]}function av(e){let t=rv(e.lan.address),n=iv(e.dhcp.start??``),r=iv(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 ov({disabled:e,label:t,lanAddress:n,value:r,onChange:i}){let a=rv(n),o=a?.split(`.`)??[``,``,``],s=iv(r),c=e=>{let t=Z_(e);i(a&&t!==``?`${a}.${t}`:t)};return W(`label`,{class:`block`,children:[W(`span`,{class:`text-sm font-extrabold text-slate-800`,children:t}),W(`div`,{class:`ssh-ipv4-segments mt-2 min-h-11`,"aria-label":`${t} 입력`,children:[o.map((e,t)=>W(`div`,{class:`ssh-ipv4-segment`,children:[W(`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||`—`}),W(`span`,{"aria-hidden":`true`,class:`ssh-ipv4-separator text-sm font-black text-slate-400`,children:`.`})]},t)),W(`div`,{class:`ssh-ipv4-segment`,children:W(`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})})]}),W(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`앞 3개 주소는 공유기 IP 주소와 동일하게 유지됩니다.`})]})}function sv(e){return`${window.location.protocol}//${e}${window.location.pathname}${window.location.search}${window.location.hash}`}function cv({feedback:e,onDismiss:t}){return W(`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:[W(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[W(`p`,{class:`m-0 font-bold leading-6`,children:e.message}),W(`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?W(`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:sv(e.newAddress),children:[`새 공유기 주소 `,e.newAddress,`로 연결`]}):null]})}function lv({data:e}){let t=e.conflict.detected;return W(`section`,{class:`grid gap-4 lg:grid-cols-3`,"aria-label":`내부 네트워크 상태`,children:[W(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[W(`span`,{class:`grid size-10 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:W(Jt,{class:`size-5`})}),W(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`SmartSafeHub LAN`}),W(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:e.lan.subnet}),W(`p`,{class:`mt-1 mb-0 text-sm font-semibold text-slate-600`,children:[`공유기 주소 `,e.lan.address]})]}),W(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[W(`span`,{class:`grid size-10 place-items-center rounded-xl bg-slate-100 text-slate-700`,children:W(ln,{class:`size-5`})}),W(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`상위 네트워크`}),W(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:e.wan.subnet??`확인 전`}),W(`p`,{class:`mt-1 mb-0 text-sm font-semibold text-slate-600`,children:e.wan.connected?`WAN ${e.wan.address??`주소 확인 중`}`:`WAN 연결 후 자동으로 확인합니다.`})]}),W(`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:[W(`span`,{class:`grid size-10 place-items-center rounded-xl ${t?`bg-red-100 text-red-700`:`bg-emerald-100 text-emerald-700`}`,children:W(t?Cn:Sn,{class:`size-5`})}),W(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] opacity-70`,children:`대역 충돌`}),W(`p`,{class:`mt-2 mb-0 text-lg font-black`,children:t?`주소 대역이 겹칩니다`:`충돌이 감지되지 않았습니다`}),W(`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 uv({action:e,data:t,error:n,feedback:r,loading:i,onApplyRecommendation:a,onDismissFeedback:o,onRetry:s,onSave:c}){let[l,u]=L(``),[d,f]=L(24),[p,m]=L(!0),[h,g]=L(``),[_,v]=L(``),[y,x]=L(`12h`),[S,C]=L(!1),[w,T]=L(null),E=(e={})=>{if(!t)return!1;let n=av(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??y)!==n.leaseTime},D=e=>{let t=rv(e);if(u(e),!t){C(E({ipAddress:e}));return}let n=iv(h),r=iv(_),i=n?`${t}.${n}`:``,a=r?`${t}.${r}`:``;g(i),v(a),C(E({ipAddress:e,dhcpStart:i,dhcpEnd:a}))},O=e=>{g(e),C(E({dhcpStart:e}))},k=e=>{v(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}))};R(()=>{if(!t||S)return;let e=av(t);u(e.ipAddress),f(e.prefixLength),m(e.dhcpEnabled),g(e.dhcpStart),v(e.dhcpEnd),x(e.leaseTime),T(null)},[t,S]);let N=Xe(()=>{let e=[...ev];return e.includes(d)||(e.push(d),e.sort((e,t)=>e-t)),e},[d]),P=Xe(()=>{let e=[...tv];return e.some(([e])=>e===y)||e.push([y,`현재 설정 (${y})`]),e},[y]);if(i)return W(vs,{});if(n)return W(ys,{message:n,onRetry:s});if(!t)return null;let F=e!==null,I=async e=>{e.preventDefault(),nv(l)?!nv(h)||!nv(_)?T(`DHCP 시작/종료 주소를 올바른 IPv4 형식으로 입력해 주세요.`):window.confirm(`내부 네트워크 설정을 적용하면 연결된 기기가 새 DHCP 정보를 받는 동안 잠시 연결이 끊길 수 있습니다. 계속하시겠습니까?`)&&(T(null),await c({ipAddress:l.trim(),prefixLength:d,dhcpEnabled:p,dhcpStart:h.trim(),dhcpEnd:_.trim(),leaseTime:y})&&C(!1)):T(`공유기 주소를 올바른 IPv4 형식으로 입력해 주세요.`)},ee=async()=>{t.recommendation&&window.confirm(`상위 네트워크와 겹치지 않는 ${t.recommendation.subnet} 대역으로 자동 변경합니다. 현재 연결이 잠시 끊길 수 있습니다. 계속하시겠습니까?`)&&(T(null),await a()&&C(!1))};return W(b,{children:[r?W(cv,{feedback:r,onDismiss:o}):null,W(J,{eyebrow:`Overview`,title:`내부 네트워크 상태`,description:`LAN 주소와 DHCP 할당 상태를 확인합니다.`}),W(lv,{data:t}),t.conflict.detected?W(`section`,{class:`mt-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-amber-950 sm:p-6`,children:W(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between`,children:[W(`div`,{class:`min-w-0`,children:[W(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-amber-700`,children:`자동 충돌 해결`}),W(`h2`,{class:`mt-2 mb-0 text-lg font-black`,children:`안전한 LAN 대역으로 변경할 수 있습니다`}),W(`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?W(`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:F,onClick:()=>void ee(),type:`button`,children:[W(Sn,{class:`size-5 ${e===`auto`?`animate-pulse`:``}`}),e===`auto`?`변경 중`:`추천 대역으로 자동 변경`]}):null]})}):null,W(`div`,{class:`mt-7`,children:W(J,{eyebrow:`Local network`,title:`LAN 및 DHCP 설정`,description:`상위 공유기와 다른 주소 대역을 사용해야 안정적으로 라우팅할 수 있습니다.`})}),W(`form`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,onSubmit:e=>void I(e),children:[S?W(`div`,{class:`mb-5 flex justify-end`,children:W(`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:[W(Cn,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]})}):null,W(`div`,{class:`grid gap-5 lg:grid-cols-2`,children:[W(`div`,{class:`block`,children:[W(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`공유기 IP 주소`}),W($_,{disabled:F,label:`공유기 IP 주소`,onChange:D,value:l})]}),W(`div`,{class:`block`,children:[W(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`현재 LAN 대역`}),W(`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}),W(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`서브넷 마스크는 아래 고급 설정에서 변경할 수 있습니다.`})]}),W(ov,{disabled:F,label:`DHCP 시작 주소`,lanAddress:l,onChange:O,value:h}),W(ov,{disabled:F,label:`DHCP 종료 주소`,lanAddress:l,onChange:k,value:_})]}),W(`details`,{class:`mt-6 rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[W(`summary`,{class:`cursor-pointer text-sm font-black text-slate-900`,children:`고급 DHCP 설정`}),W(`div`,{class:`mt-5 grid gap-5 lg:grid-cols-2`,children:[W(`div`,{class:`block`,children:[W(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`서브넷 마스크`}),W(W_,{ariaLabel:`서브넷 마스크`,className:`mt-2`,disabled:F,id:`lan-prefix-length`,onChange:e=>A(Number(e)),options:N.map(e=>({value:String(e),label:`/${e} · ${e===t.lan.prefixLength?t.lan.netmask:`CIDR /${e}`}`})),value:String(d),variant:`emphasized`}),W(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`일반 가정용 네트워크는 /24 사용을 권장합니다.`})]}),W(`div`,{class:`block`,children:[W(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`DHCP 임대 시간`}),W(W_,{ariaLabel:`DHCP 임대 시간`,className:`mt-2`,disabled:F,id:`lan-dhcp-lease-time`,onChange:j,options:P.map(([e,t])=>({value:e,label:t})),value:y,variant:`emphasized`})]}),W(`label`,{class:`inline-flex min-h-11 items-center gap-3 text-sm font-extrabold text-slate-800 lg:col-span-2`,children:[W(`input`,{checked:p,class:`size-5 accent-teal-700`,disabled:F,onChange:e=>M(e.currentTarget.checked),type:`checkbox`}),`SmartSafeHub DHCP 서버 사용`]}),W(`p`,{class:`m-0 text-xs font-semibold leading-5 text-slate-500 lg:col-span-2`,children:`DHCP를 끄면 SmartSafeHub에 연결되는 장치에 IP 주소를 자동으로 할당하지 않습니다. 별도의 DHCP 서버를 사용하는 고급 구성에서만 비활성화하세요.`})]})]}),w?W(`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,W(`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:[W(`p`,{class:`m-0 text-xs font-semibold leading-5 text-slate-500`,children:`주소가 바뀌면 현재 관리 페이지 연결이 종료되며 새 IP를 받아 다시 접속해야 합니다.`}),W(`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 ${Y_(F)} disabled:opacity-60 sm:w-auto`,disabled:F||!S,type:`submit`,children:[W(Sn,{class:`size-5 ${e===`saving`?`animate-pulse`:``}`}),e===`saving`?`저장 중`:`설정 저장`]})]})]})]})}var dv=[`dhcp`,`pppoe`,`static`],fv=`1.1.1.1`,pv=Array.from({length:33},(e,t)=>t);function mv(e){return dv.includes(e)}function hv(e){switch(e){case`dhcp`:return`자동 IP (DHCP)`;case`pppoe`:return`PPPoE`;case`static`:return`고정 IPv4`;default:return e?e.toUpperCase():`확인 전`}}function gv(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 _v(e){return e.trim()}function vv(e,t){let n=gv(t);return e.protocol===n.protocol?e.protocol===`dhcp`||!mv(e.protocol)?!1:e.protocol===`pppoe`?_v(e.pppoeUsername)!==n.pppoeUsername||e.pppoePassword.length>0:_v(e.staticAddress)!==n.staticAddress||e.staticPrefixLength!==n.staticPrefixLength||_v(e.staticGateway)!==n.staticGateway||_v(e.dnsPrimary)!==n.dnsPrimary||_v(e.dnsSecondary)!==n.dnsSecondary:!0}function yv(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 bv(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 xv(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 Sv({feedback:e,onDismiss:t}){return W(`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:W(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[W(`p`,{class:`m-0 font-bold leading-6`,children:e.message}),W(`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 Cv({data:e,busy:t,onReconnect:n}){let r=e.status,i=r.connected;return W(`section`,{class:`grid gap-4 lg:grid-cols-3`,"aria-label":`인터넷 연결 상태`,children:[W(`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:[W(`div`,{class:`flex items-start justify-between gap-3`,children:[W(`span`,{class:`grid size-10 place-items-center rounded-xl ${i?`bg-emerald-100 text-emerald-700`:`bg-amber-100 text-amber-700`}`,children:W(i?Sn:Cn,{class:`size-5`})}),W(`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:[W(pn,{class:`size-4 ${t?`animate-spin`:``}`}),`재연결`]})]}),W(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] opacity-70`,children:`인터넷 상태`}),W(`p`,{class:`mt-2 mb-0 text-xl font-black`,children:r.pending?`연결 중`:i?`연결됨`:`연결 안 됨`}),W(`p`,{class:`mt-1 mb-0 text-sm font-semibold leading-6 opacity-80`,children:[hv(r.protocol??e.configuration.protocol),` · `,xv(r.uptimeSeconds)]})]}),W(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[W(`span`,{class:`grid size-10 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:W(ln,{class:`size-5`})}),W(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`WAN IPv4`}),W(`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}`}`:`주소 없음`}),W(`p`,{class:`mt-1 mb-0 text-sm font-semibold text-slate-600`,children:r.device?`인터페이스 ${r.device}`:`인터페이스 확인 전`})]}),W(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[W(`span`,{class:`grid size-10 place-items-center rounded-xl bg-slate-100 text-slate-700`,children:W(Jt,{class:`size-5`})}),W(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`Gateway · DNS`}),W(`p`,{class:`mt-2 mb-0 break-all text-base font-black text-slate-950`,children:r.gateway??`Gateway 확인 전`}),W(`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 wv({action:e,data:t,error:n,feedback:r,loading:i,onDismissFeedback:a,onReconnect:o,onRetry:s,onSave:c}){let[l,u]=L({protocol:`dhcp`,pppoeUsername:``,pppoePassword:``,staticAddress:``,staticPrefixLength:24,staticGateway:``,dnsPrimary:``,dnsSecondary:``}),[d,f]=L(!1),[p,m]=L(!1),[h,g]=L(null);R(()=>{t&&!d&&(u(gv(t)),g(null))},[t,d]);let _=Xe(()=>{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 W(vs,{});if(n)return W(ys,{message:n,onRetry:s});if(!t)return null;let v=e!==null,y=e=>{u(n=>{let r={...n,...e};return f(vv(r,t)),r}),g(null)},x=async e=>{if(e.preventDefault(),!mv(l.protocol))g(`DHCP, PPPoE 또는 고정 IPv4 중 하나를 선택해 주세요.`);else{if(l.protocol===`pppoe`){if(!_v(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(!yv(l.staticAddress)){g(`고정 IPv4 주소를 올바르게 입력해 주세요.`);return}if(!yv(l.staticGateway)){g(`기본 게이트웨이를 올바르게 입력해 주세요.`);return}if(!yv(l.dnsPrimary)){g(`기본 DNS 서버를 올바르게 입력해 주세요.`);return}if(_v(l.dnsSecondary)&&!yv(l.dnsSecondary)){g(`보조 DNS 서버를 올바르게 입력해 주세요.`);return}}window.confirm(`인터넷 연결 설정을 적용하면 WAN 연결이 다시 시작되어 인터넷이 잠시 끊길 수 있습니다. 계속하시겠습니까?`)&&await c({protocol:l.protocol,pppoeUsername:_v(l.pppoeUsername),pppoePassword:l.pppoePassword,pppoePasswordChanged:l.pppoePassword.length>0,staticAddress:_v(l.staticAddress),staticPrefixLength:l.staticPrefixLength,staticGateway:_v(l.staticGateway),dnsPrimary:_v(l.dnsPrimary),dnsSecondary:_v(l.dnsSecondary)})&&(u(e=>({...e,pppoePassword:``})),f(!1))}};return W(b,{children:[r?W(Sv,{feedback:r,onDismiss:a}):null,W(J,{eyebrow:`Overview`,title:`인터넷 연결 상태`,description:`WAN 연결 상태와 네트워크 정보를 확인합니다.`}),W(Cv,{data:t,busy:v,onReconnect:()=>{window.confirm(`WAN 연결을 다시 시작합니다. 인터넷이 잠시 끊길 수 있습니다. 계속하시겠습니까?`)&&o()}}),t.configuration.supported?null:W(`section`,{class:`mt-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-amber-950 sm:p-6`,children:W(`div`,{class:`flex items-start gap-3`,children:[W(Cn,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),W(`div`,{children:[W(`h2`,{class:`m-0 text-base font-black`,children:`현재 WAN 방식은 직접 관리하지 않습니다`}),W(`p`,{class:`mt-2 mb-0 text-sm font-semibold leading-6 text-amber-900/80`,children:[`현재 설정은 `,t.configuration.protocol,`입니다. 아래에서 DHCP, PPPoE 또는 고정 IPv4를 선택해 저장하면 해당 방식으로 전환합니다.`]})]})]})}),W(`div`,{class:`mt-7`,children:W(J,{eyebrow:`Internet`,title:`인터넷 연결 설정`,description:`통신사 또는 상위 네트워크 환경에 맞는 WAN 연결 방식을 선택합니다.`})}),W(`form`,{class:`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:[d?W(`div`,{class:`mb-5 flex justify-end`,children:W(`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:[W(Cn,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]})}):null,W(`div`,{children:[W(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`연결 방식`}),W(W_,{ariaLabel:`WAN 연결 방식`,className:`mt-2 max-w-xl`,disabled:v,id:`wan-protocol`,onChange:e=>y(e===`static`&&!_v(l.dnsSecondary)?{protocol:e,dnsSecondary:fv}:{protocol:e}),options:_,value:l.protocol,variant:`emphasized`}),W(`span`,{class:`mt-2 block text-xs font-semibold leading-5 text-slate-500`,children:`대부분의 가정용 인터넷과 상위 공유기 연결은 자동 IP(DHCP)를 사용합니다.`})]}),l.protocol===`pppoe`?W(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[W(`label`,{class:`block`,children:[W(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`PPPoE 사용자명`}),W(`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:v,onInput:e=>y({pppoeUsername:e.currentTarget.value}),spellcheck:!1,value:l.pppoeUsername})]}),W(`label`,{class:`block`,children:[W(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`PPPoE 비밀번호`}),W(`span`,{class:`relative mt-2 block`,children:[W(`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:v,onInput:e=>y({pppoePassword:e.currentTarget.value}),placeholder:t.configuration.protocol===`pppoe`&&t.configuration.pppoe.passwordConfigured?`변경할 때만 입력`:`PPPoE 비밀번호`,...p?{type:`text`}:{type:`password`},value:l.pppoePassword}),W(`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:v,onClick:()=>m(e=>!e),type:`button`,children:W(p?bn:yn,{class:`size-5`})})]}),W(`span`,{class:`mt-2 block text-xs text-slate-500`,children:t.configuration.protocol===`pppoe`&&t.configuration.pppoe.passwordConfigured?`비워 두면 현재 저장된 비밀번호를 그대로 사용합니다.`:`통신사에서 제공한 PPPoE 비밀번호를 입력해 주세요.`})]})]}):null,l.protocol===`static`?W(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[W(`label`,{class:`block`,children:[W(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`IPv4 주소`}),W($_,{disabled:v,label:`IPv4 주소`,onChange:e=>y({staticAddress:e}),value:l.staticAddress})]}),W(`div`,{class:`block`,children:[W(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`서브넷 마스크`}),W(W_,{ariaLabel:`WAN 서브넷 마스크`,className:`mt-2`,disabled:v,id:`wan-static-prefix-length`,onChange:e=>y({staticPrefixLength:Number(e)}),options:pv.map(e=>({value:String(e),label:`/${e} · ${bv(e)}`})),value:String(l.staticPrefixLength),variant:`emphasized`})]}),W(`label`,{class:`block`,children:[W(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`기본 게이트웨이`}),W($_,{disabled:v,label:`기본 게이트웨이`,onChange:e=>y({staticGateway:e}),value:l.staticGateway})]}),W(`label`,{class:`block`,children:[W(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`기본 DNS`}),W($_,{disabled:v,label:`기본 DNS`,onChange:e=>y({dnsPrimary:e}),value:l.dnsPrimary})]}),W(`label`,{class:`block lg:col-start-2`,children:[W(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`보조 DNS (선택)`}),W($_,{disabled:v,label:`보조 DNS`,onChange:e=>y({dnsSecondary:e}),value:l.dnsSecondary})]})]}):null,l.protocol===`dhcp`?W(`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?W(`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,W(`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:[W(`p`,{class:`m-0 text-xs font-semibold leading-5 text-slate-500`,children:`설정 저장 후 WAN만 다시 연결하며 LAN 관리 페이지와 내부 네트워크 설정은 유지됩니다.`}),W(`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 ${Y_(v)} disabled:opacity-60 sm:w-auto`,disabled:v||!d,type:`submit`,children:[W(Sn,{class:`size-5 ${e===`saving`?`animate-pulse`:``}`}),e===`saving`?`저장 중`:`설정 저장`]})]})]})]})}var Tv=3,Ev=10;function Dv(e,t){return e<=0?`0.0%`:`${(t/e*100).toFixed(1)}%`}function Ov(e){return e.id===`other`?`기타 기기`:e.hostname?e.hostname:e.identified?`이름 없는 기기`:`미식별 기기`}function kv(e){return[...e].sort((e,t)=>e.id===`other`?1:t.id===`other`?-1:t.blocked-e.blocked||t.queries-e.queries||Ov(e).localeCompare(Ov(t),`ko`))}function Av({devices:e,deviceLimit:t,truncated:n}){let r=kv(e),[i,a]=L(!1),[o,s]=L(1),c=Math.max(1,Math.ceil(r.length/Ev)),l=Math.min(o,c),u=(l-1)*Ev,d=Math.min(u+Ev,r.length),f=i?r.slice(u,d):r.slice(0,Tv),p=r.length>Tv;return R(()=>{s(e=>Math.min(e,c))},[c]),W(`div`,{class:`mt-6 rounded-2xl border border-slate-100 bg-slate-50/70 p-4 sm:p-5`,children:[W(`div`,{class:`flex flex-wrap items-start justify-between gap-3`,children:[W(`div`,{children:[W(`div`,{class:`flex flex-wrap items-center gap-2`,children:[W(`p`,{class:`m-0 text-xs font-bold text-slate-500`,children:`기기별 통계`}),p&&!i?W(`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]}),W(`h3`,{class:`mt-1 mb-0 text-base font-extrabold text-slate-950`,children:`어떤 기기에서 차단되었는지 확인`}),W(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-500`,children:`DHCP 정보가 있는 기기는 MAC 주소를 기준으로 식별하며, 기기별 숫자만 현재 부팅 세션 동안 메모리에 집계합니다.`})]}),W(`div`,{class:`flex flex-wrap items-center justify-end gap-2`,children:[W(`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:[W(Yt,{class:`size-4 text-teal-700`}),Y(r.length),`개 항목`]}),p?W(`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?`간단히 보기 ↑`:`전체 ${Y(r.length)}개 기기 보기 ↓`}):null]})]}),r.length===0?W(`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 요청이 발생하면 다음 통계 갱신 시 표시됩니다.`}):W(`div`,{class:`mt-4 grid gap-3`,id:`safeshield-device-statistics-list`,children:f.map(e=>W(`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:[W(`div`,{class:`min-w-0`,children:W(`div`,{class:`flex min-w-0 flex-wrap items-center gap-2`,children:[W(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg bg-teal-50 text-teal-700`,children:W(Yt,{class:`size-4`})}),W(`div`,{class:`min-w-0`,children:[W(`h4`,{class:`m-0 truncate text-sm font-extrabold text-slate-950`,children:Ov(e)}),W(`p`,{class:`mt-1 mb-0 break-all text-xs text-slate-500`,children:[e.ip||`IP 미확인`,e.mac?` · ${e.mac}`:``]})]}),W(`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 임시 식별`})]})}),W(`dl`,{class:`m-0 grid grid-cols-3 gap-2 text-right`,children:[W(`div`,{class:`min-w-20`,children:[W(`dt`,{class:`text-[11px] font-bold text-slate-400`,children:`DNS 요청`}),W(`dd`,{class:`mt-1 mb-0 ml-0 text-sm font-extrabold text-slate-900`,children:Y(e.queries)})]}),W(`div`,{class:`min-w-20`,children:[W(`dt`,{class:`text-[11px] font-bold text-slate-400`,children:`차단`}),W(`dd`,{class:`mt-1 mb-0 ml-0 text-sm font-extrabold text-teal-700`,children:Y(e.blocked)})]}),W(`div`,{class:`min-w-20`,children:[W(`dt`,{class:`text-[11px] font-bold text-slate-400`,children:`차단율`}),W(`dd`,{class:`mt-1 mb-0 ml-0 text-sm font-extrabold text-slate-900`,children:Dv(e.queries,e.blocked)})]})]})]},e.id))}),p&&!i?W(`p`,{class:`mt-3 mb-0 text-xs font-semibold leading-5 text-slate-500`,children:[`전체 `,Y(r.length),`개 기기 중 차단 수 기준 상위 `,Tv,`개를 표시하고 있습니다.`]}):null,i&&r.length>Ev?W(`nav`,{"aria-label":`기기별 통계 페이지`,class:`mt-4 flex flex-wrap items-center justify-between gap-3`,children:[W(`p`,{class:`m-0 text-xs font-semibold text-slate-500`,children:[Y(u+1),`–`,Y(d),` /`,` `,Y(r.length),`개`]}),W(`div`,{class:`flex items-center gap-2`,children:[W(`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:`이전`}),W(`span`,{"aria-current":`page`,class:`min-w-20 text-center text-xs font-extrabold text-slate-600`,children:[Y(l),` / `,Y(c)]}),W(`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?W(`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:[`개별 통계는 최대 `,Y(t),`개 기기까지 추적합니다. 한도를 초과한 기기의 요청은 기타 기기로 합산됩니다.`]}):null]})}var jv=3600,Mv=24;function Nv(e,t){return e<=0?`0.0%`:`${(t/e*100).toFixed(1)}%`}function Pv(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/jv)*jv}function Fv(e){let t=new Map(e.hourly.map(e=>[e.bucketStart,e])),n=Pv(e)-23*jv;return Array.from({length:Mv},(e,r)=>{let i=n+r*jv;return t.get(i)??{bucketStart:i,queries:0,blocked:0}})}function Iv({emphasized:e=!1,label:t,value:n}){return W(`div`,{class:`rounded-xl bg-slate-50 p-4 sm:p-5`,children:[W(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:t}),W(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight ${e?`text-teal-700`:`text-slate-950`}`,children:n})]})}function Lv({action:e,data:t,error:n,loading:r,onRetry:i,onSetEnabled:a,refreshing:o}){if(r&&t===null)return W(b,{children:[W(J,{eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청과 차단 현황을 확인합니다.`}),W(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:W(`p`,{class:`m-0 text-sm font-bold text-slate-500`,children:`차단 통계를 불러오는 중입니다…`})})]});if(n&&t===null)return W(b,{children:[W(J,{eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청과 차단 현황을 확인합니다.`}),W(`section`,{class:`rounded-2xl border border-amber-200 bg-amber-50 p-5 sm:p-6`,children:[W(`p`,{class:`m-0 text-sm font-extrabold text-amber-950`,children:`차단 통계를 불러오지 못했습니다.`}),W(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-amber-800`,children:n}),W(`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 W(b,{children:[W(J,{eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청과 차단 현황을 확인합니다.`}),W(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:W(`p`,{class:`m-0 text-sm leading-6 text-slate-600`,children:`현재 SafeShield에서 통계 데이터를 제공하지 않습니다. SafeShield 0.3.15 버전 이상이 설치되어 있고, 통계 수집이 활성화되어 있는지 확인해 주세요.`})})]});let s=Fv(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 W(b,{children:[W(J,{action:W(`div`,{class:`flex items-center gap-3 rounded-xl border border-slate-100 bg-slate-50 px-3 py-2.5`,children:[W(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-500`,children:W(gn,{class:`size-5`})}),W(`div`,{class:`text-right`,children:[W(`p`,{class:`m-0 text-[0.68rem] font-bold text-slate-400`,children:`통계 수집`}),W(`p`,{class:`mt-1 mb-0 text-xs font-extrabold ${u||d?`text-emerald-700`:`text-slate-500`}`,role:`status`,children:f})]}),W(`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:W(`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?W(pn,{"aria-hidden":`true`,class:`size-3 animate-spin text-teal-600`}):null})})]}),eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청 원본은 저장하지 않고 숫자만 로컬 메모리에 집계합니다.`}),W(`section`,{"aria-busy":u,class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6 ${u?`cursor-wait`:``}`,children:[t.enabled?null:W(`div`,{class:`mb-4 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm leading-6 text-slate-600`,children:`통계 수집이 꺼져 있습니다. 활성화하면 DNS 요청 원본은 저장하지 않고 요청 수와 차단 수만 로컬 메모리에 집계합니다.`}),W(`dl`,{class:`mt-2 grid gap-3 sm:grid-cols-2 xl:grid-cols-4`,children:[W(Iv,{label:`최근 24시간 DNS 요청`,value:Y(l.queries)}),W(Iv,{emphasized:!0,label:`최근 24시간 차단`,value:Y(l.blocked)}),W(Iv,{label:`최근 24시간 차단율`,value:Nv(l.queries,l.blocked)}),W(Iv,{label:`현재 시간 차단`,value:Y(c?.blocked??0)})]}),W(`div`,{class:`mt-5 rounded-xl bg-slate-50/70 p-4 sm:p-5`,children:[W(`div`,{class:`flex flex-wrap items-end justify-between gap-3`,children:[W(`div`,{children:[W(`p`,{class:`m-0 text-xs font-bold text-slate-500`,children:`최근 24시간`}),W(`h3`,{class:`mt-1 mb-0 text-base font-extrabold text-slate-950`,children:`시간대별 차단 요청`})]}),W(`span`,{class:`text-xs font-bold text-slate-500`,children:o?`갱신 중…`:`마지막 집계 ${Ao(t.updatedAt)}`})]}),W(t_,{buckets:s})]}),W(Av,{deviceLimit:t.deviceLimit,devices:t.devices,truncated:t.devicesTruncated}),W(`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:[W(`span`,{children:[`수집 누적 DNS 요청 `,Y(t.totals.queries)]}),W(`span`,{children:[`수집 누적 차단 `,Y(t.totals.blocked)]}),W(`span`,{children:[`수집 시작 `,Ao(t.startedAt)]}),W(`span`,{children:[`보존 `,Y(t.retentionHours),`시간`]}),t.volatile?W(`span`,{children:`재부팅 시 초기화되는 메모리 통계`}):null]}),n?W(`p`,{class:`mt-3 mb-0 text-xs font-bold text-amber-700`,children:`최근 자동 갱신에 실패했습니다. 현재 표시된 마지막 정상 통계를 유지합니다.`}):null]})]})}function Rv({timestamp:e,fallback:t=`기록 없음`}){let[n,r]=L(()=>Date.now()/1e3),i=typeof e==`number`&&Number.isFinite(e)&&e>0&&e<=864e10;if(R(()=>{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 W(`span`,{children:t});let a=Ao(e);return W(`span`,{class:`inline-flex max-w-full flex-wrap items-center gap-1`,children:[W(`time`,{class:`min-w-0`,dateTime:new Date(e*1e3).toISOString(),title:a,children:Mo(e,n)}),W(`details`,{class:`group inline-flex max-w-full flex-wrap items-center gap-1 align-middle`,children:[W(`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:W(`svg`,{"aria-hidden":`true`,class:`size-3.5`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.8`,viewBox:`0 0 24 24`,children:[W(`circle`,{cx:`12`,cy:`12`,r:`9`}),W(`path`,{d:`M12 11v5m0-8h.01`,strokeLinecap:`round`})]})}),W(`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})]})]})}var zv=`https://www.smartsafehub.com/pricing/`;function Bv({value:e,trueLabel:t=`정상`,falseLabel:n=`확인 필요`}){return W(`span`,{class:`inline-flex items-center gap-2 text-sm font-extrabold ${e?`text-emerald-700`:`text-slate-500`}`,children:[W(`span`,{class:`size-2 rounded-full ${e?`bg-emerald-500`:`bg-slate-300`}`}),e?t:n]})}function Vv(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 Hv(e){return!e.enabled||e.status===`disabled`?`비활성화`:e.active&&e.runtime.dnsmasqRunning&&e.runtime.dnsRuntimeOk?`보호 중`:e.status===`paused`?`일시 중지`:`확인 필요`}function Uv(e){let t=Vv(e);return t===`error`&&Hv(e)===`보호 중`&&e.runtime.lastErrorCode?`갱신 실패`:{disabled:`비활성화`,refreshing:`갱신 중`,protecting:`보호 중`,paused:`일시 중지`,error:`오류`,attention:`확인 필요`}[t]}function Wv(e){return e?`자동 최적화`:`확인되지 않음`}function Gv(e){let t=No(e);return t===null?`확인되지 않음`:W(Rv,{timestamp:t})}function Kv(e){let t=Vv(e);return t===`protecting`?e.blocklist.validLineCount>0?`${Y(e.blocklist.validLineCount)}개 도메인 차단 규칙으로 DNS를 보호하고 있습니다.`:e.blocklist.installed?`차단 목록이 설치되어 있으며 DNS 보호가 정상적으로 동작하고 있습니다.`:`SafeShield 서비스와 DNS 런타임이 정상적으로 동작하고 있습니다.`:t===`refreshing`?`차단 목록을 최신 상태로 갱신하고 있습니다.`:t===`disabled`?`SafeShield DNS 보호가 비활성화되어 있습니다.`:t===`paused`?`SafeShield 작업이 일시 중지되어 있습니다.`:t===`error`?Hv(e)===`보호 중`?`기존 DNS 보호는 유지되고 있지만 최근 차단 목록 갱신을 완료하지 못했습니다.`:`SafeShield 보호 상태에 문제가 발생했습니다. 아래 오류 내용을 확인해 주세요.`:e.active?e.runtime.dnsmasqRunning?e.runtime.dnsRuntimeOk?e.summary.message:`SafeShield DNS 런타임 상태를 확인해야 합니다.`:`dnsmasq가 실행 중이 아니어서 DNS 보호 상태를 확인해야 합니다.`:`SafeShield가 활성화되어 있지만 서비스가 실행 중이 아닙니다.`}function qv(e){return{ok:`정상`,warning:`경고`,error:`오류`,unavailable:`사용 불가`}[e]??e}function Jv({data:e}){let t=Vv(e);return W(`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:Uv(e)})}function Yv({failed:e,step:t,total:n}){let r=2*Math.PI*17,i=r*.22,a=r-i;return W(`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:[W(`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:[W(`circle`,{class:`ssh-safeshield-refresh-loader-track`,cx:`24`,cy:`24`,fill:`none`,r:17,stroke:`currentColor`,strokeWidth:2.5}),e?null:W(`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})]}),W(`span`,{class:`ssh-safeshield-refresh-donut-label`,children:e?`!`:`${t}/${n}`})]})}function Xv({data:e}){let t=Wa(e.status,e.stage),n=Vv(e)===`error`;if(!t&&!n)return null;let r=Ga(e.stage),i=n?qa(e.runtime.lastErrorCode,e.stage):null;return W(`div`,{"aria-live":n?`assertive`:`polite`,class:`ssh-safeshield-refresh`,"data-state":n?`error`:`active`,role:n?`alert`:`status`,children:[W(Yv,{failed:n,step:r.number,total:r.total}),W(`div`,{class:`ssh-safeshield-refresh-copy`,children:[W(`p`,{class:`ssh-safeshield-refresh-kicker`,children:n?`갱신 실패 · ${r.number}/${r.total}`:`갱신 ${r.number}/${r.total}`}),W(`p`,{class:`ssh-safeshield-refresh-title`,children:n?`${r.label} 단계에서 문제가 발생했습니다.`:r.label}),W(`p`,{class:`ssh-safeshield-refresh-description`,children:i??r.description}),n&&e.runtime.lastErrorCode?W(`p`,{class:`ssh-safeshield-refresh-error-code`,children:[`오류 코드: `,W(`code`,{children:e.runtime.lastErrorCode})]}):null]})]})}function Zv({label:e,value:t}){return W(`div`,{class:`bg-white px-5 py-4 sm:px-6`,children:[W(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.14em] text-slate-400`,children:e}),W(`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 Qv(e){return e?.trim().toUpperCase()||`FREE`}function $v(e){return e===`FREE`?`free`:e===`PRO`?`pro`:e===`ULTIMATE`?`ultimate`:`paid`}function ey({compact:e=!1,plan:t}){let n=$v(t),r=n!==`free`;return W(`span`,{class:`ssh-safeshield-plan-badge`,"data-compact":e?`true`:`false`,"data-tier":n,title:r?`${t} 멤버십`:`무료 플랜`,children:[W(`span`,{"aria-hidden":`true`,class:`ssh-safeshield-plan-badge-mark`,children:r?`✦`:`•`}),W(`span`,{children:t})]})}function ty({plan:e}){let t=$v(e)===`free`;return W(`div`,{class:`bg-white px-5 py-4 sm:px-6`,children:[W(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.14em] text-slate-400`,children:`Plan`}),W(`dd`,{class:`mt-2 mb-0 ml-0 flex min-w-0 flex-wrap items-center gap-2`,children:[W(ey,{plan:e}),t?W(`span`,{class:`ssh-safeshield-plan-caption`,children:`기본 플랜`}):null]})]})}function ny(){return W(`div`,{class:`ssh-safeshield-upgrade-card`,children:[W(`span`,{"aria-hidden":`true`,class:`ssh-safeshield-upgrade-mark`,children:`✦`}),W(`div`,{class:`ssh-safeshield-upgrade-copy`,children:[W(`div`,{class:`ssh-safeshield-upgrade-meta`,children:[W(`span`,{children:`SmartSafeHub Membership`}),W(`span`,{class:`ssh-safeshield-upgrade-soon`,children:`Coming soon`})]}),W(`strong`,{children:`더 강력한 보호 기능을 준비하고 있습니다.`}),W(`span`,{children:`프리미엄 보호와 확장된 관리 기능을 미리 확인해 보세요.`})]}),W(`a`,{class:`ssh-safeshield-upgrade-action`,href:zv,rel:`noopener noreferrer`,target:`_blank`,children:[`요금제 보기`,W(`span`,{"aria-hidden":`true`,children:`→`})]})]})}function ry({children:e,eyebrow:t,icon:n,title:r}){return W(`article`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6 xl:p-4 2xl:p-5`,children:[W(`div`,{class:`flex min-w-0 items-start justify-between gap-4 xl:gap-2`,children:[W(`div`,{children:[W(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.16em] text-slate-400`,children:t}),W(`h3`,{class:`mt-2 mb-0 text-lg font-black tracking-tight text-slate-950 xl:text-base 2xl:text-lg`,children:r})]}),W(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-500 xl:size-9 2xl:size-10`,children:n})]}),W(`dl`,{class:`mt-5 mb-0 grid gap-3`,children:e})]})}function iy({label:e,value:t}){return W(`div`,{class:`flex min-w-0 items-center justify-between gap-2 border-b border-slate-100 pb-3 last:border-b-0 last:pb-0`,children:[W(`dt`,{class:`text-sm font-semibold text-slate-500`,children:e}),W(`dd`,{class:`m-0 min-w-0 break-words text-right text-sm font-extrabold text-slate-950`,children:t})]})}function ay({error:e,message:t,onDismiss:n}){if(!e&&!t)return null;let r=e!==null;return W(`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:[W(`div`,{class:`flex min-w-0 items-start gap-3`,children:[W(r?Cn:Sn,{class:`mt-0.5 size-5 shrink-0`}),W(`p`,{class:`m-0 font-semibold leading-6`,children:e??t})]}),W(`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 oy({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,_]=L(null);if(R(()=>{r?.available&&r.blocklist.validLineCount>0&&!Wa(r.status,r.stage)&&_(r.blocklist.validLineCount)},[r?.available,r?.blocklist.validLineCount,r?.stage,r?.status]),a)return W(vs,{});if(i)return W(ys,{message:i,onRetry:f});if(!r)return W(vs,{});if(!r.available)return W(`section`,{class:`rounded-2xl border border-amber-200 bg-amber-50 p-5 sm:p-6`,children:[W(`span`,{class:`grid size-11 place-items-center rounded-xl bg-amber-100 text-amber-800`,children:W(Cn,{class:`size-6`})}),W(`h2`,{class:`mt-5 mb-0 text-xl font-extrabold text-amber-950`,children:`SafeShield 상태 API를 찾을 수 없습니다`}),W(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-amber-900`,children:`safeshield 패키지와 rpcd ucode 플러그인이 설치되어 있는지 확인해 주세요.`})]});let v=r.enabled,y=Qv(r.entitlement.plan),x=r.status===`running`,S=e!==null,C=(Wa(r.status,r.stage)||Vv(r)===`error`)&&r.blocklist.validLineCount===0&&g!==null?g:r.blocklist.validLineCount,w=v?e===`disable`?`끄는 중…`:`보호 끄기`:e===`enable`?`켜는 중…`:`보호 켜기`;function T(){(!v||window.confirm(`보호를 끄면, 현재 차단 목록이 제거되어 위험에 노출될 수 있습니다. 그래도 계속하시겠습니까?`))&&m(!v)}return W(b,{children:[W(J,{eyebrow:`Protection`,title:`SafeShield 보호 상태`,description:`DNS 보호 상태와 적용 중인 차단 목록을 확인합니다.`}),W(`section`,{class:`mb-8 grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1.08fr)_minmax(0,0.92fr)] xl:items-stretch`,children:[W(`article`,{"aria-label":`보호 상태 요약`,class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6 xl:h-full`,children:[W(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between xl:flex-col 2xl:flex-row`,children:[W(`div`,{class:`flex min-w-0 items-start gap-3`,children:[W(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:W(qt,{class:`size-5`})}),W(`div`,{class:`min-w-0`,children:[W(`div`,{class:`flex flex-wrap items-center gap-2`,children:[W(`h3`,{class:`m-0 text-base font-black text-slate-950`,children:`현재 보호 상태`}),W(Jv,{data:r})]}),W(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:Kv(r)})]})]}),W(`div`,{class:`flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap xl:justify-end`,children:[W(`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:[W(mn,{class:`size-4 ${e===`enable`||e===`disable`?`animate-pulse`:``}`}),w]}),W(`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:[W(hn,{class:`size-4 ${e===`refresh`||x?`animate-bounce`:``}`}),e===`refresh`?`시작 중…`:x?`갱신 중…`:r.status===`error`?`다시 갱신`:`지금 갱신`]})]})]}),W(Xv,{data:r}),W(`dl`,{class:`mt-5 mb-0 grid min-w-0 grid-cols-2 overflow-hidden rounded-xl border border-slate-200 bg-slate-200`,children:[W(Zv,{label:`SafeShield`,value:r.version??`확인되지 않음`}),W(Zv,{label:`Blocklist`,value:`${Y(C)}개 도메인`}),W(Zv,{label:`Last refresh`,value:W(Rv,{timestamp:r.timestamps.lastSuccess})}),W(ty,{plan:y})]}),y===`FREE`?W(ny,{}):null]}),W(`section`,{"aria-label":`보호 데이터 요약`,class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6 xl:h-full`,children:[W(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[W(`div`,{class:`flex min-w-0 items-center gap-3`,children:[W(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-500`,children:W(gn,{class:`size-5`})}),W(`div`,{children:[W(`p`,{class:`m-0 text-[0.65rem] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Protection data`}),W(`h3`,{class:`mt-1 mb-0 text-sm font-black text-slate-950`,children:`보호 데이터`})]})]}),W(`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?W(Sn,{class:`size-3.5 shrink-0`}):W(Cn,{class:`size-3.5 shrink-0`}),r.artifact.resolved&&r.blocklist.installed&&r.blocklist.verificationOk?`최신`:`확인 필요`]})]}),W(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-500`,children:`SafeShield가 기기 성능에 맞춰 사용하는 보호 목록의 구성과 최신 상태를 확인합니다.`}),W(`dl`,{class:`mt-3 mb-0 grid grid-cols-1 gap-2 sm:grid-cols-2`,children:[W(`div`,{class:`min-w-0 rounded-lg border border-slate-200 bg-white px-3 py-2.5`,children:[W(`dt`,{class:`text-xs font-bold text-slate-500`,children:`기기 최적화`}),W(`dd`,{class:`mt-1 mb-0 text-sm font-extrabold text-slate-950`,children:Wv(r.artifact.tier)})]}),W(`div`,{class:`min-w-0 rounded-lg border border-slate-200 bg-white px-3 py-2.5`,children:[W(`dt`,{class:`text-xs font-bold text-slate-500`,children:`마지막 업데이트`}),W(`dd`,{class:`mt-1 mb-0 text-sm font-extrabold leading-5 text-slate-950`,children:Gv(r.artifact.version)})]})]}),W(`section`,{"aria-label":`보호 데이터 세부 정보`,class:`mt-3 border-t border-slate-200 pt-3`,children:[W(`h4`,{class:`m-0 text-xs font-extrabold text-slate-600`,children:`세부 정보`}),W(`dl`,{class:`mt-3 mb-0 grid gap-2`,children:[W(iy,{label:`데이터 프로필`,value:r.artifact.tier||`확인되지 않음`}),W(iy,{label:`데이터 버전`,value:r.artifact.version||`확인되지 않음`})]})]})]})]}),W(ay,{error:t,message:n,onDismiss:u}),W(Lv,{action:e,data:o,error:s,loading:c,onRetry:p,onSetEnabled:h,refreshing:l}),W(`section`,{class:`mt-7`,children:[W(J,{description:`DNS 보호 런타임, 차단 목록, 갱신 일정과 상태 점검 결과를 한곳에서 확인합니다.`,eyebrow:`Protection details`,title:`보호 구성`}),W(`div`,{class:`grid min-w-0 grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4`,children:[W(ry,{eyebrow:`Runtime`,icon:W(Sn,{class:`size-5`}),title:`DNS 보호 런타임`,children:[W(iy,{label:`SafeShield 서비스`,value:W(Bv,{falseLabel:`중지됨`,trueLabel:`동작 중`,value:r.active})}),W(iy,{label:`DNS 서비스`,value:W(Bv,{falseLabel:`중지됨`,value:r.runtime.dnsmasqRunning})}),W(iy,{label:`SafeShield DNS 연동`,value:W(Bv,{value:r.runtime.dnsRuntimeOk})}),W(iy,{label:`차단 목록 갱신`,value:W(Bv,{falseLabel:`중지됨`,value:r.runtime.refreshdRunning})})]}),W(ry,{eyebrow:`Blocklist`,icon:W(gn,{class:`size-5`}),title:`차단 목록`,children:[W(iy,{label:`적용 규칙`,value:`${Y(r.blocklist.validLineCount)}개 도메인`}),W(iy,{label:`파일 크기`,value:Do(r.blocklist.fileSizeKb*1024)}),W(iy,{label:`설치 상태`,value:W(Bv,{falseLabel:`미설치`,trueLabel:`설치됨`,value:r.blocklist.installed})}),W(iy,{label:`검증 상태`,value:W(Bv,{falseLabel:`확인 필요`,value:r.blocklist.verificationOk})})]}),W(ry,{eyebrow:`Refresh schedule`,icon:W(xn,{class:`size-5`}),title:`차단 목록 갱신`,children:[W(iy,{label:`마지막 성공`,value:W(Rv,{timestamp:r.timestamps.lastSuccess})}),W(iy,{label:`다음 갱신`,value:Ao(r.timestamps.nextRefreshAt)}),W(iy,{label:`갱신 주기`,value:Fo(r.timestamps.refreshIntervalS)}),W(iy,{label:`최근 결과`,value:r.runtime.lastResult||(x?`갱신 중`:`확인되지 않음`)})]}),W(ry,{eyebrow:`Health`,icon:W(qt,{class:`size-5`}),title:`상태 점검`,children:[W(iy,{label:`전체 상태`,value:qv(r.health.overall)}),W(iy,{label:`경고`,value:W(`span`,{class:`text-amber-700`,children:Y(r.issueCounts.warnings)})}),W(iy,{label:`오류`,value:W(`span`,{class:`text-red-700`,children:Y(r.issueCounts.errors)})}),W(iy,{label:`최근 오류 코드`,value:r.runtime.lastErrorCode||`없음`})]})]})]})]})}function sy(e,t,n,r){let i=t?e.filter(e=>e.includes(t)):e;if(!n)return{filteredRules:i,manualRules:i,managedRules:[]};let a=[],o=[];for(let e of i)(r.has(e)?o:a).push(e);return{filteredRules:i,manualRules:a,managedRules:o}}function cy(e,t){return t?e.filter(e=>e.title.toLowerCase().includes(t)||e.domains.some(e=>e.includes(t))):e}function ly(e,t){return e===t||e.length===t.length&&e.every((e,n)=>e===t[n])}function uy(e,t){return e===t||e.length===t.length&&e.every((e,n)=>{let r=t[n];return r!==void 0&&e.id===r.id&&e.title===r.title&&ly(e.domains,r.domains)})}function dy(e,t){return{domains:ly(e.domains,t.domains)?e.domains:t.domains,rules:uy(e.rules,t.rules)?e.rules:t.rules}}var fy=[],py=[];function my(e){return e.trim().toLowerCase().replace(/\.+$/,``)}function hy(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))}var gy=D_(function({action:e,kind:t,onAddRule:n,onDeleteRule:r,rules:i,recommendedDomainSet:a,recommendedKnown:o,recommendedRules:s,onRequestRevoke:c,revokeBusy:l}){let[u,d]=L(``),[f,p]=L(``),[m,h]=L(null),[g,_]=L(!0),[v,y]=L(null),b=t===`allow`,x=e!==null,S=Xe(()=>f.trim().toLowerCase(),[f]),C=S.length>0,{filteredRules:w,manualRules:T,managedRules:E}=Xe(()=>sy(i,S,b&&o,a),[i,S,b,o,a]),D=Xe(()=>b?cy(s,S):[],[b,s,S]),O=C||(v??(s.length||E.length)<=3),k=C||g;async function A(e){e.preventDefault();let r=my(u);hy(r)?(h(null),await n(t,r,()=>{d(e=>my(e)===r?``:e)})):h(`example.com 형식의 도메인을 입력해 주세요. URL, IP 주소, 와일드카드는 사용할 수 없습니다.`)}let j=b?{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:`등록된 차단 도메인이 없습니다.`};function M(n){let i=b&&o&&a.has(n),s=!b||o?i?`추천 규칙`:`직접 추가`:null,c=e?.operation===`delete`&&e.action===t&&e.domain===n;return W(`li`,{class:`flex min-w-0 items-center justify-between gap-3 px-3.5 py-3`,children:[W(`div`,{class:`flex min-w-0 flex-1 flex-wrap items-center gap-2`,children:W(`code`,{class:`min-w-0 break-all font-mono text-sm font-semibold text-slate-800`,children:n})}),i?W(`span`,{class:`shrink-0 rounded-full bg-teal-50 px-2.5 py-1 text-[11px] font-bold text-teal-800`,title:`추천 규칙의 적용과 해제는 웹사이트에서 관리합니다.`,children:`웹사이트 관리`}):W(`button`,{"aria-label":`${n} ${j.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:x||i,onClick:()=>void r(t,n),title:s===`직접 추가`?`직접 추가한 규칙 삭제`:`삭제`,type:`button`,children:W(Tn,{class:`size-4 ${c?`animate-pulse`:``}`})})]},n)}return W(`article`,{class:`min-w-0 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm shadow-slate-900/5`,children:[W(`div`,{class:`border-b border-slate-200 p-4 sm:p-6`,children:[W(`div`,{class:`flex min-w-0 items-start gap-3`,children:[W(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl ${j.icon}`,children:W(b?Sn:qt,{class:`size-6`})}),W(`div`,{class:`min-w-0`,children:[W(`div`,{class:`flex flex-wrap items-center gap-2`,children:[W(`h2`,{class:`m-0 text-xl font-extrabold tracking-tight text-slate-950`,children:j.label}),W(`span`,{class:`rounded-full px-2.5 py-1 text-xs font-extrabold ${j.badge}`,children:i.length})]}),W(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:j.description})]})]}),W(`form`,{class:`mt-5`,onSubmit:A,children:[W(`label`,{class:`block text-xs font-extrabold text-slate-600`,for:`${t}-domain`,children:`새 도메인`}),W(`div`,{class:`mt-2 flex flex-col gap-2 sm:flex-row`,children:[W(`input`,{"aria-describedby":m?`${t}-domain-error`:void 0,"aria-invalid":m?!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:x,id:`${t}-domain`,onInput:e=>d(e.currentTarget.value),placeholder:`example.com`,spellcheck:!1,type:`text`,value:u}),W(`button`,{class:`inline-flex min-h-11 w-full shrink-0 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 sm:w-auto ${j.button}`,disabled:x||u.trim().length===0,type:`submit`,children:[W(wn,{class:`size-4`}),e?.operation===`add`&&e.action===t?`추가 중…`:`추가`]})]}),m?W(`p`,{class:`mt-2 mb-0 text-xs font-semibold leading-5 text-red-700`,id:`${t}-domain-error`,children:m}):null]})]}),W(`div`,{class:`p-4 sm:p-6`,children:[W(`label`,{class:`relative block`,for:`${t}-search`,children:[W(`span`,{class:`sr-only`,children:[j.label,` 검색`]}),W(`span`,{class:`pointer-events-none absolute inset-y-0 left-0 flex w-11 items-center justify-center text-slate-400`,children:W(En,{class:`size-4`})}),W(`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=>p(e.currentTarget.value),placeholder:`${j.label} 검색`,type:`search`,value:f})]}),b&&o?W(`div`,{class:`mt-4 overflow-hidden rounded-xl border border-slate-200`,children:[W(`section`,{"aria-label":`직접 추가한 허용 규칙`,children:[W(`button`,{"aria-controls":`allow-manual-domains`,"aria-expanded":k,class:`flex min-h-14 w-full items-center justify-between gap-3 ssh-rule-group-toggle bg-slate-50 px-4 py-3 text-left focus-visible:outline-2 focus-visible:outline-teal-500`,onClick:()=>_(!k),type:`button`,children:[W(`span`,{class:`min-w-0 text-sm font-extrabold text-slate-800`,children:[`직접 추가한 규칙 `,W(`span`,{class:`ml-1 rounded-full bg-white px-2 py-0.5 text-xs font-bold text-slate-600`,children:[T.length,C?`개 검색됨`:`개`]})]}),W(`span`,{"aria-hidden":`true`,class:`shrink-0 text-sm font-bold text-slate-500`,children:k?`접기 −`:`펼치기 +`})]}),W(`div`,{hidden:!k,id:`allow-manual-domains`,children:T.length?W(`ul`,{class:`m-0 max-h-[20rem] list-none divide-y divide-slate-200 overflow-y-auto p-0`,children:T.map(M)}):W(`p`,{class:`m-0 px-4 py-5 text-sm text-slate-500`,children:C?`검색 결과가 없습니다.`:`직접 추가한 허용 도메인이 없습니다.`})})]}),W(`section`,{"aria-label":`웹사이트 추천 규칙`,class:`border-t border-slate-200`,children:[W(`button`,{"aria-controls":`allow-recommended-domains`,"aria-expanded":O,class:`flex min-h-14 w-full items-center justify-between gap-3 ssh-rule-group-toggle bg-slate-50 px-4 py-3 text-left focus-visible:outline-2 focus-visible:outline-teal-500`,onClick:()=>y(!O),type:`button`,children:[W(`span`,{class:`min-w-0 text-sm font-extrabold text-slate-800`,children:[`추천 규칙 `,W(`span`,{class:`ml-1 rounded-full bg-white px-2 py-0.5 text-xs font-bold text-teal-800`,children:[!C&&s.length?s.length:E.length,C?`개 검색됨`:`개`]})]}),W(`span`,{"aria-hidden":`true`,class:`shrink-0 text-sm font-bold text-slate-500`,children:O?`접기 −`:`펼치기 +`})]}),W(`div`,{hidden:!O,id:`allow-recommended-domains`,children:[D.length?W(`ul`,{class:`m-0 max-h-[22rem] list-none divide-y divide-slate-200 overflow-y-auto p-0`,children:D.map(e=>W(`li`,{class:`flex min-w-0 flex-wrap items-center justify-between gap-3 px-3.5 py-3`,children:[W(`div`,{class:`min-w-0 flex-1`,children:[W(`strong`,{class:`block break-words text-sm text-slate-900`,children:e.title}),W(`div`,{class:`mt-1 flex flex-wrap gap-1.5`,children:e.domains.map(e=>W(`code`,{class:`break-all text-xs text-slate-600`,children:e},e))})]}),c?W(`button`,{class:`shrink-0 rounded-lg border border-red-200 px-3 py-2 text-xs font-bold text-red-700 hover:bg-red-50 disabled:opacity-50`,disabled:l||x,onClick:()=>c(e),type:`button`,children:`적용 해제`}):W(`span`,{class:`text-xs text-slate-500`,children:`웹사이트 관리`})]},e.id))}):E.length?W(`ul`,{class:`m-0 max-h-[20rem] list-none divide-y divide-slate-200 overflow-y-auto p-0`,children:E.map(M)}):W(`p`,{class:`m-0 px-4 py-5 text-sm text-slate-500`,children:C?`검색 결과가 없습니다.`:`추천으로 추가된 허용 도메인이 없습니다.`}),C?null:W(`p`,{class:`m-0 border-t border-slate-100 px-4 py-3 text-xs leading-5 text-slate-500`,children:`추천 규칙은 이 공유기에서 적용 해제할 수 있습니다. 새 규칙의 적용은 SmartSafeHub 웹사이트에서 관리해 주세요.`})]})]})]}):W(`div`,{class:`mt-4 max-h-[28rem] overflow-y-auto rounded-xl border border-slate-200`,children:w.length?W(`ul`,{class:`m-0 list-none divide-y divide-slate-200 p-0`,children:w.map(M)}):W(`div`,{class:`px-5 py-10 text-center`,children:[W(Dn,{class:`mx-auto size-8 text-slate-300`}),W(`p`,{class:`mt-3 mb-0 text-sm font-bold text-slate-600`,children:C?`검색 결과가 없습니다.`:j.empty})]})})]})]})});function _y({action:e,data:t,error:n,feedback:r,loading:i,onAddRule:a,onDeleteRule:o,onDismissFeedback:s,onRetry:c,onLoadRecommendedRulesStatus:l,onSyncRecommendedRules:u,onRevokeRecommendedRule:d}){let[f,p]=L(null),[m,h]=L(!1),[g,_]=L(!1),[v,y]=L(null),[x,S]=L(!1),[C,w]=L(!1),[T,E]=L(!1),D=z(null),O=z(null),k=z(null),A=z(c);A.current=c;let[j,M]=L(null),[N,P]=L(null),F=f?.domains??fy,I=f?.rules??py,ee=Xe(()=>new Set(F),[F]),te=B(e=>{p(t=>{if(!t)return e;let{domains:n,rules:r}=dy(t,e);return{...e,domains:n,rules:r}}),h(!0);let t=JSON.stringify([e.revision,e.domains]),n=!1;return e.syncStatus===`confirmed`&&k.current!==t&&(O.current!==null&&(A.current(),n=!0),k.current=t),O.current=t,D.current!==null&&e.revokeOperation?.phase===`failed`&&e.revokeOperation.argument===String(D.current)&&(D.current=null,w(!1),E(!1),P(`추천 규칙 적용 해제를 완료하지 못했습니다. 연결 상태를 확인하고 다시 시도해 주세요.`)),e.syncStatus===`confirmed`&&D.current===null&&e.revokeOperation?.phase!==`failed`&&P(null),D.current!==null&&e.syncStatus===`confirmed`&&!e.rules.some(e=>e.id===D.current)&&(D.current=null,w(!1),E(!1),P(null),M(`추천 규칙 적용 해제를 확인했습니다.`)),n},[]);R(()=>{if(!l)return;let e=l,t=!0,n=!1;function r(){n||g||x||(n=!0,e().then(e=>{t&&te(e)}).catch(()=>{}).finally(()=>{n=!1}))}r();let i=window.setInterval(r,15e3),a=()=>r(),o=()=>{document.hidden||r()};return window.addEventListener(`focus`,a),document.addEventListener(`visibilitychange`,o),()=>{t=!1,window.clearInterval(i),window.removeEventListener(`focus`,a),document.removeEventListener(`visibilitychange`,o)}},[l,g,x,te]),R(()=>{if(!C)return;let e=window.setTimeout(()=>E(!0),6e4);return()=>window.clearTimeout(e)},[C]);async function ne(){if(!(!u||g||x)){_(!0),P(null),M(null);try{let e=await u(),t=te(e);M(e.syncStatus===`confirmed`?`서버에서 규칙을 가져와 SafeShield 허용 목록을 확인했습니다.`:`추천 규칙 상태를 확인 중입니다. 진행 결과는 자동으로 갱신됩니다.`),e.syncStatus===`confirmed`&&!t&&c()}catch{P(`추천 규칙을 동기화하지 못했습니다. 서버 연결 및 기기 등록 상태를 확인해 주세요.`)}finally{_(!1)}}}async function re(){if(!v||x||!d)return;let e=v.id;D.current=e,S(!0),w(!0),E(!1),P(null),M(null);try{let t=await d(e);te(t),y(null),D.current!==null&&M(`적용 해제 요청을 처리했습니다. 공유기의 허용 목록을 확인하고 있습니다.`)}catch{y(null),P(`적용 해제 결과를 아직 확인하지 못했습니다. 공유기의 상태를 자동으로 다시 확인합니다.`);try{let e=await l?.();e&&te(e)}catch{}}finally{S(!1)}}let ie=f?.syncStage,ae=f?.syncStatus===`pending`,oe=f?.syncStatus===`delayed`,se=f?.syncStatus===`failed`,ce=ie===`server_sync`?`서버 동기화 중`:ie===`safeshield_refreshing`?`SafeShield 갱신 중`:ie===`applying`?`추천 규칙 적용 중`:ie===`verifying`?`허용 목록 확인 중`:oe?`재확인 지연`:se?`동기화 실패`:ae?`동기화 대기`:`허용 목록 확인`,le=ie===`safeshield_refreshing`&&ae?`SafeShield가 보호 목록을 갱신하고 있습니다. 갱신이 끝나면 추천 규칙을 자동으로 다시 확인합니다.`:ie===`applying`&&ae?`추천 규칙을 SafeShield 허용 목록에 반영하고 있습니다.`:ie===`verifying`&&ae?`SafeShield 허용 목록에서 추천 규칙 반영 결과를 확인하고 있습니다.`:ie===`server_sync`&&ae?`SmartSafeHub 서버에서 최신 추천 규칙을 확인하고 있습니다.`:oe?`서버 응답이 지연되고 있습니다. 이미 확인한 허용 규칙은 유지되며 자동으로 다시 시도합니다.`:se?f?.syncReason===`auth_rejected`?`SmartSafeHub 기기 인증이 거부되었습니다. 공유기 계정 연결 상태를 확인해 주세요.`:f?.syncReason===`api_rejected`?`서버가 추천 규칙 요청을 거부했습니다. Hub API 배포 상태를 확인해 주세요.`:f?.syncReason===`invalid_response`?`서버 응답을 확인할 수 없습니다. 규칙은 유지되며 다음 동기화에서 다시 확인합니다.`:`추천 규칙 반영을 완료하지 못했습니다. 다음 자동 동기화에서 다시 시도합니다.`:ae?`추천 규칙의 적용 결과를 기다리고 있습니다.`:null,ue=C&&D.current!==null?{tone:`amber`,label:T?`적용 해제 처리 지연`:`적용 해제 확인 중`,description:T?`적용 해제 확인이 지연되고 있습니다. SafeShield 갱신 중이라면 완료 후 자동으로 재확인합니다.`:le??`적용 해제 상태를 확인하고 있습니다. 확인되면 목록과 상태가 자동 갱신됩니다.`}:ae||oe||se?{tone:se?`red`:`amber`,label:ce,description:le??``}:N?{tone:`red`,label:`상태 확인 필요`,description:N}:f?.syncWarning?{tone:`amber`,label:`서버 재확인 지연`,description:`이미 확인한 허용 규칙은 유지되며 자동으로 다시 시도합니다.`}:j?{tone:`green`,label:`허용 목록 확인`,description:j}:null;return i&&!t?W(vs,{}):n&&!t?W(ys,{message:n,onRetry:c}):t?W(b,{children:[v?W(`div`,{class:`fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70 p-4`,role:`presentation`,onClick:()=>{x||y(null)},children:W(`section`,{"aria-labelledby":`recommended-revoke-title`,"aria-modal":`true`,class:`w-full max-w-md rounded-2xl border border-slate-200 bg-white p-5 shadow-xl`,onClick:e=>e.stopPropagation(),role:`dialog`,children:[W(`h2`,{class:`m-0 text-lg font-extrabold text-slate-950`,id:`recommended-revoke-title`,children:`추천 규칙 적용 해제`}),W(`p`,{class:`mt-3 text-sm text-slate-700`,children:[W(`strong`,{children:v.title}),` 규칙을 이 공유기에서 해제할까요?`]}),W(`p`,{class:`mt-2 text-xs leading-5 text-slate-600`,children:[`포함된 도메인 `,v.domains.length,`개를 다시 확인합니다. 직접 추가한 허용 규칙과 다른 추천 규칙에 필요한 도메인은 유지됩니다.`]}),W(`div`,{class:`mt-5 flex justify-end gap-2`,children:[W(`button`,{class:`rounded-lg border border-slate-200 px-4 py-2 text-sm font-bold text-slate-700 disabled:cursor-wait disabled:opacity-50`,disabled:x,onClick:()=>y(null),type:`button`,children:`취소`}),W(`button`,{class:`inline-flex min-w-24 items-center justify-center gap-2 rounded-lg bg-red-700 px-4 py-2 text-sm font-bold text-white transition hover:bg-red-800 disabled:cursor-progress disabled:bg-red-700 disabled:opacity-80`,"aria-busy":x,disabled:x,onClick:()=>void re(),type:`button`,children:[x?W(`svg`,{class:`size-4 animate-spin`,"aria-hidden":`true`,viewBox:`0 0 24 24`,fill:`none`,children:[W(`circle`,{cx:`12`,cy:`12`,r:`9`,stroke:`currentColor`,"stroke-width":`3`,opacity:`0.25`}),W(`path`,{d:`M21 12a9 9 0 0 0-9-9`,stroke:`currentColor`,"stroke-width":`3`,"stroke-linecap":`round`})]}):null,W(`span`,{role:`status`,children:x?`해제 중…`:`적용 해제`})]})]})]})}):null,W(J,{eyebrow:`Local overrides`,title:`사용자 도메인 규칙`,description:`허용 목록은 오탐을 해제하고, 차단 목록은 원하는 도메인을 SafeShield 보호 목록에 추가합니다.`}),W(`section`,{"aria-label":`규칙 개수 요약`,class:`min-w-0`,children:W(`dl`,{class:`m-0 grid grid-cols-1 gap-3 sm:grid-cols-3`,children:[W(`div`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white px-5 py-4 shadow-sm shadow-slate-900/5`,children:[W(`dt`,{class:`text-sm font-extrabold text-emerald-700`,children:`허용 도메인`}),W(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight text-slate-950`,children:[t.counts.allow,W(`span`,{class:`ml-1 text-sm font-bold text-slate-500`,children:`개`})]}),W(`p`,{class:`mt-1 mb-0 text-xs text-slate-500`,children:`차단하지 않도록 등록한 도메인`})]}),W(`div`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white px-5 py-4 shadow-sm shadow-slate-900/5`,children:[W(`dt`,{class:`text-sm font-extrabold text-red-700`,children:`차단 도메인`}),W(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight text-slate-950`,children:[t.counts.block,W(`span`,{class:`ml-1 text-sm font-bold text-slate-500`,children:`개`})]}),W(`p`,{class:`mt-1 mb-0 text-xs text-slate-500`,children:`추가로 차단하도록 등록한 도메인`})]}),W(`div`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white px-5 py-4 shadow-sm shadow-slate-900/5`,children:[W(`dt`,{class:`text-sm font-extrabold text-slate-600`,children:`전체 등록 규칙`}),W(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight text-slate-950`,children:[t.counts.total,W(`span`,{class:`ml-1 text-sm font-bold text-slate-500`,children:`개`})]}),W(`p`,{class:`mt-1 mb-0 text-xs text-slate-500`,children:`허용 및 차단 규칙의 합계`})]})]})}),W(`div`,{class:`mt-7`,children:W(J,{eyebrow:`Recommendations`,title:`추천 사용자 규칙`,description:`웹사이트에서 선택한 권장 허용 규칙의 동기화 상태를 확인하고 관리합니다.`})}),W(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,"aria-label":`추천 사용자 규칙`,children:[W(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between`,children:[W(`div`,{class:`min-w-0 flex-1`,children:[W(`div`,{class:`flex flex-wrap items-center gap-2`,children:[W(`span`,{class:`grid size-8 place-items-center rounded-lg bg-teal-50 text-teal-700`,children:W(Sn,{class:`size-4`})}),W(`h3`,{class:`m-0 text-base font-extrabold text-slate-950`,children:`동기화 상태`}),l?W(`span`,{"aria-live":`polite`,class:`rounded-full px-2.5 py-1 text-xs font-bold ${f?.syncStatus===`confirmed`?`bg-emerald-100 text-emerald-800`:f?.syncStatus===`failed`?`bg-red-100 text-red-800`:`bg-amber-100 text-amber-800`}`,children:f?.syncStatus===`confirmed`?`허용 목록 확인`:ce}):null]}),W(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:`웹사이트에서 선택한 허용 규칙을 약 30초마다 확인합니다. 적용 해제는 이 공유기에서도 할 수 있습니다.`}),W(`div`,{class:`mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs font-semibold text-slate-600`,children:[W(`span`,{children:[`추천으로 추가 `,W(`strong`,{class:`text-teal-800`,children:[m?F.length:`—`,`개 도메인`]})]}),f?.syncStatus===`confirmed`?W(`span`,{children:[`허용 확인 `,W(`strong`,{class:`text-emerald-800`,children:[f.confirmedDomains.length,`개`]})]}):null,f?.lastCheckedAt?W(`span`,{children:[f.syncWarning?`최근 재확인 시도`:`최근 확인`,`: `,W(Rv,{timestamp:f.lastCheckedAt})]}):null]})]}),W(`div`,{class:`flex w-full flex-wrap gap-2 sm:w-auto lg:shrink-0`,children:[u?W(`button`,{class:`inline-flex min-h-10 flex-1 items-center justify-center 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 sm:flex-none`,disabled:g,onClick:()=>void ne(),type:`button`,children:g?`동기화 중…`:`지금 동기화`}):null,W(`a`,{class:`inline-flex min-h-10 flex-1 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:flex-none`,href:`https://www.smartsafehub.com/dashboard/recommended-rules/`,target:`_blank`,rel:`noopener noreferrer`,children:`추천 규칙 보기 ↗`})]})]}),ue?W(`div`,{"aria-live":`polite`,class:`mt-3 flex items-start gap-2 rounded-xl px-3 py-2.5 text-sm ${ue.tone===`red`?`bg-red-50 text-red-800`:ue.tone===`green`?`bg-emerald-50 text-emerald-800`:`bg-amber-50 text-amber-800`}`,role:`status`,children:[W(`span`,{"aria-hidden":`true`,class:`mt-1 size-2 shrink-0 rounded-full ${ue.tone===`red`?`bg-red-500`:ue.tone===`green`?`bg-emerald-500`:`bg-amber-500`}`}),W(`div`,{class:`min-w-0`,children:[W(`strong`,{children:ue.label}),W(`p`,{class:`mt-1 mb-0 leading-5`,children:ue.description})]})]}):null,W(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`추천 규칙 동기화는 SafeShield 차단 목록 갱신과 별개입니다. 허용 목록 확인은 DNS 처리나 서비스 재생 성공을 보장하지 않습니다.`})]}),t.applyLocalOverrides?null:W(`section`,{class:`mt-4 flex items-start gap-3 rounded-2xl border border-amber-200 bg-amber-50 p-4`,role:`alert`,children:[W(Cn,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),W(`div`,{children:[W(`p`,{class:`m-0 text-sm font-extrabold text-amber-950`,children:`로컬 규칙 적용이 꺼져 있습니다`}),W(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:`규칙은 저장되지만 SafeShield의 apply_local_overrides 설정을 켜기 전까지 DNS에 반영되지 않습니다.`})]})]}),t.safeshieldEnabled?null:W(`section`,{class:`mt-4 flex items-start gap-3 rounded-2xl border border-slate-200 bg-slate-100 p-4`,role:`status`,children:[W(qt,{class:`mt-0.5 size-5 shrink-0 text-slate-600`}),W(`div`,{children:[W(`p`,{class:`m-0 text-sm font-extrabold text-slate-950`,children:`SafeShield가 비활성화되어 있습니다`}),W(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`규칙을 미리 저장할 수 있으며, SafeShield를 켜고 차단 목록을 갱신하면 적용됩니다.`})]})]}),r?W(`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:[W(`div`,{class:`flex min-w-0 items-start gap-3`,children:[W(Sn,{class:`mt-0.5 size-5 shrink-0 text-teal-700`}),W(`p`,{class:`m-0 text-sm font-semibold leading-6 text-teal-950`,children:r})]}),W(`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?W(`section`,{"aria-live":`assertive`,class:`mt-4 flex items-start gap-3 rounded-2xl border border-red-200 bg-red-50 p-4`,children:[W(Cn,{class:`mt-0.5 size-5 shrink-0 text-red-700`}),W(`p`,{class:`m-0 text-sm font-semibold leading-6 text-red-900`,children:n})]}):null,W(`section`,{class:`mt-5 grid gap-5 xl:grid-cols-2`,children:[W(gy,{action:e,kind:`allow`,onAddRule:a,onDeleteRule:o,rules:t.allow,recommendedDomainSet:ee,recommendedKnown:m,recommendedRules:I,...d?{onRequestRevoke:y}:{},revokeBusy:x}),W(gy,{action:e,kind:`block`,onAddRule:a,onDeleteRule:o,rules:t.block,recommendedDomainSet:ee,recommendedKnown:m,recommendedRules:I,revokeBusy:x})]})]}):W(ys,{message:`사용자 규칙 응답이 비어 있습니다.`,onRetry:c})}function vy(e,t){return U(`smartsafehub`,`system_root_password_change`,{current_password:e,new_password:t})}var yy=Array.from({length:24},(e,t)=>{let n=String(t).padStart(2,`0`);return{value:n,label:`${n}시`}}),by=Array.from({length:60},(e,t)=>{let n=String(t).padStart(2,`0`);return{value:n,label:`${n}분`}});function xy(e){let t=/^([01]\d|2[0-3]):([0-5]\d)$/.exec(e);return{hour:t?.[1]??`03`,minute:t?.[2]??`00`}}function Sy({ariaLabel:e,className:t=``,disabled:n=!1,id:r,onChange:i,value:a,variant:o=`default`}){let{hour:s,minute:c}=xy(a),l=(e,t)=>{i(`${e}:${t}`)};return W(`div`,{"aria-label":e,class:`ssh-time-select ${t}`.trim(),"data-time-select":r,role:`group`,children:[W(W_,{ariaLabel:`${e} 시`,disabled:n,id:`${r}-hour`,onChange:e=>l(e,c),options:yy,value:s,variant:o}),W(W_,{ariaLabel:`${e} 분`,disabled:n,id:`${r}-minute`,onChange:e=>l(s,e),options:by,value:c,variant:o})]})}function Cy(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone||null}catch{return null}}function wy(e){return W(`article`,{class:`h-full min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6 ${e.className??``}`,children:[W(`div`,{class:`flex min-w-0 items-start justify-between gap-3`,children:[W(`div`,{class:`flex min-w-0 items-start gap-4`,children:[W(`div`,{class:`flex size-11 shrink-0 items-center justify-center rounded-xl bg-slate-100 text-slate-600`,children:e.icon}),W(`div`,{class:`min-w-0`,children:[W(`div`,{class:`flex flex-wrap items-center gap-2`,children:[W(`h2`,{class:`m-0 text-lg font-black text-slate-950`,children:e.title}),e.titleActions]}),W(`p`,{class:`mt-1.5 mb-0 text-sm leading-6 text-slate-500`,children:e.description})]})]}),e.headerActions?W(`div`,{class:`shrink-0`,children:e.headerActions}):null]}),W(`div`,{class:`mt-5`,children:e.children})]})}function Ty(e){let[t,n]=L(``),[r,i]=L(e.data?.localtime??0),a=Cy();R(()=>{e.data?.zonename&&n(e.data.zonename)},[e.data?.zonename]),R(()=>{let t=e.data?.localtime??0,n=Date.now();if(i(t),!Number.isFinite(t)||t<=0)return;let r=window.setInterval(()=>{i(t+Math.floor((Date.now()-n)/1e3))},1e3);return()=>window.clearInterval(r)},[e.data?.localtime]);let o=e.data?Object.keys(e.data.timezones).sort((e,t)=>e.localeCompare(t)):[],s=Po(r,e.data?.zonename??`UTC`),c=a!==null&&e.data?.timezones[a]!==void 0,l=e.data!==null&&t.length>0&&t!==e.data.zonename;return W(b,{children:[W(wy,{description:`공유기의 기준 시간대를 설정합니다. 로그, 통계, 자동 설치와 예약 재부팅 시각도 이 시간대를 기준으로 동작합니다.`,icon:W(dn,{class:`size-5`}),title:`시간 및 시간대`,children:[(e.saveError||e.saveMessage)&&W(`div`,{class:`mb-5 flex min-w-0 items-start justify-between gap-3 rounded-xl border px-4 py-3 text-sm font-bold ${e.saveError?`border-rose-200 bg-rose-50 text-rose-800`:`border-emerald-200 bg-emerald-50 text-emerald-800`}`,children:[W(`span`,{class:`min-w-0`,children:e.saveError||e.saveMessage}),W(`button`,{class:`shrink-0 rounded-lg px-2 py-1 text-xs font-extrabold hover:bg-black/5`,onClick:e.onDismissFeedback,type:`button`,children:`닫기`})]}),e.error&&!e.data?W(`div`,{class:`rounded-xl border border-rose-200 bg-rose-50 p-4`,children:[W(`p`,{class:`m-0 text-sm font-bold text-rose-800`,children:e.error}),W(`button`,{class:`mt-3 inline-flex min-h-10 items-center rounded-xl border border-rose-300 bg-white px-3 py-2 text-xs font-extrabold text-rose-700 transition hover:bg-rose-100`,onClick:e.onRetry,type:`button`,children:`다시 불러오기`})]}):W(b,{children:[W(`div`,{class:`grid min-w-0 grid-cols-1 gap-3 sm:grid-cols-2`,children:[W(`div`,{class:`rounded-xl bg-slate-50 p-4`,children:[W(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.12em] text-slate-500`,children:`현재 시간`}),W(`p`,{class:`mt-2 mb-0 text-sm font-extrabold leading-6 text-slate-900`,children:s}),W(`p`,{class:`mt-1 mb-0 break-words text-xs leading-5 text-slate-500`,children:e.data?.zonename??`시간대 확인 중`})]}),W(`div`,{class:`rounded-xl bg-slate-50 p-4`,children:[W(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.12em] text-slate-500`,children:`시간 동기화`}),W(`p`,{class:`mt-2 mb-0 text-sm font-extrabold ${e.data?.ntpEnabled?`text-emerald-700`:`text-amber-700`}`,children:e.data?.ntpEnabled?`자동 동기화 설정됨`:`자동 동기화 꺼짐`}),W(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`장치의 기본 NTP 설정 상태입니다.`}),W(`button`,{class:`mt-3 inline-flex min-h-9 items-center justify-center gap-2 rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-extrabold text-slate-700 transition hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-50`,disabled:!e.data?.ntpEnabled||e.saving||e.syncing,onClick:()=>void e.onSync(),type:`button`,children:e.syncing?`동기화 요청 중`:`지금 동기화`})]})]}),W(`div`,{class:`mt-5`,children:[W(`label`,{class:`mb-2 block text-sm font-extrabold text-slate-800`,for:`smartsafehub-timezone`,children:`시간대`}),W(W_,{ariaLabel:`시간대`,disabled:e.loading||e.saving||e.syncing||!e.data,id:`smartsafehub-timezone`,onChange:n,options:e.data?o.map(e=>({value:e,label:e})):[{value:``,label:`시간대 불러오는 중`}],value:t}),W(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-500`,children:`시간대를 변경하면 시스템에 즉시 적용되며 자동 설치와 예약 재부팅 일정도 새 기준 시간으로 다시 계산합니다.`})]}),W(`div`,{class:`mt-4 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:justify-end`,children:[c&&a!==t&&W(`button`,{class:`inline-flex min-h-11 w-full items-center justify-center rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-extrabold text-slate-700 transition hover:bg-slate-50 disabled:opacity-60 sm:w-auto`,disabled:e.saving||e.syncing,onClick:()=>n(a),type:`button`,children:`브라우저 시간대 사용`}),W(`button`,{class:`inline-flex min-h-11 w-full items-center justify-center rounded-xl bg-teal-700 px-4 py-2.5 text-sm font-extrabold text-white transition hover:bg-teal-800 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto`,disabled:!l||e.saving||e.syncing,onClick:()=>void e.onSave(t),type:`button`,children:e.saving?`시간대 저장 중`:`시간대 저장`})]})]})]}),W(`div`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:W(Ly,{data:e.scheduledRebootData,error:e.scheduledRebootError,loading:e.scheduledRebootLoading,onDismissFeedback:e.onDismissScheduledRebootFeedback,onRetry:e.onRetry,onSave:e.onSaveScheduledReboot,saveError:e.scheduledRebootSaveError,saveMessage:e.scheduledRebootSaveMessage,saving:e.scheduledRebootSaving})})]})}function Ey(e){return e===`initializing`?{badge:`bg-teal-100 text-teal-800`,panel:`border-teal-200 bg-teal-50`,label:`준비 중`}:e===`critical`?{badge:`bg-rose-100 text-rose-800`,panel:`border-rose-200 bg-rose-50`,label:`이상`}:e===`warning`?{badge:`bg-amber-100 text-amber-800`,panel:`border-amber-200 bg-amber-50`,label:`주의`}:e===`unknown`?{badge:`bg-slate-200 text-slate-700`,panel:`border-slate-200 bg-slate-50`,label:`확인 불가`}:{badge:`bg-emerald-100 text-emerald-800`,panel:`border-emerald-200 bg-emerald-50`,label:`정상`}}function Dy(e){if(!e.enabled)return`보고 중지됨`;switch(e.lastResult){case`reported`:return`정상 보고`;case`initializing`:return e.lastReportAt>0?`상태 갱신 중`:`첫 보고 진행 중`;case`failed`:return`최근 보고 실패`;case`idle`:return`다음 보고 대기`;case`ineligible`:return`멤버십 확인 필요`;case`disabled`:case`never`:return`첫 보고 준비 중`;default:return`보고 준비 중`}}function Oy(e){return e.lastReportAt>0?jo(e.lastReportAt):e.enabled?`첫 보고 대기 중`:`보고 기록 없음`}function ky(e){let t=e?.plan?.trim().toUpperCase(),n=e?.entitlementStatus?.trim().toLowerCase();return t&&(n===`trial`||n===`trialing`)?`${t} · 체험`:t||`유료 · Trial`}function Ay(e){let t=e.data,n=Ey(t?.overall??`unknown`),r=t?.checks.filter(e=>e.status!==`ok`)??[],i=r.length>0?r.slice(0,4):t?.checks.filter(e=>[`system.memory`,`network.wan`,`service.dnsmasq`,`safeshield.runtime`].includes(e.id))??[],a=t?.reporter,o=a?.enabled===!0,s=a?.eligible===!0||o,c=e.savingReporter?o?`원격 상태 보고를 켜고 있습니다.`:`원격 상태 보고를 끄고 있습니다.`:o?`원격 상태 보고가 켜져 있습니다.`:`원격 상태 보고가 꺼져 있습니다.`,l=o?`장치 상태가 바뀌면 즉시 보고하고, 정상 상태에서도 주기적으로 SmartSafeHub 서버에 보고합니다.`:`로컬 진단은 계속 동작하지만 장치 상태는 SmartSafeHub 서버에 자동으로 전송되지 않습니다.`;return W(b,{children:[W(wy,{description:`SmartSafeHub가 장치 상태를 직접 점검하고 이상 항목을 알려줍니다.`,icon:W(hn,{class:`size-5`}),title:`장치 진단`,children:[(e.actionError||e.actionMessage)&&W(`div`,{class:`mb-4 flex items-start justify-between gap-3 rounded-xl border px-4 py-3 text-sm font-bold ${e.actionError?`border-rose-200 bg-rose-50 text-rose-800`:`border-emerald-200 bg-emerald-50 text-emerald-800`}`,children:[W(`span`,{children:e.actionError||e.actionMessage}),W(`button`,{class:`shrink-0 rounded-lg px-2 py-1 text-xs font-extrabold hover:bg-black/5`,onClick:e.onDismissFeedback,type:`button`,children:`닫기`})]}),e.error&&!t?W(`div`,{class:`rounded-xl border border-rose-200 bg-rose-50 p-4`,children:[W(`p`,{class:`m-0 text-sm font-bold text-rose-800`,children:e.error}),W(`button`,{class:`mt-3 inline-flex min-h-10 items-center rounded-xl border border-rose-300 bg-white px-3 py-2 text-xs font-extrabold text-rose-700 transition hover:bg-rose-100`,onClick:e.onRetry,type:`button`,children:`다시 불러오기`})]}):W(b,{children:[W(`div`,{class:`rounded-xl border p-4 ${n.panel}`,children:W(`div`,{class:`flex flex-wrap items-start justify-between gap-3`,children:[W(`div`,{class:`flex min-w-0 items-start gap-3`,children:[W(`span`,{class:`mt-0.5 shrink-0`,children:t?.overall===`ok`?W(Sn,{class:`size-5 text-emerald-700`}):t?.overall===`initializing`?W(dn,{class:`size-5 text-teal-700`}):W(Cn,{class:`size-5 text-amber-700`})}),W(`div`,{class:`min-w-0`,children:[W(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:t?.summary.message??`장치 상태를 확인하고 있습니다.`}),W(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-600`,children:[`마지막 진단 : `,t?jo(t.generatedAt):`확인 중`]})]})]}),W(`span`,{class:`rounded-full px-2.5 py-1 text-xs font-extrabold ${n.badge}`,children:n.label})]})}),t&&W(`div`,{class:`mt-4 grid grid-cols-3 gap-2`,children:[W(`div`,{class:`rounded-xl bg-slate-50 p-3`,children:[W(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`진단 항목`}),W(`p`,{class:`mt-1 mb-0 text-lg font-black text-slate-950`,children:t.summary.total})]}),W(`div`,{class:`rounded-xl bg-slate-50 p-3`,children:[W(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`주의`}),W(`p`,{class:`mt-1 mb-0 text-lg font-black text-amber-700`,children:t.summary.warning})]}),W(`div`,{class:`rounded-xl bg-slate-50 p-3`,children:[W(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`이상`}),W(`p`,{class:`mt-1 mb-0 text-lg font-black text-rose-700`,children:t.summary.critical})]})]}),i.length>0&&W(`div`,{class:`mt-4 space-y-2`,children:i.map(e=>{let t=Ey(e.status);return W(`div`,{class:`flex min-w-0 items-start justify-between gap-3 rounded-xl border border-slate-200 px-3 py-2.5`,children:[W(`div`,{class:`min-w-0`,children:[W(`p`,{class:`m-0 text-xs font-extrabold text-slate-900`,children:e.label}),W(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:e.message})]}),W(`span`,{class:`shrink-0 rounded-full px-2 py-1 text-[11px] font-extrabold ${t.badge}`,children:t.label})]},e.id)})}),W(`div`,{class:`mt-4 flex flex-col gap-2 sm:flex-row`,children:[W(`button`,{class:`inline-flex min-h-11 w-full items-center justify-center 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 sm:w-auto`,disabled:e.running||e.loading,onClick:e.onRun,type:`button`,children:e.running?`진단 중`:`지금 진단`}),W(`button`,{class:`inline-flex min-h-11 w-full items-center justify-center 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 sm:w-auto`,disabled:e.downloadBusy,onClick:e.onDownload,type:`button`,children:e.downloadBusy?`진단 정보 생성 중`:`진단 정보 다운로드`})]}),W(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`다운로드 파일에는 호스트명, WAN IP와 Wi-Fi SSID 같은 네트워크 식별 정보가 포함될 수 있으므로 외부 전달 전에 내용을 확인해 주세요.`})]})]}),W(`div`,{class:`grid min-w-0 content-start gap-4`,children:[W(wy,{description:`장치의 최소 상태 정보와 이상 코드만 서버로 전송합니다. 직접 켜고 언제든지 끌 수 있습니다.`,icon:W(qt,{class:`size-5`}),title:`원격 상태 보고`,titleActions:W(`span`,{class:`inline-flex rounded-full bg-slate-100 px-2.5 py-1 text-[11px] font-extrabold uppercase tracking-[0.08em] text-slate-600`,children:ky(a)}),headerActions:W(`button`,{"aria-checked":a?.enabled===!0,"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 ${a?.enabled?`border-teal-600 bg-teal-600`:`border-slate-300 bg-slate-200`}`,disabled:!t||e.savingReporter||!s,onClick:()=>e.onSetReporter(!(a?.enabled??!1)),role:`switch`,type:`button`,children:W(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${a?.enabled?`left-6`:`left-1`}`})}),children:W(`div`,{class:`min-w-0`,children:a?.eligible?W(`div`,{class:`rounded-xl bg-slate-50 p-4`,children:[W(`div`,{class:`mb-4 flex items-start gap-3 rounded-xl border p-3 ${o?`border-emerald-200 bg-emerald-50`:`border-slate-200 bg-white`}`,children:[W(`span`,{class:`mt-0.5 shrink-0`,children:o?W(Sn,{class:`size-5 text-emerald-700`}):W(mn,{class:`size-5 text-slate-500`})}),W(`div`,{class:`min-w-0 flex-1`,children:[W(`div`,{class:`flex flex-wrap items-center gap-2`,children:[W(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:c}),W(`span`,{class:`rounded-full px-2 py-0.5 text-[10px] font-extrabold ${e.savingReporter?`bg-slate-200 text-slate-700`:o?`bg-emerald-100 text-emerald-800`:`bg-slate-200 text-slate-700`}`,children:e.savingReporter?`설정 중`:o?`켜짐`:`꺼짐`})]}),W(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-600`,children:l})]})]}),W(`div`,{class:`grid grid-cols-1 gap-3 sm:grid-cols-2`,children:[W(`div`,{children:[W(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`최근 전송 상태`}),W(`p`,{class:`mt-1 mb-0 text-sm font-black text-slate-900`,children:e.savingReporter?`설정 반영 중`:Dy(a)})]}),W(`div`,{children:[W(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`마지막 서버 보고`}),W(`p`,{class:`mt-1 mb-0 text-sm font-black text-slate-900`,children:Oy(a)})]})]}),a.enabled&&a.lastErrorCode&&W(`p`,{class:`mt-3 mb-0 text-xs font-bold text-rose-700`,children:[`최근 보고 오류 : `,a.lastErrorCode]})]}):W(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[W(`p`,{class:`m-0 text-sm font-extrabold text-slate-900`,children:`원격 상태 보고는 유료 멤버십 또는 체험 기간에 사용할 수 있습니다.`}),W(`a`,{class:`mt-3 inline-flex text-xs font-extrabold text-teal-700 hover:text-teal-800`,href:`https://www.smartsafehub.com/pricing/`,rel:`noopener noreferrer`,target:`_blank`,children:`멤버십 알아보기`})]})})}),W(wy,{description:`진단에 필요한 최소한의 상태 정보만 전송하며, 네트워크 식별 정보와 이용 내용은 보내지 않습니다.`,icon:W(qt,{class:`size-5`}),title:`개인정보 보호`,children:W(`div`,{class:`grid grid-cols-1 gap-3 sm:grid-cols-2`,children:[W(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-3.5`,children:[W(`div`,{class:`flex items-center gap-2`,children:[W(`span`,{class:`flex size-7 shrink-0 items-center justify-center rounded-lg bg-white text-slate-600`,children:W(gn,{class:`size-4`})}),W(`p`,{class:`m-0 text-sm font-black text-slate-900`,children:`전송되는 정보`})]}),W(`div`,{class:`mt-3 space-y-3`,children:[W(`div`,{children:[W(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`기기 상태 수치`}),W(`p`,{class:`mt-1 mb-0 text-xs font-semibold leading-5 text-slate-700`,children:`메모리 · 부하 · 저장 공간`})]}),W(`div`,{children:[W(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`진단 정보`}),W(`p`,{class:`mt-1 mb-0 text-xs font-semibold leading-5 text-slate-700`,children:`진단 상태 · 이상 코드 · 보고 시각`})]})]})]}),W(`div`,{class:`rounded-xl border border-teal-200 bg-teal-50 p-3.5`,children:[W(`div`,{class:`flex items-center gap-2`,children:[W(`span`,{class:`flex size-7 shrink-0 items-center justify-center rounded-lg bg-white text-teal-700`,children:W(qt,{class:`size-4`})}),W(`p`,{class:`m-0 text-sm font-black text-slate-900`,children:`전송하지 않는 정보`})]}),W(`div`,{class:`mt-3 space-y-3`,children:[W(`div`,{children:[W(`p`,{class:`m-0 text-[11px] font-extrabold text-teal-800`,children:`개인 식별 · 네트워크 정보`}),W(`p`,{class:`mt-1 mb-0 text-xs font-semibold leading-5 text-slate-700`,children:`호스트명 · WAN IP · Wi-Fi SSID/MAC`})]}),W(`div`,{children:[W(`p`,{class:`m-0 text-[11px] font-extrabold text-teal-800`,children:`이용 내용 및 원문 데이터`}),W(`p`,{class:`mt-1 mb-0 text-xs font-semibold leading-5 text-slate-700`,children:`DNS 요청 내용 · 시스템 로그 원문`})]})]})]})]})})]})]})}function jy(e){let[t,n]=L(!1);return W(`label`,{class:`block min-w-0`,for:e.id,children:[W(`span`,{class:`mb-2 block text-sm font-extrabold text-slate-800`,children:e.label}),W(`span`,{class:`relative block min-w-0`,children:[W(`input`,{autoComplete:e.autoComplete,class:`ssh-password-input min-h-11 w-full rounded-xl border border-slate-300 bg-white px-3 py-2.5 pr-12 text-sm font-bold text-slate-900 outline-none transition focus:border-teal-500 focus:ring-2 focus:ring-teal-100 disabled:cursor-wait disabled:opacity-60`,disabled:e.disabled,id:e.id,name:e.name,onInput:t=>e.onInput(t.currentTarget.value),placeholder:e.placeholder,...t?{type:`text`}:{type:`password`},value:e.value}),W(`button`,{"aria-label":t?`${e.label} 숨기기`:`${e.label} 표시`,"aria-pressed":t,class:`absolute inset-y-0 right-1 my-auto flex size-9 items-center justify-center rounded-lg text-slate-500 transition hover:bg-slate-100 hover:text-slate-800 disabled:opacity-50`,disabled:e.disabled,onClick:()=>n(e=>!e),title:t?`비밀번호 숨기기`:`비밀번호 표시`,type:`button`,children:W(t?bn:yn,{class:`size-4`})})]})]})}function My(e){return W(`li`,{class:`flex min-w-0 items-center gap-1.5 text-xs font-bold ${e.met?`text-emerald-700`:`text-slate-500`}`,children:[W(Sn,{class:`size-3.5 shrink-0`,"aria-hidden":`true`}),W(`span`,{children:e.label})]})}function Ny(e){let[t,n]=L(``),[r,i]=L(``),[a,o]=L(``),[s,c]=L(!1),[l,u]=L(null),d=Bn(r),f=Vn(d),p=a.length>0&&r===a,m=t.length>0&&r.length>0&&t!==r,h=!s&&m&&f&&p,g=async()=>{if(u(null),!t)u(`현재 관리자 비밀번호를 입력해 주세요.`);else if(!f)u(`새 비밀번호는 8자 이상이며 영문과 숫자를 각각 하나 이상 포함해야 합니다.`);else if(t===r)u(`새 관리자 비밀번호는 현재 비밀번호와 다르게 설정해 주세요.`);else if(r!==a)u(`새 비밀번호 확인 값이 일치하지 않습니다.`);else{c(!0);try{await vy(t,r),n(``),i(``),o(``);try{await Ee()}catch{}c(!1),e.onChanged()}catch(e){u(q(e,`관리자 비밀번호를 변경하지 못했습니다. 잠시 후 다시 시도해 주세요.`)),c(!1)}}};return W(wy,{description:`현재 비밀번호를 확인한 뒤 공유기 관리자 비밀번호를 변경합니다. 변경이 완료되면 새 비밀번호로 다시 로그인해야 합니다.`,icon:W(_n,{class:`size-5`}),title:`관리자 비밀번호`,children:W(`form`,{class:`space-y-4`,noValidate:!0,onSubmit:e=>{e.preventDefault(),g()},children:[l&&W(`div`,{"aria-live":`polite`,class:`rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm font-bold text-rose-800`,role:`alert`,children:l}),W(jy,{autoComplete:`current-password`,disabled:s,id:`smartsafehub-current-admin-password`,label:`현재 비밀번호`,name:`current-password`,onInput:n,placeholder:`현재 관리자 비밀번호`,value:t}),W(jy,{autoComplete:`new-password`,disabled:s,id:`smartsafehub-new-admin-password`,label:`새 비밀번호`,name:`new-password`,onInput:i,placeholder:`새 관리자 비밀번호`,value:r}),W(`ul`,{"aria-label":`새 비밀번호 요구 사항`,class:`m-0 grid list-none grid-cols-1 gap-2 p-0 sm:grid-cols-3`,children:[W(My,{label:`8자 이상`,met:d.length}),W(My,{label:`영문자 포함`,met:d.letter}),W(My,{label:`숫자 포함`,met:d.number})]}),W(`div`,{class:`pt-2`,children:W(jy,{autoComplete:`new-password`,disabled:s,id:`smartsafehub-confirm-admin-password`,label:`새 비밀번호 확인`,name:`confirm-password`,onInput:o,placeholder:`새 관리자 비밀번호 다시 입력`,value:a})}),a.length>0&&W(`p`,{"aria-live":`polite`,class:`m-0 text-xs font-bold ${p?`text-emerald-700`:`text-rose-700`}`,role:`status`,children:p?`새 비밀번호가 일치합니다.`:`새 비밀번호 확인 값이 일치하지 않습니다.`}),r.length>0&&t===r&&W(`p`,{class:`m-0 text-xs font-bold text-amber-700`,children:`새 비밀번호는 현재 비밀번호와 다르게 설정해 주세요.`}),W(`div`,{class:`pt-2`,children:W(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-3 text-xs leading-5 text-slate-600`,children:`비밀번호는 현재 공유기에 직접 적용되며 외부 서버로 전송되지 않습니다. 변경 후 현재 로그인 세션은 종료됩니다.`})}),W(`div`,{class:`rounded-xl border border-amber-200 bg-amber-50 p-3 text-xs leading-5 text-amber-900`,children:[W(`strong`,{class:`block font-extrabold`,children:`비밀번호를 잊었을 때`}),`전원이 켜진 상태에서 Reset 버튼을 5~9초 누른 뒤 놓으면 관리자 비밀번호만 복구할 수 있습니다. 네트워크, Wi-Fi와 SafeShield 설정은 유지됩니다. 10초 이상 누르면 모든 사용자 설정을 초기화하는 기기 초기화가 실행됩니다.`]}),W(`div`,{class:`flex justify-end`,children:W(`button`,{class:`inline-flex min-h-11 w-full items-center justify-center rounded-xl bg-teal-700 px-4 py-2.5 text-sm font-extrabold text-white transition hover:bg-teal-800 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto`,disabled:!h,type:`submit`,children:s?`비밀번호 변경 중`:`비밀번호 변경`})})]})})}function Py(e){let t=z(null),[n,r]=L(null),[i,a]=L(!1),o=e.action!==null;R(()=>{e.validated||a(!1)},[e.validated]);let s=()=>{!o&&!e.restoreAccepted&&t.current?.click()},c=async()=>{n&&await e.onUpload(n)&&(r(null),t.current&&(t.current.value=``))};return W(wy,{description:`펌웨어 업데이트나 복구 전에 현재 OpenWrt 설정을 표준 백업 파일로 저장하고, 필요할 때 검증 후 복원합니다.`,icon:W(gn,{class:`size-5`}),title:`설정 백업 및 복원`,children:[(e.error||e.message)&&W(`div`,{class:`mb-5 flex min-w-0 items-start justify-between gap-3 rounded-xl border px-4 py-3 text-sm font-bold ${e.error?`border-rose-200 bg-rose-50 text-rose-800`:`border-emerald-200 bg-emerald-50 text-emerald-800`}`,children:[W(`span`,{class:`min-w-0`,children:e.error||e.message}),!e.restoreAccepted&&W(`button`,{class:`shrink-0 rounded-lg px-2 py-1 text-xs font-extrabold hover:bg-black/5`,onClick:e.onDismissFeedback,type:`button`,children:`닫기`})]}),W(`div`,{class:`grid min-w-0 grid-cols-1 gap-3`,children:[W(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4 sm:p-5`,children:[W(`p`,{class:`m-0 text-sm font-black text-slate-900`,children:`현재 설정 백업`}),W(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-600`,children:`네트워크, Wi-Fi, SmartSafeHub와 SafeShield 등 OpenWrt가 관리하는 설정을 표준 백업 파일로 저장합니다.`}),W(`button`,{class:`mt-4 inline-flex min-h-11 w-full 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 sm:w-auto`,disabled:o||e.restoreAccepted,onClick:e.onDownload,type:`button`,children:[W(hn,{class:`size-4`}),e.action===`download`?`백업 생성 중`:`설정 백업 다운로드`]}),W(`p`,{class:`mt-3 mb-0 text-xs font-bold leading-5 text-amber-700`,children:`백업에는 Wi-Fi 비밀번호, 관리자 설정, VPN 키나 기기 인증 정보 같은 민감한 값이 포함될 수 있으므로 안전한 위치에 보관해 주세요.`})]}),W(`div`,{class:`rounded-xl border border-slate-200 bg-white p-4 sm:p-5`,children:[W(`p`,{class:`m-0 text-sm font-black text-slate-900`,children:`설정 복원`}),W(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-600`,children:`SmartSafeHub 또는 OpenWrt 백업을 업로드하고 검증한 뒤 복원합니다. 최대 16MB까지 사용할 수 있습니다.`}),e.validated?W(`div`,{class:`mt-4 rounded-xl border border-emerald-200 bg-emerald-50 p-4`,children:[W(`div`,{class:`flex min-w-0 flex-col gap-1 sm:flex-row sm:items-center sm:justify-between`,children:W(`div`,{class:`min-w-0`,children:[W(`p`,{class:`m-0 break-words text-sm font-extrabold text-emerald-900`,children:e.validated.filename}),W(`p`,{class:`mt-1 mb-0 text-xs font-bold text-emerald-700`,children:[Do(e.validated.sizeBytes),` · 검증 완료`]})]})}),i?W(`div`,{class:`mt-4 rounded-xl border border-rose-200 bg-white p-4`,children:[W(`p`,{class:`m-0 text-sm font-black text-rose-900`,children:`현재 설정을 백업 파일의 내용으로 덮어쓰시겠습니까?`}),W(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-rose-700`,children:`LAN 주소, Wi-Fi, 관리자 접속 정보가 바뀌어 현재 연결이 끊길 수 있습니다. 복원 직후 공유기가 자동으로 재부팅됩니다. 같은 장치와 호환되는 백업만 사용해 주세요.`}),W(`div`,{class:`mt-4 flex flex-col gap-3 sm:flex-row sm:flex-wrap`,children:[W(`button`,{class:`inline-flex min-h-10 w-full items-center justify-center rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-extrabold text-slate-700 transition hover:bg-slate-50 disabled:opacity-60 sm:w-auto`,disabled:o,onClick:()=>a(!1),type:`button`,children:`취소`}),W(`button`,{class:`inline-flex min-h-10 w-full items-center justify-center rounded-xl bg-rose-700 px-4 py-2 text-sm font-extrabold text-white transition hover:bg-rose-800 disabled:cursor-wait disabled:opacity-60 sm:w-auto`,disabled:o,onClick:()=>void e.onRestore(),type:`button`,children:e.action===`restore`?`설정 복원 중`:`설정 복원 및 재부팅`})]})]}):W(`div`,{class:`mt-4 flex flex-col gap-3 sm:flex-row sm:flex-wrap`,children:[W(`button`,{class:`inline-flex min-h-10 w-full items-center justify-center rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-extrabold text-slate-700 transition hover:bg-slate-50 disabled:opacity-60 sm:w-auto`,disabled:o||e.restoreAccepted,onClick:()=>void e.onDiscard(),type:`button`,children:e.action===`discard`?`파일 삭제 중`:`파일 삭제`}),W(`button`,{class:`inline-flex min-h-10 w-full items-center justify-center rounded-xl bg-amber-600 px-4 py-2 text-sm font-extrabold text-white transition hover:bg-amber-700 disabled:opacity-60 sm:w-auto`,disabled:o||e.restoreAccepted,onClick:()=>a(!0),type:`button`,children:`복원 준비`})]})]}):W(b,{children:[W(`input`,{accept:`.tar.gz,.tgz,application/gzip,application/x-gzip`,class:`sr-only`,disabled:o||e.restoreAccepted,onChange:e=>r(e.currentTarget.files?.[0]??null),ref:t,type:`file`}),W(`button`,{class:`mt-4 flex min-h-11 w-full items-center justify-between gap-3 rounded-xl border border-dashed border-slate-300 bg-slate-50 px-4 py-3 text-left text-sm font-bold text-slate-700 transition hover:border-teal-400 hover:bg-teal-50 disabled:cursor-wait disabled:opacity-60`,disabled:o||e.restoreAccepted,onClick:s,type:`button`,children:[W(`span`,{class:`min-w-0 truncate`,children:n?.name??`복원할 백업 파일 선택`}),W(`span`,{class:`shrink-0 text-xs font-extrabold text-teal-700`,children:`찾아보기`})]}),n&&W(`div`,{class:`mt-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[W(`span`,{class:`text-xs font-bold text-slate-500`,children:Do(n.size)}),W(`button`,{class:`inline-flex min-h-10 w-full items-center justify-center rounded-xl border border-teal-300 bg-white px-4 py-2 text-sm font-extrabold text-teal-800 transition hover:bg-teal-50 disabled:cursor-wait disabled:opacity-60 sm:w-auto`,disabled:o||e.restoreAccepted,onClick:()=>void c(),type:`button`,children:e.action===`upload`?`업로드 중 ${Math.round(e.uploadProgress??0)}%`:e.action===`validate`?`백업 검증 중`:`업로드 및 검증`})]})]})]})]})]})}function Fy(e){return W(wy,{description:`공유기 재부팅과 SmartSafeHub에서 제공하지 않는 고급 관리 도구를 한곳에서 제공합니다.`,...e.className===void 0?{}:{className:e.className},icon:W(an,{class:`size-5`}),title:`시스템 도구`,children:W(`div`,{class:`space-y-3`,children:[W(`section`,{"aria-labelledby":`router-reboot-heading`,class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[W(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[W(`div`,{class:`flex min-w-0 items-start gap-3`,children:[W(`span`,{class:`flex size-9 shrink-0 items-center justify-center rounded-lg bg-rose-50 text-rose-700`,children:W(mn,{class:`size-4`})}),W(`div`,{class:`min-w-0`,children:[W(`p`,{class:`m-0 text-sm font-black text-rose-950`,id:`router-reboot-heading`,children:`공유기 재부팅`}),W(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`재부팅 중에는 인터넷과 Wi-Fi 연결이 잠시 중단됩니다.`})]})]}),!e.confirmingReboot&&W(`button`,{class:`inline-flex min-h-10 w-full shrink-0 items-center justify-center rounded-xl border border-rose-300 bg-white px-3 py-2 text-xs font-extrabold text-rose-700 transition hover:bg-rose-50 disabled:opacity-60 sm:w-auto`,disabled:e.action!==null||e.rebootAccepted,onClick:e.onConfirmReboot,type:`button`,children:`재부팅`})]}),e.confirmingReboot&&W(`div`,{class:`mt-3 rounded-xl border border-rose-200 bg-rose-50 p-3`,children:[W(`p`,{class:`m-0 text-xs font-extrabold leading-5 text-rose-900`,children:`지금 공유기를 재부팅하시겠습니까? 저장되지 않은 LuCI 설정이 있다면 먼저 저장해 주세요.`}),W(`div`,{class:`mt-3 flex flex-col gap-2 sm:flex-row sm:flex-wrap`,children:[W(`button`,{class:`inline-flex min-h-10 w-full items-center justify-center rounded-xl border border-slate-300 bg-white px-3 py-2 text-xs font-extrabold text-slate-700 transition hover:bg-slate-50 sm:w-auto`,disabled:e.action!==null,onClick:e.onCancelReboot,type:`button`,children:`취소`}),W(`button`,{class:`inline-flex min-h-10 w-full items-center justify-center rounded-xl bg-rose-700 px-3 py-2 text-xs font-extrabold text-white transition hover:bg-rose-800 disabled:cursor-wait disabled:opacity-60 sm:w-auto`,disabled:e.action!==null,onClick:e.onReboot,type:`button`,children:e.action===`reboot`?`재부팅 요청 중`:`지금 재부팅`})]})]})]}),W(`section`,{"aria-labelledby":`advanced-tools-heading`,class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[W(`div`,{class:`min-w-0`,children:[W(`p`,{class:`m-0 text-sm font-black text-slate-900`,id:`advanced-tools-heading`,children:`고급 도구`}),W(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`SmartSafeHub에서 아직 제공하지 않는 상세 시스템 설정이나 원본 로그가 필요할 때 사용합니다.`})]}),W(`div`,{class:`mt-3 flex flex-col gap-2 sm:flex-row sm:flex-wrap`,children:[W(`a`,{class:`inline-flex min-h-10 w-full items-center justify-center rounded-xl border border-slate-300 bg-white px-3 py-2 text-xs font-extrabold text-slate-800 no-underline transition hover:bg-slate-50 sm:w-auto`,href:e.advancedSystemUrl,children:`LuCI 고급 설정 열기`}),W(`a`,{class:`inline-flex min-h-10 w-full items-center justify-center rounded-xl border border-slate-300 bg-white px-3 py-2 text-xs font-extrabold text-slate-800 no-underline transition hover:bg-slate-50 sm:w-auto`,href:e.logsUrl,children:`시스템 로그 열기`})]})]})]})})}var Iy=[{value:`mon`,label:`월요일`},{value:`tue`,label:`화요일`},{value:`wed`,label:`수요일`},{value:`thu`,label:`목요일`},{value:`fri`,label:`금요일`},{value:`sat`,label:`토요일`},{value:`sun`,label:`일요일`}];function Ly(e){let[t,n]=L(!1),[r,i]=L(`weekly`),[a,o]=L(`sun`),[s,c]=L(`04:00`);R(()=>{e.data&&(n(e.data.enabled),i(e.data.frequency),o(e.data.dayOfWeek),c(e.data.time))},[e.data?.dayOfWeek,e.data?.enabled,e.data?.frequency,e.data?.time]);let l=e.data!==null&&(t!==e.data.enabled||r!==e.data.frequency||a!==e.data.dayOfWeek||s!==e.data.time),u=Iy.find(e=>e.value===a),d=t?r===`daily`?`매일 ${s}`:`매주 ${u?.label??`일요일`} ${s}`:`예약 재부팅 꺼짐`;return W(`section`,{"aria-labelledby":`scheduled-reboot-heading`,class:`min-w-0`,children:[W(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[W(`div`,{class:`flex min-w-0 items-start gap-3`,children:[W(`span`,{class:`flex size-11 shrink-0 items-center justify-center rounded-xl bg-slate-100 text-slate-600`,children:W(xn,{class:`size-5`})}),W(`div`,{class:`min-w-0`,children:[W(`h2`,{class:`m-0 text-lg font-black text-slate-950`,id:`scheduled-reboot-heading`,children:`예약 재부팅`}),W(`p`,{class:`mt-1.5 mb-0 text-sm leading-6 text-slate-500`,children:`공유기를 정해진 시간에 자동으로 재부팅합니다. 기본값은 꺼짐이며 필요한 장치에서만 사용하세요.`})]})]}),W(`div`,{class:`flex shrink-0 flex-wrap items-center justify-between gap-3 sm:justify-end`,children:[l&&W(`span`,{"aria-live":`polite`,class:`inline-flex 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:[W(Cn,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}),W(`span`,{class:`text-xs font-extrabold text-slate-500`,children:t?`사용 중`:`꺼짐`}),W(`button`,{"aria-checked":t,"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 ${t?`border-teal-600 bg-teal-600`:`border-slate-300 bg-slate-200`}`,disabled:e.loading||e.saving||!e.data,onClick:()=>n(e=>!e),role:`switch`,type:`button`,children:W(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${t?`left-6`:`left-1`}`})})]})]}),(e.saveError||e.saveMessage)&&W(`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.saveError?`border-rose-200 bg-rose-50 text-rose-800`:`border-emerald-200 bg-emerald-50 text-emerald-800`}`,children:[W(`span`,{class:`min-w-0`,children:e.saveError||e.saveMessage}),W(`button`,{class:`shrink-0 rounded-lg px-2 py-1 text-xs font-extrabold hover:bg-black/5`,onClick:e.onDismissFeedback,type:`button`,children:`닫기`})]}),e.error&&!e.data?W(`div`,{class:`mt-4 rounded-xl border border-rose-200 bg-rose-50 p-4`,children:[W(`p`,{class:`m-0 text-sm font-bold text-rose-800`,children:e.error}),W(`button`,{class:`mt-3 inline-flex min-h-10 items-center rounded-xl border border-rose-300 bg-white px-3 py-2 text-xs font-extrabold text-rose-700 transition hover:bg-rose-100`,onClick:e.onRetry,type:`button`,children:`다시 불러오기`})]}):W(b,{children:[W(`div`,{class:`mt-4 grid min-w-0 grid-cols-1 gap-4 md:grid-cols-3`,children:[W(`div`,{children:[W(`label`,{class:`mb-2 block text-sm font-extrabold text-slate-800`,for:`scheduled-reboot-frequency`,children:`주기`}),W(W_,{ariaLabel:`예약 재부팅 주기`,disabled:!t||e.saving||!e.data,id:`scheduled-reboot-frequency`,onChange:e=>i(e),options:[{value:`weekly`,label:`매주`},{value:`daily`,label:`매일`}],value:r})]}),W(`div`,{children:[W(`label`,{class:`mb-2 block text-sm font-extrabold text-slate-800`,for:`scheduled-reboot-day`,children:`요일`}),W(W_,{ariaLabel:`예약 재부팅 요일`,disabled:!t||r!==`weekly`||e.saving||!e.data,id:`scheduled-reboot-day`,onChange:e=>o(e),options:Iy.map(e=>({value:e.value,label:e.label})),value:a})]}),W(`div`,{children:[W(`label`,{class:`mb-2 block text-sm font-extrabold text-slate-800`,for:`scheduled-reboot-time-hour`,children:`재부팅 시간`}),W(Sy,{ariaLabel:`예약 재부팅 시간`,disabled:!t||e.saving||!e.data,id:`scheduled-reboot-time`,onChange:c,value:s})]})]}),W(`div`,{class:`mt-4 flex flex-col gap-3 rounded-xl bg-slate-50 p-4 sm:flex-row sm:items-end sm:justify-between`,children:[W(`div`,{class:`min-w-0`,children:[W(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.12em] text-slate-500`,children:`예약 일정`}),W(`p`,{class:`mt-1.5 mb-0 text-sm font-extrabold text-slate-900`,children:d}),W(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:[e.data?.timezone??`장치 시간대`,` 기준 · 업데이트 작업 중이면 15분 단위로 최대 2시간 연기합니다.`]})]}),W(`button`,{class:`inline-flex min-h-11 w-full shrink-0 items-center justify-center rounded-xl bg-teal-700 px-4 py-2.5 text-sm font-extrabold text-white transition hover:bg-teal-800 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto`,disabled:!l||e.saving||!e.data,onClick:()=>void e.onSave({enabled:t,frequency:r,dayOfWeek:a,time:s}),type:`button`,children:e.saving?`예약 저장 중`:`예약 저장`})]})]})]})}function Ry({activeTab:e,action:t,backupAction:n,backupError:r,backupMessage:i,backupRestoreAccepted:a,backupUploadProgress:o,backupValidated:s,data:c,error:l,feedbackError:u,feedbackMessage:d,health:f,healthActionError:p,healthActionMessage:m,healthError:h,healthLoading:g,healthRunning:_,healthSavingReporter:v,loading:y,rebootAccepted:b,scheduledRebootData:x,scheduledRebootError:S,scheduledRebootLoading:C,scheduledRebootSaveError:w,scheduledRebootSaveMessage:T,scheduledRebootSaving:E,timeData:D,timeError:O,timeLoading:k,timeSaveError:A,timeSaveMessage:j,timeSaving:M,timeSyncing:N,onBackupDiscard:P,onBackupDownload:F,onBackupRestore:I,onBackupUpload:ee,onDismissBackupFeedback:te,onDismissFeedback:ne,onDismissHealthFeedback:re,onDismissTimeFeedback:ie,onDownloadDiagnostics:ae,onRunHealth:oe,onSetHealthReporter:se,onReboot:ce,onRetry:le,onDismissScheduledRebootFeedback:ue,onSaveScheduledReboot:de,onSaveTimezone:fe,onSyncTime:pe,onAdministratorPasswordChanged:me}){let[he,ge]=L(!1),_e=ve(`/admin/system`),ye=ve(`/admin/status/logs`);return W(`section`,{class:`min-w-0 space-y-7`,children:[(u||d)&&W(`div`,{class:`flex min-w-0 flex-col gap-3 rounded-xl border px-4 py-3 text-sm font-bold sm:flex-row sm:items-center sm:justify-between ${u?`border-rose-200 bg-rose-50 text-rose-800`:`border-emerald-200 bg-emerald-50 text-emerald-800`}`,children:[W(`span`,{class:`min-w-0`,children:u||d}),!b&&W(`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:ne,type:`button`,children:`닫기`})]}),W(ci,{active:e,label:`설정 메뉴`,tabs:[{id:`diagnostics`,label:`진단 및 지원`,href:`#settings`},{id:`time`,label:`시간 및 예약`,href:`#settings/time`},{id:`management`,label:`관리 및 복구`,href:`#settings/management`}]}),W(`section`,{"aria-labelledby":`diagnostics-tab`,class:`min-w-0`,hidden:e!==`diagnostics`,id:`diagnostics-panel`,role:`tabpanel`,children:[l&&!c?W(`div`,{class:`mb-4 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm font-semibold text-amber-900`,role:`status`,children:[W(`p`,{class:`m-0`,children:[`시스템 정보를 불러오지 못해 진단 파일 다운로드가 제한될 수 있습니다. `,l]}),W(`button`,{class:`mt-2 rounded-lg border border-current px-3 py-1.5 text-xs font-extrabold`,onClick:le,type:`button`,children:`다시 시도`})]}):y&&!c?W(`p`,{class:`mb-4 text-sm font-semibold text-slate-500`,role:`status`,children:`진단 파일에 필요한 시스템 정보를 확인하고 있습니다.`}):null,W(J,{eyebrow:`Device health`,title:`진단 및 지원`,description:`장치 진단과 원격 상태 보고를 확인하고 개인정보 보호 정책을 관리합니다.`}),W(`div`,{class:`grid min-w-0 grid-cols-1 gap-4 lg:grid-cols-2`,children:W(Ay,{actionError:p,actionMessage:m,data:f,downloadBusy:t===`diagnostics`,error:h,loading:g,onDismissFeedback:re,onDownload:ae,onRetry:le,onRun:oe,onSetReporter:se,running:_,savingReporter:v})})]}),W(`section`,{"aria-labelledby":`time-tab`,class:`min-w-0`,hidden:e!==`time`,id:`time-panel`,role:`tabpanel`,children:[W(J,{eyebrow:`Device settings`,title:`시간 및 예약`,description:`시간 기준과 예약 재부팅을 SmartSafeHub에서 직접 관리합니다.`}),W(`div`,{class:`grid min-w-0 grid-cols-1 gap-4 lg:grid-cols-2`,children:W(Ty,{data:D,error:O,loading:k,onDismissFeedback:ie,onDismissScheduledRebootFeedback:ue,onRetry:le,onSave:fe,onSaveScheduledReboot:de,onSync:pe,saveError:A,saveMessage:j,saving:M,scheduledRebootData:x,scheduledRebootError:S,scheduledRebootLoading:C,scheduledRebootSaveError:w,scheduledRebootSaveMessage:T,scheduledRebootSaving:E,syncing:N})})]}),W(`section`,{"aria-labelledby":`management-tab`,class:`min-w-0`,hidden:e!==`management`,id:`management-panel`,role:`tabpanel`,children:[W(J,{eyebrow:`System management`,title:`시스템 관리`,description:`관리자 비밀번호, 설정 백업·복원과 공유기 시스템 도구를 한곳에서 관리합니다.`}),W(`div`,{class:`grid min-w-0 grid-cols-1 gap-4 lg:grid-cols-2`,children:[W(Ny,{onChanged:me}),W(Py,{action:n,error:r,message:i,onDiscard:P,onDismissFeedback:te,onDownload:F,onRestore:I,onUpload:ee,restoreAccepted:a,uploadProgress:o,validated:s}),W(Fy,{action:t,className:`lg:col-span-2`,advancedSystemUrl:_e,confirmingReboot:he,logsUrl:ye,onCancelReboot:()=>ge(!1),onConfirmReboot:()=>ge(!0),onReboot:ce,rebootAccepted:b})]})]})]})}var zy=`https://www.smartsafehub.com/dashboard/devices/`;async function By(e){if(navigator.clipboard?.writeText)try{await navigator.clipboard.writeText(e);return}catch{}let t=document.createElement(`textarea`);t.value=e,t.setAttribute(`readonly`,``),t.style.position=`fixed`,t.style.inset=`0 auto auto -9999px`,t.style.opacity=`0`,document.body.appendChild(t),t.select(),t.setSelectionRange(0,e.length);let n=document.execCommand(`copy`);if(t.remove(),!n)throw Error(`clipboard_copy_failed`)}function Vy(e){return(e||`free`).toUpperCase()}function Hy(e){return e===`FREE`?`free`:e===`PRO`?`pro`:e===`ULTIMATE`?`ultimate`:`paid`}function Uy({plan:e}){let t=Hy(e),n=t!==`free`;return W(`span`,{class:`ssh-safeshield-plan-badge`,"data-compact":`false`,"data-tier":t,title:n?`${e} 멤버십`:`무료 플랜`,children:[W(`span`,{"aria-hidden":`true`,class:`ssh-safeshield-plan-badge-mark`,children:n?`✦`:`•`}),W(`span`,{children:e})]})}function Wy(e){if(!e.pairingCode||!e.pairingExpiresAt)return!1;let t=Date.parse(e.pairingExpiresAt);return Number.isFinite(t)&&t<=Date.now()}function Gy({data:e,error:t,loading:n,pairingBusy:r,refreshing:i,syncError:a,onRequestPairing:o,onRetry:s,demo:c=!1,onDemoAction:l}){let[u,d]=L(null),[f,p]=L(!1);if(R(()=>{if(!f)return;let e=window.setTimeout(()=>p(!1),1800);return()=>window.clearTimeout(e)},[f]),n&&!e)return W(vs,{});if(t&&!e)return W(ys,{message:t,onRetry:s});let m=e,h=m?.accountRegistered===!0,g=Vy(m?.plan??null),_=m?Wy(m):!1,v=!h&&!_?m?.pairingCode:null,y=r||m?.pairingOperation?.phase===`starting`||m?.pairingOperation?.phase===`running`;async function b(){d(null),p(!1);try{await o()}catch{d(`계정 연결 코드를 발급하지 못했습니다. 잠시 후 다시 시도해 주세요.`)}}async function x(){if(v)try{await By(v),d(null),p(!0)}catch{p(!1),d(`연결 코드를 복사하지 못했습니다. 코드를 직접 선택해 복사해 주세요.`)}}return W(`div`,{class:`min-w-0`,children:[W(J,{eyebrow:`Account connection`,title:`기기 계정 연결`,description:`계정에 연결하면 웹사이트에서 기기를 관리하고 Cloud 기능과 구독 권한을 사용할 수 있습니다.`}),W(`div`,{class:`grid items-start gap-5 xl:items-stretch xl:grid-cols-[minmax(0,1.2fr)_minmax(18rem,0.8fr)]`,children:[W(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6 xl:h-full`,children:h?W(`div`,{children:[W(`div`,{class:`flex items-center gap-3`,children:[W(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-full bg-emerald-100 text-emerald-700`,children:W(Sn,{class:`size-5`})}),W(`div`,{children:[W(`p`,{class:`m-0 text-base font-black text-slate-950`,children:`SmartSafeHub 계정에 연결됨`}),W(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`이 기기가 계정과 정상적으로 연결되어 있습니다. 연결 코드는 더 이상 필요하지 않습니다.`})]})]}),c?W(`button`,{class:`mt-5 inline-flex min-h-11 items-center justify-center rounded-xl border border-emerald-700 bg-emerald-700 px-4 py-2 text-sm font-extrabold text-white transition hover:bg-emerald-800`,onClick:l,type:`button`,children:`웹사이트에서 기기 관리`}):W(`a`,{class:`mt-5 inline-flex min-h-11 items-center justify-center rounded-xl border border-emerald-700 bg-emerald-700 px-4 py-2 text-sm font-extrabold text-white no-underline transition hover:bg-emerald-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-emerald-100`,href:zy,rel:`noopener noreferrer`,target:`_blank`,children:`웹사이트에서 기기 관리`}),W(`p`,{class:`mt-4 mb-0 border-t border-slate-100 pt-4 text-xs font-semibold leading-5 text-slate-500`,children:`웹사이트에서 기기 등록을 해제하면 공유기도 다음 계정 상태 동기화에서 자동으로 연결 해제를 반영합니다.`})]}):W(`div`,{children:[W(`div`,{children:[W(`div`,{class:`flex flex-wrap items-center justify-between gap-2`,children:[W(`p`,{class:`m-0 text-sm font-black text-slate-900`,children:`계정 연결 코드`}),W(`span`,{class:`inline-flex items-center gap-1.5 rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-[0.7rem] font-black text-amber-800`,children:[W(dn,{class:`size-3.5`}),`10분 동안 유효`]})]}),W(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-500`,children:`코드를 발급한 뒤 smartsafehub.com에 로그인하여 기기를 연결하세요.`})]}),v?W(`div`,{class:`mt-4 flex flex-col gap-3 rounded-2xl border border-teal-200 bg-teal-50 p-4 sm:flex-row sm:items-center sm:justify-between`,role:`group`,"aria-label":`계정 연결 코드`,children:[W(`div`,{class:`min-w-0`,children:[W(`span`,{class:`block text-[0.7rem] font-black text-teal-700`,children:`연결 코드`}),W(`code`,{class:`mt-1 block select-all break-all font-mono text-2xl font-black tracking-[0.14em] text-slate-950`,children:v})]}),W(`button`,{"aria-label":`계정 연결 코드 복사`,class:`inline-flex min-h-11 shrink-0 items-center justify-center gap-2 rounded-xl border border-teal-300 bg-white px-4 text-sm font-black text-teal-800 transition hover:bg-teal-100 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100`,onClick:()=>void x(),type:`button`,children:[W(f?Sn:vn,{class:`size-4`}),W(`span`,{"aria-live":`polite`,children:f?`복사됨`:`복사`})]})]}):_?W(`p`,{class:`mt-4 mb-0 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm font-bold text-amber-800`,children:`이전 연결 코드가 만료되었습니다. 새 코드를 발급해 주세요.`}):null,W(`button`,{class:`mt-4 inline-flex min-h-11 w-full items-center justify-center gap-2 rounded-xl border border-teal-700 bg-teal-700 px-4 py-2 text-sm font-black text-white transition hover:bg-teal-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-not-allowed disabled:opacity-50`,disabled:y,onClick:()=>void b(),type:`button`,children:[y?W(pn,{class:`size-4 animate-spin`}):null,y?`발급 중…`:v?`새 코드 발급`:`연결 코드 발급`]}),m?.pairingOperation?.phase===`failed`?W(`p`,{class:`mt-3 mb-0 text-sm font-semibold text-red-700`,children:`연결 코드 발급에 실패했습니다. 다시 시도해 주세요.`}):null,u?W(`p`,{class:`mt-3 mb-0 text-sm font-semibold text-red-700`,children:u}):null]})}),W(`aside`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6 xl:h-full`,children:[W(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Device status`}),W(`h2`,{class:`mt-2 mb-0 text-lg font-black tracking-tight text-slate-950`,children:`연결 상태`}),W(`dl`,{class:`mt-5 mb-0 grid gap-4`,children:[W(`div`,{class:`flex items-center justify-between gap-4 border-b border-slate-100 pb-4`,children:[W(`dt`,{class:`text-sm font-bold text-slate-500`,children:`계정`}),W(`dd`,{class:`m-0 text-sm font-black ${h?`text-emerald-700`:`text-slate-700`}`,children:h?`연결됨`:`연결 필요`})]}),W(`div`,{class:`flex items-center justify-between gap-4 border-b border-slate-100 pb-4`,children:[W(`dt`,{class:`text-sm font-bold text-slate-500`,children:`요금제`}),W(`dd`,{class:`m-0 flex min-w-0 flex-wrap items-center justify-end gap-2`,children:[W(Uy,{plan:g}),g===`FREE`?W(`span`,{class:`ssh-safeshield-plan-caption`,children:`기본 플랜`}):null]})]}),W(`div`,{class:`flex items-center justify-between gap-4`,children:[W(`dt`,{class:`text-sm font-bold text-slate-500`,children:`마지막 확인`}),W(`dd`,{class:`m-0 text-sm font-black text-slate-700`,children:jo(m?.lastSuccessAt??0)})]})]}),i?W(`p`,{class:`mt-5 mb-0 text-xs font-bold text-teal-700`,children:`최신 상태를 확인하고 있습니다…`}):null,a?h?W(`p`,{class:`mt-5 mb-0 text-xs font-semibold leading-5 text-slate-500`,children:`최신 상태 확인이 지연되고 있습니다. 마지막으로 확인된 연결 상태를 표시합니다.`}):W(`p`,{class:`mt-5 mb-0 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs font-bold leading-5 text-amber-800`,children:[a,` 로컬에 저장된 마지막 상태를 표시합니다.`]}):null]})]})]})}function Ky(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 Jy(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 Yy(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 Xy(e){return e.phase===`checking`||e.phase===`downloading`||e.phase===`verifying`||e.phase===`flashing`?W(pn,{class:`size-3.5 animate-spin`}):e.phase===`error`?W(Cn,{class:`size-3.5`}):e.phase===`ready`||e.updateAvailable?W(hn,{class:`size-3.5`}):e.lastCheckAt?W(Sn,{class:`size-3.5`}):W(dn,{class:`size-3.5`})}function Zy({checked:e,disabled:t,onChange:n}){return W(`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:W(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${e?`left-6`:`left-1`}`})})}function Qy({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=z(null),[_,v]=L(null),[y,x]=L(!0),[S,C]=L(!1);R(()=>{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=Ky(k,A),M=async()=>{_&&await h(_)&&(v(null),g.current&&(g.current.value=``))};return W(`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:[W(`div`,{class:`p-5 sm:p-6`,children:[W(`div`,{class:`flex flex-col gap-5 lg:flex-row lg:items-start lg:justify-between`,children:[W(`div`,{class:`flex min-w-0 gap-4`,children:[W(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-sky-50 text-sky-700`,children:W(Jt,{class:`size-6`})}),W(`div`,{class:`min-w-0`,children:[W(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-sky-700`,children:`Firmware update`}),W(`h2`,{class:`mt-2 mb-0 text-xl font-black text-slate-950 sm:text-2xl`,children:`펌웨어 업데이트`}),W(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500 lg:whitespace-nowrap`,children:`시스템 펌웨어를 확인하고 설치합니다. 온라인·수동 설치는 안전성 검증 후 진행됩니다.`})]})]}),n?W(`div`,{class:`flex shrink-0 flex-col gap-2 sm:flex-row lg:justify-end`,children:[W(`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 ${Yy(n)}`,children:[Xy(n),Jy(n)]}),W(`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:[W(pn,{class:`size-4 ${E?`animate-spin`:``}`}),E?`확인 중...`:`펌웨어 확인`]}),n.updateAvailable&&!T?W(`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?W(pn,{class:`size-4 animate-spin`}):W(hn,{class:`size-4`}),D?`준비 중...`:`다운로드 및 검증`]}):null]}):null]}),t||o?W(`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:[W(`span`,{children:t||o}),W(`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`?W(`div`,{class:`mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4 sm:p-5`,role:`status`,children:W(`div`,{class:`flex gap-3`,children:[W(`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:W(pn,{class:`size-5 animate-spin`})}),W(`div`,{children:[W(`strong`,{class:`block text-sm font-black text-amber-950`,children:`펌웨어를 설치하고 재부팅하고 있습니다.`}),W(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:`전원을 끄지 마세요. 현재 주소에서 공유기가 다시 응답하면 SmartSafeHub가 자동으로 새로고침됩니다.`})]})]})}):null,i&&!n?W(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-800`,children:[W(`p`,{class:`m-0 font-bold`,children:i}),W(`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?W(`div`,{class:`mt-5 flex items-center gap-3 rounded-xl bg-slate-50 p-4 text-sm font-bold text-slate-500`,children:[W(pn,{class:`size-4 animate-spin`}),`펌웨어 상태를 확인하고 있습니다.`]}):n?W(b,{children:[n.lastError?W(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 sm:p-5`,children:W(`div`,{class:`flex gap-3`,children:[W(`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:W(Cn,{class:`size-5`})}),W(`div`,{class:`min-w-0 flex-1`,children:[W(`strong`,{class:`block text-sm font-black text-rose-950`,children:`펌웨어 작업을 완료하지 못했습니다.`}),W(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-rose-800`,children:n.lastError.message}),W(`details`,{class:`mt-3 rounded-lg border border-rose-200 bg-white p-3`,children:[W(`summary`,{class:`cursor-pointer text-xs font-extrabold text-rose-800`,children:`상세 정보 보기`}),W(`p`,{class:`mt-2 mb-0 break-all font-mono text-[11px] text-slate-600`,children:n.lastError.code})]})]})]})}):null,n.current.metadataAvailable?null:W(`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:[W(`strong`,{class:`block font-extrabold`,children:`현재 펌웨어의 버전 정보를 완전히 확인할 수 없습니다.`}),W(`span`,{class:`mt-1 block text-amber-800`,children:`새 펌웨어를 설치하면 이후부터 정확한 설치 버전을 자동으로 확인할 수 있습니다.`}),W(`details`,{class:`mt-2`,children:[W(`summary`,{class:`cursor-pointer font-extrabold text-amber-800`,children:`기술 상세 보기`}),W(`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.`})]})]}),W(`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:[W(`div`,{class:`bg-white p-4`,children:[W(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Device`}),W(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black text-slate-950`,children:n.current.deviceCode||n.current.boardName||`미확인`})]}),W(`div`,{class:`bg-white p-4`,children:[W(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Current version`}),W(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black text-slate-950`,children:n.current.releaseVersion||`미확인`}),n.current.buildId?W(`span`,{class:`mt-1 block break-all text-[11px] leading-5 text-slate-400`,children:[`빌드 ID `,n.current.buildId]}):n.current.openwrtVersion?W(`span`,{class:`mt-1 block text-[11px] leading-5 text-slate-400`,children:[`OpenWrt `,n.current.openwrtVersion]}):null]}),W(`div`,{class:`bg-white p-4`,children:[W(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Available version`}),W(`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?W(`span`,{class:`mt-1 block break-all text-[11px] leading-5 text-slate-400`,children:[`빌드 ID `,n.release.buildId]}):null]}),W(`div`,{class:`bg-white p-4`,children:[W(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Last check`}),W(`dd`,{class:`mt-2 mb-0 ml-0 text-sm font-black text-slate-950`,children:W(Rv,{fallback:`아직 없음`,timestamp:n.lastCheckAt})})]})]})]}):null,n?.updateAvailable&&n.release?W(`section`,{class:`mt-5 rounded-xl border border-sky-200 bg-sky-50/60 p-4 sm:p-5`,"data-section":`available-firmware`,children:[W(`div`,{class:`flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4`,children:[W(`div`,{children:[W(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-sky-700`,children:`Available firmware`}),W(`h3`,{class:`mt-2 mb-0 text-lg font-black text-slate-950`,children:`새 펌웨어가 있습니다.`}),W(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:[`펌웨어 `,n.release.version||`미확인`,` · `,qy(n.release.sysupgrade.sizeBytes),` · `,(n.release.channel||n.settings.channel)===`beta`?`Beta`:`Stable`]})]}),W(`div`,{class:`flex shrink-0 flex-wrap items-center justify-start gap-2 sm:justify-end`,children:[W(`span`,{class:`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?W(`span`,{class:`inline-flex items-center gap-1.5 rounded-full bg-white px-3 py-1.5 text-xs font-extrabold text-slate-700 ring-1 ring-inset ring-sky-200`,"data-section":`firmware-bundled-software-chip`,children:[W(`span`,{class:`text-[11px] uppercase tracking-[0.12em] text-slate-500`,children:`관리 SW`}),W(`span`,{class:`text-slate-950`,children:k})]}):null]})]}),k?W(`div`,{class:`mt-4 rounded-xl border border-sky-100 bg-white p-4`,"data-section":`firmware-bundled-software`,children:[W(`div`,{class:`flex flex-wrap items-center justify-between gap-2`,children:[W(`span`,{class:`text-xs font-extrabold text-slate-800`,children:`포함된 관리 소프트웨어`}),W(`div`,{class:`flex flex-wrap items-center gap-2`,children:[W(`span`,{class:`text-sm font-black text-slate-950`,children:k}),j===`same`?W(`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]})]}),W(`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?W(`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)=>W(`li`,{children:e},`${t}-${e}`))}):null]}):null,T?W(`section`,{class:`mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4 sm:p-5`,"data-section":`prepared-firmware`,children:W(`div`,{class:`flex gap-4`,children:[W(`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:W(Sn,{class:`size-5`})}),W(`div`,{class:`min-w-0 flex-1`,children:[W(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-amber-700`,children:`Verified image`}),W(`h3`,{class:`mt-2 mb-0 text-lg font-black text-amber-950`,children:`설치할 펌웨어가 준비되었습니다.`}),W(`p`,{class:`mt-2 mb-0 break-all text-sm leading-6 text-amber-900`,children:[T.filename||`firmware.bin`,` · `,qy(T.sizeBytes)]}),T.sha256?W(`p`,{class:`mt-2 mb-0 break-all font-mono text-[11px] leading-5 text-amber-800`,children:[`SHA-256 `,T.sha256]}):null,W(`div`,{class:`mt-5 flex items-start justify-between gap-4 rounded-xl border border-amber-200 bg-white p-4`,children:[W(`div`,{children:[W(`strong`,{class:`block text-sm font-black text-slate-950`,children:`현재 설정 유지`}),W(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`Wi-Fi, SafeShield, SmartSafeHub 설정을 유지합니다.`}),T.allowBackup?null:W(`span`,{class:`mt-2 block text-xs font-bold text-rose-700`,children:`이 이미지는 설정 유지 업그레이드를 지원하지 않습니다.`})]}),W(Zy,{checked:y,disabled:!T.allowBackup,onChange:x})]}),S?W(`div`,{class:`mt-4 rounded-xl border border-rose-200 bg-rose-50 p-4`,children:[W(`strong`,{class:`block text-sm font-black text-rose-950`,children:`펌웨어 설치를 시작할까요?`}),W(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-rose-800`,children:`설치 중 전원을 끄면 장치가 복구 불가능한 상태가 될 수 있습니다. 이미지 검증은 설치 직전에 다시 수행됩니다.`}),W(`div`,{class:`mt-4 flex flex-wrap justify-end gap-2`,children:[W(`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:`취소`}),W(`button`,{class:`min-h-10 rounded-lg bg-rose-700 px-3 py-2 text-xs font-extrabold text-white`,onClick:()=>{C(!1),f(y)},type:`button`,children:`설치 시작`})]})]}):W(`div`,{class:`mt-4 flex flex-wrap justify-end gap-2`,children:[W(`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:`파일 삭제`}),W(`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:[W(rn,{class:`size-4`}),`펌웨어 설치`]})]})]})]})}):null]}),W(`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:[W(`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:[W(`div`,{class:`flex min-w-0 items-center gap-3`,children:[W(`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:W(hn,{class:`size-5 rotate-180`})}),W(`div`,{class:`min-w-0`,children:[W(`strong`,{class:`block text-sm font-black text-slate-950`,children:`수동 펌웨어 설치`}),W(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`펌웨어 파일(.bin)을 직접 업로드하여 검증한 뒤 설치합니다.`})]})]}),W(`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:`열기`}),W(`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:`닫기`})]}),W(`div`,{class:`border-t border-slate-200 px-5 pb-5 pt-3 sm:px-6`,children:[W(`p`,{class:`mt-0 mb-4 text-xs leading-5 text-slate-500 lg:whitespace-nowrap`,children:"이 장치에 맞는 Sysupgrade 이미지(.bin)만 사용하세요. 업로드한 파일은 이미지 검증과 `sysupgrade --test`를 모두 통과해야 설치할 수 있으며 강제 설치는 제공하지 않습니다."}),W(`div`,{class:`rounded-xl border border-slate-200 bg-white p-4 sm:p-5`,"data-layout":`firmware-file-picker`,children:[W(`input`,{accept:`.bin,application/octet-stream`,"aria-label":`펌웨어 파일 선택`,class:`sr-only`,disabled:w||T!==null,onChange:e=>v(e.currentTarget.files?.[0]??null),ref:g,type:`file`}),W(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between`,children:[W(`div`,{class:`flex min-w-0 items-center gap-3`,children:[W(`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:W(hn,{class:`size-5 rotate-180`})}),W(`div`,{class:`min-w-0`,children:[W(`strong`,{class:`block truncate text-sm font-black text-slate-900`,children:_?_.name:`펌웨어 파일을 선택하세요`}),W(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:_?`${qy(_.size)} · Sysupgrade 이미지 (.bin)`:`이 장치에 맞는 Sysupgrade 이미지 (.bin)를 선택합니다.`})]})]}),W(`div`,{class:`flex shrink-0 flex-wrap gap-2 sm:justify-end`,children:[W(`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:_?`다른 파일 선택`:`파일 선택`}),W(`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?W(pn,{class:`size-4 animate-spin`}):W(hn,{class:`size-4 rotate-180`}),O?`처리 중...`:`업로드 및 검증`]})]})]}),c===null?null:W(`div`,{class:`mt-4 border-t border-slate-100 pt-4`,children:[W(`div`,{class:`mb-2 flex items-center justify-between gap-3 text-xs font-bold text-slate-600`,children:[W(`span`,{children:`업로드 중`}),W(`span`,{children:[c,`%`]})]}),W(`div`,{class:`h-2 overflow-hidden rounded-full bg-slate-200`,children:W(`div`,{class:`h-full rounded-full bg-sky-600 transition-[width]`,style:{width:`${Math.max(0,Math.min(100,c))}%`}})})]})]})]})]})]})}var $y=[{value:3600,label:`1시간마다`},{value:21600,label:`6시간마다`},{value:43200,label:`12시간마다`},{value:86400,label:`24시간마다`}],eb=`luci-app-smartsafehub`;function tb(e){return e?new Date(e*1e3).toLocaleString(`ko-KR`,{dateStyle:`medium`,timeStyle:`short`}):`아직 없음`}function nb(e){return e===`stable`?`Stable`:e===`beta`?`Beta`:`미확인`}function rb(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 ib(e){return e.code===`UPDATES_INDEX_REFRESH_FAILED`?{title:`패키지 저장소를 확인하지 못했습니다.`,description:`OpenWrt 패키지 저장소 동기화 또는 네트워크 상태가 일시적으로 불안정할 수 있습니다. 잠시 후 업데이트 확인을 다시 실행해 주세요.`}:e.code===`UPDATES_INSTALL_FAILED`?{title:`관리 소프트웨어 업데이트 설치에 실패했습니다.`,description:`패키지 설치가 완료되지 않았습니다. 네트워크 연결과 저장 공간을 확인한 뒤 다시 시도해 주세요.`}:{title:`최근 업데이트 작업을 완료하지 못했습니다.`,description:`잠시 후 다시 시도해 주세요. 문제가 반복되면 상세 정보를 확인해 주세요.`}}function ab(e,t){return e.phase===`checking`?`업데이트 확인 중`:e.phase===`installing`?`업데이트 설치 중`:e.phase===`error`?`확인 필요`:e.updateCount>0?`업데이트 가능`:t?`업데이트 확인 지연`:e.lastCheckAt?`최신 상태`:`확인 전`}function ob(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 sb(e,t){return e.phase===`checking`||e.phase===`installing`?W(pn,{class:`size-3.5 shrink-0 animate-spin`}):e.phase===`error`?W(Cn,{class:`size-3.5 shrink-0`}):e.updateCount>0?W(hn,{class:`size-3.5 shrink-0`}):t?W(Cn,{class:`size-3.5 shrink-0`}):e.lastCheckAt?W(Sn,{class:`size-3.5 shrink-0`}):W(dn,{class:`size-3.5 shrink-0`})}function cb({checked:e,disabled:t=!1,label:n,onChange:r}){return W(`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:W(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${e?`left-6`:`left-1`}`})})}function lb({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]=L(!0),[m,h]=L(21600),[g,_]=L(!1),[v,y]=L(`03:00`),[x,S]=L(!1),[C,w]=L(!1);R(()=>{n&&!x&&(p(n.settings.checkEnabled),h(n.settings.checkIntervalSeconds),_(n.settings.autoInstall),y(n.settings.autoInstallTime))},[n,x]);let T=e===`check`||n?.phase===`checking`,E=e===`install`||n?.phase===`installing`,D=T||E,O=u_(n),k=n?.phase===`error`&&n.lastError?.code===`UPDATES_INDEX_REFRESH_FAILED`,A=n?.lastError?ib(n.lastError):null,j=n?.packages.find(e=>e.name===eb)??n?.packages[0]??null,M=r?.updateAvailable?r.release?.bundledSoftware?.smartsafehub:null,N=!(!n||n.releaseNotesComplete||n.releaseNotes.length!==1||j?.name!==eb||n.releaseNotes[0]?.version!==j.availableVersion),P=!(!n?.lastCheckAt||n.updateCount!==0||O||j?.name!==eb)&&Ky(M,j?.installedVersion)===`same`,F=(e={})=>n?(e.checkEnabled??f)!==n.settings.checkEnabled||(e.checkIntervalSeconds??m)!==n.settings.checkIntervalSeconds||(e.autoInstall??g)!==n.settings.autoInstall||(e.autoInstallTime??v)!==n.settings.autoInstallTime:!1,I=async()=>{await d({checkEnabled:f,checkIntervalSeconds:m,autoInstall:g,autoInstallTime:v})&&S(!1)};return W(`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:[W(`section`,{class:`min-w-0 xl:col-start-1 xl:row-start-1`,"data-section":`software-update-status`,children:[W(`div`,{class:`p-5 sm:p-6`,children:[W(`div`,{class:`flex flex-col gap-5 lg:flex-row lg:items-start lg:justify-between`,children:[W(`div`,{class:`flex min-w-0 gap-4`,children:[W(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-teal-50 text-teal-700`,children:W(rn,{class:`size-6`})}),W(`div`,{class:`min-w-0`,children:[W(`div`,{class:`flex flex-wrap items-center gap-2`,children:[W(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700`,children:`Management software`}),n?W(`span`,{class:`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-extrabold ring-1 ring-inset ${ob(n,O)}`,children:[W(`span`,{"aria-hidden":`true`,class:`flex items-center justify-center`,children:sb(n,O)}),ab(n,O)]}):null]}),W(`h2`,{class:`mt-2 mb-0 text-xl font-black text-slate-950 sm:text-2xl`,children:`관리 소프트웨어 업데이트`}),W(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500 xl:whitespace-nowrap`,children:`새 버전을 확인하고 안전하게 설치합니다.`})]})]}),n?W(`div`,{class:`flex shrink-0 flex-col gap-2 sm:flex-row lg:justify-end`,children:[W(`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:[W(pn,{class:`size-4 ${T?`animate-spin`:``}`}),T?`확인 중...`:`업데이트 확인`]}),n.updateCount>0&&!C?W(`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?W(pn,{class:`size-4 animate-spin`}):W(hn,{class:`size-4`}),E?`설치 중...`:`업데이트 설치`]}):null]}):null]}),(t||o)&&W(`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:[W(`span`,{children:t||o}),W(`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?W(`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:[W(`div`,{class:`flex items-start gap-3`,children:[W(`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:W(pn,{"aria-hidden":`true`,class:`size-5 animate-spin`})}),W(`div`,{class:`min-w-0`,children:[W(`h3`,{class:`m-0 text-sm font-black text-sky-950`,children:`관리 소프트웨어를 업데이트하고 있습니다.`}),W(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-sky-800`,children:`설치가 완료되면 이 화면이 자동으로 갱신됩니다. 작업 중에는 웹 연결이 잠시 끊길 수 있습니다.`})]})]}),W(`div`,{"aria-hidden":`true`,class:`ssh-update-progress-track mt-4`,children:W(`span`,{class:`ssh-update-progress-bar`})})]}):null,i&&!n?W(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-800`,children:[W(`p`,{class:`m-0 font-bold`,children:i}),W(`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?W(`div`,{class:`mt-5 flex items-center gap-3 rounded-xl bg-slate-50 p-4 text-sm font-bold text-slate-500`,children:[W(`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:W(pn,{"aria-hidden":`true`,class:`size-4 animate-spin`})}),W(`span`,{children:`관리 소프트웨어 업데이트 상태를 확인하고 있습니다.`})]}):n?W(b,{children:[n.lastError&&A?W(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 sm:p-5`,children:W(`div`,{class:`flex items-start gap-3`,children:[W(`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:W(Cn,{class:`size-5`})}),W(`div`,{class:`min-w-0 flex-1`,children:[W(`p`,{class:`m-0 text-sm font-extrabold text-rose-950`,children:A.title}),W(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-rose-800`,children:A.description}),n.lastError.at?W(`p`,{class:`mt-2 mb-0 text-xs font-bold text-rose-700`,children:[`발생 시각 `,tb(n.lastError.at)]}):null,W(`details`,{class:`mt-3 rounded-lg border border-rose-200 bg-white p-3`,children:[W(`summary`,{class:`cursor-pointer text-xs font-extrabold text-rose-800`,children:`상세 정보 보기`}),W(`div`,{class:`mt-3 grid gap-2 text-xs leading-5 text-slate-600`,children:[W(`p`,{class:`m-0`,children:[`오류 코드 `,W(`strong`,{class:`font-black text-slate-900`,children:n.lastError.code})]}),W(`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?W(`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:[W(`div`,{class:`min-w-0`,children:[W(`strong`,{class:`block text-sm font-black text-amber-900`,children:`관리 소프트웨어를 지금 업데이트할까요?`}),W(`span`,{class:`mt-1 block text-xs leading-5 text-amber-800`,children:`설치 중 SmartSafeHub 웹 화면 연결이 잠시 끊길 수 있습니다.`})]}),W(`div`,{class:`flex shrink-0 gap-2`,children:[W(`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:`취소`}),W(`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?W(`div`,{class:`px-5 pb-5 sm:px-6 sm:pb-6`,children:[W(`h3`,{class:`mb-3 text-sm font-black text-slate-950`,children:`현재 상태`}),W(`dl`,{class:`grid overflow-hidden rounded-xl border border-slate-200 bg-slate-200 sm:grid-cols-2`,children:[W(`div`,{class:`bg-white p-4`,children:[W(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Installed`}),W(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black text-slate-950`,children:j?.installedVersion||`미확인`})]}),W(`div`,{class:`bg-white p-4`,children:[W(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Available`}),W(`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?`확인 필요`:`최신 버전`})]}),W(`div`,{class:`bg-white p-4`,children:[W(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Last check`}),W(`dd`,{class:`mt-2 mb-0 ml-0 text-sm font-black text-slate-950`,children:W(Rv,{fallback:`아직 없음`,timestamp:n.lastCheckAt})})]}),W(`div`,{class:`bg-white p-4`,children:[W(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Auto install`}),W(`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?W(`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:W(`div`,{class:`flex gap-3`,children:[W(`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:W(Cn,{class:`size-5`})}),W(`div`,{children:[W(`h3`,{class:`m-0 text-base font-black text-amber-950`,children:`업데이트 확인이 지연되고 있습니다.`}),W(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:[`마지막 업데이트 확인: `,W(Rv,{timestamp:n.lastCheckAt}),`. 자동 확인 상태를 점검하거나 업데이트 확인을 실행해 주세요.`]})]})]})}):W(`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:W(`div`,{class:`flex gap-3`,children:[W(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-emerald-700`,children:W(Sn,{class:`size-5`})}),W(`div`,{children:[W(`h3`,{class:`m-0 text-base font-black text-emerald-950`,children:`최신 버전을 사용 중입니다.`}),W(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-emerald-800`,children:[`마지막 업데이트 확인: `,W(Rv,{timestamp:n.lastCheckAt}),`.`]}),P?W(`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?W(`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:W(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between`,children:[W(`div`,{class:`flex min-w-0 gap-3`,children:[W(`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:W(pn,{class:`size-5`})}),W(`div`,{class:`min-w-0`,children:[W(`h3`,{class:`m-0 text-base font-black text-slate-900`,children:`업데이트 상태를 아직 확인하지 않았습니다.`}),W(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`업데이트 확인을 실행하면 현재 설치 버전과 사용 가능한 최신 버전을 확인합니다.`})]})]}),W(`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:[W(pn,{class:`size-4`}),`지금 확인`]})]})}):null]}),n?W(b,{children:[W(`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:[W(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[W(`div`,{class:`flex min-w-0 gap-4`,children:[W(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-600`,children:W(an,{class:`size-5`})}),W(`div`,{class:`min-w-0`,children:[W(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Settings`}),W(`h3`,{class:`mt-2 mb-0 text-lg font-black text-slate-950`,children:`자동 업데이트`}),W(`p`,{class:`mt-2 mb-0 max-w-3xl text-sm leading-6 text-slate-500`,children:`관리 소프트웨어의 자동 확인과 자동 설치를 설정합니다.`})]})]}),x?W(`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:[W(Cn,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}):null]}),W(`div`,{class:`ssh-software-settings-grid mt-4`,children:[W(`div`,{class:`ssh-software-settings-channel rounded-xl border border-slate-200 p-4 sm:p-5`,children:W(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[W(`div`,{class:`min-w-0`,children:[W(`strong`,{class:`block text-sm font-black text-slate-950`,children:`업데이트 채널`}),W(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`현재 관리 소프트웨어 업데이트를 확인하는 배포 채널입니다.`})]}),W(`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 ${rb(n.settings.channel)}`,title:`현재 업데이트 확인 채널`,children:nb(n.settings.channel)})]})}),W(`div`,{class:`min-w-0 overflow-hidden rounded-xl border border-slate-200 p-4 sm:p-5`,children:[W(`div`,{class:`flex min-w-0 items-start justify-between gap-4`,children:[W(`div`,{class:`min-w-0`,children:W(`strong`,{class:`block text-sm font-black text-slate-950`,children:`자동으로 업데이트 확인`})}),W(cb,{checked:f,label:`자동 업데이트 확인`,onChange:e=>{p(e),S(F({checkEnabled:e}))}})]}),W(`div`,{class:`mt-4 block`,children:[W(`span`,{class:`text-xs font-extrabold text-slate-700`,children:`확인 주기`}),W(W_,{ariaLabel:`업데이트 확인 주기`,className:`mt-2`,disabled:!f,id:`software-update-check-interval`,onChange:e=>{let t=Number(e);h(t),S(F({checkIntervalSeconds:t}))},options:$y.map(e=>({value:String(e.value),label:e.label})),value:String(m),variant:`emphasized`})]})]}),W(`div`,{class:`min-w-0 overflow-hidden rounded-xl border border-slate-200 p-4 sm:p-5`,children:[W(`div`,{class:`flex min-w-0 items-start justify-between gap-4`,children:[W(`div`,{class:`min-w-0`,children:W(`strong`,{class:`block text-sm font-black text-slate-950`,children:`자동으로 업데이트 설치`})}),W(cb,{checked:g,label:`자동 업데이트 설치`,onChange:e=>{_(e),S(F({autoInstall:e}))}})]}),W(`div`,{class:`mt-4 min-w-0`,children:[W(`span`,{class:`text-xs font-extrabold text-slate-700`,children:`설치 시각`}),W(Sy,{ariaLabel:`자동 업데이트 설치 시각`,className:`mt-2`,disabled:!g,id:`software-update-auto-install-time`,onChange:e=>{y(e),S(F({autoInstallTime:e}))},value:v,variant:`emphasized`})]})]})]}),g?W(`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,W(`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:[W(`p`,{class:`m-0 text-xs leading-5 text-slate-500`,children:[`마지막 설치: `,tb(n.lastInstallAt)]}),W(`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 I(),type:`button`,children:e===`settings`?`저장 중`:`자동 업데이트 설정 저장`})]})]}),n.updateCount>0?W(`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:[W(`div`,{class:`flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4`,children:[W(`div`,{children:[W(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700`,children:`Release notes`}),W(`h3`,{class:`mt-2 mb-0 text-lg font-black text-slate-950`,children:`업데이트 내용`}),W(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500`,children:N?`릴리즈 내역이 많거나 일부 내역을 가져올 수 없어 최신 버전의 변경 사항만 표시합니다.`:`현재 설치된 버전 이후 포함되는 주요 변경 사항입니다.`})]}),n.releaseNotes.length?W(`span`,{class:`shrink-0 rounded-full bg-slate-100 px-3 py-1.5 text-xs font-extrabold text-slate-600`,children:N?`최신 릴리즈 노트`:n.releaseNotes.length===1?`1개 릴리즈`:`${n.releaseNotes.length}개 릴리즈`}):null]}),n.releaseNotes.length?W(`div`,{class:`mt-5 grid gap-3`,children:[n.releaseNotes.map(e=>W(`article`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4 sm:p-5`,children:[W(`div`,{class:`flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-4`,children:[W(`strong`,{class:`break-all text-sm font-black text-slate-950`,children:e.version}),e.date?W(`span`,{class:`text-xs font-bold text-slate-500`,children:e.date}):null]}),e.summary?W(`p`,{class:`mt-3 mb-0 text-sm leading-6 text-slate-700`,children:e.summary}):null,e.sections.length?W(`details`,{class:`mt-4 rounded-lg border border-slate-200 bg-white p-3`,open:n.releaseNotes.length===1,children:[W(`summary`,{class:`cursor-pointer text-xs font-extrabold text-slate-800`,children:`자세한 변경 사항`}),W(`div`,{class:`mt-3 grid gap-4`,children:e.sections.map(t=>W(`section`,{children:[W(`h4`,{class:`m-0 text-xs font-extrabold text-slate-700`,children:t.title}),W(`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)=>W(`li`,{children:n},`${e.version}-${t.title}-${r}`))})]},`${e.version}-${t.title}`))})]}):null]},e.version)),n.releaseNotesComplete?null:W(`p`,{class:`m-0 rounded-xl bg-amber-50 px-4 py-3 text-xs font-bold leading-5 text-amber-800`,children:N?`최신 버전의 릴리즈 노트만 표시합니다. 이전 버전의 변경 사항은 생략되었을 수 있습니다.`:`일부 릴리즈 노트를 불러오지 못했습니다. 업데이트 확인과 설치는 계속 사용할 수 있습니다.`})]}):W(`p`,{class:`mt-4 mb-0 rounded-xl bg-slate-50 px-4 py-3 text-sm text-slate-500`,children:`릴리즈 노트를 불러오지 못했습니다. 업데이트 확인과 설치는 계속 사용할 수 있습니다.`})]}):null]}):null]})}function ub({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 W(`section`,{class:`min-w-0 space-y-5`,children:[W(`div`,{children:W(J,{eyebrow:`Firmware`,title:`시스템 펌웨어`,description:`공유기 운영체제의 새 버전을 확인하고 설치합니다.`})}),W(Qy,{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}),W(`div`,{class:`pt-2`,children:W(J,{eyebrow:`Software`,title:`관리 소프트웨어`,description:`SmartSafeHub 관리 화면 및 서비스의 업데이트를 확인합니다.`})}),W(lb,{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 db={};db.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},db.QRErrorCorrectLevel=(e,t)=>{e.exports={L:1,M:0,Q:3,H:2}},db.QRMaskPattern=(e,t)=>{e.exports={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7}},db.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}},db.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(pb(`index`))(0,pb(`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 hb(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 gb(e){return`${(e.replace(/[\\/:*?"<>|\x00-\x1f\x7f]/g,`_`).replace(/[. ]+$/g,``).trim()||`Wi-Fi`).slice(0,64)}-QR.png`}function _b(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 vb({load:e,onClose:t}){let[n,r]=L(null),[i,a]=L(null),[o,s]=L(null);R(()=>{let t=!0;return e().then(e=>{if(t)try{r({ssid:e.ssid,matrix:mb(hb(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 W(`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:W(`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:[W(`h2`,{class:`m-0 text-xl font-extrabold`,children:`Wi-Fi QR 코드`}),W(`p`,{class:`mt-2 break-all text-sm font-semibold text-slate-600`,children:n?.ssid??`Wi-Fi 정보를 확인하는 중`}),i?W(`p`,{role:`alert`,class:`my-6 text-sm font-bold text-red-700`,children:i}):null,!n&&!i?W(`p`,{class:`my-6 text-sm`,children:`QR 코드를 만드는 중...`}):null,n?W(`div`,{class:`mx-auto my-5 w-full max-w-[288px] rounded-xl border border-slate-200 bg-white p-3`,children:W(`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:[W(`rect`,{width:c+8,height:c+8,fill:`white`}),n.matrix.map((e,t)=>e.map((e,n)=>e?W(`rect`,{x:n+4,y:t+4,width:`1`,height:`1`,fill:`black`},`${t}-${n}`):null))]})}):null,W(`p`,{class:`text-xs leading-5 text-slate-500`,children:[W(`span`,{class:`block`,children:`카메라로 스캔해 Wi-Fi에 연결하세요.`}),W(`span`,{class:`block`,children:`QR 코드에는 비밀번호가 포함됩니다.`})]}),o?W(`p`,{role:`alert`,class:`mt-3 text-sm font-bold text-red-700`,children:o}):null,n?W(`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`);_b(n.matrix,r,8);let i=document.createElement(`a`);i.href=t.toDataURL(`image/png`),i.download=gb(n.ssid),i.click()}catch{s(`QR 코드 이미지를 저장하지 못했습니다. 다시 시도해 주세요.`)}}},type:`button`,children:[W(hn,{class:`size-5`}),`QR 코드 다운로드`]}):null,W(`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 yb({guest:e,busy:t,saving:n,onUpdate:r,onLoadQr:i}){let[a,o]=L(!1),[s,c]=L(!1),[l,u]=L(e.ssid),[d,f]=L(``),[p,m]=L(e.enabled),[h,g]=L(null);if(R(()=>{u(e.ssid),m(e.enabled),f(``),g(null)},[e]),!e.supported)return W(`section`,{class:`mt-5 rounded-2xl border border-slate-200 bg-white p-5 sm:p-6`,children:[W(`h2`,{class:`m-0 text-lg font-extrabold text-slate-900`,children:`게스트 Wi-Fi`}),W(`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,v=t||_||!e.enabled||!e.passwordConfigured,y=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 W(b,{children:[W(`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 y(e),children:[W(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[W(`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:[W(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:W(Jt,{class:`size-6`})}),W(`span`,{class:`min-w-0 flex-1`,children:[W(`span`,{class:`block text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`2.4 GHz · 분리 네트워크`}),W(`span`,{class:`mt-2 block text-xl font-extrabold tracking-tight text-slate-950`,children:`게스트 Wi-Fi`}),W(`span`,{class:`mt-2 block break-all text-sm text-slate-600`,children:e.ssid})]}),W(An,{class:`mt-3 size-5 shrink-0 text-slate-500 transition-transform ${a?`rotate-180`:``}`})]}),W(`div`,{class:`flex flex-wrap items-center gap-2 sm:justify-end`,children:[_&&W(`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:[W(Cn,{class:`size-3.5`}),`저장되지 않음`]}),W(`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:v,onClick:()=>c(!0),title:e.enabled?void 0:`게스트 Wi-Fi를 켜고 저장하면 QR 코드를 사용할 수 있습니다.`,type:`button`,children:[W(sn,{class:`size-4 shrink-0`}),`QR 코드 보기`]}),W(`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?`사용 중`:`꺼짐`})]})]}),W(`div`,{class:a?``:`hidden`,id:`ssh-guest-settings`,children:[W(`p`,{class:`mt-5 mb-0 text-sm leading-6 text-slate-600`,children:`방문자는 인터넷만 이용하며 내부 네트워크와 공유기 관리 화면에는 접근할 수 없습니다.`}),W(`div`,{class:`mt-5 grid gap-5 lg:grid-cols-2`,children:[W(`label`,{class:`block`,children:[W(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`게스트 Wi-Fi 이름 (SSID)`}),W(`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})]}),W(`label`,{class:`block`,children:[W(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`게스트 Wi-Fi 비밀번호`}),W(`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}),W(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`WPA2/WPA3 혼합 · 기본 Wi-Fi와 다른 비밀번호를 권장합니다.`})]})]}),W(`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&&W(`span`,{class:`block text-xs text-slate-500`,children:[`게스트 게이트웨이: `,e.subnet]})]}),W(`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:[W(`label`,{class:`inline-flex min-h-11 items-center gap-3 text-sm font-extrabold text-slate-800`,children:[W(`input`,{checked:p,class:`size-5 accent-teal-700`,disabled:t,onChange:e=>m(e.currentTarget.checked),type:`checkbox`}),`게스트 Wi-Fi 사용`]}),W(`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 ${Y_(t)}`,disabled:t||!_,type:`submit`,children:[W(Sn,{class:`size-5 ${n?`animate-pulse`:``}`}),n?`적용 중`:`설정 저장`]})]}),h&&W(`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&&W(vb,{load:i,onClose:()=>c(!1)})]})}var bb=[{value:`sae-mixed`,label:`WPA2/WPA3 혼합 (권장)`},{value:`sae`,label:`WPA3-Personal`},{value:`psk2`,label:`WPA2-Personal`},{value:`none`,label:`암호화 없음`}];function xb(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 Sb(e){return e.length>=8&&e.length<=63||/^[0-9a-fA-F]{64}$/.test(e)}function Cb({network:e,busy:t,saving:n,onUpdate:r,onLoadQr:i}){let[a,o]=L(e.ssid),[s,c]=L(e.enabled),[l,u]=L(e.security===`custom`?`keep`:e.security),[d,f]=L(``),[p,m]=L(!1),h=B(()=>i(e.section),[i,e.section]),[g,_]=L(null);R(()=>{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})`}]:[],...bb],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&&!Sb(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 W(`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:[W(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[W(`div`,{class:`flex min-w-0 items-start gap-3`,children:[W(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:W(Jt,{class:`size-6`})}),W(`div`,{class:`min-w-0`,children:[W(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:e.bandLabel}),W(`h2`,{class:`mt-2 mb-0 break-words text-xl font-extrabold tracking-tight text-slate-950`,children:e.ssid||`이름 없는 Wi-Fi`}),W(`p`,{class:`mt-2 mb-0 text-sm text-slate-600`,children:[`채널 `,e.channel??`자동`,` · 연결 기기 `,e.clientCount,`대`]})]})]}),W(`div`,{class:`flex flex-wrap items-center gap-2 sm:justify-end`,children:[x?W(`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:[W(Cn,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}):null,W(`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:[W(sn,{class:`size-4 shrink-0`}),`QR 코드 보기`]}),W(`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:[W(`span`,{class:`size-2 rounded-full ${e.runtimeUp?`bg-emerald-500`:`bg-slate-400`}`}),e.runtimeUp?`동작 중`:e.enabled?`시작 대기`:`꺼짐`]})]})]}),W(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[W(`label`,{class:`block`,children:[W(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`Wi-Fi 이름 (SSID)`}),W(`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})]}),W(`div`,{class:`block`,children:[W(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`보안 방식`}),W(W_,{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`}),W(`span`,{class:`mt-2 block text-xs text-slate-500`,children:[`현재: `,xb(e)]})]}),W(`label`,{class:`block lg:col-span-2`,children:[W(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`새 비밀번호`}),W(`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}),W(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`저장된 비밀번호는 일반 설정 조회에 노출되지 않습니다. QR 코드에는 비밀번호가 포함됩니다.`})]})]}),W(`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:[W(`label`,{class:`inline-flex min-h-11 items-center gap-3 text-sm font-extrabold text-slate-800`,children:[W(`input`,{checked:s,class:`size-5 accent-teal-700`,disabled:t,onChange:e=>c(e.currentTarget.checked),type:`checkbox`}),`이 Wi-Fi 사용`]}),W(`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 ${Y_(t)} disabled:opacity-60 sm:w-auto`,disabled:t||!x,type:`submit`,children:[W(Sn,{class:`size-5 ${n?`animate-pulse`:``}`}),n?`적용 중`:`설정 저장`]})]}),p?W(vb,{load:h,onClose:()=>m(!1)}):null,g?W(`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 wb({feedback:e,onDismiss:t}){return W(`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:[W(`p`,{class:`m-0 font-bold leading-6`,children:e.message}),W(`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 Tb({data:e,error:t,feedback:n,loading:r,updatingSection:i,onDismissFeedback:a,onRetry:o,onUpdate:s,onUpdateGuest:c,onLoadQr:l,onLoadGuestQr:u}){return r?W(vs,{}):t?W(ys,{message:t,onRetry:o}):e?W(b,{children:[n?W(wb,{feedback:n,onDismiss:a}):null,W(`section`,{class:`mb-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-amber-950 sm:p-6`,children:[W(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-amber-700`,children:`연결 주의`}),W(`h2`,{class:`mt-2 mb-0 text-lg font-extrabold`,children:`Wi-Fi 이름이나 비밀번호를 바꾸면 현재 연결이 끊어집니다`}),W(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-amber-900/80`,children:`변경한 Wi-Fi 이름과 비밀번호로 다시 연결하면 SmartSafeHub를 계속 사용할 수 있습니다. VLAN 등 고급 무선 옵션은 기존 LuCI에서 관리합니다.`})]}),W(J,{eyebrow:`Wireless`,title:`기본 Wi-Fi`,description:`무선 네트워크의 이름, 비밀번호, 보안 방식과 QR 코드를 관리합니다.`}),e.networks.length?W(`section`,{"aria-label":`기본 Wi-Fi 설정`,class:`grid gap-5 xl:grid-cols-2`,children:e.networks.map(e=>W(Cb,{busy:i!==null,network:e,onUpdate:s,onLoadQr:l,saving:i===e.section},e.section))}):W(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 text-center shadow-sm shadow-slate-900/5 sm:p-8`,children:[W(Jt,{class:`mx-auto size-10 text-slate-400`}),W(`h2`,{class:`mt-4 mb-0 text-lg font-extrabold text-slate-950`,children:`관리할 기본 Wi-Fi를 찾지 못했습니다`}),W(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:`기존 LuCI에서 AP 모드의 무선 네트워크를 먼저 구성해 주세요.`})]}),e.guest&&W(b,{children:[W(`div`,{role:`separator`,"aria-label":`기본 Wi-Fi와 게스트 Wi-Fi 구분`,class:`mt-5 w-full border-t border-slate-200`}),W(`div`,{class:`mt-5`,children:W(J,{eyebrow:`Guest network`,title:`방문자용 네트워크`,description:`기본 네트워크와 분리된 게스트 Wi-Fi를 관리합니다.`})}),W(yb,{guest:e.guest,busy:i!==null,saving:i===`ssh_guest`,onUpdate:c,onLoadQr:u})]}),e.guestError&&W(`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 정보를 불러오지 못했습니다. 패키지를 설치한 직후라면 로그아웃 후 다시 로그인해 주세요.`,W(`button`,{class:`ml-3 underline font-bold`,onClick:o,type:`button`,children:`다시 시도`})]})]}):null}function Eb(){return U(`smartsafehub`,`netwatch_status`,{},{priority:`low`,timeoutMs:5e3})}function Db(e){return U(`smartsafehub`,`netwatch_enabled_update`,{enabled:e},{priority:`high`,timeoutMs:6e3})}var Ob=120,kb=40;function Ab(e,t){return t.limited?{label:`판단 보류`,tone:`bg-slate-100 text-slate-600`}:t.alert?.ip===e.ip?{label:`주의 기록`,tone:`bg-amber-100 text-amber-800`}:e.connections>=Ob||e.destinations>=kb?{label:`연결 많음`,tone:`bg-amber-50 text-amber-800`}:{label:`관찰 중`,tone:`bg-slate-100 text-slate-600`}}function jb(e){return(e?.events??[]).filter(e=>e.eventType===`network_watch.anomaly.warning`&&e.source===`network_watch`&&Number.isFinite(e.occurredAt)&&e.occurredAt>0).sort((e,t)=>t.occurredAt-e.occurredAt).slice(0,3)}function Mb(e){let t=e.metadata?.source_ip;return typeof t==`string`&&/^\d{1,3}(?:\.\d{1,3}){3}$/.test(t)?t:null}function Nb({data:e,error:t,loading:n,onRetry:r,onSetEnabled:i,devices:a,activity:o,demo:s=!1,onDemoAction:c}){let[l,u]=L(!1),[d,f]=L(null),[p,m]=L(null),h=!!e?.enabled,g=h&&e?.phase===`ready`,_=l?`저장 중…`:e?h?e.phase===`ready`?`사용 중 · 관찰 중`:e.phase===`unavailable`?`사용 중 · 수집 불가`:`사용 중 · 준비 중`:`사용 안 함`:n?`확인 중`:`상태 확인 불가`,v=e&&h&&e.phase===`ready`?`text-emerald-700`:e&&h&&e.phase===`unavailable`?`text-amber-700`:`text-slate-500`;R(()=>{if(!p)return;let e=window.setTimeout(()=>m(null),3500);return()=>window.clearTimeout(e)},[p]);let y=async()=>{if(s)c?.();else if(!l&&e){u(!0),f(null),m(null);try{await i(!h),m(h?`Network Watch를 껐습니다.`:`Network Watch를 켰습니다. 다음 관찰 주기부터 반영됩니다.`)}catch(e){f(e instanceof Error?e.message:`설정을 저장하지 못했습니다.`)}finally{u(!1)}}},x=new Map;for(let e of a?.devices??[])e.online&&e.ipv4Address&&e.hostname?.trim()&&x.set(e.ipv4Address,e.hostname.trim());let S=jb(o);return W(`div`,{class:`space-y-5`,children:[W(J,{eyebrow:`Security`,title:`Network Watch`,description:`연결된 기기의 외부 통신 변화를 ${e?.intervalSeconds??60}초 간격으로 관찰합니다. 의심 징후는 참고 정보이며 악성 앱을 확정하거나 연결을 자동 차단하지 않습니다.`,action:W(`div`,{class:`flex items-center gap-3 rounded-xl border border-slate-100 bg-slate-50 px-3 py-2.5`,"aria-busy":l,children:[W(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-500`,children:W($t,{class:`size-5`})}),W(`div`,{class:`text-right`,children:[W(`p`,{class:`m-0 text-[0.68rem] font-bold text-slate-400`,children:`관찰 수집`}),W(`p`,{class:`mt-1 mb-0 whitespace-nowrap text-xs font-extrabold ${v}`,role:`status`,children:_})]}),W(`button`,{type:`button`,role:`switch`,"aria-checked":h,"aria-label":`네트워크 이상 감지 사용`,disabled:l||!e,onClick:()=>void y(),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 disabled:cursor-not-allowed disabled:opacity-50 ${h?`bg-teal-600`:`bg-slate-300`}`,children:W(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${h?`left-6`:`left-1`}`})})]})}),p?W(`p`,{role:`status`,class:`text-sm text-teal-700`,children:p}):null,d?W(`p`,{role:`alert`,class:`text-sm text-rose-700`,children:d}):null,n&&!e?W(vs,{}):null,t&&!e?W(ys,{message:t,onRetry:r}):null,e&&!h?W(`p`,{role:`status`,class:`rounded-xl border border-slate-200 bg-slate-50 p-4 text-sm text-slate-600`,children:`네트워크 관찰이 꺼져 있습니다. 기존 방화벽과 SafeShield 보호에는 영향이 없습니다.`}):null,h&&e?.phase===`preparing`?W(`p`,{role:`status`,class:`rounded-xl border border-slate-200 bg-slate-50 p-4 text-sm text-slate-600`,children:`첫 연결 관찰을 준비하고 있습니다. 잠시 후 자동으로 갱신됩니다.`}):null,h&&e?.phase===`unavailable`?W(`div`,{role:`alert`,class:`rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900`,children:`연결 정보를 읽을 수 없습니다. OpenWrt의 conntrack 지원 여부를 확인해 주세요. 실제 감시가 수행 중인 것으로 표시하지 않습니다.`}):null,g&&e?W(b,{children:[W(`div`,{class:`grid gap-3 sm:grid-cols-3`,children:[W(`div`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm`,children:[W(`p`,{class:`text-sm font-semibold text-slate-500`,children:`현재 연결 수`}),W(`p`,{class:`mt-2 text-xl font-extrabold text-slate-900`,children:Y(e.totalConnections)})]}),W(`div`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm`,children:[W(`p`,{class:`text-sm font-semibold text-slate-500`,children:`관찰된 기기 수`}),W(`p`,{class:`mt-2 text-xl font-extrabold text-slate-900`,children:Y(e.totalDevices)})]}),W(`div`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm`,children:[W(`p`,{class:`text-sm font-semibold text-slate-500`,children:`마지막 갱신`}),W(`p`,{class:`mt-2 text-xl font-extrabold text-slate-900`,children:e.sampledAt>0?jo(e.sampledAt):`—`}),e.sampledAt>0?W(`p`,{class:`mt-1 text-xs text-slate-500`,title:`브라우저 시간대 기준`,children:Ao(e.sampledAt)}):null]})]}),e.limited?W(`p`,{role:`status`,class:`rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800`,children:`연결 수가 관찰 한도를 초과하여 일부 연결만 집계했습니다. 이번 결과로 이상 여부를 판단하지 않습니다.`}):null,e.alert?W(`p`,{role:`status`,class:`rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900`,children:[e.alert.ip,` 기기에서 연결 또는 목적지 수가 연속해 높게 관찰되었습니다. 정상적인 앱 사용일 수도 있습니다.`]}):null,W(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm`,children:[W(`h2`,{class:`text-base font-extrabold text-slate-900`,children:`기기별 외부 연결 현황`}),W(`p`,{class:`mt-1 text-xs text-slate-500`,children:`IPv4 기준 현재 연결 스냅샷 · 최대 12대 표시 · 콘텐츠 분석 없음`}),e.devices.length?W(`div`,{class:`mt-4 overflow-x-auto`,children:W(`table`,{class:`w-full text-left text-sm`,children:[W(`thead`,{class:`text-slate-500`,children:W(`tr`,{children:[W(`th`,{class:`pb-3`,children:`기기`}),W(`th`,{class:`pb-3 text-right`,children:`연결 수`}),W(`th`,{class:`pb-3 text-right`,children:`외부 목적지 수`}),W(`th`,{class:`pb-3 text-right`,children:`관찰 상태`})]})}),W(`tbody`,{children:e.devices.map(t=>{let n=Ab(t,e),r=x.get(t.ip);return W(`tr`,{class:`border-t border-slate-100 text-slate-700`,children:[W(`td`,{class:`py-3`,children:[W(`div`,{class:`font-semibold`,children:r||t.ip}),r?W(`div`,{class:`mt-0.5 text-xs text-slate-500`,children:t.ip}):null]}),W(`td`,{class:`py-3 text-right`,children:Y(t.connections)}),W(`td`,{class:`py-3 text-right`,children:Y(t.destinations)}),W(`td`,{class:`py-3 text-right`,children:W(`span`,{class:`inline-block whitespace-nowrap rounded-full px-2.5 py-1 text-xs font-bold ${n.tone}`,children:n.label})})]},t.ip)})})]})}):W(`p`,{class:`mt-4 text-sm text-slate-500`,children:`현재 확인된 외부 연결이 없습니다.`}),W(`p`,{class:`mt-3 text-xs text-slate-500`,children:`‘연결 많음’은 관찰량 기준이며 악성 통신 판정이 아닙니다. 기기 이름을 확인할 수 없으면 IP만 표시합니다.`})]}),W(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm`,children:[W(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[W(`h2`,{class:`text-base font-extrabold text-slate-900`,children:`최근 감지 이벤트`}),W(`a`,{href:`#activity`,class:`text-xs font-bold text-teal-700 hover:underline`,children:`최근 활동 전체 보기`})]}),W(`p`,{class:`mt-1 text-xs text-slate-500`,children:`현재 부팅 후 기록된 Network Watch 주의 이벤트 중 최신 3건`}),S.length?W(`ul`,{class:`mt-4 divide-y divide-slate-100`,children:S.map(e=>W(`li`,{class:`flex flex-wrap items-start justify-between gap-2 py-3 first:pt-0 last:pb-0`,children:[W(`div`,{class:`min-w-0`,children:[W(`p`,{class:`text-sm font-bold text-slate-800`,children:`외부 통신 증가 감지`}),W(`p`,{class:`mt-1 text-xs text-slate-600`,children:[Mb(e)??`연결된 기기`,` · 비정상 통신 확정 아님`]})]}),W(`time`,{class:`text-xs text-slate-500`,dateTime:new Date(e.occurredAt*1e3).toISOString(),title:Ao(e.occurredAt),children:jo(e.occurredAt)})]},e.eventId))}):W(`p`,{class:`mt-4 text-sm text-slate-500`,children:`최근 감지된 주의 이벤트가 없습니다.`})]})]}):null,W(`section`,{"aria-label":`Network Watch 관찰 범위`,class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[W(`h2`,{class:`text-sm font-bold text-slate-700`,children:`관찰 범위 안내`}),W(`ul`,{class:`mt-2 list-disc space-y-1 pl-5 text-xs leading-relaxed text-slate-600`,children:[W(`li`,{children:`개인정보 보호를 위해 통신 내용은 읽지 않고 연결 요약만 임시 메모리에 저장합니다.`}),W(`li`,{children:`IPv6, 짧게 종료된 연결 및 일부 하드웨어 가속 연결은 관찰에서 제외될 수 있습니다.`}),W(`li`,{children:`주의 이벤트는 최근 활동에 기록되며, Cloud 활동 전송을 직접 켠 경우 내부 IP·연결 수 등의 요약 정보가 전송될 수 있습니다.`})]})]})]})}function Pb({onAdministratorPasswordChanged:e}){let t=zi(),n=Ui(),r=xi(),i=di(t===`activity`),a=li({active:t===`apps`,fallbackError:`검증된 앱 목록을 불러오지 못했습니다.`,loader:or,pollInterval:e=>e?.refreshing?2e3:e?.ready?6e4:e?.refreshFailed?15e3:2e3,staleTimeMs:2e3}),o=li({active:t===`apps`,fallbackError:`앱 설치 상태를 불러오지 못했습니다.`,loader:cr,pollInterval:e=>!e?.ready||e?.refreshing?2e3:e?.job.phase===`running`||e?.job.phase===`starting`||e?.restore?.phase===`running`||e?.restore?.phase===`starting`?3e3:12e3,staleTimeMs:3e3}),s=li({active:t===`apps`,fallbackError:`저장 공간 정보를 확인하지 못했습니다.`,loader:ur,pollInterval:e=>e?.refreshing||!e?.ready?3e3:6e4,staleTimeMs:5e3}),c=ro(t===`home`||t===`settings`),l=to(!0),u=Ii(!0),d=ea(t===`network`),f=Qi(t===`home`||t===`network`),p=Xi(t===`iptv`),m=Co(t===`wifi`),h=li({active:t===`netwatch`,fallbackError:`Network Watch 상태를 읽을 수 없습니다.`,loader:Eb,pollInterval:6e4,staleTimeMs:5e3}),g=Ci(t===`home`,!1),_=Ci(t===`devices`||t===`netwatch`,t===`devices`),v=li({active:t===`netwatch`,fallbackError:`Network Watch 최근 이벤트를 확인할 수 없습니다.`,loader:Jn,pollInterval:6e4,staleTimeMs:5e3}),y=Xa(t===`home`||t===`safeshield`),b=Va(t===`home`||t===`safeshield`,t===`safeshield`),x=y,S=b,C=za(t===`rules`),w=vo(t===`account`),T=bo(t===`account`,w.data?.accountRegistered??null),E=po(c.data),D=Yi(t===`home`||t===`settings`),O=io(t===`settings`),k=mo(t===`settings`);R(()=>{let e=Cy();e&&Er(e).then(e=>{e.applied&&t===`settings`&&k.refresh()}).catch(()=>{})},[]);let A=o.data?.job.operationId,j=o.data?.job.phase,M=o.data?.restore?.phase,N=z(``);R(()=>{if(t!==`apps`||j!==`success`&&M!==`success`)return;let e=`${A??``}:${j}:${M??``}`;e!==N.current&&(N.current=e,Promise.allSettled([lr(),dr()]).then(()=>{o.refresh(),s.refresh()}))},[t,A,j,M]);let P=Aa(y.refresh,b.refresh),F=t===`activity`?i:t===`netwatch`?h:t===`network`?d:t===`wifi`?m:t===`iptv`?p:t===`devices`?_:t===`safeshield`?y:t===`rules`?C:t===`account`?w:t===`apps`?a:t===`system`?l:c,I;switch(t){case`netwatch`:I=W(Nb,{data:h.data,devices:_.data,activity:v.data,error:h.error,loading:h.loading,onRetry:()=>void h.refresh(),onSetEnabled:async e=>{await Db(e),await h.refresh()}});break;case`apps`:I=W(_s,{data:a.data?.ready===!1?null:a.data,error:a.error??(a.data?.refreshFailed&&!a.data?.ready?`앱 목록 갱신에 실패했습니다. 서버 연결을 확인해 주세요.`:null),loading:a.loading||!!a.data&&!a.data.ready&&!a.data.refreshFailed,refreshing:a.refreshing||!!a.data?.refreshing,onRetry:()=>void sr().then(()=>a.refresh()).then(()=>Promise.all([o.refresh(),s.refresh()])).catch(()=>a.refresh()),installStatus:o.data?.ready===!1?null:o.data,metrics:s.data,statusError:o.error??(o.data?.ready===!1?`설치 상태를 확인하고 있습니다.`:null),onRefreshStatus:()=>void o.refresh(),onInstall:async e=>{await fr(e),await o.refresh()},onUpdate:async e=>{await pr(e),await o.refresh()},onRemove:async e=>{await mr(e),await o.refresh()}});break;case`network`:I=W(`div`,{class:`min-w-0`,children:[W(ci,{active:n.network===`internet`?`internet`:`lan`,label:`네트워크 설정 메뉴`,tabs:[{id:`internet`,label:`인터넷 연결`,href:`#network`},{id:`lan`,label:`LAN 및 DHCP`,href:`#network/lan`}]}),W(`div`,{"aria-label":`인터넷 연결 설정`,"aria-labelledby":`internet-tab`,class:`min-w-0`,hidden:n.network!==`internet`,id:`internet-panel`,role:`tabpanel`,children:W(wv,{action:d.action,data:d.data,error:d.error,feedback:d.feedback,loading:d.loading,onDismissFeedback:d.dismissFeedback,onReconnect:()=>void d.reconnect(),onRetry:()=>void d.refresh(),onSave:d.save})}),W(`div`,{"aria-label":`내부 네트워크 설정`,"aria-labelledby":`lan-tab`,class:`min-w-0`,hidden:n.network!==`lan`,id:`lan-panel`,role:`tabpanel`,children:W(uv,{action:f.action,data:f.data,error:f.error,feedback:f.feedback,loading:f.loading,onApplyRecommendation:f.applyRecommendation,onDismissFeedback:f.dismissFeedback,onRetry:()=>void f.refresh(),onSave:f.save})})]});break;case`activity`:I=W(ts,{actionError:i.actionError,actionMessage:i.actionMessage,data:i.data,error:i.error,loading:i.loading,onDismissActionFeedback:i.dismissActionFeedback,onRetry:()=>void i.refresh(),onSetCloudSync:e=>void i.setCloudSyncEnabled(e),savingCloudSync:i.savingCloudSync});break;case`wifi`:I=W(Tb,{data:m.data,error:m.error,feedback:m.feedback,loading:m.loading,onDismissFeedback:m.dismissFeedback,onRetry:()=>void m.refresh(),onUpdate:m.update,onUpdateGuest:m.updateGuest,onLoadQr:Ur,onLoadGuestQr:Wr,updatingSection:m.updatingSection});break;case`iptv`:I=W(J_,{data:p.data,error:p.error,feedback:p.feedback,loading:p.loading,onDismissFeedback:p.dismissFeedback,onRetry:()=>void p.refresh(),onSave:p.save,saving:p.saving});break;case`devices`:I=W(js,{data:_.data,error:_.error,loading:_.loading,onRetry:()=>void _.refresh()});break;case`account`:I=W(Gy,{data:w.data,error:w.error,loading:w.loading,pairingBusy:w.pairingBusy,refreshing:w.refreshing,syncError:w.syncError,onRequestPairing:w.requestPairing,onRetry:()=>void w.refresh()});break;case`system`:I=W(ub,{action:l.action,actionError:l.actionError,data:l.data,error:l.error,firmware:u,loading:l.loading,message:l.message,onCheck:()=>void l.check(),onDismissFeedback:l.dismissFeedback,onInstall:()=>void l.install(),onRetry:()=>void l.refresh(),onSaveSettings:l.saveSettings});break;case`settings`:I=W(Ry,{activeTab:n.settings,action:E.action,backupAction:r.action,backupError:r.error,backupMessage:r.message,backupRestoreAccepted:r.restoreAccepted,backupUploadProgress:r.uploadProgress,backupValidated:r.validated,data:c.data,error:c.error,feedbackError:E.error,feedbackMessage:E.message,health:D.data,healthActionError:D.actionError,healthActionMessage:D.actionMessage,healthError:D.error,healthLoading:D.loading,healthRunning:D.running,healthSavingReporter:D.savingReporter,loading:c.loading,onBackupDiscard:r.discard,onBackupDownload:()=>void r.download(),onBackupRestore:r.restore,onBackupUpload:r.upload,onDismissBackupFeedback:r.dismissFeedback,onDismissFeedback:E.dismissFeedback,onDismissHealthFeedback:D.dismissActionFeedback,onDismissTimeFeedback:k.dismissSaveFeedback,onDownloadDiagnostics:()=>void E.downloadDiagnostics(),onRunHealth:()=>void D.runDiagnostic(),onSetHealthReporter:e=>void D.setReporterEnabled(e),onReboot:()=>void E.reboot(),onRetry:()=>void Promise.all([c.refresh(),u.refresh(),k.refresh(),O.refresh(),D.refresh()]),onDismissScheduledRebootFeedback:O.dismissSaveFeedback,onSaveScheduledReboot:O.saveSettings,onSaveTimezone:k.saveTimezone,onSyncTime:k.syncTime,onAdministratorPasswordChanged:e,rebootAccepted:E.rebootAccepted,scheduledRebootData:O.data,scheduledRebootError:O.error,scheduledRebootLoading:O.loading,scheduledRebootSaveError:O.saveError,scheduledRebootSaveMessage:O.saveMessage,scheduledRebootSaving:O.saving,timeData:k.data,timeError:k.error,timeLoading:k.loading,timeSaveError:k.saveError,timeSaveMessage:k.saveMessage,timeSaving:k.saving,timeSyncing:k.syncing});break;case`safeshield`:I=W(oy,{action:P.action,actionError:P.error,actionMessage:P.message,data:y.data,error:y.error,loading:y.loading,statistics:b.data,statisticsError:b.error,statisticsLoading:b.loading,statisticsRefreshing:b.refreshing,onDismissFeedback:P.dismissFeedback,onRefreshBlocklist:()=>void P.refreshBlocklist(),onRetry:()=>void y.refresh(),onRetryStatistics:()=>void b.refresh(),onSetEnabled:e=>void P.setEnabled(e),onSetStatisticsEnabled:e=>void P.setStatisticsEnabled(e)});break;case`rules`:I=W(_y,{action:C.action,data:C.data,error:C.error,feedback:C.feedback,loading:C.loading,onAddRule:C.addRule,onDeleteRule:C.deleteRule,onLoadRecommendedRulesStatus:Zn,onSyncRecommendedRules:Qn,onRevokeRecommendedRule:$n,onDismissFeedback:C.dismissFeedback,onRetry:()=>void C.refresh()});break;default:I=W(C_,{accountRegistered:T,activity:c.data?.activityHistory??null,activityError:c.error,activityLoading:c.loading,data:c.data,devices:g.data,devicesError:g.error,devicesLoading:g.loading,error:c.error,firmware:u.data,firmwareError:u.error,firmwareLoading:u.loading,health:D.data,healthError:D.error,healthLoading:D.loading,lan:f.data,lanError:f.error,lanLoading:f.loading,loading:c.loading,onRetry:()=>void Promise.all([c.refresh(),g.refresh(),y.refresh(),b.refresh(),l.refresh(),u.refresh(),D.refresh(),f.refresh()]),safeshield:x.data,safeshieldError:x.error,safeshieldLoading:x.loading,statistics:S.data,statisticsError:S.error,statisticsLoading:S.loading,statisticsRefreshing:S.refreshing,updates:l.data,updatesError:l.error,updatesLoading:l.loading})}let ee=()=>{t===`home`?Promise.all([c.refresh(),g.refresh(),x.refresh(),S.refresh(),l.refresh(),u.refresh(),D.refresh(),f.refresh()]):t===`network`?Promise.all([d.refresh(),f.refresh()]):t===`system`?Promise.all([l.refresh(),u.refresh()]):t===`settings`?Promise.all([c.refresh(),u.refresh(),k.refresh(),O.refresh(),D.refresh()]):t===`safeshield`?Promise.all([y.refresh(),b.refresh()]):F.refresh()},te=+((l.data?.updateCount??0)>0)+ +!!u.data?.updateAvailable;return W(si,{accountRegistered:T,loading:t===`network`?d.loading||f.loading:F.loading,onRefresh:ee,refreshing:F.refreshing||t===`network`&&f.refreshing||t===`home`&&(i.refreshing||g.refreshing||x.refreshing||S.refreshing||l.refreshing||u.refreshing||D.refreshing||f.refreshing)||t===`system`&&u.refreshing||t===`settings`&&(u.refreshing||k.refreshing||O.refreshing||D.refreshing)||t===`safeshield`&&b.refreshing,route:t,updateCount:te,children:I})}function Fb({onAdministratorPasswordChanged:e,onPasswordConfigured:t}){let[n,r]=L(`checking`),[i,a]=L(!1),o=B(async()=>{r(`checking`);try{let e=await Ut();a(e.recovery),r(e.configured?`ready`:`required`)}catch{r(`error`)}},[]);return R(()=>{o()},[o]),n===`ready`?W(Pb,{onAdministratorPasswordChanged:e}):n===`required`?W(Wn,{onCompleted:()=>t(i),recovery:i}):W(`main`,{class:`ssh-initial-setup-probe`,children:W(`div`,{class:`ssh-initial-setup-probe-card`,role:n===`error`?`alert`:`status`,children:[n===`checking`?W(pn,{class:`ssh-login-probe-spinner`,"aria-hidden":`true`}):W(qt,{"aria-hidden":`true`}),W(`strong`,{children:n===`checking`?`관리자 보안 상태 확인 중`:`관리자 보안 상태를 확인할 수 없습니다`}),W(`span`,{children:n===`checking`?`root 비밀번호 설정 여부를 확인하고 있습니다.`:`공유기 연결을 확인한 뒤 다시 시도해 주세요.`}),n===`error`?W(`button`,{onClick:()=>void o(),type:`button`,children:`다시 시도`}):null]})})}function Ib({icon:e,title:t,description:n}){return W(`div`,{class:`ssh-login-feature`,children:[W(`span`,{class:`ssh-login-feature-icon`,"aria-hidden":`true`,children:e}),W(`span`,{class:`ssh-login-feature-copy`,children:[W(`strong`,{children:t}),W(`span`,{children:n})]})]})}function Lb({theme:e}){return W(e===`dark`?Fn:Pn,{})}function Rb({notice:e,onAuthenticated:t,probing:n=!1}){let r=Xe(xe,[]),i=z(null),a=z(null),[o,s]=L(`root`),[c,l]=L(``),[u,d]=L(`ready`),[f,p]=L(null),[m,h]=L(e??null),[g,_]=L(!1),[v,y]=L(!1),[b,x]=L(!1),[S,C]=L(Ln),w=u===`submitting`,T=S===`dark`?`라이트 모드로 전환`:`다크 모드로 전환`;R(()=>{Rn(S),zn(S)},[S]),R(()=>{if(n)return;let e=window.requestAnimationFrame(()=>a.current?.focus());return()=>window.cancelAnimationFrame(e)},[n]),R(()=>{if(h(e??null),!e)return;let t=window.setTimeout(()=>h(null),7e3);return()=>window.clearTimeout(t)},[e]);let E=e=>{x(e.getModifierState?.(`CapsLock`)??!1)};return W(`main`,{class:`ssh-login-page`,"data-theme":S,children:[m?W(`div`,{"aria-atomic":`true`,"aria-live":`assertive`,class:`ssh-login-toast`,role:`alert`,children:[W(qt,{"aria-hidden":`true`}),W(`span`,{children:m})]}):null,W(`section`,{class:`ssh-login-brand`,"aria-labelledby":`ssh-login-brand-title`,children:[W(`div`,{class:`ssh-login-brand-orb ssh-login-brand-orb-one`,"aria-hidden":`true`}),W(`div`,{class:`ssh-login-brand-orb ssh-login-brand-orb-two`,"aria-hidden":`true`}),W(`div`,{class:`ssh-login-brand-content`,children:[W(`span`,{class:`ssh-login-brand-mark`,"aria-hidden":`true`,children:W(qt,{})}),W(`p`,{class:`ssh-login-eyebrow`,children:`SMART NETWORK PROTECTION`}),W(`h1`,{id:`ssh-login-brand-title`,children:`SmartSafeHub`}),W(`p`,{class:`ssh-login-brand-description`,children:`네트워크와 보안을 하나의 허브에서 편리하게 관리하세요.`}),W(`div`,{class:`ssh-login-features`,"aria-label":`SmartSafeHub 주요 기능`,children:[W(Ib,{description:`DNS 기반 광고·피싱·추적 차단과 사용자 보호 정책을 관리합니다.`,icon:W(qt,{}),title:`SafeShield 보호`}),W(Ib,{description:`Wi-Fi 설정과 현재 연결된 기기의 상태를 한눈에 확인합니다.`,icon:W(on,{}),title:`네트워크 관리`}),W(Ib,{description:`관리 기능과 인증은 현재 SmartSafeHub 장치에서 직접 처리됩니다.`,icon:W(_n,{}),title:`로컬 우선 관리`})]})]})]}),W(`section`,{class:`ssh-login-panel`,"aria-labelledby":`ssh-login-title`,children:[W(`button`,{"aria-label":T,class:`ssh-login-theme-toggle`,onClick:()=>C(e=>e===`dark`?`light`:`dark`),title:T,type:`button`,children:W(Lb,{theme:S})}),W(`div`,{class:`ssh-login-panel-inner`,children:[n?W(`div`,{class:`ssh-login-probe`,role:`status`,"aria-live":`polite`,children:[W(pn,{class:`ssh-login-probe-spinner`,"aria-hidden":`true`}),W(`strong`,{children:`SmartSafeHub 연결 확인 중`}),W(`span`,{children:`로그인 세션을 확인하고 있습니다.`})]}):W(`div`,{class:`ssh-login-card`,children:[W(`div`,{class:`ssh-login-card-heading`,children:[W(`span`,{class:`ssh-login-mobile-mark`,"aria-hidden":`true`,children:W(qt,{})}),W(`div`,{children:[W(`p`,{class:`ssh-login-kicker`,children:`SMARTSAFEHUB ACCESS`}),W(`h2`,{id:`ssh-login-title`,children:`로그인`}),W(`p`,{class:`ssh-login-subtitle`,children:`공유기 관리자 계정의 사용자 이름과 비밀번호를 입력해 주세요.`})]})]}),f?W(`div`,{class:`ssh-login-alert`,id:`ssh-login-error`,role:`alert`,"aria-live":`polite`,children:f}):null,W(`form`,{class:`ssh-login-form`,onSubmit:async e=>{e.preventDefault();let n=o.trim(),r=c;if(p(null),_(!1),!n||!r&&n!==`root`)p(n?`비밀번호를 입력해 주세요.`:`사용자 이름을 입력해 주세요.`),window.requestAnimationFrame(()=>{n?a.current?.focus():i.current?.focus()});else{d(`submitting`);try{let e=await De(n,r);if(e){l(``),t(e);return}l(``),_(!0),p(`로그인하지 못했습니다. 계정 정보를 확인해 주세요. 추가 인증이 설정된 장치는 기본 LuCI 로그인에서 계속할 수 있습니다.`),window.requestAnimationFrame(()=>a.current?.focus())}catch{p(`공유기와 통신할 수 없습니다. 네트워크 연결을 확인한 뒤 다시 시도해 주세요.`)}finally{d(`ready`)}}},noValidate:!0,children:[W(`label`,{class:`ssh-login-field`,for:`smartsafehub-username`,children:[W(`span`,{class:`ssh-login-label`,children:`사용자 이름`}),W(`span`,{class:`ssh-login-input-wrap`,children:[W(`span`,{class:`ssh-login-field-icon`,"aria-hidden":`true`,children:W(Zt,{})}),W(`input`,{autoCapitalize:`none`,autoComplete:`username`,disabled:w,id:`smartsafehub-username`,name:`username`,onInput:e=>s(e.currentTarget.value),placeholder:`관리자 계정`,ref:i,required:!0,spellcheck:!1,type:`text`,value:o})]})]}),W(`label`,{class:`ssh-login-field`,for:`smartsafehub-password`,children:[W(`span`,{class:`ssh-login-label`,children:`비밀번호`}),W(`span`,{class:`ssh-login-input-wrap`,children:[W(`span`,{class:`ssh-login-field-icon`,"aria-hidden":`true`,children:W(_n,{})}),W(`input`,{autoComplete:`current-password`,disabled:w,id:`smartsafehub-password`,name:`password`,onBlur:()=>x(!1),onInput:e=>l(e.currentTarget.value),onKeyDown:E,onKeyUp:E,placeholder:`관리자 비밀번호`,ref:a,...v?{type:`text`}:{type:`password`},value:c}),W(`button`,{"aria-label":v?`비밀번호 숨기기`:`비밀번호 표시`,"aria-pressed":v,class:`ssh-login-password-toggle`,disabled:w,onClick:()=>{y(e=>!e),window.requestAnimationFrame(()=>a.current?.focus())},title:v?`비밀번호 숨기기`:`비밀번호 표시`,type:`button`,children:W(v?bn:yn,{})})]})]}),o.trim()===`root`&&c.length===0?W(`p`,{class:`ssh-login-password-hint`,children:`초기 설정 전 root 계정은 비밀번호가 비어 있을 수 있습니다.`}):null,b?W(`p`,{class:`ssh-login-caps`,role:`status`,children:`Caps Lock이 켜져 있습니다.`}):null,W(`button`,{class:`ssh-login-submit`,disabled:w,type:`submit`,children:[w?W(pn,{class:`ssh-login-submit-spinner`,"aria-hidden":`true`}):null,W(`span`,{children:w?`로그인 중…`:`로그인`})]})]}),W(`div`,{class:`ssh-login-security-note`,children:[W(_n,{"aria-hidden":`true`}),W(`span`,{children:`관리자 비밀번호를 잊었다면 전원이 켜진 상태에서 Reset 버튼을 5~9초 누른 뒤 놓아 비밀번호만 복구할 수 있습니다. 10초 이상 누르면 모든 사용자 설정을 초기화하는 기기 초기화가 실행됩니다.`})]}),g?W(`a`,{class:`ssh-login-fallback`,href:r,children:`기본 LuCI 로그인으로 계속`}):null]}),W(`footer`,{class:`ssh-login-footer`,children:[W(`span`,{children:`SmartSafeHub`}),W(`span`,{"aria-hidden":`true`,children:`·`}),W(`span`,{children:window.location.host})]})]})]})]})}var zb=`smartsafehub-entry-root`,Bb=`data-smartsafehub-mount`,Vb=`data-smartsafehub-shadow-style`;function Hb(){let e=_e(`/`);new Set([e,e.replace(/\/$/,``),_e(`/smartsafehub`),_e(`/admin/smartsafehub`)]).has(window.location.pathname)&&window.history.replaceState(null,``,`${ye()}${window.location.search}${window.location.hash}`)}function Ub(e,t){je(),window.__SMARTHUB_BOOTSTRAP__=Object.freeze({sessionId:e,rpcUrl:_e(`/admin/ubus`),assetBase:t.dataset.assetBase??`/luci-static/smartsafehub/`,assetVersion:t.dataset.assetVersion??`0.2.32-r1`,locale:document.documentElement.lang||`ko`})}function Wb(e,t){let n=window.__SMARTHUB_BOOTSTRAP__,r=n?.assetBase??t.dataset.assetBase??`/luci-static/smartsafehub/`,i=n?.assetVersion??t.dataset.assetVersion,a=`${r}${r.endsWith(`/`)?``:`/`}${e}`;return i?`${a}?v=${encodeURIComponent(i)}`:a}function Gb(e,t){let n=e.shadowRoot??e.attachShadow({mode:`open`}),r=n.querySelector(`link[${Vb}]`),i=Wb(`app.css`,e);r?r.getAttribute(`href`)!==i&&(r.href=i):(r=document.createElement(`link`),r.rel=`stylesheet`,r.href=i,r.setAttribute(Vb,``),n.appendChild(r));let a=n.querySelector(`[${Bb}]`);return a?a.className!==t&&(a.className=t):(a=document.createElement(`div`),a.className=t,a.setAttribute(Bb,``),n.appendChild(a)),a}function Kb(e,t,n){Ub(n,e),t.className=`smartsafehub-shadow-root`,ae(W(Fb,{onAdministratorPasswordChanged:()=>{qb(e,t,`관리자 비밀번호가 변경되었습니다. 새 비밀번호로 다시 로그인해 주세요.`)},onPasswordConfigured:n=>{qb(e,t,n?`관리자 비밀번호 복구가 완료되었습니다. 새 비밀번호로 다시 로그인해 주세요.`:`관리자 비밀번호가 설정되었습니다. 새 비밀번호로 다시 로그인해 주세요.`)}}),t)}function qb(e,t,n){delete window.__SMARTHUB_BOOTSTRAP__,t.className=`smartsafehub-login-shadow-root`,ae(W(Rb,{notice:n,onAuthenticated:n=>{Kb(e,t,n)}}),t)}async function Jb(){zn(Ln());let e=document.getElementById(zb);if(!e)return;Hb(),e.style.display=`block`,e.style.width=`100%`,e.style.minWidth=`0`,e.style.minHeight=`100dvh`,e.style.maxWidth=`100%`,e.style.isolation=`isolate`;let t=Gb(e,`smartsafehub-login-shadow-root`);window.addEventListener(Oe,()=>{qb(e,t,ke)}),ae(W(Rb,{onAuthenticated:n=>{Kb(e,t,n)},probing:!0}),t);try{let n=await de();if(n){Kb(e,t,n);return}}catch{}try{let n=await Te();if(n){Kb(e,t,n);return}}catch{}qb(e,t)}Jb(); \ No newline at end of file diff --git a/luci-app-smartsafehub/spec/contracts_spec.sh b/luci-app-smartsafehub/spec/contracts_spec.sh index 97e403353..1f82185cb 100755 --- a/luci-app-smartsafehub/spec/contracts_spec.sh +++ b/luci-app-smartsafehub/spec/contracts_spec.sh @@ -8,6 +8,13 @@ Describe 'SmartSafeHub 셸 계약 테스트' The error should be blank End + It '신규 셸 파일의 문법 검사 자동 검색을 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-shell-syntax-discovery.sh" + The status should be success + The output should start with 'PASS:' + The error should be blank + End + It '공유기 각 메뉴의 카드 밖 섹션 제목과 기능 버튼 보존을 검증한다' When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-section-layout.sh" The status should be success @@ -70,6 +77,20 @@ Describe 'SmartSafeHub 셸 계약 테스트' The error should be blank End + It 'Network Watch의 경량 연결 관찰과 중복 경고 제한을 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-netwatch.sh" + The status should be success + The output should start with 'PASS:' + The error should be blank + End + + It 'Network Watch 토글·기기명·최근 감지 이벤트 화면을 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-netwatch-ui.sh" + The status should be success + The output should start with 'PASS:' + The error should be blank + End + It '공유기 최근 활동 RPC와 UI 계약을 검증한다' When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-activity-ui-contract.sh" The status should be success @@ -91,6 +112,13 @@ Describe 'SmartSafeHub 셸 계약 테스트' The error should be blank End + It '기기 버전과 추천 규칙 확인을 함께 전송하는 동기화 요청을 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-device-sync-version.sh" + The status should be success + The output should start with 'PASS:' + The error should be blank + End + It '추천 사용자 규칙의 빠른 적용 확인 보고와 재전송 방지를 검증한다' When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-recommended-ack.sh" The status should be success @@ -105,6 +133,20 @@ Describe 'SmartSafeHub 셸 계약 테스트' The error should be blank End + It '공통 ucode의 숫자와 JSON 검증을 확인한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-ucode-common.sh" + The status should be success + The output should start with 'PASS:' + The error should be blank + End + + It '공통 잠금의 경합과 소유권 보호를 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-lock-common.sh" + The status should be success + The output should start with 'PASS:' + The error should be blank + End + It '공통 JSON shell helper 계약을 검증한다' When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-common-shell.sh" The status should be success @@ -329,6 +371,13 @@ Describe 'SmartSafeHub 셸 계약 테스트' The error should be blank End + It 'Apps 저장 공간의 procd 및 statvfs 계산 계약을 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-apps-storage.sh" + The status should be success + The output should start with 'PASS: Apps storage' + The error should be blank + End + It 'SmartSafeHub Apps 공식 UI 새 탭 실행과 알림 닫기 계약을 검증한다' When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-apps-launch.sh" The status should be success @@ -343,6 +392,20 @@ Describe 'SmartSafeHub 셸 계약 테스트' The error should be blank End + It 'Apps 공식 제공 유형과 구버전 API 및 체험 화면 호환성을 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-apps-publisher.sh" + The status should be success + The output should start with 'PASS: Apps publisher type' + The error should be blank + End + + It 'Travelmate 앱의 표시와 고정 패키지 및 관리 화면 계약을 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-travelmate.sh" + The status should be success + The output should start with 'PASS: Travelmate Apps demo and catalog contract' + The error should be blank + End + It '모든 rpcd ucode 진입점과 모듈의 실제 컴파일을 검증한다' When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-ucode-syntax.sh" The status should be success diff --git a/luci-app-smartsafehub/tests/runtime/apps-publisher.test.mjs b/luci-app-smartsafehub/tests/runtime/apps-publisher.test.mjs new file mode 100644 index 000000000..d6e71037a --- /dev/null +++ b/luci-app-smartsafehub/tests/runtime/apps-publisher.test.mjs @@ -0,0 +1,15 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import { isFirstPartyPublisher } from '../../frontend/src/utils/appsPublisher.ts'; + +test('공식 앱: Hub에서 명시한 first_party만 공식 제공으로 표시한다', () => { + assert.equal(isFirstPartyPublisher('first_party'), true); + assert.equal(isFirstPartyPublisher('third_party'), false); +}); + +test('공식 앱: 이전 Hub 응답·알 수 없는 값은 검증된 앱으로 표시한다', () => { + for (const value of [undefined, null, '', 'partner', 'official', 'First_Party', 1, true, {}, []]) { + assert.equal(isFirstPartyPublisher(value), false, String(value)); + } +}); diff --git a/luci-app-smartsafehub/tests/runtime/dark-theme-coverage.test.mjs b/luci-app-smartsafehub/tests/runtime/dark-theme-coverage.test.mjs new file mode 100644 index 000000000..45374f65f --- /dev/null +++ b/luci-app-smartsafehub/tests/runtime/dark-theme-coverage.test.mjs @@ -0,0 +1,81 @@ +import assert from 'node:assert/strict'; +import { readdirSync, readFileSync } from 'node:fs'; +import { dirname, join, relative, resolve } from 'node:path'; +import { test } from 'node:test'; +import { fileURLToPath } from 'node:url'; +import { auditDarkTheme, collectColorUtilities } from '../support/dark-theme-audit.mjs'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); +const styles = readFileSync(resolve(root, 'frontend/src/styles/app.css'), 'utf8'); +const exceptions = JSON.parse(readFileSync(resolve(root, 'tests/support/dark-theme-exceptions.json'), 'utf8')); + +function readTsx(path, found = {}) { + for (const item of readdirSync(path, { withFileTypes: true })) { + const full = join(path, item.name); + if (item.isDirectory()) readTsx(full, found); + else if (item.name.endsWith('.tsx')) found[relative(root, full)] = readFileSync(full, 'utf8'); + } + return found; +} + +const sources = readTsx(resolve(root, 'frontend/src')); + +test('모든 TSX의 밝은 Tailwind 유틸리티는 다크 모드 대응 또는 명시적 예외가 있다', () => { + const report = auditDarkTheme({ sources, css: styles, exceptions }); + assert.ok(Object.keys(sources).length > 20, 'TSX search accidentally disabled'); + assert.ok(report.checked > 50, 'class scanner unexpectedly skipped most colors'); + assert.deepEqual(report.problems, [], report.problems.join('\n')); +}); + +test('템플릿/조건부 클래스와 새 구분선·배경·테두리·hover 누락을 감지한다', () => { + const fixture = { 'NewPage.tsx': ` + const badge = ok ? 'bg-violet-50 border-violet-200' : 'bg-violet-100'; + return
    ; + ` }; + const names = [...collectColorUtilities(fixture).keys()]; + for (const expected of ['bg-violet-50', 'bg-violet-100', 'border-violet-200', 'divide-violet-300', 'hover:bg-violet-100']) { + assert.ok(names.includes(expected), `failed to scan ${expected}`); + } + const report = auditDarkTheme({ sources: fixture, css: styles }); + for (const expected of names) assert.ok(report.problems.some(p => p.includes(expected)), `missed ${expected}`); +}); + +test('부모만 지정한 divide 규칙·속성 누락·밝은 선언도 실패한다', () => { + const sources = { 'NewPage.tsx': '
      ' }; + const good = auditDarkTheme({ sources, css: styles }); + assert.deepEqual(good.problems, [], good.problems.join('\n')); + const wrongParent = styles.replace(/\[class~='divide-slate-100'\] > :not\(:last-child\)/g, "[class~='divide-slate-100']"); + assert.ok(auditDarkTheme({ sources, css: wrongParent }).problems.some(p => p.includes('divide-slate-100'))); + const missingValue = styles.replace("[class~='bg-rose-100'],", ''); + assert.ok(auditDarkTheme({ sources, css: missingValue }).problems.some(p => p.includes('bg-rose-100'))); + const paleValue = styles.replace('background-color: rgb(244 63 94 / 0.12);', 'background-color: #ffffff;'); + assert.ok(auditDarkTheme({ sources, css: paleValue }).problems.some(p => p.includes('bg-rose-100'))); + const darkText = styles.replace(/(\[class~='text-teal-950'\] \{\s*color: )#5eead4;/, '$1#000000;'); + assert.ok(auditDarkTheme({ sources: { 'NewPage.tsx': '' }, css: darkText }).problems.some(p => p.includes('text-teal-800'))); +}); + +test('예외는 특정 클래스를 지정하고 사유가 충분해야 하며 미사용·중복 예외는 거부한다', () => { + const fixture = { 'NewPage.tsx': '
      ' }; + const valid = [{ file: 'NewPage.tsx', utility: 'bg-violet-50', reason: '이 색상은 테마와 독립된 브랜드 미리보기 요소로 사용합니다.' }]; + assert.deepEqual(auditDarkTheme({ sources: fixture, css: styles, exceptions: valid }).problems, []); + const anotherFile = { ...fixture, 'AnotherPage.tsx': '
      ' }; + assert.ok(auditDarkTheme({ sources: anotherFile, css: styles, exceptions: valid }).problems.some(p => p.includes('AnotherPage.tsx'))); + for (const bad of [ + [{ ...valid[0], reason: '의도적' }], + [{ ...valid[0], glob: '*' }], + [{ ...valid[0], utility: 'bg-violet-100' }], + [valid[0], valid[0]], + [{ ...valid[0], file: '*.tsx' }], + ]) { + assert.ok(auditDarkTheme({ sources: fixture, css: styles, exceptions: bad }).problems.length > 0); + } +}); + +test('hover/focus 선택자·다크 테마 범위를 실제 스타일과 정확히 대조한다', () => { + const sources = { 'NewPage.tsx': '
      ' }; + assert.deepEqual(auditDarkTheme({ sources, css: styles }).problems, []); + const onlyLight = styles.replace('.ssh-app[data-theme=\'dark\'] .hover\\:bg-rose-100:hover', '.ssh-app[data-theme=\'light\'] .hover\\:bg-rose-100:hover'); + assert.ok(auditDarkTheme({ sources, css: onlyLight }).problems.some(p => p.includes('hover:bg-rose-100'))); + const noFocus = styles.replace('.focus-visible\\:ring-teal-100:focus-visible', '.focus-visible\\:ring-teal-100:hover'); + assert.ok(auditDarkTheme({ sources, css: noFocus }).problems.some(p => p.includes('focus-visible:ring-teal-100'))); +}); diff --git a/luci-app-smartsafehub/tests/runtime/dark-theme-css.test.mjs b/luci-app-smartsafehub/tests/runtime/dark-theme-css.test.mjs new file mode 100644 index 000000000..1828a2d8b --- /dev/null +++ b/luci-app-smartsafehub/tests/runtime/dark-theme-css.test.mjs @@ -0,0 +1,69 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { test } from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); +const css = readFileSync(resolve(root, 'frontend/src/styles/app.css'), 'utf8'); +const networkWatch = readFileSync(resolve(root, 'frontend/src/pages/NetworkWatchPage.tsx'), 'utf8'); +const rulesPage = readFileSync(resolve(root, 'frontend/src/pages/SafeShieldRulesPage.tsx'), 'utf8'); +const homePage = readFileSync(resolve(root, 'frontend/src/pages/HomePage.tsx'), 'utf8'); +const settingsPage = readFileSync(resolve(root, 'frontend/src/pages/SettingsPage.tsx'), 'utf8'); +const firmwareCard = readFileSync(resolve(root, 'frontend/src/components/FirmwareUpdatesCard.tsx'), 'utf8'); + +// Read the actual declarations, not just a string anywhere in the stylesheet. +// A rule on the parent does not override Tailwind v4's child border-color. +const rules = [...css.matchAll(/([^{}]+)\{([^{}]*)\}/g)].map(([, selector, body]) => ({ + selector: selector.trim(), + body: body.trim(), +})); + +function darkRule(selector, declaration) { + const found = rules.find((rule) => rule.selector.includes(selector) && rule.body.includes(declaration)); + assert.ok(found, `missing dark rule for ${selector}: ${declaration}`); + assert.ok(found.selector.includes(".ssh-app[data-theme='dark']"), 'dark override must never affect the light theme'); + return found; +} + +test('Network Watch와 사용자 규칙의 구분선은 Tailwind v4의 자식 요소를 직접 지정한다', () => { + assert.match(networkWatch, /divide-y divide-slate-100/); + assert.match(rulesPage, /divide-y divide-slate-200/); + for (const shade of ['100', '200']) { + const selector = `[class~='divide-slate-${shade}'] > :not(:last-child)`; + const rule = darkRule(selector, 'border-color: #1e293b;'); + assert.ok(rule.selector.includes(selector), 'do not regress to parent-only border-color'); + } +}); + +test('심각 상태 배지의 밝은 rose 배경을 다크 모드에서 변환한다', () => { + assert.match(homePage, /bg-rose-100 text-rose-800/); + assert.match(settingsPage, /bg-rose-100 text-rose-800/); + darkRule("[class~='bg-rose-100']", 'background-color: rgb(244 63 94 / 0.12);'); + darkRule("[class~='text-rose-800']", 'color: #fda4af;'); +}); + +test('펌웨어 관리 소프트웨어 카드의 연한 sky 테두리도 다크 색으로 변환한다', () => { + assert.match(firmwareCard, /border-sky-100/); + darkRule("[class~='border-sky-100']", 'border-color: #075985;'); +}); + +test('연한 hover 배경만 다크 팔레트로 전환하고 강한 색 버튼은 유지한다', () => { + const expected = new Map([ + ['slate-100', '#334155'], + ['teal-50', 'rgb(13 148 136 / 0.24)'], + ['teal-100', 'rgb(13 148 136 / 0.24)'], + ['amber-50', 'rgb(245 158 11 / 0.22)'], + ['amber-100', 'rgb(245 158 11 / 0.22)'], + ['rose-100', 'rgb(244 63 94 / 0.22)'], + ]); + for (const [shade, color] of expected) { + darkRule(`.hover\\:bg-${shade}:hover`, `background-color: ${color};`); + } + // Full-color action buttons are already legible in the dark theme. + // Their hover utility should not be replaced by a pale-surface override. + for (const shade of ['teal-800', 'amber-800', 'red-800', 'sky-800']) { + assert.ok(!rules.some((rule) => rule.selector.includes(`.hover\\:bg-${shade}:hover`) + && rule.selector.includes(".ssh-app[data-theme='dark']")), `strong action hover must remain untouched: ${shade}`); + } +}); diff --git a/luci-app-smartsafehub/tests/runtime/navigation-routes.test.mjs b/luci-app-smartsafehub/tests/runtime/navigation-routes.test.mjs new file mode 100644 index 000000000..acc123aa7 --- /dev/null +++ b/luci-app-smartsafehub/tests/runtime/navigation-routes.test.mjs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Execute the real route registry rather than matching its source formatting. +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { DEMO_ROUTES, ROUTE_BY_NAME, ROUTES } from '../../frontend/src/app/routes.ts'; + +test('네트워크 설정은 WAN과 LAN을 하나의 화면으로 제공한다', () => { + assert.equal(ROUTE_BY_NAME.network.route, 'network'); + assert.equal(ROUTE_BY_NAME.network.hash, '#network'); + assert.equal(ROUTE_BY_NAME.network.label, '네트워크'); + assert.ok(!('wan' in ROUTE_BY_NAME), 'WAN must not be exposed as a separate route'); + assert.ok(!('lan' in ROUTE_BY_NAME), 'LAN must not be exposed as a separate route'); +}); + +test('라우팅 이름과 URL 해시는 유일하고 데모 링크는 실제 화면만 사용한다', () => { + const names = ROUTES.map(({ route }) => route); + const hashes = ROUTES.map(({ hash }) => hash); + assert.equal(new Set(names).size, ROUTES.length, 'route names must be unique'); + assert.equal(new Set(hashes).size, ROUTES.length, 'hash URLs must be unique'); + for (const definition of ROUTES) { + assert.strictEqual(ROUTE_BY_NAME[definition.route], definition); + assert.ok(definition.hash.startsWith('#')); + } + for (const demoRoute of DEMO_ROUTES) { + assert.ok(ROUTE_BY_NAME[demoRoute], `unknown demo route: ${demoRoute}`); + } +}); diff --git a/luci-app-smartsafehub/tests/runtime/router-demo.test.mjs b/luci-app-smartsafehub/tests/runtime/router-demo.test.mjs index 5ab1fdf70..67e6ed166 100644 --- a/luci-app-smartsafehub/tests/runtime/router-demo.test.mjs +++ b/luci-app-smartsafehub/tests/runtime/router-demo.test.mjs @@ -114,8 +114,9 @@ test('demo showcases verified Apps versions, upgrade badge, and installed sizes assert.ok(catalog.indexOf("id: 'retro-arcade'") < catalog.indexOf("id: 'network-usage-monitor'"), 'demo card ordering should match the actual Apps page'); - assert.match(catalog, /id: 'retro-arcade',[\s\S]*?version: '1\.1\.0-r5'/); + assert.match(catalog, /id: 'retro-arcade',[\s\S]*?publisherType: 'first_party',[\s\S]*?version: '1\.2\.0-r4'/); assert.match(catalog, /id: 'network-usage-monitor',[\s\S]*?version: '2025\.06\.02~29236be6-r1'/); + assert.match(catalog, /id: 'travelmate',[\s\S]*?publisherType: 'third_party',[\s\S]*?version: '2\.4\.9-r6'/); assert.match(catalog, /id: 'wake-on-lan',[\s\S]*?category: 'utilities',[\s\S]*?version: '26\.288\.56989~75a306d'/); assert.ok(catalog.indexOf("id: 'network-usage-monitor'") < catalog.indexOf("id: 'wake-on-lan'")); assert.match(status, /'wake-on-lan': \{\s*installed: false,\s*partial: false,\s*managed: false/); @@ -172,6 +173,23 @@ test('public demo actions never install APKs, pair an account, or open a live ap assert.doesNotMatch(demo, /import .*from ['"]\.\.\/api\//); }); +test('Network Watch 안내 문서와 데모 연결은 함께 유지된다', () => { + const routes = readFileSync(resolve(root, 'app/routes.ts'), 'utf8'); + const app = readFileSync(resolve(root, 'demo/main.tsx'), 'utf8'); + const sidebar = readFileSync(resolve(root, 'components/ProductNavigation.tsx'), 'utf8'); + const fixtures = readFileSync(resolve(root, 'demo/data.ts'), 'utf8'); + const docs = readFileSync(resolve(root, '../../docs/ROUTER_DEMO.md'), 'utf8'); + assert.match(routes, /'netwatch'/); + assert.match(sidebar, /routes: \['safeshield', 'netwatch', 'rules'\]/); + assert.match(app, /case 'netwatch':/); + assert.match(app, / { const readme = readFileSync(resolve(root, '../../README.md'), 'utf8'); assert.match(readme, /https:\/\/www\.smartsafehub\.com\/router-demo\//); diff --git a/luci-app-smartsafehub/tests/runtime/save-button-cursor.test.mjs b/luci-app-smartsafehub/tests/runtime/save-button-cursor.test.mjs new file mode 100644 index 000000000..d50545b44 --- /dev/null +++ b/luci-app-smartsafehub/tests/runtime/save-button-cursor.test.mjs @@ -0,0 +1,43 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { test } from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import { saveButtonCursor } from '../../frontend/src/utils/saveButtonCursor.ts'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); + +function source(path) { + return readFileSync(resolve(root, path), 'utf8'); +} + +test('설정 변경이 없어서 저장이 비활성화된 경우 로딩 커서를 사용하지 않는다', () => { + assert.equal(saveButtonCursor(false), 'disabled:cursor-not-allowed'); + assert.equal(saveButtonCursor(true), 'disabled:cursor-wait'); +}); + +test('WAN, LAN, 기본 Wi-Fi, 게스트 Wi-Fi 저장 버튼은 실제 작업 중에만 로딩 커서를 사용한다', () => { + for (const [path, dirty] of [ + ['frontend/src/pages/WanPage.tsx', 'settingsDirty'], + ['frontend/src/pages/LanPage.tsx', 'settingsDirty'], + ['frontend/src/pages/WifiPage.tsx', 'settingsDirty'], + ['frontend/src/components/GuestWifiCard.tsx', 'dirty'], + ]) { + const page = source(path); + // Check the actual save button, not just an unused import or a helper elsewhere. + const save = [...page.matchAll(//g)] + .map(([markup]) => markup) + .find(markup => markup.includes(`disabled={busy || !${dirty}}`) && markup.includes('type="submit"')); + assert.ok(save, `저장 버튼을 찾을 수 없음: ${path}`); + assert.match(save, /\$\{saveButtonCursor\(busy\)\}/, `상태별 커서 선택이 누락됨: ${path}`); + assert.doesNotMatch(save, /(? { + const helper = source('frontend/src/utils/saveButtonCursor.ts'); + assert.match(helper, /'disabled:cursor-wait'/); + assert.match(helper, /'disabled:cursor-not-allowed'/); + assert.doesNotMatch(helper, /`disabled:cursor-\$\{/); +}); diff --git a/luci-app-smartsafehub/tests/support/dark-theme-audit.mjs b/luci-app-smartsafehub/tests/support/dark-theme-audit.mjs new file mode 100644 index 000000000..5d72edc7d --- /dev/null +++ b/luci-app-smartsafehub/tests/support/dark-theme-audit.mjs @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Source-only dark theme contract: no browser, Tailwind compiler or build artifacts. +// It detects complete static Tailwind utilities, including those assembled in +// string/template expressions. Dynamically constructed names are out of scope. + +const CLASS = /(?(?:[a-z][a-z-]*:)*)((?(?:divide|bg|border|ring|text)-(?:[a-z]+-(?:50|100|200|300|400|500|600|700|800|900|950)(?:\/\d{1,3})?|white(?:\/\d{1,3})?)))(?![\w./-])/g; +const DARK = ".ssh-app[data-theme='dark']"; +const PALE = new Set(['50', '100', '200', '300']); + +function category(utility) { + const match = /^(divide|bg|border|ring|text)-([a-z]+)(?:-(\d+))?(?:\/\d+)?$/.exec(utility); + if (!match) return null; + const [, family, color, shade] = match; + if (family === 'divide' && shade && Number(shade) <= 400) return 'border-color'; + if (family === 'bg' && (color === 'white' || PALE.has(shade))) return 'background-color'; + if (family === 'border' && (color === 'white' || PALE.has(shade))) return 'border-color'; + if (family === 'ring' && (color === 'white' || PALE.has(shade))) return '--tw-ring-color'; + // Dark-mode text contrasts need an explicit mapping for strong dark text. + if (family === 'text' && shade && Number(shade) >= 700) return 'color'; + return null; +} + +export function collectColorUtilities(sources) { + const found = new Map(); + for (const [filename, source] of Object.entries(sources)) { + for (const match of source.matchAll(CLASS)) { + const utility = match.groups.utility; + const declaration = category(utility); + if (!declaration) continue; + const variants = match.groups.variants.replace(/:$/, ''); + const key = variants ? `${variants}:${utility}` : utility; + if (!found.has(key)) found.set(key, { key, utility, variants, declaration, files: new Set() }); + found.get(key).files.add(filename); + } + } + return found; +} + +function cssRules(css) { + // Split grouped selectors *before* matching them. Otherwise an unrelated + // selector in the same rule could falsely satisfy a requested utility. + const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, ''); + return [...withoutComments.matchAll(/([^{}]+)\{([^{}]*)\}/g)].flatMap(([, selectors, block]) => { + const declarations = new Map(); + for (const [, name, value] of block.matchAll(/(--[\w-]+|[\w-]+)\s*:\s*([^;]+);/g)) { + declarations.set(name, value.trim()); + } + return selectors.split(',').map((selector) => ({ selector: selector.trim(), declarations })); + }); +} + +function contrastSafe(value, property) { + // Text in a dark surface must be visibly brighter; pale surfaces/rings + // must remain dark. These are coarse luminance guards, not WCAG checks. + const hex = /^#([\da-f]{3}|[\da-f]{6})$/i.exec(value); + const rgb = /^rgba?\(\s*(\d+)\s*[ ,]\s*(\d+)\s*[ ,]\s*(\d+)(?:\s*\/\s*(0?\.\d+|1(?:\.0+)?)|\s*,\s*(0?\.\d+|1(?:\.0+)?))?\s*\)$/i.exec(value); + let channels, alpha = 1; + if (hex) { + const v = hex[1].length === 3 ? [...hex[1]].map(x => x + x).join('') : hex[1]; + channels = [0, 2, 4].map(i => parseInt(v.slice(i, i + 2), 16)); + } else if (rgb) { + channels = [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])]; + alpha = Number(rgb[4] ?? rgb[5] ?? 1); + } else { + return false; // Unresolvable values must be explicitly reviewed. + } + if (channels.some(c => c > 255) || alpha < 0 || alpha > 1) return false; + // Composite translucent colors over the actual dark card background. + const mixed = channels.map((x, i) => x * alpha + [15, 23, 42][i] * (1 - alpha)); + const srgb = mixed.map(v => { + const c = v / 255; + return c <= .04045 ? c / 12.92 : ((c + .055) / 1.055) ** 2.4; + }); + const luminance = srgb[0] * .2126 + srgb[1] * .7152 + srgb[2] * .0722; + return property === 'color' ? luminance >= .28 : luminance < .40; +} + +function selectorFor(entry) { + if (!entry.variants) return `[class~='${entry.utility}']`; + const variants = entry.variants.split(':'); + if (variants.length !== 1 || !['hover', 'focus', 'focus-visible', 'disabled'].includes(variants[0])) return null; + const state = variants[0]; + const className = `${state}\\:${entry.utility.replaceAll('/', '\\/')}`; + return `.${className}:${state}`; +} + +function hasOverride(entry, rules) { + const target = selectorFor(entry); + if (!target) return false; + return rules.some(({ selector, declarations }) => { + if (!selector.startsWith(DARK) || !selector.includes(target)) return false; + // Tailwind v4 divide-* colors apply to list children. Parent rules do not work. + if (entry.utility.startsWith('divide-') && !selector.includes(`${target} > :not(:last-child)`)) return false; + const value = declarations.get(entry.declaration); + return value !== undefined && contrastSafe(value, entry.declaration); + }); +} + +export function auditDarkTheme({ sources, css, exceptions = [] }) { + const found = collectColorUtilities(sources); + const rules = cssRules(css); + const ignored = new Set(); + const problems = []; + for (const exception of exceptions) { + if (!exception || typeof exception.file !== 'string' || !/^[\w./-]+\.tsx$/.test(exception.file) || + exception.file.includes('..') || typeof exception.utility !== 'string' || !exception.utility || + typeof exception.reason !== 'string' || exception.reason.trim().length < 20 || + Object.keys(exception).some(key => !['file', 'utility', 'reason'].includes(key))) { + problems.push('예외에는 특정 TSX 파일, 정확한 utility와 20자 이상의 사유가 필요합니다.'); + continue; + } + const token = `${exception.file}::${exception.utility}`; + if (ignored.has(token)) problems.push(`중복 다크 모드 예외: ${token}`); + else if (!found.get(exception.utility)?.files.has(exception.file)) problems.push(`사용하지 않는 다크 모드 예외: ${token}`); + else ignored.add(token); + } + for (const entry of found.values()) { + if (hasOverride(entry, rules)) continue; + for (const filename of [...entry.files].sort()) { + if (ignored.has(`${filename}::${entry.key}`)) continue; + problems.push(`${filename}: ${entry.key} 다크 모드 대응 누락 또는 밝은 색상 유지 (예외 등록 시 사유 필수)`); + } + } + return { problems, checked: found.size, exceptions: ignored.size }; +} diff --git a/luci-app-smartsafehub/tests/support/dark-theme-exceptions.json b/luci-app-smartsafehub/tests/support/dark-theme-exceptions.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/luci-app-smartsafehub/tests/support/dark-theme-exceptions.json @@ -0,0 +1 @@ +[] diff --git a/luci-app-smartsafehub/tests/test-apps-publisher.sh b/luci-app-smartsafehub/tests/test-apps-publisher.sh new file mode 100755 index 000000000..74a57b665 --- /dev/null +++ b/luci-app-smartsafehub/tests/test-apps-publisher.sh @@ -0,0 +1,44 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +set -eu +ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +python3 - "$ROOT" <<'PY_TEST' +from pathlib import Path +import re +import sys + +root = Path(sys.argv[1]) +rpc = (root / 'root/usr/share/rpcd/ucode/smartsafehub/apps.uc').read_text() +types = (root / 'frontend/src/types/apps.ts').read_text() +ui = (root / 'frontend/src/pages/AppsPage.tsx').read_text() +css = (root / 'frontend/src/styles/app.css').read_text() +demo = (root / 'frontend/src/demo/data.ts').read_text() +helper = (root / 'frontend/src/utils/appsPublisher.ts').read_text() + +# Hub schema 1 transports snake_case; public router RPC exposes camelCase. +assert "item?.publisher_type == 'first_party' ? 'first_party' : 'third_party'" in rpc +assert 'publisherType:' in rpc +assert "publisherType?: 'first_party' | 'third_party'" in types +assert "return type === 'first_party';" in helper +assert 'isFirstPartyPublisher(app.publisherType)' in ui +assert 'app.id ===' not in ui.split('ssh-apps-official-badge')[0].split('
      &2 + exit 1 +} + +[ -f "$UCODE_ROOT/smartsafehub/apps-storage.uc" ] || fail 'Apps storage ucode module not found' + +if ! command -v ucode >/dev/null 2>&1; then + [ "${SMARTSAFEHUB_REQUIRE_UCODE:-0}" != '1' ] || fail 'Apps storage runtime requires ucode in PATH' + printf '%s\n' 'PASS: Apps storage module exists (runtime requires CI ucode)' + exit 0 +fi + +TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/smartsafehub-apps-storage.XXXXXX")" +trap 'rm -rf "$TMP_ROOT"' EXIT HUP INT TERM + +# Run the existing assertions against the production module, never a copy. +# The temporary runner keeps tests/ limited to ShellSpec-discovered *.sh tests. +ln -s "$UCODE_ROOT/smartsafehub" "$TMP_ROOT/smartsafehub" +cat > "$TMP_ROOT/runner.uc" <<'EOF_UCODE' 'use strict'; -import { from_system_root, from_statvfs } from '../root/usr/share/rpcd/ucode/smartsafehub/apps-storage.uc'; +import { from_system_root, from_statvfs } from './smartsafehub/apps-storage.uc'; function check(ok, message) { if (!ok) { warn(`FAIL: ${message}\n`); @@ -24,4 +50,9 @@ check(from_statvfs({ frsize: 4096, blocks: 20000, bavail: 20001 }, '/') == null, check(from_statvfs({ frsize: 4096, blocks: 20000, bavail: 1 }, '/tmp') == null, 'must never use tmpfs instead of APK storage'); check(from_statvfs(null, '/overlay') == null, 'missing statvfs stats must be optional'); -print('PASS: Apps storage procd and ucode statvfs conversions\n'); +EOF_UCODE + +ucode -L "$TMP_ROOT" -L "$UCODE_ROOT" "$TMP_ROOT/runner.uc" || \ + fail 'Apps storage ucode runtime contract failed' + +printf '%s\n' 'PASS: Apps storage procd and statvfs conversions' diff --git a/luci-app-smartsafehub/tests/test-apps-ui.sh b/luci-app-smartsafehub/tests/test-apps-ui.sh index fd3fdbeab..e0468589d 100755 --- a/luci-app-smartsafehub/tests/test-apps-ui.sh +++ b/luci-app-smartsafehub/tests/test-apps-ui.sh @@ -232,12 +232,3 @@ check('setDismissedJob(null)' not in page, 'starting a new operation must not resurrect the previous failure') PY echo 'PASS: Apps removal confirmation, theme and operation states' - -# Real ucode runtime coverage: prevents wrong numeric type names and unit -# conversions from passing the static Apps UI contract again. -if command -v ucode >/dev/null 2>&1; then - ucode "$ROOT/tests/apps-storage-test.uc" -elif [ "${SMARTSAFEHUB_REQUIRE_UCODE:-0}" = '1' ]; then - echo 'FAIL: Apps storage runtime test requires ucode' >&2 - exit 1 -fi diff --git a/luci-app-smartsafehub/tests/test-common-shell.sh b/luci-app-smartsafehub/tests/test-common-shell.sh index acb68d78c..949cef19a 100755 --- a/luci-app-smartsafehub/tests/test-common-shell.sh +++ b/luci-app-smartsafehub/tests/test-common-shell.sh @@ -18,6 +18,8 @@ sh -n "$COMMON_LIB" || fail '공통 shell library가 POSIX shell 문법 검사 command -v json_escape >/dev/null 2>&1 || fail '공통 library가 json_escape 함수를 제공해야 합니다.' command -v ascii_upper >/dev/null 2>&1 || fail '공통 library가 ascii_upper 함수를 제공해야 합니다.' command -v ascii_lower >/dev/null 2>&1 || fail '공통 library가 ascii_lower 함수를 제공해야 합니다.' +command -v ssh_lock_acquire >/dev/null 2>&1 || fail '공통 library가 잠금 기능을 제공해야 합니다.' +command -v ssh_lock_release >/dev/null 2>&1 || fail '공통 library가 안전한 잠금 해제를 제공해야 합니다.' [ "$(ascii_upper 'pro-Ultimate_1')" = 'PRO-ULTIMATE_1' ] || fail 'ascii_upper는 라이선스 토큰을 명시적 ASCII 매핑으로 대문자화해야 합니다.' [ "$(ascii_lower 'PRO-Ultimate_1')" = 'pro-ultimate_1' ] || fail 'ascii_lower는 라이선스 토큰을 명시적 ASCII 매핑으로 소문자화해야 합니다.' diff --git a/luci-app-smartsafehub/tests/test-device-sync-version.sh b/luci-app-smartsafehub/tests/test-device-sync-version.sh new file mode 100755 index 000000000..deba34f91 --- /dev/null +++ b/luci-app-smartsafehub/tests/test-device-sync-version.sh @@ -0,0 +1,48 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +set -eu +ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +HELPER="$ROOT/root/usr/libexec/smartsafehub-device" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT INT TERM +# Execute the actual body builder in isolation: never contact a real cloud. +sed -n '/^build_sync_request() {/,/^}/p' "$HELPER" > "$TMP/helper.sh" +[ -s "$TMP/helper.sh" ] || { echo 'FAIL: sync builder is missing' >&2; exit 1; } +. "$TMP/helper.sh" +json_escape() { printf '%s' "$1"; } +SAFESHIELD_VERSION_FILE="$TMP/version" +RECOMMENDED_RECEIPT="$TMP/receipt" +OUT="$TMP/request.json" + +assert_json() { + python3 - "$OUT" "$1" "$2" <<'PY_JSON' +import json, sys +payload = json.load(open(sys.argv[1], encoding='utf-8')) +version = sys.argv[2] +ack = sys.argv[3] +if version == 'absent': + assert 'safeshield_version' not in payload, payload +else: + assert payload['safeshield_version'] == version, payload +if ack == 'present': + assert payload['recommended_rules_ack'] == {'revision': 'a' * 64, 'status': 'confirmed'}, payload +else: + assert 'recommended_rules_ack' not in payload, payload +PY_JSON +} +printf '0.3.25-r1\n' > "$SAFESHIELD_VERSION_FILE" +build_sync_request "$OUT" +assert_json 0.3.25-r1 absent +printf '{"revision":"%s","status":"confirmed"}\n' "$(printf '%064d' 0 | tr 0 a)" > "$RECOMMENDED_RECEIPT" +build_sync_request "$OUT" +assert_json 0.3.25-r1 present +rm -f "$SAFESHIELD_VERSION_FILE" +build_sync_request "$OUT" +assert_json absent present +rm -f "$RECOMMENDED_RECEIPT" +build_sync_request "$OUT" +assert_json absent absent +printf 'invalid version\n' > "$SAFESHIELD_VERSION_FILE" +build_sync_request "$OUT" +assert_json absent absent +printf 'PASS: device sync version and receipt payloads\n' diff --git a/luci-app-smartsafehub/tests/test-firmware-identity.sh b/luci-app-smartsafehub/tests/test-firmware-identity.sh index 1305a53c3..b58aa0914 100755 --- a/luci-app-smartsafehub/tests/test-firmware-identity.sh +++ b/luci-app-smartsafehub/tests/test-firmware-identity.sh @@ -84,7 +84,7 @@ fi grep -Fq "const ROM_FIRMWARE_METADATA_FILE = '/rom/usr/share/smartsafehub/firmware.json';" "$RPC_MODULE" || \ fail 'firmware RPC must read immutable ROM metadata before overlay metadata' -grep -Fq 'read_json_file(ROM_FIRMWARE_METADATA_FILE, 65536)' "$RPC_MODULE" || \ +grep -Fq 'read_json_object(ROM_FIRMWARE_METADATA_FILE, 65536)' "$RPC_MODULE" || \ fail 'firmware RPC metadata reader must prefer the ROM file' grep -Fq 'resolved_build_id != current.buildId' "$RPC_MODULE" || \ fail 'firmware RPC must reject resolve data generated for a different installed build' diff --git a/luci-app-smartsafehub/tests/test-iptv.sh b/luci-app-smartsafehub/tests/test-iptv.sh index b9cbe64cf..63e91bce5 100755 --- a/luci-app-smartsafehub/tests/test-iptv.sh +++ b/luci-app-smartsafehub/tests/test-iptv.sh @@ -94,8 +94,7 @@ fi grep -Fq "route: 'iptv'" "$ROUTES" || fail 'IPTV route가 필요합니다.' grep -Fq "hash: '#iptv'" "$ROUTES" || fail 'IPTV route는 #iptv hash를 사용해야 합니다.' grep -Fq "'#iptv': 'iptv'" "$HASH_ROUTE" || fail '#iptv hash router 연결이 필요합니다.' -grep -Fq "{ label: 'Network', routes: ['network', 'wifi', 'iptv', 'devices'] }" "$NAVIGATION" || \ - fail 'IPTV 메뉴는 Network 그룹에서 네트워크 설정과 Wi-Fi 다음에 표시되어야 합니다.' +# Shared sidebar placement is tested in test-navigation-contract.sh. grep -Fq "routeName === 'iptv'" "$NAVIGATION" || fail 'IPTV 메뉴에 Beta badge 조건이 필요합니다.' grep -Fq 'Beta' "$NAVIGATION" || fail 'IPTV 메뉴에 Beta 표시가 필요합니다.' grep -Fq 'class="absolute right-0.5 top-0.5 inline-flex size-5' "$NAVIGATION" || \ diff --git a/luci-app-smartsafehub/tests/test-lan-settings.sh b/luci-app-smartsafehub/tests/test-lan-settings.sh index 2c89e37c2..8134838a6 100755 --- a/luci-app-smartsafehub/tests/test-lan-settings.sh +++ b/luci-app-smartsafehub/tests/test-lan-settings.sh @@ -12,9 +12,6 @@ HOOK="$ROOT_DIR/frontend/src/hooks/useLan.ts" PAGE="$ROOT_DIR/frontend/src/pages/LanPage.tsx" IPV4_INPUT="$ROOT_DIR/frontend/src/components/Ipv4OctetInput.tsx" APP_STYLE="$ROOT_DIR/frontend/src/styles/app.css" -ROUTES="$ROOT_DIR/frontend/src/app/routes.ts" -HASH_ROUTE="$ROOT_DIR/frontend/src/hooks/useHashRoute.ts" -NAVIGATION="$ROOT_DIR/frontend/src/components/ProductNavigation.tsx" APP="$ROOT_DIR/frontend/src/app/App.tsx" fail() { @@ -60,7 +57,7 @@ assert_ucode_export_terminated() { fi } -for file in "$RPC_ENTRY" "$LAN_RPC_ENTRY" "$LAN_MODULE" "$ACL" "$API" "$HOOK" "$PAGE" "$IPV4_INPUT" "$ROUTES" "$HASH_ROUTE" "$NAVIGATION" "$APP"; do +for file in "$RPC_ENTRY" "$LAN_RPC_ENTRY" "$LAN_MODULE" "$ACL" "$API" "$HOOK" "$PAGE" "$IPV4_INPUT" "$APP"; do [ -f "$file" ] || fail "LAN 설정 계약 파일이 없습니다: ${file#$ROOT_DIR/}" done @@ -163,11 +160,7 @@ grep -Fq "export function useLan(active: boolean)" "$HOOK" || \ grep -Fq 'newAddress: result.newAddress ?? result.settings.lan.address' "$HOOK" || \ fail 'LAN IP 변경 뒤 새 관리 주소를 사용자에게 전달해야 합니다.' -grep -Fq "route: 'network'" "$ROUTES" || fail 'WAN과 LAN을 통합한 네트워크 설정 route가 등록되어야 합니다.' -grep -Fq "hash: '#network'" "$ROUTES" || fail '네트워크 설정 route는 #network hash를 사용해야 합니다.' -grep -Fq "'#lan': 'network'" "$HASH_ROUTE" || fail '기존 #lan hash는 통합 네트워크 설정으로 연결되어야 합니다.' -grep -Fq "{ label: 'Network', routes: ['network', 'wifi', 'iptv', 'devices'] }" "$NAVIGATION" || \ - fail 'Network 메뉴에서 통합 네트워크 설정이 Wi-Fi와 연결된 기기보다 먼저 표시되어야 합니다.' +# Shared route/hash/navigation registration is tested in test-navigation-contract.sh. grep -Fq "case 'network':" "$APP" || fail 'App이 통합 네트워크 설정 화면을 렌더링해야 합니다.' grep -Fq '&2; exit 1; } +. "$LIB" + +# A live owner must preserve the existing BUSY/75 behavior of updater, firmware +# and activity-sync; a foreign process must never remove a live owner's lock. +sleep 3 & live_pid=$! +mkdir "$TMP/live.lock" +printf '%s\n' "$live_pid" > "$TMP/live.lock/pid" +if ssh_lock_acquire "$TMP/live.lock" 3 0.01 sleep 1; then + fail 'a live lock may not be stolen by a fast-fail contender' +fi +[ "$(cat "$TMP/live.lock/pid")" = "$live_pid" ] || fail 'live pid owner was replaced' +ssh_lock_release "$TMP/live.lock" +[ -d "$TMP/live.lock" ] || fail 'release by a non-owner removed a live lock' +kill "$live_pid" 2>/dev/null || true +wait "$live_pid" 2>/dev/null || true + +# A dead owner can be reclaimed, and only the acquiring process may release it. +mkdir "$TMP/stale.lock" +printf '%s\n' '99999999' > "$TMP/stale.lock/pid" +ssh_lock_acquire "$TMP/stale.lock" 3 0.01 sleep 1 || fail 'dead owner lock was not recovered' +[ "$(cat "$TMP/stale.lock/pid")" = "$$" ] || fail 'stale recovery did not publish this owner' +ssh_lock_release "$TMP/stale.lock" +[ ! -e "$TMP/stale.lock" ] || fail 'owner release did not remove the lock' + +# An orphaned/invalid pid may be reclaimed only after a bounded grace period. +mkdir "$TMP/orphan.lock" +printf 'incomplete' > "$TMP/orphan.lock/pid" +ssh_lock_acquire "$TMP/orphan.lock" 3 0.01 sleep 1 || fail 'invalid owner marker could not be recovered' +ssh_lock_release "$TMP/orphan.lock" + +# A fresh mkdir lock with no pid is INITIALIZING, not stale. Verify that a +# contender cannot take it over before the original writer publishes a pid. +mkdir "$TMP/initializing.lock" +( + . "$LIB" + ssh_lock_acquire "$TMP/initializing.lock" 30 0.02 sleep 1 || exit 1 + printf '%s\n' 'acquired' > "$TMP/initializing.result" + ssh_lock_release "$TMP/initializing.lock" +) & initializing_pid=$! +sleep 0.10 +[ ! -f "$TMP/initializing.lock/pid" ] || fail 'pid-less initializing lock was stolen prematurely' +[ ! -f "$TMP/initializing.result" ] || fail 'contender entered while owner was initializing' +rmdir "$TMP/initializing.lock" +wait "$initializing_pid" || fail 'contender did not acquire after owner released' +[ -f "$TMP/initializing.result" ] || fail 'contender was not executed' + +# Separate processes must serialize read-modify-write operations (not just +# return success), including during the mkdir -> pid window. +printf '0\n' > "$TMP/counter" +pids='' +i=0 +while [ "$i" -lt 4 ]; do + i=$((i + 1)) + sh -c ' + . "$1" + j=0 + while [ "$j" -lt 8 ]; do + ssh_lock_acquire "$2/parallel.lock" 100 0.01 sleep 0 || exit 1 + value="$(cat "$2/counter")" + printf "%s\n" "$((value + 1))" > "$2/counter.tmp.$$" + mv "$2/counter.tmp.$$" "$2/counter" + ssh_lock_release "$2/parallel.lock" + j=$((j + 1)) + done + ' sh "$LIB" "$TMP" & + pids="$pids $!" +done +for pid in $pids; do wait "$pid" || fail 'one parallel contender failed'; done +[ "$(cat "$TMP/counter")" = 32 ] || fail 'parallel lock lost a critical section update' +[ ! -d "$TMP/parallel.lock" ] || fail 'lock directory remains after successful releases' + +# UCI/JSON reading helpers use the caller's binaries and never require a file +# when optional metadata is absent. +cat > "$TMP/uci" <<'EOF_UCI' +#!/bin/sh +[ "$1" = '-q' ] && [ "$2" = get ] && [ "$3" = 'smartsafehub.test.enabled' ] || exit 1 +printf '1\n' +EOF_UCI +cat > "$TMP/jsonfilter" <<'EOF_JSON' +#!/bin/sh +[ "$1" = '-i' ] && [ "$3" = '-e' ] && [ "$4" = '@.enabled' ] || exit 1 +cat "$2" +EOF_JSON +chmod +x "$TMP/uci" "$TMP/jsonfilter" +[ "$(ssh_uci_get "$TMP/uci" smartsafehub.test.enabled)" = 1 ] || fail 'shared UCI reader returned unexpected data' +[ -z "$(ssh_json_value "$TMP/jsonfilter" "$TMP/missing" '@.enabled')" ] || fail 'missing JSON returned data' +printf 'yes\n' > "$TMP/input.json" +[ "$(ssh_json_value "$TMP/jsonfilter" "$TMP/input.json" '@.enabled' nonempty)" = yes ] || fail 'shared JSON reader returned unexpected data' +: > "$TMP/input.json" +[ -z "$(ssh_json_value "$TMP/jsonfilter" "$TMP/input.json" '@.enabled' nonempty)" ] || fail 'nonempty policy accepted empty JSON input' + +printf 'PASS: shared lock handles initializing, live, orphaned, stale and parallel ownership safely\n' diff --git a/luci-app-smartsafehub/tests/test-netwatch-ui.sh b/luci-app-smartsafehub/tests/test-netwatch-ui.sh new file mode 100755 index 000000000..1b920b76e --- /dev/null +++ b/luci-app-smartsafehub/tests/test-netwatch-ui.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +# Network Watch screen contract: reuse existing APIs; no collector or polling proliferation. +set -eu +ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +PAGE="$ROOT/frontend/src/pages/NetworkWatchPage.tsx" +APP="$ROOT/frontend/src/app/App.tsx" +DEMO="$ROOT/frontend/src/demo/main.tsx" +DATA="$ROOT/frontend/src/demo/data.ts" +fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } +for file in "$PAGE" "$APP" "$DEMO" "$DATA"; do [ -f "$file" ] || fail "missing $file"; done +for str in 'role="switch"' 'aria-checked={enabled}' 'ssh-switch-control' 'ssh-switch-thumb' 'setTimeout(() => setNotice(null), 3500)' 'formatTimestamp(data.sampledAt)' '관찰 상태' '연결 많음' '판단 보류' '기기 이름을 확인할 수 없으면 IP만 표시합니다' 'recentWarnings(activity)' '최근 감지 이벤트' '관찰 범위 안내' 'list-disc'; do + grep -Fq "$str" "$PAGE" || fail "화면 설정 및 표시 계약 누락: $str" +done +# Match SafeShield's compact SectionHeading action instead of a full-width settings card. +for str in 'action={' 'NetworkNodesIcon class="size-5"' '관찰 수집' '사용 중 · 관찰 중' '사용 중 · 수집 불가' '상태 확인 불가' 'aria-busy={saving}'; do + grep -Fq "$str" "$PAGE" || fail "Network Watch 상단 상태·토글 계약 누락: $str" +done +! grep -Fq '

      네트워크 이상 감지

      ' "$PAGE" || fail '상단의 기존 전체 너비 설정 카드가 남아 있음' +# A warning is never an automatic security verdict; the count threshold is only a label. +grep -Fq "return { label: '관찰 중'" "$PAGE" || fail '정상·안전 확정 배지를 추가하면 안 됨' +grep -Fq 'event.eventType === '\''network_watch.anomaly.warning'\''' "$PAGE" || fail '무관한 최근 활동을 감지 이벤트로 표시하면 안 됨' +grep -Fq 'event.source === '\''network_watch'\''' "$PAGE" || fail '이벤트 출처 필터 누락' +grep -Fq '.slice(0, 3)' "$PAGE" || fail '최근 이벤트 3개 제한 누락' +grep -Fq "const devices = useConnectedDevices(route === 'devices' || route === 'netwatch', route === 'devices');" "$APP" || fail '기존 기기 조회를 재사용하고 기기 화면에만 15초 polling 유지해야 함' +grep -Fq 'loader: fetchActivityHistory,' "$APP" || fail '기존 최근 활동 API를 재사용해야 함' +grep -Fq 'active: route === '\''netwatch'\''' "$APP" || fail 'Netwatch에서만 추가 활동 정보를 조회해야 함' +grep -Fq 'devices={devices.data}' "$APP" || fail '기기명 데이터 전달 누락' +grep -Fq 'activity={netwatchActivity.data}' "$APP" || fail '최근 이벤트 전달 누락' +grep -Fq 'devices={demoDevices} activity={demoActivity}' "$DEMO" || fail '데모 페이지에 신규 UI 데이터 반영 누락' +grep -Fq "eventType: 'network_watch.anomaly.warning'" "$DATA" || fail '데모 이벤트 예시 누락' +! grep -Eq 'conntrack|/proc/net|nlbwmon' "$APP" || fail '프론트엔드에서 연결 관찰기를 실행하면 안 됨' +printf 'PASS: Network Watch 화면·데모·기존 API 재사용 계약\n' diff --git a/luci-app-smartsafehub/tests/test-netwatch.sh b/luci-app-smartsafehub/tests/test-netwatch.sh new file mode 100755 index 000000000..2ef160229 --- /dev/null +++ b/luci-app-smartsafehub/tests/test-netwatch.sh @@ -0,0 +1,277 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +set -eu +ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +HELPER="$ROOT/root/usr/libexec/smartsafehub-netwatch" +COMMON="$ROOT/root/usr/lib/smartsafehub/common.sh" +PARSER="$ROOT/root/usr/lib/smartsafehub/netwatch.awk" +EVENTS_DAEMON="$ROOT/root/usr/libexec/smartsafehub-events" +RPC="$ROOT/root/usr/share/rpcd/ucode/smartsafehub.uc" +NETWATCH_RPC="$ROOT/root/usr/share/rpcd/ucode/smartsafehub/netwatch.uc" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT INT TERM +fail() { echo "FAIL: $*" >&2; exit 1; } +[ -x "$HELPER" ] && [ -f "$PARSER" ] || fail 'Network Watch 실행 파일이 누락됨' +sh -n "$HELPER" || fail '실행 파일 POSIX 셸 오류' +mkdir -p "$TMP/bin" "$TMP/run" +cat > "$TMP/bin/uci" <<'SH' +#!/bin/sh +[ "$1" = '-q' ] && [ "$2" = 'get' ] || exit 1 +[ "$3" = 'smartsafehub.netwatch.enabled' ] || exit 1 +printf '%s\n' "${MOCK_ENABLED:-1}" +SH +cat > "$TMP/bin/date" <<'SH' +#!/bin/sh +[ "$1" = '+%s' ] || exit 1 +printf '%s\n' "${MOCK_DATE:-1800000000}" +SH +cat > "$TMP/bin/event" <<'SH' +#!/bin/sh +printf '%s\n' "$*" >> "$MOCK_EVENT_LOG" +SH +cat > "$TMP/bin/conntrack" <<'SH' +#!/bin/sh +[ "$1" = '-L' ] && [ "$2" = '-f' ] && [ "$3" = 'ipv4' ] || exit 1 +cat "$MOCK_CONNTRACK_FILE" +SH +# Detect regressions even on hosts where flock happens to be installed. +cat > "$TMP/bin/flock" <<'SH' +#!/bin/sh +printf 'unexpected flock call\n' >> "$MOCK_FLOCK_LOG" +exit 127 +SH +chmod +x "$TMP/bin/"* +export MOCK_FLOCK_LOG="$TMP/flock-calls.log" +export MOCK_EVENT_LOG="$TMP/events.log" +export MOCK_CONNTRACK_FILE="$TMP/source.txt" +: > "$MOCK_EVENT_LOG" +run() { + SMARTSAFEHUB_NETWATCH_COMMON_LIB="$COMMON" \ + SMARTSAFEHUB_NETWATCH_RUNTIME_DIR="$TMP/run" \ + SMARTSAFEHUB_NETWATCH_PROC="${MOCK_SOURCE:-$MOCK_CONNTRACK_FILE}" \ + SMARTSAFEHUB_NETWATCH_PARSER="$PARSER" \ + SMARTSAFEHUB_NETWATCH_EVENTS="$TMP/bin/event" \ + SMARTSAFEHUB_NETWATCH_UCI="$TMP/bin/uci" \ + SMARTSAFEHUB_NETWATCH_DATE="$TMP/bin/date" \ + SMARTSAFEHUB_NETWATCH_CONNTRACK="$TMP/bin/conntrack" \ + PATH="$TMP/bin:$PATH" "$HELPER" sample +} + +# Unrelated reverse flows and private LAN-to-LAN flows must be excluded. +cat > "$MOCK_CONNTRACK_FILE" <<'DATA' +ipv4 2 tcp 6 430 ESTABLISHED src=192.168.8.20 dst=8.8.8.8 sport=50000 dport=443 src=8.8.8.8 dst=192.168.8.20 sport=443 dport=50000 [ASSURED] +ipv4 2 tcp 6 430 ESTABLISHED src=8.8.8.8 dst=192.168.8.20 sport=443 dport=50000 src=192.168.8.20 dst=8.8.8.8 sport=50000 dport=443 +ipv4 2 tcp 6 430 ESTABLISHED src=192.168.8.20 dst=192.168.8.1 sport=50000 dport=443 +ipv6 2 tcp 6 430 ESTABLISHED src=fd00::2 dst=2001:4860:4860::8888 sport=50000 dport=443 +DATA +run +jq -e '.phase=="ready" and .totalConnections==1 and .totalDevices==1 and .devices[0].ip=="192.168.8.20" and .devices[0].destinations==1 and .alert==null' "$TMP/run/status.json" >/dev/null || fail '사설→공인 IPv4만 정확히 관찰해야 함' +[ ! -s "$MOCK_EVENT_LOG" ] || fail '정상 통신에 오탐을 기록하면 안 됨' +[ ! -e "$MOCK_FLOCK_LOG" ] || fail 'flock 없는 기본 환경에서도 동작해야 함' +[ ! -e "$TMP/run/sample.lock.d" ] || fail '공통 잠금을 정상적으로 해제해야 함' +[ ! -e "$TMP/run/sample.lock" ] || fail '구버전 잠금을 새로 만들면 안 됨' + +# A running shared-lock owner must never be preempted. +mkdir "$TMP/run/sample.lock.d" +printf '%s\n' "$$" > "$TMP/run/sample.lock.d/pid" +cp "$TMP/run/status.json" "$TMP/before-lock.json" +run +cmp -s "$TMP/before-lock.json" "$TMP/run/status.json" || fail '실행 중인 공통 잠금 상태를 덮어쓰면 안 됨' +[ "$(cat "$TMP/run/sample.lock.d/pid")" = "$$" ] || fail '공통 잠금의 현재 소유자를 보존해야 함' +rm -rf "$TMP/run/sample.lock.d" + +# The shared library must recover a dead lock holder, including the legacy +# sample.lock.reclaim case that stalled the previous symlink implementation. +mkdir "$TMP/run/sample.lock.d" +printf '%s\n' '99999999' > "$TMP/run/sample.lock.d/pid" +mkdir "$TMP/run/sample.lock.reclaim" +ln -s 99999999 "$TMP/run/sample.lock" +run +[ ! -d "$TMP/run/sample.lock.d" ] || fail '공통 잠금의 죽은 소유자를 복구해야 함' +[ ! -e "$TMP/run/sample.lock" ] && [ ! -d "$TMP/run/sample.lock.reclaim" ] || fail '구버전 잔여 잠금도 정리해야 함' +jq -e '.phase=="ready" and .totalConnections==1' "$TMP/run/status.json" >/dev/null || fail '예전 reclaim 디렉터리가 있어도 수집되어야 함' + +# Respect an active sampler from the previous release until it finishes. +rm -f "$TMP/run/sample.lock" +ln -s "$$" "$TMP/run/sample.lock" +cp "$TMP/run/status.json" "$TMP/before-lock.json" +run +cmp -s "$TMP/before-lock.json" "$TMP/run/status.json" || fail '구버전 실행 중인 관찰기를 방해하면 안 됨' +rm -f "$TMP/run/sample.lock" + +# Interrupted runs may leave temporary files behind. Remove dead PID files, +# but never files owned by an active process. +printf stale > "$TMP/run/status.tmp.99999999" +printf stale > "$TMP/run/conntrack.tmp.99999999" +printf stale > "$TMP/run/conntrack-pid.tmp.99999999" +printf active > "$TMP/run/previous.tmp.$$" +run +[ ! -f "$TMP/run/status.tmp.99999999" ] || fail '죽은 작업의 status 임시 파일을 정리해야 함' +[ ! -f "$TMP/run/conntrack.tmp.99999999" ] || fail '죽은 작업의 conntrack 임시 파일을 정리해야 함' +[ ! -f "$TMP/run/conntrack-pid.tmp.99999999" ] || fail '죽은 작업의 conntrack PID 임시 파일을 정리해야 함' +[ -f "$TMP/run/previous.tmp.$$" ] || fail '실행 중인 프로세스의 임시 파일을 지우면 안 됨' +rm -f "$TMP/run/previous.tmp.$$" +[ ! -e "$MOCK_FLOCK_LOG" ] || fail '공통 잠금도 flock을 사용하면 안 됨' + +# High counts need two consecutive scans; warning should be rate-limited. +: > "$MOCK_CONNTRACK_FILE" +i=1 +while [ "$i" -le 125 ]; do + printf 'ipv4 2 tcp 6 430 ESTABLISHED src=192.168.8.21 dst=8.8.4.4 sport=%s dport=443\n' "$((10000+i))" >> "$MOCK_CONNTRACK_FILE" + i=$((i+1)) +done +run +[ ! -s "$MOCK_EVENT_LOG" ] || fail '첫 관찰의 연결 급증에 바로 경보하면 안 됨' +run +[ "$(wc -l < "$MOCK_EVENT_LOG")" -eq 1 ] || fail '2회 연속 높은 연결 수에 한 번만 이벤트가 필요함' +jq -e '.alert.ip=="192.168.8.21" and .alert.connections==125 and .totalConnections==125' "$TMP/run/status.json" >/dev/null || fail '이상 연결 건수/기기가 올바르게 기록되어야 함' +grep -Fq 'network_watch.anomaly.warning warning' "$MOCK_EVENT_LOG" || fail '최근 활동 이벤트가 발행되어야 함' +run +[ "$(wc -l < "$MOCK_EVENT_LOG")" -eq 1 ] || fail '30분 안에는 중복 경보를 내지 않아야 함' +MOCK_DATE=1800001801 run +[ "$(wc -l < "$MOCK_EVENT_LOG")" -eq 2 ] || fail '30분 경과 시 재알림이 가능해야 함' + +# Two different devices must not suppress one another for 30 minutes. +: > "$MOCK_CONNTRACK_FILE" +i=1 +while [ "$i" -le 125 ]; do + printf 'ipv4 2 tcp 6 430 ESTABLISHED src=192.168.8.21 dst=8.8.4.4 sport=%s dport=443\n' "$((10000+i))" >> "$MOCK_CONNTRACK_FILE" + printf 'ipv4 2 tcp 6 430 ESTABLISHED src=192.168.8.23 dst=8.8.8.8 sport=%s dport=443\n' "$((10000+i))" >> "$MOCK_CONNTRACK_FILE" + i=$((i+1)) +done +MOCK_DATE=1800001802 run +MOCK_DATE=1800001802 run +[ "$(wc -l < "$MOCK_EVENT_LOG")" -eq 3 ] || fail '다른 기기는 기존 30분 제한과 독립적으로 경고해야 함' +grep -Fq '"source_ip":"192.168.8.23"' "$MOCK_EVENT_LOG" || fail '두 번째 기기의 경고 누락' +MOCK_DATE=1800001803 run +[ "$(wc -l < "$MOCK_EVENT_LOG")" -eq 3 ] || fail '각 기기의 30분 제한을 유지해야 함' +[ "$(wc -l < "$TMP/run/alerts.tsv")" -eq 2 ] || fail '기기별 경고 제한 상태를 저장해야 함' + +# Restore a single high-count device for the fallback baseline checks. +: > "$MOCK_CONNTRACK_FILE" +i=1 +while [ "$i" -le 125 ]; do + printf 'ipv4 2 tcp 6 430 ESTABLISHED src=192.168.8.21 dst=8.8.4.4 sport=%s dport=443\n' "$((10000+i))" >> "$MOCK_CONNTRACK_FILE" + i=$((i+1)) +done + +# Keep the disabled-mode fixture in a subshell: some /bin/sh variants retain +# assignments made before shell functions, which can pollute later tests. +( + MOCK_ENABLED=0 run + jq -e '.phase=="disabled" and .totalConnections==0' "$TMP/run/status.json" >/dev/null || fail '비활성 상태는 수집을 하지 않아야 함' + [ ! -e "$TMP/run/previous.tsv" ] || fail '꺼진 뒤에는 이전 연결 기준선도 지워야 함' +) + +# No procfs and no working conntrack must be reported, not a healthy empty snapshot. +cat > "$TMP/bin/conntrack" <<'SH' +#!/bin/sh +exit 1 +SH +chmod +x "$TMP/bin/conntrack" +# Force enabled=1 explicitly: the missing-source state must be independent +# of the preceding disabled-mode test even on macOS /bin/sh. +MOCK_ENABLED=1 MOCK_SOURCE="$TMP/no-proc" run +jq -e '.phase=="unavailable"' "$TMP/run/status.json" >/dev/null || \ + fail "소스를 읽을 수 없는 상황에서 정상으로 오인하면 안 됨 (실제 상태: $(cat "$TMP/run/status.json" 2>/dev/null || printf 'missing'))" + +# A kernel without procfs can still read flows through the installed +# conntrack utility. The missing-source failure must have cleared baseline. +cat > "$TMP/bin/conntrack" <<'SH' +#!/bin/sh +[ "$1" = '-L' ] && [ "$2" = '-f' ] && [ "$3" = 'ipv4' ] || exit 1 +cat "$MOCK_CONNTRACK_FILE" +SH +chmod +x "$TMP/bin/conntrack" +MOCK_ENABLED=1 MOCK_SOURCE="$TMP/no-proc" run +jq -e '.phase=="ready" and .totalConnections==125 and .alert==null' "$TMP/run/status.json" >/dev/null || fail 'procfs가 없어도 conntrack 명령으로 수집해야 함' +[ ! -e "$TMP/run/conntrack.tmp.$$" ] || fail '대체 조회의 임시 파일을 수집 후 남기면 안 됨' + +# The userspace fallback must stop reading once the bounded parser input is +# full, rather than writing the entire unbounded conntrack dump to tmpfs. +# Ignore SIGPIPE to model a shell producer whose printf fails but whose loop +# continues; a consumer-side head alone is not a reliable stop mechanism. +cat > "$TMP/bin/conntrack" <<'SH' +#!/bin/sh +[ "$1" = '-L' ] && [ "$2" = '-f' ] && [ "$3" = 'ipv4' ] || exit 1 +trap '' PIPE +i=0 +while [ "$i" -lt 20000 ]; do + i=$((i+1)) + printf 'ipv4 2 tcp 6 430 ESTABLISHED src=192.168.8.24 dst=8.8.8.8 sport=%s dport=443\n' "$i" +done +printf 'completed\n' > "$MOCK_FALLBACK_FINISHED" +SH +chmod +x "$TMP/bin/conntrack" +MOCK_FALLBACK_FINISHED="$TMP/producer-finished"; export MOCK_FALLBACK_FINISHED +MOCK_ENABLED=1 MOCK_SOURCE="$TMP/no-proc" run +jq -e '.phase=="ready" and .limited==true and .alert==null and .totalConnections<=2048' "$TMP/run/status.json" >/dev/null || fail 'conntrack 대체 조회에서 상한을 유지해야 함' +[ ! -e "$MOCK_FALLBACK_FINISHED" ] || fail '전체 20000개 연결을 끝까지 생성하면 안 됨' +[ ! -e "$TMP/run/conntrack.tmp.$$" ] || fail '대체 조회 후 파일 잔여물을 남기면 안 됨' + +# Failed fallback must not silently become a valid empty snapshot. +cat > "$TMP/bin/conntrack" <<'SH' +#!/bin/sh +exit 7 +SH +chmod +x "$TMP/bin/conntrack" +MOCK_SOURCE="$TMP/no-proc" run +jq -e '.phase=="unavailable"' "$TMP/run/status.json" >/dev/null || fail 'conntrack 실패를 정상 상태로 오인하면 안 됨' + +# A stalled netlink reader must be terminated rather than hanging a worker. +cat > "$TMP/bin/conntrack" <<'SH' +#!/bin/sh +exec sleep 30 +SH +chmod +x "$TMP/bin/conntrack" +MOCK_SOURCE="$TMP/no-proc" run +jq -e '.phase=="unavailable"' "$TMP/run/status.json" >/dev/null || fail '멈춘 conntrack 대체 조회를 제한 시간 후 중단해야 함' +[ ! -d "$TMP/run/sample.lock.d" ] || fail 'conntrack 시간 제한 후에도 공통 잠금을 해제해야 함' + +# Restore the normal fixture for subsequent tests. +cat > "$TMP/bin/conntrack" <<'SH' +#!/bin/sh +[ "$1" = '-L' ] && [ "$2" = '-f' ] && [ "$3" = 'ipv4' ] || exit 1 +cat "$MOCK_CONNTRACK_FILE" +SH +chmod +x "$TMP/bin/conntrack" + +# Repeated contacts with 40 unique public destinations are only informational +# warning candidates, not evidence of a malware infection. +: > "$MOCK_CONNTRACK_FILE" +i=1 +while [ "$i" -le 40 ]; do + printf 'ipv4 2 udp 17 120 src=192.168.8.22 dst=8.8.1.%s sport=50000 dport=443\n' "$i" >> "$MOCK_CONNTRACK_FILE" + i=$((i+1)) +done +MOCK_DATE=1800003602 run +jq -e '.alert == null and .devices[0].destinations == 40' "$TMP/run/status.json" >/dev/null || fail '목적지 증가 첫 관찰은 경고하지 않아야 함' +MOCK_DATE=1800003602 run +jq -e '.alert.ip == "192.168.8.22" and .alert.destinations == 40' "$TMP/run/status.json" >/dev/null || fail '외부 목적지 수 증가를 2회 연속 확인해야 함' + +# Explicitly cap large tables and avoid warnings on partial snapshots. +MOCK_SOURCE="$MOCK_CONNTRACK_FILE" run +: > "$MOCK_CONNTRACK_FILE" +i=1 +while [ "$i" -le 2100 ]; do + printf 'ipv4 2 tcp 6 430 ESTABLISHED src=192.168.8.21 dst=8.8.4.4 sport=%s dport=443\n' "$((10000+i))" >> "$MOCK_CONNTRACK_FILE" + i=$((i+1)) +done +MOCK_SOURCE="$MOCK_CONNTRACK_FILE" run +jq -e '.phase=="ready" and .limited==true and .alert==null and .totalConnections<=2048' "$TMP/run/status.json" >/dev/null || fail '연결 2048개 제한과 부분 관찰 안전성이 필요함' +[ ! -s "$TMP/run/previous.tsv" ] || fail '부분 관찰 결과를 다음 경고의 기준선으로 사용하면 안 됨' + +# RPC/UI/demo and packaging contract. +grep -Fq 'ssh_lock_acquire' "$HELPER" || fail 'Network Watch는 공통 잠금 모듈을 사용해야 함' +grep -Fq 'netwatch_status:' "$RPC" || fail 'RPC 조회 누락' +grep -Fq 'netwatch_enabled_update:' "$RPC" || fail 'RPC 설정 누락' +grep -Fq "ctx.commit('smartsafehub')" "$NETWATCH_RPC" || fail 'UCI commit 누락' +grep -Fq 'NETWATCH_HELPER" sample' "$EVENTS_DAEMON" || fail '기존 이벤트 서비스 스케줄러와 연동 누락' +grep -Fq '+conntrack' "$ROOT/Makefile" || fail '25.12 procfs 대안의 패키지 의존성 누락' +grep -Fq 'smartsafehub.netwatch=netwatch' "$ROOT/Makefile" || fail '실시간 패키지 업그레이드 마이그레이션 누락' +grep -Fq 'uci -q set smartsafehub.netwatch=' "$ROOT/root/etc/uci-defaults/90-smartsafehub-system-defaults" || fail '펌웨어 업그레이드 마이그레이션 누락' +grep -Fq "'#netwatch': 'netwatch'" "$ROOT/frontend/src/hooks/useHashRoute.ts" || fail '화면 경로 누락' +grep -Fq "case 'netwatch':" "$ROOT/frontend/src/demo/main.tsx" || fail '데모 반영 누락' +grep -Fq "case 'network_watch.anomaly.warning':" "$ROOT/frontend/src/components/ActivityTimeline.tsx" || fail '최근 활동 표시 누락' +python3 -m json.tool "$ROOT/root/usr/share/rpcd/acl.d/luci-app-smartsafehub.json" >/dev/null || fail 'ACL JSON 문법 오류' +echo 'PASS: Network Watch collector/RPC/UI/demo/limits contract' diff --git a/luci-app-smartsafehub/tests/test-network-settings.sh b/luci-app-smartsafehub/tests/test-network-settings.sh index f5a04496d..7b3e75758 100755 --- a/luci-app-smartsafehub/tests/test-network-settings.sh +++ b/luci-app-smartsafehub/tests/test-network-settings.sh @@ -22,19 +22,12 @@ for file in "$ROUTES" "$HASH_ROUTE" "$NAVIGATION" "$APP" "$HOME" "$README" "$FEA done grep -Fq " | 'network'" "$ROUTES" || fail 'AppRoute must expose the combined network route' -grep -Fq "route: 'network'" "$ROUTES" || fail 'combined network settings route must be registered' -grep -Fq "hash: '#network'" "$ROUTES" || fail 'combined network settings route must use #network' grep -Fq "label: '네트워크'" "$ROUTES" || fail 'combined route label must be 네트워크' grep -Fq "title: '네트워크'" "$ROUTES" || fail 'combined page title must be 네트워크' if grep -Fq "route: 'wan'" "$ROUTES" || grep -Fq "route: 'lan'" "$ROUTES"; then fail 'WAN and LAN must not remain as separate visible routes' fi -grep -Fq "'#network': 'network'" "$HASH_ROUTE" || fail '#network must resolve to combined network settings' -grep -Fq "'#wan': 'network'" "$HASH_ROUTE" || fail 'legacy #wan must remain compatible' -grep -Fq "'#lan': 'network'" "$HASH_ROUTE" || fail 'legacy #lan must remain compatible' -grep -Fq "{ label: 'Network', routes: ['network', 'wifi', 'iptv', 'devices'] }" "$NAVIGATION" || \ - fail 'Network sidebar must expose a single combined settings item' grep -Fq "case 'network':" "$NAVIGATION" || fail 'combined network item must have an icon' if grep -Fq "case 'wan':" "$NAVIGATION" || grep -Fq "case 'lan':" "$NAVIGATION"; then fail 'sidebar icon routing must not retain separate WAN/LAN entries' diff --git a/luci-app-smartsafehub/tests/test-openwrt-commands.sh b/luci-app-smartsafehub/tests/test-openwrt-commands.sh index 2ba6296b7..141f28204 100755 --- a/luci-app-smartsafehub/tests/test-openwrt-commands.sh +++ b/luci-app-smartsafehub/tests/test-openwrt-commands.sh @@ -5,9 +5,6 @@ set -eu ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" CHECKER="$ROOT_DIR/tools/check_openwrt_commands.py" POLICY="$ROOT_DIR/tools/openwrt-25.12-commands.txt" -TMP="$(mktemp -d)" -trap 'rm -rf "$TMP"' EXIT INT TERM - fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } # Never silently enlarge the allowlist to hide a build failure. These applets @@ -23,6 +20,17 @@ done python3 "$CHECKER" >/dev/null || fail 'current router sources violate the OpenWrt 25.12 command policy' +# ShellSpec checks real router sources on every run. CI separately executes the +# slower mutation suite once, before installing dependencies. +case "${1:-}" in + '') printf 'PASS: OpenWrt 25.12 BusyBox and performance source audit\n'; exit 0 ;; + --self-test) ;; + *) fail "unknown test mode: $1" ;; +esac +[ "$#" -le 1 ] || fail 'too many test arguments' +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT INT TERM + mkdir -p "$TMP/root/usr/libexec" "$TMP/root/etc/init.d" FILE="$TMP/root/usr/libexec/smartsafehub-fixture" @@ -247,4 +255,4 @@ done EOF check_allowed -printf 'PASS: OpenWrt 25.12 BusyBox and performance command audit\n' +printf 'PASS: OpenWrt 25.12 BusyBox and performance command audit (mutation tests)\n' diff --git a/luci-app-smartsafehub/tests/test-router-demo-contract.sh b/luci-app-smartsafehub/tests/test-router-demo-contract.sh index 3badbbde0..14ef38266 100755 --- a/luci-app-smartsafehub/tests/test-router-demo-contract.sh +++ b/luci-app-smartsafehub/tests/test-router-demo-contract.sh @@ -38,6 +38,14 @@ grep -Fq 'storage: demoAppsMetrics.storage' "$FIXTURES" || { exit 1 } grep -Fq "demoApps" "$FIXTURES" +# Keep the public demo's Network Watch, first-party label and sample releases visible. +grep -Fq "case 'netwatch':" "$DEMO" +grep -Fq ' "$TMP/catalog" <<'SH' #!/bin/sh printf 'fetch\n' >> "$RPC_TEST_COUNT" -sleep 1 -[ ! -e "$RPC_TEST_FAIL" ] +sleep 0.3 +[ ! -e "$RPC_TEST_FAIL" ] || exit 1 +printf '{"schema":1}\n' > "$RPC_TEST_CACHE" SH cat > "$TMP/manager" <<'SH' #!/bin/sh printf '%s\n' "$1" >> "$RPC_TEST_COUNT" -sleep 1 +sleep 0.3 SH cat > "$TMP/device" <<'SH' #!/bin/sh printf '%s:%s\n' "$1" "${2:-}" >> "$RPC_TEST_COUNT" -sleep 1 +sleep 0.3 [ ! -e "$RPC_TEST_FAIL" ] SH chmod +x "$TMP/daemon" "$TMP/catalog" "$TMP/manager" "$TMP/device" @@ -47,11 +48,23 @@ export SMARTSAFEHUB_RPC_JOBS_MANAGER="$TMP/manager" export SMARTSAFEHUB_RPC_JOBS_DEVICE="$TMP/device" export RPC_TEST_COUNT="$TMP/count" export RPC_TEST_FAIL="$TMP/fail" +export RPC_TEST_CACHE="$TMP/apps-catalog.json" +export SMARTSAFEHUB_RPC_JOBS_CATALOG_CACHE="$RPC_TEST_CACHE" +export SMARTSAFEHUB_APPS_ROM_METADATA="$TMP/rom-firmware.json" +export SMARTSAFEHUB_APPS_METADATA="$TMP/overlay-firmware.json" +printf '{"device_code":"iptime-ax3000sm","build_id":"build-A","channel":"stable"}\n' > "$SMARTSAFEHUB_APPS_METADATA" + +count_fetches() { grep -c '^fetch$' "$TMP/count"; } +backdate_catalog() { + sed "s/^updated$(printf '\t')[0-9][0-9]*/updated$(printf '\t')$1/" \ + "$TMP/run/catalog.state" > "$TMP/old-state" + mv "$TMP/old-state" "$TMP/run/catalog.state" +} wait_phase() { target="$1" phase="$2" for _i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do grep -q "$(printf 'phase\t%s' "$phase")" "$TMP/run/$target.state" 2>/dev/null && return 0 - sleep 0.2 + sleep 0.1 done fail "$target never reached $phase" } @@ -71,7 +84,58 @@ wait_phase catalog success "$HELPER" request catalog wait_phase catalog running wait_phase catalog success -[ "$(grep -c '^fetch$' "$TMP/count")" -eq 2 ] || fail 'manual refresh did not start' +[ "$(count_fetches)" -eq 2 ] || fail 'manual refresh did not start' +# A failed fetch must retry after 60 seconds, not after the 10-minute success TTL. +# Even an empty cache must not allow automatic retry storms after a failure. +touch "$RPC_TEST_FAIL" +rm -f "$RPC_TEST_CACHE" +"$HELPER" request catalog +wait_phase catalog failed +[ "$(count_fetches)" -eq 3 ] || fail 'failed fetch was not executed' +"$HELPER" ensure catalog +[ "$(count_fetches)" -eq 3 ] || fail 'failed catalog retried too soon' +backdate_catalog "$(($(date +%s) - 61))" +"$HELPER" ensure catalog +wait_phase catalog failed +[ "$(count_fetches)" -eq 4 ] || fail 'failed catalog did not retry after 60 seconds' +# A successful retry must return to the 600-second cache TTL. +rm -f "$RPC_TEST_FAIL" +backdate_catalog "$(($(date +%s) - 61))" +"$HELPER" ensure catalog +wait_phase catalog success +[ "$(count_fetches)" -eq 5 ] || fail 'failed catalog did not recover' +"$HELPER" ensure catalog +[ "$(count_fetches)" -eq 5 ] || fail 'recovered catalog did not retain 10-minute TTL' +# Changing the firmware bypasses the TTL, including after an earlier failure. +printf '{"device_code":"iptime-ax3000sm","build_id":"build-B","channel":"stable"}\n' > "$SMARTSAFEHUB_APPS_METADATA" +"$HELPER" ensure catalog +wait_phase catalog success +[ "$(count_fetches)" -eq 6 ] || fail 'firmware change did not refresh catalog' +"$HELPER" ensure catalog +[ "$(count_fetches)" -eq 6 ] || fail 'same firmware fetched twice' +# ROM metadata takes precedence over the writable overlay version. +printf '{"device_code":"iptime-ax3000sm","build_id":"rom-A","channel":"stable"}\n' > "$SMARTSAFEHUB_APPS_ROM_METADATA" +"$HELPER" ensure catalog +wait_phase catalog success +[ "$(count_fetches)" -eq 7 ] || fail 'ROM firmware identity not adopted' +printf '{"device_code":"iptime-ax3000sm","build_id":"overlay-C","channel":"stable"}\n' > "$SMARTSAFEHUB_APPS_METADATA" +"$HELPER" ensure catalog +[ "$(count_fetches)" -eq 7 ] || fail 'overlay metadata overrode ROM' +# The cached display snapshot can disappear independently of the job state. +rm -f "$RPC_TEST_CACHE" +"$HELPER" ensure catalog +wait_phase catalog success +[ "$(count_fetches)" -eq 8 ] || fail 'missing catalog cache did not refresh' +# A firmware change during failure backoff must be retried immediately. +touch "$RPC_TEST_FAIL" +"$HELPER" request catalog +wait_phase catalog failed +[ "$(count_fetches)" -eq 9 ] || fail 'failed refresh did not run' +printf '{"device_code":"iptime-ax3000sm","build_id":"rom-B","channel":"stable"}\n' > "$SMARTSAFEHUB_APPS_ROM_METADATA" +rm -f "$RPC_TEST_FAIL" +"$HELPER" ensure catalog +wait_phase catalog success +[ "$(count_fetches)" -eq 10 ] || fail 'firmware change did not bypass failure backoff' "$HELPER" ensure status wait_phase status success "$HELPER" ensure sizes @@ -88,5 +152,82 @@ rm -f "$RPC_TEST_FAIL" wait_phase recommended-revoke running "$HELPER" request recommended-revoke 43 >/dev/null 2>&1 && fail 'concurrent revoke target replaced' wait_phase recommended-revoke success +# Four consecutive failures (initial request + 3 retries) impose a 20-minute +# cooldown. Page visits and missing snapshots must not bypass the cooldown. +touch "$RPC_TEST_FAIL" +rm -f "$RPC_TEST_CACHE" +base="$(count_fetches)" +"$HELPER" request catalog +wait_phase catalog failed +[ "$(count_fetches)" -eq "$((base + 1))" ] || fail 'initial attempt missing' +grep -q "$(printf 'failures\t1')" "$TMP/run/catalog.state" || fail 'initial failure counter incorrect' +for retry in 2 3 4; do + "$HELPER" ensure catalog + [ "$(count_fetches)" -eq "$((base + retry - 1))" ] || fail 'retry ran before 60 seconds' + backdate_catalog "$(($(date +%s) - 61))" + "$HELPER" ensure catalog + wait_phase catalog failed + [ "$(count_fetches)" -eq "$((base + retry))" ] || fail '60-second retry missing' + grep -q "$(printf 'failures\t%s' "$retry")" "$TMP/run/catalog.state" || fail 'failure count not persisted' +done +"$HELPER" ensure catalog +[ "$(count_fetches)" -eq "$((base + 4))" ] || fail 'fifth attempt ran during cooldown' +backdate_catalog "$(($(date +%s) - 1199))" +"$HELPER" ensure catalog +[ "$(count_fetches)" -eq "$((base + 4))" ] || fail '20-minute cooldown bypassed by missing cache' +# An expired cooldown begins a fresh cycle with a first failure, not failure 5. +backdate_catalog "$(($(date +%s) - 1201))" +"$HELPER" ensure catalog +wait_phase catalog failed +[ "$(count_fetches)" -eq "$((base + 5))" ] || fail 'cooldown expiry did not restart refresh' +grep -q "$(printf 'failures\t1')" "$TMP/run/catalog.state" || fail 'new retry cycle was not reset' +# A manual retry bypasses a fresh failure delay and resets the counter. +"$HELPER" request catalog +wait_phase catalog failed +[ "$(count_fetches)" -eq "$((base + 6))" ] || fail 'manual refresh was throttled' +grep -q "$(printf 'failures\t1')" "$TMP/run/catalog.state" || fail 'manual refresh did not reset retry cycle' +# Firmware changes also clear backoff even without a catalog snapshot. +printf '{"device_code":"iptime-ax3000sm","build_id":"rom-C","channel":"stable"}\n' > "$SMARTSAFEHUB_APPS_ROM_METADATA" +"$HELPER" ensure catalog +wait_phase catalog failed +[ "$(count_fetches)" -eq "$((base + 7))" ] || fail 'firmware change was throttled' +grep -q "$(printf 'failures\t1')" "$TMP/run/catalog.state" || fail 'firmware change did not reset counter' +# Success returns to the normal 10-minute refresh interval. +rm -f "$RPC_TEST_FAIL" +backdate_catalog "$(($(date +%s) - 61))" +"$HELPER" ensure catalog +wait_phase catalog success +[ "$(count_fetches)" -eq "$((base + 8))" ] || fail 'recovery attempt missing' +grep -q "$(printf 'failures\t0')" "$TMP/run/catalog.state" || fail 'recovery did not clear failure counter' +"$HELPER" ensure catalog +[ "$(count_fetches)" -eq "$((base + 8))" ] || fail 'successful refresh ignored 10-minute TTL' +# Exhaust another retry cycle and verify that a firmware upgrade bypasses +# the full 20-minute pause, without inheriting the previous failure count. +touch "$RPC_TEST_FAIL" +base="$(count_fetches)" +"$HELPER" request catalog +wait_phase catalog failed +for retry in 1 2 3; do + backdate_catalog "$(($(date +%s) - 61))" + "$HELPER" ensure catalog + wait_phase catalog failed +done +[ "$(count_fetches)" -eq "$((base + 4))" ] || fail 'could not exhaust retry allowance' +grep -q "$(printf 'failures\t4')" "$TMP/run/catalog.state" || fail 'cooldown state was not saved' +printf '{"device_code":"iptime-ax3000sm","build_id":"rom-D","channel":"stable"}\n' > "$SMARTSAFEHUB_APPS_ROM_METADATA" +"$HELPER" ensure catalog +wait_phase catalog failed +[ "$(count_fetches)" -eq "$((base + 5))" ] || fail 'firmware upgrade did not bypass cooldown' +grep -q "$(printf 'failures\t1')" "$TMP/run/catalog.state" || fail 'firmware upgrade inherited exhausted retries' +# Manual refresh must also bypass a full cooldown and start a clean cycle. +for retry in 1 2 3; do + backdate_catalog "$(($(date +%s) - 61))" + "$HELPER" ensure catalog + wait_phase catalog failed +done +grep -q "$(printf 'failures\t4')" "$TMP/run/catalog.state" || fail 'manual-cooldown setup failed' +"$HELPER" request catalog +wait_phase catalog failed +grep -q "$(printf 'failures\t1')" "$TMP/run/catalog.state" || fail 'manual refresh inherited exhausted retries' sh -n "$HELPER" || fail 'launcher syntax invalid' -echo 'PASS: detached RPC worker, cache TTL, single-flight, validation and failure states' +echo 'PASS: detached RPC worker, 3 retries, 20-minute cooldown, firmware cache identity and failure states' diff --git a/luci-app-smartsafehub/tests/test-section-layout.sh b/luci-app-smartsafehub/tests/test-section-layout.sh index 3c934bb91..fe40d7fc7 100755 --- a/luci-app-smartsafehub/tests/test-section-layout.sh +++ b/luci-app-smartsafehub/tests/test-section-layout.sh @@ -13,6 +13,20 @@ grep -Fq 'ssh-section-heading mb-4 min-w-0' "$COMP/SectionHeading.tsx" || fail ' grep -Fq '

      &2; exit 1; } +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT HUP INT TERM +for directory in tests spec root/usr/lib root/usr/libexec root/etc/init.d root/etc/rc.button; do + mkdir -p "$TMP/$directory" +done +printf '#!/bin/sh\nprintf "ok\\n"\n' > "$TMP/tests/test-existing.sh" +sh "$CHECKER" "$TMP" >/dev/null || fail 'valid fixture rejected' +for relative in tests/test-new.sh spec/new_spec.sh root/usr/lib/common.sh \ + root/usr/libexec/smartsafehub-new root/etc/init.d/smartsafehub-new root/etc/rc.button/new; do + printf '#!/bin/sh\nif true; then\n' > "$TMP/$relative" + if sh "$CHECKER" "$TMP" >"$TMP/out" 2>"$TMP/err"; then + fail "new malformed shell source was ignored: $relative" + fi + grep -Fq 'invalid shell syntax:' "$TMP/err" || \ + fail "unexpected syntax checker failure: $relative" + rm -f "$TMP/$relative" +done +sh "$CHECKER" "$TMP" >/dev/null || fail 'fixture not restored' +printf 'PASS: automatic syntax discovery catches new test and router shell files\n' diff --git a/luci-app-smartsafehub/tests/test-static-validation.sh b/luci-app-smartsafehub/tests/test-static-validation.sh index ae98dc6d4..81f347936 100755 --- a/luci-app-smartsafehub/tests/test-static-validation.sh +++ b/luci-app-smartsafehub/tests/test-static-validation.sh @@ -31,62 +31,10 @@ if grep -Fq ' .ssh-safeshield-license-summary,' "$APP_STYLE"; then fail 'mobile license styles must not duplicate SafeShield upgrade-card alignment' fi -for script in \ - root/etc/init.d/smartsafehub-events \ - root/etc/init.d/smartsafehub-updater \ - root/etc/init.d/smartsafehub-firmware \ - root/etc/init.d/smartsafehub-maintenance \ - root/etc/init.d/smartsafehub-health \ - root/etc/init.d/smartsafehub-device \ - root/usr/lib/smartsafehub/common.sh \ - root/usr/libexec/smartsafehub-events \ - root/usr/libexec/smartsafehub-updater \ - root/usr/libexec/smartsafehub-firmware \ - root/usr/libexec/smartsafehub-maintenance \ - root/usr/libexec/smartsafehub-health \ - root/usr/libexec/smartsafehub-device \ - root/usr/libexec/smartsafehub-backup \ - spec/contracts_spec.sh \ - tests/test-static-validation.sh \ - tests/test-shell-pipeline-safety.sh \ - tests/test-package-contract.sh \ - tests/test-events.sh \ - tests/test-activity-ui-contract.sh \ - tests/test-activity-cloud-sync.sh \ - tests/test-common-shell.sh \ - tests/test-navigation-contract.sh \ - tests/test-tailwind-shadow-dom.sh \ - tests/test-document-ui-contract.sh \ - tests/test-login-ui-contract.sh \ - tests/test-initial-password-setup.sh \ - tests/test-dashboard-ui-contract.sh \ - tests/test-network-input-contract.sh \ - tests/test-lan-settings.sh \ - tests/test-lan-uci-runtime.sh \ - tests/test-wan-settings.sh \ - tests/test-wan-uci-runtime.sh \ - tests/test-iptv.sh \ - tests/test-update-ui-contract.sh \ - tests/test-relative-time-contract.sh \ - tests/test-reload-safety.sh \ - tests/test-settings-ui-contract.sh \ - tests/test-section-layout.sh \ - tests/test-system-time-contract.sh \ - tests/test-scheduled-reboot.sh \ - tests/test-health.sh \ - tests/test-device-registration.sh \ - tests/test-backup-restore.sh \ - tests/test-rpc-contract.sh \ - tests/test-rules-ui-contract.sh \ - tests/test-safeshield-page-contract.sh \ - tests/test-statistics-ui-contract.sh \ - tests/test-ucode-syntax.sh \ - tests/test-ucode-imports.sh \ - tests/test-updater.sh \ - tests/test-firmware-updater.sh \ - tests/test-firmware-identity.sh; do - sh -n "$ROOT_DIR/$script" -done +# Discover shell tests and shipped router entry points automatically. +# A new test or daemon is validated without editing this script. +sh "$ROOT_DIR/tools/check_shell_syntax.sh" "$ROOT_DIR" || \ + fail 'shell syntax validation failed' # 실제 ucode 컴파일은 test-ucode-syntax.sh가 OpenWrt 전용 모듈 stub과 # target module search path를 구성한 뒤 단일 진입점으로 수행해야 합니다. diff --git a/luci-app-smartsafehub/tests/test-statistics-ui-contract.sh b/luci-app-smartsafehub/tests/test-statistics-ui-contract.sh index 50d019ed6..6d6936558 100755 --- a/luci-app-smartsafehub/tests/test-statistics-ui-contract.sh +++ b/luci-app-smartsafehub/tests/test-statistics-ui-contract.sh @@ -22,8 +22,8 @@ jq -e \ '.["luci-app-smartsafehub"].read.ubus.safeshield | index("statistics") != null' \ "$ACL" >/dev/null || fail 'statistics must be allowed by the SafeShield read ACL' -grep -Fq 'LUCI_EXTRA_DEPENDS:=safeshield (>=0.3.25)' "$MAKEFILE" || \ - fail 'SmartSafeHub must require safeshield 0.3.25 or later' +grep -Fq 'LUCI_EXTRA_DEPENDS:=safeshield (>=0.3.26)' "$MAKEFILE" || \ + fail 'SmartSafeHub must require safeshield 0.3.26 or later' grep -Fq "callSafeShield('statistics')" "$API" || \ fail 'frontend API must call the safeshield statistics RPC' grep -Fq 'const STATISTICS_REFRESH_INTERVAL_MS = 60_000;' "$HOOK" || \ diff --git a/luci-app-smartsafehub/tests/test-travelmate.sh b/luci-app-smartsafehub/tests/test-travelmate.sh new file mode 100755 index 000000000..9a99066f4 --- /dev/null +++ b/luci-app-smartsafehub/tests/test-travelmate.sh @@ -0,0 +1,190 @@ +#!/bin/sh +set -eu +ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +python3 - "$ROOT" <<'PY' +from pathlib import Path +import sys +root=Path(sys.argv[1]) +demo=(root/'frontend/src/demo/data.ts').read_text() +assert "id: 'travelmate'" in demo +assert "version: '2.4.9-r6'" in demo +assert "webUi: null, // Demo never exposes actual LuCI administration routes." in demo +assert "'travelmate': {" in demo +assert "installed: false" in demo +assert "category: 'network'" in demo +assert "luci-app-travelmate" in (root/'docs/APPS.md').read_text() +assert "case 'network': return 'network'" in (root/'frontend/src/components/AppsCategoryIcon.tsx').read_text() +manager = (root/'root/usr/libexec/smartsafehub-apps-manager').read_text() +view = (root/'root/usr/share/rpcd/ucode/smartsafehub/apps.uc').read_text() +assert '[ "$id" = travelmate ]' in manager +assert '[ "$app" = travelmate ]' in manager +assert 'travelmate_pins_match_arch "$arch" "$pins"' in manager +assert 'libopenssl3 libatomic1' in manager +assert 'reviewed_arch=' in manager +assert 'primary_package=luci-app-travelmate' in manager +assert 'web_ui.path")" = /cgi-bin/luci/admin/services/travelmate' in manager +assert "id == 'travelmate'" in view +assert "item?.web_ui?.package == 'luci-app-travelmate'" in view +assert "item?.web_ui?.path == '/cgi-bin/luci/admin/services/travelmate'" in view +assert 'shared_travelmate_dep' in manager +assert 'rpcd-mod-rpcsys|libopenssl3|libatomic1' in manager +assert 'shared_travelmate_dep "$name" && return 42' in manager +assert 'app="$id" # Status checks' in manager +assert 'append_owned_pin "$name"' in manager +page=(root/'frontend/src/pages/AppsPage.tsx').read_text() +assert "removeTarget.id === 'travelmate'" in page +assert '공용 패키지는 안전을 위해 유지됩니다.' in page +assert 'state?.partial && !managed' in page +PY +printf '%s\n' 'PASS: Travelmate Apps demo and catalog contract' + +# Exercise the exact APK transaction-plan validators, not only static labels. +# Both exact reviewed dependency sets are accepted; core upgrades, +# missing prerequisites, wrong-architecture pins and auto-removal fail closed. +MANAGER="$ROOT/root/usr/libexec/smartsafehub-apps-manager" +TMPDIR_TM="$(mktemp -d)" || exit 1 +trap 'rm -rf "$TMPDIR_TM"' EXIT HUP INT TERM +awk '/^travelmate_required_packages\(\)/,/^}/' "$MANAGER" > "$TMPDIR_TM/functions" +awk '/^travelmate_pins_match_arch\(\)/,/^}/' "$MANAGER" >> "$TMPDIR_TM/functions" +awk '/^shared_travelmate_dep\(\)/,/^}/' "$MANAGER" >> "$TMPDIR_TM/functions" +awk '/^review_install_plan\(\)/,/^}/' "$MANAGER" >> "$TMPDIR_TM/functions" +awk '/^review_remove_plan\(\)/,/^}/' "$MANAGER" >> "$TMPDIR_TM/functions" +# shellcheck source=/dev/null +. "$TMPDIR_TM/functions" +AARCH="$(travelmate_required_packages aarch64_cortex-a53)" || exit 1 +[ "$(travelmate_required_packages x86_64)" = "$AARCH" ] || { echo 'FAIL: x86 package set differs' >&2; exit 1; } +MIPS="$(travelmate_required_packages mipsel_24kc)" || exit 1 +if travelmate_required_packages mipsel >/dev/null; then + echo 'FAIL: unknown feed architecture accepted' >&2; exit 1 +fi +# Exercise the exact preflight validator on pinned TSV files. +: > "$TMPDIR_TM/aarch-pins" +for name in $AARCH; do + printf '%s\t%s\t%s\n' "$name" '1-r1' 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' >> "$TMPDIR_TM/aarch-pins" +done +travelmate_pins_match_arch aarch64_cortex-a53 "$TMPDIR_TM/aarch-pins" || { echo 'FAIL: approved aarch pins rejected' >&2; exit 1; } +if travelmate_pins_match_arch mipsel_24kc "$TMPDIR_TM/aarch-pins"; then + echo 'FAIL: incomplete MIPS pins accepted' >&2; exit 1 +fi +cp "$TMPDIR_TM/aarch-pins" "$TMPDIR_TM/mips-pins" +printf 'libatomic1\t14.3.0-r5\t%s\n' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' >> "$TMPDIR_TM/mips-pins" +travelmate_pins_match_arch mipsel_24kc "$TMPDIR_TM/mips-pins" || { echo 'FAIL: approved MIPS pins rejected' >&2; exit 1; } +if travelmate_pins_match_arch x86_64 "$TMPDIR_TM/mips-pins"; then + echo 'FAIL: MIPS-only pin accepted on x86' >&2; exit 1 +fi +grep -v '^libopenssl3[[:space:]]' "$TMPDIR_TM/aarch-pins" > "$TMPDIR_TM/missing-pins" +if travelmate_pins_match_arch aarch64_cortex-a53 "$TMPDIR_TM/missing-pins"; then + echo 'FAIL: missing OpenSSL pin accepted' >&2; exit 1 +fi +app=travelmate +shared_travelmate_dep libopenssl3 || { echo 'FAIL: OpenSSL is not protected' >&2; exit 1; } +shared_travelmate_dep libatomic1 || { echo 'FAIL: libatomic is not protected' >&2; exit 1; } +if shared_travelmate_dep travelmate; then + echo 'FAIL: application package marked shared' >&2; exit 1 +fi +printf '%s\n' \ + '(1/8) Installing curl (signed)' \ + '(2/8) Installing libcurl4 (signed)' \ + '(3/8) Installing libnghttp2-14 (signed)' \ + '(4/8) Installing iwinfo (signed)' \ + '(5/8) Installing rpcd-mod-rpcsys (signed)' \ + '(6/8) Installing libopenssl3 (signed)' \ + '(7/8) Installing travelmate (signed)' \ + '(8/8) Installing luci-app-travelmate (signed)' > "$TMPDIR_TM/plan" +review_install_plan "$TMPDIR_TM/plan" "$AARCH" || { echo 'FAIL: signed aarch/x86 plan rejected' >&2; exit 1; } +if review_install_plan "$TMPDIR_TM/plan" "$MIPS"; then + echo 'FAIL: missing MIPS libatomic accepted' >&2; exit 1 +fi +printf '%s\n' '(9/9) Installing libatomic1 (signed)' >> "$TMPDIR_TM/plan" +review_install_plan "$TMPDIR_TM/plan" "$MIPS" || { echo 'FAIL: signed MIPS plan rejected' >&2; exit 1; } +if review_install_plan "$TMPDIR_TM/plan" "$AARCH"; then + echo 'FAIL: unexpected libatomic on aarch accepted' >&2; exit 1 +fi +printf '%s\n' '(10/10) Upgrading rpcd (unapproved)' >> "$TMPDIR_TM/plan" +if review_install_plan "$TMPDIR_TM/plan" "$MIPS"; then + echo 'FAIL: core RPC upgrade accepted' >&2; exit 1 +fi +sed '/Installing libopenssl3 /d; /Upgrading rpcd /d' "$TMPDIR_TM/plan" > "$TMPDIR_TM/missing" +if review_install_plan "$TMPDIR_TM/missing" "$MIPS"; then + echo 'FAIL: incomplete OpenSSL dependency plan accepted' >&2; exit 1 +fi +printf '%s\n' '(1/2) Purging travelmate (test)' '(2/2) Purging luci-app-travelmate (test)' > "$TMPDIR_TM/remove" +review_remove_plan "$TMPDIR_TM/remove" 'travelmate luci-app-travelmate' || { echo 'FAIL: app-only removal rejected' >&2; exit 1; } +printf '%s\n' '(3/3) Purging rpcd-mod-rpcsys (test)' >> "$TMPDIR_TM/remove" +if review_remove_plan "$TMPDIR_TM/remove" 'travelmate luci-app-travelmate'; then + echo 'FAIL: shared RPC package removal accepted' >&2; exit 1 +fi +printf '%s\n' 'PASS: Travelmate signed dependencies and core package protection' + +# Exercise the actual status() implementation with a fake installed-package +# database. Shared dependencies surviving removal must NOT appear as a partial +# Travelmate install; interrupted owned installs must still be recoverable. +awk '/^status\(\)/,/^}/' "$MANAGER" >> "$TMPDIR_TM/functions" +cat > "$TMPDIR_TM/status-test.sh" <<'SH_STATUS' +#!/bin/sh +set -eu +. "$1" +RUNTIME="$2/runtime" +OWNED="$2/owned" +CATALOG="$2/catalog.json" +TM_INSTALLED="$2/installed" +TM_PKG_NAMES="$2/packages" +mkdir -p "$RUNTIME" "$OWNED" +: > "$CATALOG" +: > "$TM_INSTALLED" +printf '%s\n' 'travelmate' 'luci-app-travelmate' \ + 'curl' 'libcurl4' 'libnghttp2-14' 'iwinfo' \ + 'rpcd-mod-rpcsys' 'libopenssl3' > "$TM_PKG_NAMES" +fail_status() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } +catalog_ids() { printf '%s\n' travelmate; } +valid_id() { return 0; } +valid_package() { return 0; } +valid_version() { return 0; } +owned_manifest() { printf '%s/%s' "$OWNED" "$1"; } +installed() { grep -Fxq "$1" "$TM_INSTALLED"; } +installed_version() { printf '%s\n' '2.4.9-r5'; } +web_ui_package() { printf '%s\n' luci-app-travelmate; } +extract_pins() { return 1; } +select_upgrades() { return 1; } +field() { + case "$2" in + '@.apps[0].package_arch') printf '%s\n' aarch64_cortex-a53;; + '@.apps[0].packages['*'].name') + index="${2#*.packages[}" + index="${index%%]*}" + sed -n "$((index + 1))p" "$TM_PKG_NAMES";; + esac +} +assert_status() { + status || fail_status 'status command failed' + actual="$(awk -F '\t' '{print $2 ":" $3 ":" $4}' "$RUNTIME/apps-status.tsv")" + [ "$actual" = "$1" ] || fail_status "$2: expected $1, got $actual" +} +# No installed packages and no ownership. +assert_status '0:0:0' 'fresh app' +# APKs deliberately preserved by remove_app() are not an app installation. +for dep in curl libcurl4 libnghttp2-14 iwinfo rpcd-mod-rpcsys libopenssl3; do + printf '%s\n' "$dep" >> "$TM_INSTALLED" +done +assert_status '0:0:0' 'shared dependencies remain after app removal' +# A genuinely partial manual install cannot be silently adopted by Apps. +printf '%s\n' travelmate >> "$TM_INSTALLED" +assert_status '0:1:0' 'manually installed Travelmate remains partial' +sed '/^travelmate$/d' "$TM_INSTALLED" > "$TM_INSTALLED.tmp" +mv -f "$TM_INSTALLED.tmp" "$TM_INSTALLED" +printf '%s\n' luci-app-travelmate >> "$TM_INSTALLED" +assert_status '0:1:0' 'manually installed LuCI component remains partial' +sed '/^luci-app-travelmate$/d' "$TM_INSTALLED" > "$TM_INSTALLED.tmp" +mv -f "$TM_INSTALLED.tmp" "$TM_INSTALLED" +# An unfinished Apps-owned transaction must retain its repair action. +printf '%s\n' 'travelmate 2.4.9-r6 test-hash' > "$OWNED/travelmate" +assert_status '0:1:1' 'owned interrupted install remains repairable' +rm -f "$OWNED/travelmate" +# Full installation remains fully installed and is never reclassified. +printf '%s\n' travelmate luci-app-travelmate >> "$TM_INSTALLED" +assert_status '1:1:0' 'fully present app not classified as removed' +printf '%s\n' 'travelmate 2.4.9-r6 test-hash' > "$OWNED/travelmate" +assert_status '1:1:1' 'fully Apps-managed app not classified as removed' +printf '%s\n' 'PASS: Travelmate removal retains shared packages without confusing install status' +SH_STATUS +sh "$TMPDIR_TM/status-test.sh" "$TMPDIR_TM/functions" "$TMPDIR_TM" diff --git a/luci-app-smartsafehub/tests/test-ucode-common.sh b/luci-app-smartsafehub/tests/test-ucode-common.sh new file mode 100755 index 000000000..865168ea9 --- /dev/null +++ b/luci-app-smartsafehub/tests/test-ucode-common.sh @@ -0,0 +1,77 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +set -eu +ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +CORE="$ROOT/root/usr/share/rpcd/ucode/smartsafehub/core.uc" +fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } +[ -f "$CORE" ] || fail 'core ucode module not found' +for module in activity firmware updates network-management iptv-management health; do + file="$ROOT/root/usr/share/rpcd/ucode/smartsafehub/$module.uc" + grep -Eq '^[[:space:]]*integer_value,' "$file" || fail "$module does not import the common integer parser" + if grep -Eq '^function integer_value\(' "$file"; then + fail "$module still reimplements common integer validation" + fi +done +for module in activity firmware health; do + file="$ROOT/root/usr/share/rpcd/ucode/smartsafehub/$module.uc" + grep -Eq '^[[:space:]]*read_json_object,' "$file" || fail "$module does not import the common JSON reader" +done +if ! command -v ucode >/dev/null 2>&1; then + [ "${SMARTSAFEHUB_REQUIRE_UCODE:-0}" != 1 ] || fail 'CI must execute common ucode runtime cases' + printf 'PASS: common ucode helper imports (runtime requires CI ucode)\n' + exit 0 +fi +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT INT TERM +ln -s "$ROOT/root/usr/share/rpcd/ucode/smartsafehub" "$TMP/smartsafehub" +cat > "$TMP/fs.uc" <<'EOF_FS' +export function readfile(path) { + if (path == '/valid') return '{"enabled":true,"count":3}'; + if (path == '/array') return '[1,2]'; + if (path == '/bad') return '{bad'; + if (path == '/big') return '{"more":"than 3 bytes"}'; + return null; +}; +EOF_FS +cat > "$TMP/ubus.uc" <<'EOF_UBUS' +export function connect() { return null; }; +EOF_UBUS +cat > "$TMP/uci.uc" <<'EOF_UCI' +export function cursor() { return null; }; +EOF_UCI +cat > "$TMP/runner.uc" <<'EOF_RUNNER' +import { integer_value, read_json_object } from './smartsafehub/core.uc'; +function expect_integer(label, value, fallback, expected) { + const actual = integer_value(value, fallback); + // Strict equality is important here: ucode's == may coerce NaN. + if (actual !== expected) { + printf('FAIL: integer parser %s: actual=%J expected=%J\n', label, actual, expected); + exit(1); + } +} +expect_integer('numeric string', '17', 9, 17); +expect_integer('invalid string', 'invalid', 9, 9); +expect_integer('invalid with zero fallback', 'invalid', 0, 0); +expect_integer('null', null, 9, 9); +expect_integer('null with negative fallback', null, -1, -1); +expect_integer('null with zero fallback', null, 0, 0); +expect_integer('empty string', '', 9, 9); +expect_integer('empty string with negative fallback', '', -1, -1); +expect_integer('zero', 0, 9, 0); +expect_integer('numeric zero string', '0', 9, 0); +expect_integer('boolean false', false, 9, 0); +expect_integer('negative', -3, 9, -3); +expect_integer('leading numeric substring', '18ms', 9, 18); +expect_integer('NaN', int('invalid'), 9, 9); +const value = read_json_object('/valid', 100); +if (value == null || value.count != 3 || value.enabled != true || + read_json_object('/valid', 4) != null || + read_json_object('/array') != null || + read_json_object('/bad') != null || + read_json_object('/missing') != null) { + printf('FAIL: JSON reader accepted malformed, oversized or nonobject data\n'); + exit(1); +} +printf('PASS: shared ucode integer and JSON helpers preserve parsing contracts\n'); +EOF_RUNNER +ucode -L "$TMP" -L "$ROOT/root/usr/share/rpcd/ucode" "$TMP/runner.uc" || fail 'ucode common helper runtime contract failed' diff --git a/luci-app-smartsafehub/tests/test-update-ui-contract.sh b/luci-app-smartsafehub/tests/test-update-ui-contract.sh index 04575513d..290de1b56 100755 --- a/luci-app-smartsafehub/tests/test-update-ui-contract.sh +++ b/luci-app-smartsafehub/tests/test-update-ui-contract.sh @@ -101,6 +101,16 @@ if grep -Fq 'SmartSafeHub 관리 화면과 관련 소프트웨어의 새 버전 fail 'management software card must not keep the long wrapping description' fi +# The newest note must be labeled honestly when older notes are unavailable. +grep -Fq 'const latestReleaseNoteOnly = Boolean(' "$UPDATES_CARD" || \ + fail 'release notes must distinguish a newest-only fallback from complete history' +grep -Fq 'data.releaseNotes[0]?.version === currentPackage.availableVersion' "$UPDATES_CARD" || \ + fail 'newest-only release note label must match the actual available version' +grep -Fq '최신 릴리즈 노트' "$UPDATES_CARD" || \ + fail 'release note fallback must label the newest entry' +grep -Fq '이전 버전의 변경 사항은 생략되었을 수 있습니다.' "$UPDATES_CARD" || \ + fail 'newest-only release note must disclose omitted prior changes' + # Manual firmware safety guidance should use the full card width on desktop and avoid an orphaned second line. grep -Fq 'class="mt-0 mb-4 text-xs leading-5 text-slate-500 lg:whitespace-nowrap"' "$FIRMWARE_CARD" || \ fail 'manual firmware safety guidance must stay on one line in the desktop layout' diff --git a/luci-app-smartsafehub/tests/test-updater.sh b/luci-app-smartsafehub/tests/test-updater.sh index 5f1c62d6c..cd21d5539 100755 --- a/luci-app-smartsafehub/tests/test-updater.sh +++ b/luci-app-smartsafehub/tests/test-updater.sh @@ -117,6 +117,18 @@ cp "${MOCK_RELEASE_ROOT:?}/$name" "$output" MOCKFETCH chmod +x "$TMP/bin/uclient-fetch" +cat > "$TMP/bin/jsonfilter" <<'MOCKJSON' +#!/bin/sh +# Test-only adapter for OpenWrt jsonfilter's -i/-e interface. +[ "${1:-}" = '-i' ] && [ "${3:-}" = '-e' ] || exit 1 +case "${4:-}" in + '@.package') jq -er '.package' "$2" ;; + '@.version') jq -er '.version' "$2" ;; + *) exit 1 ;; +esac +MOCKJSON +chmod +x "$TMP/bin/jsonfilter" + cat > "$TMP/bin/apk" <<'MOCKAPK' #!/bin/sh set -eu @@ -222,6 +234,7 @@ export SMARTSAFEHUB_UPDATER_APK_WORLD_FILE="$TMP/world" export SMARTSAFEHUB_UPDATER_REPOSITORY_DIR="$TMP/repos" export SMARTSAFEHUB_UPDATER_APK_BIN="$TMP/bin/apk" export SMARTSAFEHUB_UPDATER_UCLIENT_FETCH_BIN="$TMP/bin/uclient-fetch" +export SMARTSAFEHUB_UPDATER_JSONFILTER_BIN="$TMP/bin/jsonfilter" export SMARTSAFEHUB_UPDATER_UCI_BIN="$TMP/bin/uci" export SMARTSAFEHUB_UPDATER_RPCD_INIT='' export SMARTSAFEHUB_UPDATER_LUCI_INDEX_CACHE="$TMP/luci-indexcache" @@ -243,6 +256,62 @@ assert_contains "$TMP/release-notes.json" "\"version\": \"0.2.2-r1\"" assert_not_contains "$TMP/release-notes.json" "\"version\": \"0.2.1-r1\"" assert_contains "$TMP/release-notes.json" '"complete": true' +# When many releases fall between installed and available, avoid dozens of +# sequential downloads and show only the latest release note (compact JSON is valid). +cp "$TMP/releases/index.json" "$TMP/releases/index.original.json" +cp "$TMP/releases/$RELEASE_VERSION.json" "$TMP/releases/latest.original.json" +cat > "$TMP/releases/$RELEASE_VERSION.json" < "$TMP/releases/index.json" +rm -f "$TMP/fetch.log" "$TMP/release-notes.json" +"$UPDATER" check +assert_contains "$TMP/release-notes.json" '"complete": false' +assert_contains "$TMP/release-notes.json" '"summary":"Latest release only"' +assert_contains "$TMP/fetch.log" "${RELEASE_VERSION}.json" +assert_not_contains "$TMP/fetch.log" '0.2.30-r15.json' +jq -e --arg version "$RELEASE_VERSION" \ + '.releases | length == 1 and .[0].version == $version' "$TMP/release-notes.json" >/dev/null || \ + fail 'large release history must keep only the newest release note' + +# If the newest note becomes unreachable, preserve a matching cached note +# instead of showing older entries as if they were the current release. +cp "$TMP/release-notes.json" "$TMP/release-notes.before.json" +MOCK_FETCH_FAIL_NAME="$RELEASE_VERSION.json" "$UPDATER" check +cmp -s "$TMP/release-notes.before.json" "$TMP/release-notes.json" || \ + fail 'failed latest release fetch must preserve the last-known-good note' + +# An index outage must also fall back to the available version directly. +rm -f "$TMP/release-notes.json" "$TMP/fetch.log" +MOCK_FETCH_FAIL_NAME=index.json "$UPDATER" check +assert_contains "$TMP/release-notes.json" '"complete": false' +jq -e --arg version "$RELEASE_VERSION" \ + '.releases | length == 1 and .[0].version == $version' "$TMP/release-notes.json" >/dev/null || \ + fail 'index fetch failure must still fetch the latest note' + +cp "$TMP/releases/index.original.json" "$TMP/releases/index.json" +cp "$TMP/releases/latest.original.json" "$TMP/releases/$RELEASE_VERSION.json" +rm -f "$TMP/release-notes.json" + +# A newly fetched note with invalid JSON must not be shown as the newest. +# Do not make extra requests for historical notes when the newest is unusable. +printf '{"package":"luci-app-smartsafehub","version":"%s","summary":"broken"' \ + "$RELEASE_VERSION" > "$TMP/releases/$RELEASE_VERSION.json" +rm -f "$TMP/release-notes.json" "$TMP/fetch.log" +"$UPDATER" check +[ ! -e "$TMP/release-notes.json" ] || fail 'malformed latest release note must not be cached' +assert_not_contains "$TMP/fetch.log" '0.2.3-r1.json' +cp "$TMP/releases/latest.original.json" "$TMP/releases/$RELEASE_VERSION.json" + # One missing intermediate note should preserve the other notes and mark the bundle incomplete. rm -f "$TMP/release-notes.json" MOCK_FETCH_FAIL_NAME='0.2.3-r1.json' "$UPDATER" check diff --git a/luci-app-smartsafehub/tests/test-wan-settings.sh b/luci-app-smartsafehub/tests/test-wan-settings.sh index 141aef830..c0659fe17 100755 --- a/luci-app-smartsafehub/tests/test-wan-settings.sh +++ b/luci-app-smartsafehub/tests/test-wan-settings.sh @@ -12,9 +12,6 @@ PAGE="$ROOT_DIR/frontend/src/pages/WanPage.tsx" IPV4_INPUT="$ROOT_DIR/frontend/src/components/Ipv4OctetInput.tsx" LAN_PAGE="$ROOT_DIR/frontend/src/pages/LanPage.tsx" TYPES="$ROOT_DIR/frontend/src/types/wan.ts" -ROUTES="$ROOT_DIR/frontend/src/app/routes.ts" -HASH_ROUTE="$ROOT_DIR/frontend/src/hooks/useHashRoute.ts" -NAVIGATION="$ROOT_DIR/frontend/src/components/ProductNavigation.tsx" APP="$ROOT_DIR/frontend/src/app/App.tsx" ACTIVITY="$ROOT_DIR/frontend/src/components/ActivityTimeline.tsx" @@ -23,7 +20,7 @@ fail() { exit 1 } -for file in "$NETWORK_RPC" "$NETWORK_MODULE" "$ACL" "$API" "$HOOK" "$PAGE" "$IPV4_INPUT" "$LAN_PAGE" "$TYPES" "$ROUTES" "$HASH_ROUTE" "$NAVIGATION" "$APP" "$ACTIVITY"; do +for file in "$NETWORK_RPC" "$NETWORK_MODULE" "$ACL" "$API" "$HOOK" "$PAGE" "$IPV4_INPUT" "$LAN_PAGE" "$TYPES" "$APP" "$ACTIVITY"; do [ -f "$file" ] || fail "WAN 설정 계약 파일이 없습니다: ${file#$ROOT_DIR/}" done @@ -94,11 +91,7 @@ grep -Fq 'inputMode="numeric"' "$IPV4_INPUT" || fail '공용 IPv4 octet 입력 grep -Fq "octets.every((octet) => octet === '') ? '' : octets.join('.')" "$IPV4_INPUT" || fail '선택 IPv4 입력은 모든 octet을 지우면 빈 값으로 복원되어야 합니다.' grep -Fq "from '../components/Ipv4OctetInput'" "$LAN_PAGE" || fail 'LAN도 WAN과 동일한 공용 IPv4 octet 입력을 재사용해야 합니다.' grep -Fq 'onReconnect={reconnect}' "$PAGE" || fail 'WAN 화면에 수동 재연결 기능이 있어야 합니다.' -grep -Fq "route: 'network'" "$ROUTES" || fail 'WAN과 LAN을 통합한 네트워크 설정 route가 필요합니다.' -grep -Fq "hash: '#network'" "$ROUTES" || fail '네트워크 설정 route는 #network hash를 사용해야 합니다.' -grep -Fq "'#wan': 'network'" "$HASH_ROUTE" || fail '기존 #wan hash는 통합 네트워크 설정으로 연결되어야 합니다.' -grep -Fq "{ label: 'Network', routes: ['network', 'wifi', 'iptv', 'devices'] }" "$NAVIGATION" || \ - fail 'Network 메뉴는 통합 네트워크 설정을 Wi-Fi보다 먼저 노출해야 합니다.' +# Shared route/hash/navigation registration is tested in test-navigation-contract.sh. grep -Fq "case 'network':" "$APP" || fail 'App이 통합 네트워크 설정 화면을 렌더링해야 합니다.' grep -Fq '&2; exit 1; } +for directory in tests spec root/usr/lib root/usr/libexec root/etc/init.d root/etc/rc.button; do + [ -d "$ROOT_DIR/$directory" ] || fail "missing shell source directory: $directory" +done +TMP="$(mktemp)" +trap 'rm -f "$TMP"' EXIT HUP INT TERM +{ + find "$ROOT_DIR/tests" "$ROOT_DIR/spec" -type f -name '*.sh' -print + find "$ROOT_DIR/root/usr/lib" -type f -name '*.sh' -print + find "$ROOT_DIR/root/usr/libexec" "$ROOT_DIR/root/etc/init.d" \ + "$ROOT_DIR/root/etc/rc.button" -type f -print +} | LC_ALL=C sort -u > "$TMP" +[ -s "$TMP" ] || fail 'no shell sources found' +while IFS= read -r script; do + [ -f "$script" ] || fail "missing shell source: $script" + sh -n "$script" || fail "invalid shell syntax: ${script#"$ROOT_DIR"/}" +done < "$TMP" +printf 'PASS: auto-discovered shell source syntax\n' diff --git a/luci-app-smartsafehub/tools/check_test_coverage.py b/luci-app-smartsafehub/tools/check_test_coverage.py index 4f0149094..62e62c01f 100644 --- a/luci-app-smartsafehub/tools/check_test_coverage.py +++ b/luci-app-smartsafehub/tools/check_test_coverage.py @@ -74,12 +74,14 @@ def check_runtime(root: Path) -> int: name = path.name looks_like_test = ( '.test.' in name or '.spec.' in name or - name.startswith(('test-', 'test_')) or name.endswith('_test.py') + name.startswith(('test-', 'test_')) or name.endswith('_test.py') or + name.endswith(('-test.uc', '_test.uc')) ) if looks_like_test and path not in runtime: raise CoverageError( f'CI 실행 대상이 아닌 테스트 파일: {path.relative_to(root)} ' - '(JavaScript 테스트는 tests/runtime/*.test.mjs에 배치하세요)' + '(JavaScript는 tests/runtime/*.test.mjs, ucode 실행 테스트는 ' + 'ShellSpec에 등록된 tests/test-*.sh 형식으로 작성하세요)' ) return len(runtime) @@ -121,7 +123,7 @@ def check_ci(root: Path) -> None: 'Backend CI에서 전체 ShellSpec 테스트를 실행하지 않습니다.', ) require( - command_is_unconditional(backend, 'sh tests/test-openwrt-commands.sh'), + command_is_unconditional(backend, 'sh tests/test-openwrt-commands.sh --self-test'), 'Backend CI에서 OpenWrt 25.12 명령 호환성 검사가 누락되거나 조건부로 실행됩니다.', ) require( @@ -158,7 +160,7 @@ def self_test() -> None: - name: Test run: shellspec - name: OpenWrt BusyBox - run: sh tests/test-openwrt-commands.sh + run: sh tests/test-openwrt-commands.sh --self-test frontend: runs-on: ubuntu-latest steps: @@ -207,6 +209,11 @@ def self_test() -> None: unsupported_python.write_text('', encoding='utf-8') fails_with('CI 실행 대상이 아닌') unsupported_python.unlink() + # A standalone ucode test is not discovered by ShellSpec or frontend CI. + standalone_ucode = root / 'tests/apps-storage-test.uc' + standalone_ucode.write_text('exit(0);\n', encoding='utf-8') + fails_with('CI 실행 대상이 아닌') + standalone_ucode.unlink() nested = root / 'tests/runtime/nested' nested.mkdir() (nested / 'new.test.mjs').write_text('', encoding='utf-8') @@ -214,8 +221,9 @@ def self_test() -> None: (nested / 'new.test.mjs').unlink() for old, replacement, error in ( - ('run: sh tests/test-openwrt-commands.sh', '# run: sh tests/test-openwrt-commands.sh', '명령 호환성'), - ('run: sh tests/test-openwrt-commands.sh', 'run: sh tests/test-openwrt-commands.sh || true', '명령 호환성'), + ('run: sh tests/test-openwrt-commands.sh --self-test', '# run: sh tests/test-openwrt-commands.sh --self-test', '명령 호환성'), + ('run: sh tests/test-openwrt-commands.sh --self-test', 'run: sh tests/test-openwrt-commands.sh', '명령 호환성'), + ('run: sh tests/test-openwrt-commands.sh --self-test', 'run: sh tests/test-openwrt-commands.sh --self-test || true', '명령 호환성'), ('run: sh tests/test-suite-integrity.sh', '# run: sh tests/test-suite-integrity.sh', '독립 테스트'), ('run: shellspec', 'run: shellspec spec/selected_spec.sh', 'ShellSpec 테스트'), ('run: node --experimental-strip-types --test ../tests/runtime/*.test.mjs', diff --git a/luci-app-wwand/po/pl/wwand.po b/luci-app-wwand/po/pl/wwand.po new file mode 100644 index 000000000..8fec7994e --- /dev/null +++ b/luci-app-wwand/po/pl/wwand.po @@ -0,0 +1,4952 @@ +# +msgid "" +msgstr "" +"Project-Id-Version: luci-app-wwand\n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" + +#: htdocs/luci-static/resources/view/wwand/settings.js:713 +msgid "" +"\"%s\" is already a \"%s\" section in /etc/config/network — refusing to " +"overwrite it. Pick a different name." +msgstr "" +"„%s” jest już sekcją „%s” w /etc/config/network — odmowa nadpisania. Wybierz " +"inną nazwę." + +#: htdocs/luci-static/resources/wwand/modemopts.js:574 +msgid "\"%s\" is listed twice" +msgstr "" + +#: htdocs/luci-static/resources/wwand/graph.js:563 +msgid "%.1f %s (avg %.1f, peak %.1f)" +msgstr "%.1f %s (śr. %.1f, szczyt %.1f)" + +#: htdocs/luci-static/resources/wwand/graph.js:559 +msgid "%d %s (avg %.1f, peak %d)" +msgstr "%d %s (śr. %.1f, szczyt %d)" + +#: htdocs/luci-static/resources/wwand/graph.js:561 +msgid "%d (avg %.1f, peak %d)" +msgstr "%d (śr. %.1f, szczyt %d)" + +#: htdocs/luci-static/resources/wwand/simlist.js:30 +msgid "%d d" +msgstr "%d d" + +#: htdocs/luci-static/resources/view/wwand/sims.js:33 +msgid "%d days ago" +msgstr "%d dni temu" + +#: htdocs/luci-static/resources/view/wwand/status.js:425 +msgid "%d demuxed packets over %d USB frames" +msgstr "%d zdemultipleksowanych pakietów w %d ramkach USB" + +#: htdocs/luci-static/resources/wwand/simlist.js:30 +msgid "%d h" +msgstr "%d godz." + +#: htdocs/luci-static/resources/view/wwand/sims.js:32 +msgid "%d h ago" +msgstr "%d godz. temu" + +#: htdocs/luci-static/resources/view/wwand/status.js:295 +msgid "%d in use" +msgstr "%d używanych" + +#: htdocs/luci-static/resources/view/wwand/status.js:294 +msgid "%d in use of %d in view" +msgstr "%d używanych z %d widocznych" + +#: htdocs/luci-static/resources/view/wwand/status.js:296 +msgid "%d in view" +msgstr "%d widocznych" + +#: htdocs/luci-static/resources/wwand/simlist.js:29 +msgid "%d min" +msgstr "%d min" + +#: htdocs/luci-static/resources/view/wwand/sims.js:30 +msgid "%d min ago" +msgstr "%d min temu" + +#: htdocs/luci-static/resources/wwand/logbox.js:209 +msgid "%d of %d wwand lines, from the last %d log entries on the box." +msgstr "" +"%d z %d wierszy wwand, spośród ostatnich %d wpisów dziennika na urządzeniu." + +#: htdocs/luci-static/resources/view/wwand/status.js:319 +msgid "%d parsed, %d not" +msgstr "%d zinterpretowanych, %d nie" + +#: htdocs/luci-static/resources/view/wwand/sims.js:28 +msgid "%d s ago" +msgstr "%d s temu" + +#: htdocs/luci-static/resources/wwand/format.js:393 +msgid "%d slots" +msgstr "%d slotów" + +#: htdocs/luci-static/resources/view/wwand/status.js:872 +msgid "%d timeouts on %s, moving now" +msgstr "%d przekroczeń limitu czasu na %s — przechodzę do następnego kroku" + +#: htdocs/luci-static/resources/view/wwand/status.js:775 +msgid "%s (%d attempts)" +msgstr "%s (prób: %d)" + +#: htdocs/luci-static/resources/view/wwand/status.js:362 +#: htdocs/luci-static/resources/view/wwand/status.js:366 +msgid "%s protocol · up to %d datagrams / %s" +msgstr "protokół %s · do %d datagramów / %s" + +#: htdocs/luci-static/resources/wwand/simlist.js:38 +msgid "%s · slot %d" +msgstr "%s, slot %d" + +#: htdocs/luci-static/resources/view/wwand/status.js:825 +msgid "" +"%s — reported by the daemon; this page is older than that step and cannot " +"describe it." +msgstr "" +"%s — zgłoszone przez demona; ta strona jest starsza niż ten krok i nie " +"potrafi go opisać." + +#: htdocs/luci-static/resources/view/wwand/sims.js:21 +msgid "%s, slot %d" +msgstr "%s, slot %d" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1033 +msgid "(incomplete)" +msgstr "(niekompletne)" + +#: htdocs/luci-static/resources/wwand/simlist.js:219 +msgid "(interface default)" +msgstr "(domyślne interfejsu)" + +#: htdocs/luci-static/resources/view/wwand/status.js:902 +msgid "(last attempt)" +msgstr "(ostatnia próba)" + +#: htdocs/luci-static/resources/wwand/modemopts.js:596 +#: htdocs/luci-static/resources/wwand/simlist.js:232 +msgid "(none)" +msgstr "brak" + +#: htdocs/luci-static/resources/wwand/format.js:1114 +msgid "(roaming)" +msgstr "roaming" + +#: htdocs/luci-static/resources/wwand/logbox.js:368 +msgid "(runtime only)" +msgstr "(tylko w czasie działania)" + +#: htdocs/luci-static/resources/view/wwand/settings.js:237 +msgid "(slot without a name)" +msgstr "" + +#: htdocs/luci-static/resources/wwand/esim.js:102 +#: htdocs/luci-static/resources/wwand/format.js:576 +#: htdocs/luci-static/resources/wwand/netsel.js:118 +msgid "(unnamed)" +msgstr "(bez nazwy)" + +#: htdocs/luci-static/resources/view/wwand/settings.js:206 +msgid "+ add a non-listed band" +msgstr "+ dodaj pasmo spoza listy" + +#: htdocs/luci-static/resources/view/wwand/status.js:854 +#: htdocs/luci-static/resources/view/wwand/status.js:856 +msgid ", then" +msgstr ", następnie" + +#: htdocs/luci-static/resources/wwand/modemopts.js:440 +msgid "0 — leave as-is" +msgstr "0 — bez zmian" + +#: htdocs/luci-static/resources/view/wwand/modems.js:159 +msgid "2 — USB" +msgstr "2 — USB" + +#: htdocs/luci-static/resources/view/wwand/status.js:239 +msgid "" +"2D is a position without a reliable height; 3D has one. Reported by the " +"receiver itself (NMEA GSA), and on a multi-constellation receiver it is the " +"best of them." +msgstr "" +"2D to pozycja bez wiarygodnej wysokości; 3D ją zawiera. Podawane przez sam " +"odbiornik (NMEA GSA); w odbiorniku wielosystemowym jest to najlepszy z " +"wyników." + +#: htdocs/luci-static/resources/view/wwand/modems.js:160 +msgid "3 — PCIe" +msgstr "3 — PCIe" + +#: htdocs/luci-static/resources/wwand/graph.js:119 +msgid "3G signal quality (Ec/Io)" +msgstr "Jakość sygnału 3G (Ec/Io)" + +#: htdocs/luci-static/resources/view/wwand/status.js:47 +msgid "3GPP frequency band of the cell (B… = LTE, n… = 5G NR)" +msgstr "Pasmo częstotliwości 3GPP komórki (B… = LTE, n… = 5G NR)" + +#: htdocs/luci-static/resources/view/wwand/status.js:975 +msgid "" +"3GPP frequency band of the serving cell (B… = LTE, n… = 5G NR) — lower bands " +"travel further, higher bands carry more bandwidth" +msgstr "" +"Pasmo częstotliwości 3GPP komórki obsługującej (B… = LTE, n… = 5G NR) — " +"niższe pasma mają większy zasięg, wyższe oferują większą przepustowość" + +#: htdocs/luci-static/resources/view/wwand/modems.js:161 +msgid "4 — embedded" +msgstr "4 — wbudowany" + +#: htdocs/luci-static/resources/view/wwand/settings.js:914 +#: htdocs/luci-static/resources/wwand/modemopts.js:618 +msgid "5G NR SA cell lock" +msgstr "Blokada komórki 5G NR SA" + +#: htdocs/luci-static/resources/view/wwand/status.js:1210 +msgid "5G NR neighbour cells" +msgstr "Komórki sąsiednie 5G NR" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1310 +msgid "" +"5G bands for non-standalone operation, where a 5G carrier rides on an LTE " +"anchor. Which of the two lists applies depends on the network: the Status " +"page names the mode the modem is actually in." +msgstr "" +"Pasma 5G dla pracy niesamodzielnej (NSA), w której nośna 5G współpracuje z " +"kotwicą LTE. To, która z dwóch list ma zastosowanie, zależy od sieci: strona " +"Status wskazuje tryb, w jakim modem faktycznie pracuje." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1308 +msgid "" +"5G bands for standalone operation, where the modem talks 5G only with no LTE " +"anchor." +msgstr "" +"Pasma 5G dla pracy samodzielnej (SA), w której modem komunikuje się " +"wyłącznie w 5G, bez kotwicy LTE." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1306 +msgid "" +"5G bands. This modem has one 5G band list for standalone and non-standalone " +"operation alike." +msgstr "" +"Pasma 5G. Ten modem ma jedną listę pasm 5G, wspólną dla pracy samodzielnej " +"(SA) i niesamodzielnej (NSA)." + +#: htdocs/luci-static/resources/view/wwand/status.js:996 +msgid "5G cell" +msgstr "Komórka 5G" + +#: htdocs/luci-static/resources/view/wwand/status.js:1003 +msgid "" +"5G frequency range as the modem reports it: FR1 = sub-6 GHz, FR2 = mmWave " +"(24 GHz and above)" +msgstr "" +"Zakres częstotliwości 5G w postaci raportowanej przez modem: FR1 = sub-6 " +"GHz, FR2 = fale milimetrowe (od 24 GHz wzwyż)" + +#: htdocs/luci-static/resources/view/wwand/modems.js:398 +msgid "" +"A SIM override for ICCID %s already exists (section %s) — edit it in the " +"SIMs list below." +msgstr "" +"Nadpisanie dla karty SIM o ICCID %s już istnieje (sekcja %s) — edytuj je na " +"liście SIM poniżej." + +#: htdocs/luci-static/resources/wwand/modemopts.js:359 +msgid "A datapath name: lowercase letters, digits, underscore and hyphen." +msgstr "Nazwa ścieżki danych: małe litery, cyfry, podkreślenie i myślnik." + +#: htdocs/luci-static/resources/view/wwand/settings.js:537 +msgid "" +"A different configuration is selected and waiting for a modem reset — the " +"radio is still running the previous one." +msgstr "" +"Wybrano inną konfigurację i czeka ona na reset modemu — radio nadal działa " +"na poprzedniej." + +#: htdocs/luci-static/resources/wwand/simlist.js:145 +msgid "" +"A name for this card, e.g. \"Work\" or \"Travel\". Shown on the SIM cards " +"page and the modem status." +msgstr "" +"Nazwa tej karty, np. „Praca” lub „Podróż”. Wyświetlana na stronie kart SIM i " +"w statusie modemu." + +#: htdocs/luci-static/resources/wwand/netsel.js:228 +msgid "" +"A network scan takes the radio off the network for minutes, so it needs " +"write access." +msgstr "" +"Skanowanie sieci wyłącza radio z sieci na kilka minut, dlatego wymaga " +"uprawnień zapisu." + +#: htdocs/luci-static/resources/view/wwand/settings.js:915 +msgid "A single 5G SA cell as \"pci:arfcn:scs:band\"." +msgstr "Pojedyncza komórka 5G SA w formacie „pci:arfcn:scs:band”." + +#: htdocs/luci-static/resources/view/wwand/modems.js:792 +#: htdocs/luci-static/resources/wwand/simlist.js:186 +msgid "APN" +msgstr "APN" + +#: htdocs/luci-static/resources/view/wwand/status.js:934 +msgid "APN %s" +msgstr "APN %s" + +#: htdocs/luci-static/resources/wwand/simlist.js:197 +msgid "" +"APN authentication for this card (only needed when the operator requires a " +"username/password)." +msgstr "" +"Uwierzytelnianie APN dla tej karty (potrzebne tylko wtedy, gdy operator " +"wymaga nazwy użytkownika i hasła)." + +#: htdocs/luci-static/resources/wwand/modemopts.js:224 +msgid "" +"APN for the initial network attach, when the network wants a different one " +"from the data connection (an IMS or admin bearer). Leave empty to attach " +"with the interface's own APN, which is the normal case." +msgstr "" +"APN używany przy początkowym dołączaniu do sieci, gdy sieć wymaga innego niż " +"dla połączenia danych (nośnik IMS lub administracyjny). Pozostaw puste, aby " +"dołączać z własnym APN interfejsu — to normalny przypadek." + +#: htdocs/luci-static/resources/wwand/modemopts.js:200 +msgid "AT control TTY" +msgstr "Port TTY sterowania AT" + +#: htdocs/luci-static/resources/view/wwand/settings.js:398 +#: htdocs/luci-static/resources/view/wwand/settings.js:604 +msgid "Access technologies" +msgstr "Technologie dostępu" + +#: htdocs/luci-static/resources/view/wwand/modems.js:538 +msgid "Actions" +msgstr "Akcje" + +#: htdocs/luci-static/resources/view/wwand/status.js:153 +msgid "Active connections" +msgstr "Aktywne połączenia" + +#: htdocs/luci-static/resources/wwand/simlist.js:109 +msgid "Add SIM" +msgstr "Dodaj SIM" + +#: htdocs/luci-static/resources/view/wwand/modems.js:123 +msgid "" +"Add a modem here, then point an interface at it with the \"Modem\" field on " +"the interface page." +msgstr "" +"Dodaj tutaj modem, a następnie wskaż go w interfejsie w polu „Modem” na " +"stronie interfejsu." + +#: htdocs/luci-static/resources/view/wwand/settings.js:730 +msgid "Add entry" +msgstr "Dodaj wpis" + +#: htdocs/luci-static/resources/view/wwand/modems.js:126 +msgid "Add modem" +msgstr "Dodaj modem" + +#: htdocs/luci-static/resources/wwand/esim.js:484 +msgid "" +"After a download or profile change the eUICC queues notifications that " +"confirm the operation to the operator. Send them once the router has " +"internet." +msgstr "" +"Po pobraniu lub zmianie profilu eUICC kolejkuje powiadomienia potwierdzające " +"operację u operatora. Wyślij je, gdy router uzyska dostęp do internetu." + +#: htdocs/luci-static/resources/wwand/graph.js:114 +msgid "Aggregate bandwidth" +msgstr "Łączna szerokość pasma" + +#: htdocs/luci-static/resources/wwand/graph.js:111 +msgid "Aggregated carriers" +msgstr "Zagregowane nośne" + +#: htdocs/luci-static/resources/wwand/modemopts.js:424 +msgid "Aggregation DL datagram size" +msgstr "Rozmiar datagramu agregacji DL" + +#: htdocs/luci-static/resources/wwand/modemopts.js:536 +msgid "All" +msgstr "Wszystkie" + +#: htdocs/luci-static/resources/wwand/modemopts.js:750 +msgid "All sentences" +msgstr "" + +#: htdocs/luci-static/resources/view/wwand/status.js:611 +msgid "" +"An environmental limit the modem reports (temperature, voltage or charge " +"state). It is not throttling the radio." +msgstr "" +"Ograniczenie środowiskowe zgłaszane przez modem (temperatura, napięcie lub " +"stan naładowania). Nie ogranicza ono pracy radia." + +#: htdocs/luci-static/resources/view/wwand/status.js:679 +msgid "" +"An optional package holds this modem's radio off. Its interfaces stay down " +"until the reason is gone, then the radio is switched on again by itself." +msgstr "" +"Opcjonalny pakiet utrzymuje radio tego modemu wyłączone. Jego interfejsy " +"pozostają wyłączone do ustąpienia przyczyny, po czym radio zostanie włączone " +"ponownie samoczynnie." + +#: htdocs/luci-static/resources/view/wwand/settings.js:829 +msgid "" +"Applied, except the RAT priority: the modem accepted it and kept its own " +"order. Its firmware does not take a priority this way." +msgstr "" + +#: htdocs/luci-static/resources/view/wwand/settings.js:820 +msgid "Applied, except the band settings — the modem refused them (%s)." +msgstr "Zastosowano, z wyjątkiem ustawień pasm — modem je odrzucił (%s)." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1323 +msgid "Apply" +msgstr "Zastosuj" + +#: htdocs/luci-static/resources/wwand/esim.js:73 +msgid "Asking the modem directly…" +msgstr "Odpytywanie modemu bezpośrednio…" + +#: htdocs/luci-static/resources/view/wwand/modems.js:494 +#: htdocs/luci-static/resources/view/wwand/modems.js:529 +msgid "Attach" +msgstr "Dołącz" + +#: htdocs/luci-static/resources/wwand/modemopts.js:223 +msgid "Attach APN" +msgstr "APN dołączania" + +#: htdocs/luci-static/resources/view/wwand/status.js:1009 +msgid "Attach TAI" +msgstr "TAI dołączenia" + +#: htdocs/luci-static/resources/wwand/modemopts.js:229 +msgid "Attach authentication" +msgstr "Uwierzytelnianie przy dołączaniu" + +#: htdocs/luci-static/resources/view/wwand/modems.js:491 +msgid "Attach failed: %s." +msgstr "Dołączenie nie powiodło się: %s." + +#: htdocs/luci-static/resources/wwand/modemopts.js:246 +msgid "Attach password" +msgstr "Hasło dołączania" + +#: htdocs/luci-static/resources/view/wwand/modems.js:480 +msgid "" +"Attach the modem to the network again; its interfaces reconnect on their own." +msgstr "" +"Dołącz modem do sieci ponownie; jego interfejsy połączą się samoczynnie." + +#: htdocs/luci-static/resources/wwand/modemopts.js:240 +msgid "Attach username" +msgstr "Nazwa użytkownika dołączania" + +#: htdocs/luci-static/resources/wwand/esim.js:30 +msgid "Authenticating" +msgstr "Uwierzytelnianie" + +#: htdocs/luci-static/resources/wwand/modemopts.js:230 +msgid "" +"Authentication for the attach bearer. Only used together with an attach APN." +msgstr "" +"Uwierzytelnianie nośnika dołączania. Używane tylko razem z APN dołączania." + +#: htdocs/luci-static/resources/wwand/simlist.js:196 +msgid "Authentication type" +msgstr "Typ uwierzytelniania" + +#: htdocs/luci-static/resources/wwand/modemopts.js:755 +msgid "Auto-correct modem config" +msgstr "Automatyczna korekta konfiguracji modemu" + +#: htdocs/luci-static/resources/wwand/modemopts.js:58 +#: htdocs/luci-static/resources/wwand/modemopts.js:353 +msgid "Automatic" +msgstr "Automatycznie" + +#: htdocs/luci-static/resources/wwand/netsel.js:259 +msgid "" +"Automatic lets the modem choose the best operator. A manual scan lists the " +"visible operators so you can force one (e.g. to prefer a partner network " +"while roaming)." +msgstr "" +"Tryb automatyczny pozwala modemowi wybrać najlepszego operatora. Skanowanie " +"ręczne wyświetla widocznych operatorów, aby można było wymusić jednego z " +"nich (np. aby preferować sieć partnerską w roamingu)." + +#: htdocs/luci-static/resources/wwand/netsel.js:244 +msgid "Automatic network selection enabled." +msgstr "Włączono automatyczny wybór sieci." + +#: htdocs/luci-static/resources/view/wwand/modems.js:197 +#: htdocs/luci-static/resources/view/wwand/status.js:349 +msgid "Backend" +msgstr "Backend" + +#: htdocs/luci-static/resources/view/wwand/status.js:47 +#: htdocs/luci-static/resources/view/wwand/status.js:975 +msgid "Band" +msgstr "Pasmo" + +#: htdocs/luci-static/resources/wwand/graph.js:86 +msgid "Band power (RSSI)" +msgstr "Moc pasma (RSSI)" + +#: htdocs/luci-static/resources/view/wwand/modems.js:523 +msgid "Band, operator, SIM, eSIM and SMS tools for this modem" +msgstr "Narzędzia pasma, operatora, SIM, eSIM i SMS dla tego modemu" + +#: htdocs/luci-static/resources/view/wwand/status.js:50 +msgid "Bandwidth" +msgstr "Szerokość pasma" + +#: htdocs/luci-static/resources/wwand/modemopts.js:103 +msgid "Bind by IMEI" +msgstr "Powiąż według IMEI" + +#: htdocs/luci-static/resources/wwand/modemopts.js:85 +msgid "Bind by USB serial" +msgstr "Powiąż według numeru seryjnego USB" + +#: htdocs/luci-static/resources/view/wwand/modems.js:440 +msgid "Cancel" +msgstr "Anuluj" + +#: htdocs/luci-static/resources/view/wwand/settings.js:104 +msgid "Cancelled." +msgstr "Anulowano." + +#: htdocs/luci-static/resources/view/wwand/status.js:581 +msgid "Capabilities" +msgstr "Możliwości" + +#: htdocs/luci-static/resources/view/wwand/status.js:1133 +msgid "Carrier aggregation" +msgstr "Agregacja nośnych" + +#: htdocs/luci-static/resources/view/wwand/settings.js:534 +msgid "Carrier configuration" +msgstr "Konfiguracja operatora" + +#: htdocs/luci-static/resources/view/wwand/settings.js:523 +msgid "Carrier configuration selected. It takes effect after a modem reset." +msgstr "Wybrano konfigurację operatora. Zacznie obowiązywać po resecie modemu." + +#: htdocs/luci-static/resources/view/wwand/settings.js:850 +#: htdocs/luci-static/resources/view/wwand/settings.js:911 +msgid "Cell lock" +msgstr "Blokada komórki" + +#: htdocs/luci-static/resources/view/wwand/settings.js:898 +msgid "Cell lock saved. Reconnect the interface to apply." +msgstr "" +"Zapisano blokadę komórki. Połącz interfejs ponownie, aby ją zastosować." + +#: htdocs/luci-static/resources/view/wwand/status.js:879 +msgid "" +"Cell/frequency locks the modem currently has armed — the read-back of the " +"cell-lock editor, showing what the modem ACTUALLY locked" +msgstr "" +"Blokady komórki/częstotliwości aktualnie uzbrojone w modemie — odczyt " +"zwrotny edytora blokady komórki, pokazujący, co modem FAKTYCZNIE zablokował" + +#: htdocs/luci-static/resources/statistics/rrdtool/definitions/wwand.js:32 +msgid "Cellular modem" +msgstr "Modem komórkowy" + +#: htdocs/luci-static/resources/view/wwand/modems.js:120 +msgid "" +"Cellular modems managed by wwand. Each modem is referenced from an interface " +"(Network → Interfaces, protocol \"Cellular / 5G\") by its name; several " +"interfaces can share one modem through different mux channels. \"Config\" " +"edits the modem, \"Tools\" opens band/operator/SIM/eSIM/SMS tools for it." +msgstr "" +"Modemy komórkowe zarządzane przez wwand. Każdy modem jest wskazywany z " +"interfejsu (Sieć → Interfejsy, protokół „Cellular / 5G”) przez swoją nazwę; " +"kilka interfejsów może współdzielić jeden modem przez różne kanały mux. " +"„Konfiguracja” edytuje modem, „Narzędzia” otwiera narzędzia pasma/operatora/" +"SIM/eSIM/SMS dla niego." + +#: htdocs/luci-static/resources/view/wwand/status.js:50 +msgid "Channel bandwidth of the carrier" +msgstr "Szerokość kanału nośnej" + +#: htdocs/luci-static/resources/view/wwand/settings.js:965 +msgid "Choose a storage and click Load." +msgstr "Wybierz pamięć i kliknij Wczytaj." + +#: htdocs/luci-static/resources/wwand/esim.js:98 +msgid "Class" +msgstr "Klasa" + +#: htdocs/luci-static/resources/view/wwand/settings.js:947 +msgid "Clear lock" +msgstr "Usuń blokadę" + +#: htdocs/luci-static/resources/view/wwand/modems.js:241 +msgid "Config" +msgstr "Konfiguracja" + +#: htdocs/luci-static/resources/view/wwand/settings.js:540 +msgid "Configuration" +msgstr "Konfiguracja" + +#: htdocs/luci-static/resources/wwand/format.js:1084 +msgid "Configuration warnings" +msgstr "Ostrzeżenia konfiguracji" + +#: htdocs/luci-static/resources/view/wwand/modems.js:720 +#: htdocs/luci-static/resources/view/wwand/modems.js:724 +msgid "Configure" +msgstr "Konfiguruj" + +#: htdocs/luci-static/resources/view/wwand/settings.js:771 +msgid "Configured list restored to the modem." +msgstr "Skonfigurowana lista została przywrócona w modemie." + +#: htdocs/luci-static/resources/wwand/esim.js:373 +msgid "Confirm install to operator" +msgstr "Potwierdź instalację u operatora" + +#: htdocs/luci-static/resources/wwand/esim.js:474 +msgid "" +"Confirm the install to the operator automatically (recommended; uncheck only " +"for testing)" +msgstr "" +"Potwierdź instalację u operatora automatycznie (zalecane; odznacz tylko do " +"testów)" + +#: htdocs/luci-static/resources/view/wwand/modems.js:202 +msgid "Connections" +msgstr "Połączenia" + +#: htdocs/luci-static/resources/wwand/esim.js:28 +msgid "Contacting SM-DP+" +msgstr "Łączenie z SM-DP+" + +#: htdocs/luci-static/resources/wwand/esim.js:384 +msgid "Contacting operator…" +msgstr "Łączenie z operatorem…" + +#: htdocs/luci-static/resources/view/wwand/modems.js:672 +msgid "" +"Control devices present on the system without a modem configuration. " +"Configure stages a prefilled modem section — press Save & Apply afterwards, " +"then attach an interface to it (or let autosetup handle it on an " +"unconfigured system)." +msgstr "" +"Urządzenia sterujące obecne w systemie bez konfiguracji modemu. „Konfiguruj” " +"przygotowuje wstępnie wypełnioną sekcję modemu — następnie naciśnij Zapisz i " +"zastosuj, po czym przypisz do niej interfejs (lub pozwól, aby zajął się tym " +"autosetup w systemie bez konfiguracji)." + +#: htdocs/luci-static/resources/wwand/modemopts.js:210 +msgid "Control protocol" +msgstr "Protokół sterowania" + +#: htdocs/luci-static/resources/view/wwand/status.js:558 +msgid "" +"Control protocol wwand uses to drive this modem: QMI (Qualcomm native), MBIM " +"(the USB standard) or NCM (AT commands with an ethernet-style data port)" +msgstr "" +"Protokół sterowania używany przez wwand do obsługi tego modemu: QMI (natywny " +"Qualcomm), MBIM (standard USB) lub NCM (polecenia AT z portem danych typu " +"Ethernet)" + +#: htdocs/luci-static/resources/view/wwand/modems.js:722 +msgid "Could not configure modem: %s" +msgstr "Nie można skonfigurować modemu: %s" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1130 +msgid "Could not read messages: %s" +msgstr "Nie można odczytać wiadomości: %s" + +#: htdocs/luci-static/resources/view/wwand/settings.js:656 +msgid "Could not read the %s list: %s" +msgstr "Nie można odczytać listy %s: %s" + +#: htdocs/luci-static/resources/wwand/logbox.js:270 +msgid "Could not read the system log: %s" +msgstr "Nie można odczytać dziennika systemowego: %s" + +#: htdocs/luci-static/resources/wwand/esim.js:467 +msgid "Could not start:" +msgstr "Nie można uruchomić:" + +#: htdocs/luci-static/resources/view/wwand/status.js:259 +msgid "Course" +msgstr "Kurs" + +#: htdocs/luci-static/resources/view/wwand/sims.js:70 +msgid "Create" +msgstr "Utwórz" + +#: htdocs/luci-static/resources/view/wwand/modems.js:389 +msgid "" +"Create a per-ICCID SIM override for the inserted card (ICCID %s), pre-filled " +"with the modem's current PIN and the interface APN/auth. Edit it in the SIMs " +"list below, then Save & Apply." +msgstr "" +"Utwórz nadpisanie SIM dla wsuniętej karty (ICCID %s), wstępnie wypełnione " +"bieżącym PIN-em modemu oraz APN/uwierzytelnianiem interfejsu. Edytuj je na " +"liście SIM poniżej, a następnie Zapisz i zastosuj." + +#: htdocs/luci-static/resources/view/wwand/sims.js:66 +msgid "Create settings for this SIM card" +msgstr "Utwórz ustawienia dla tej karty SIM" + +#: htdocs/luci-static/resources/view/wwand/modems.js:402 +msgid "" +"Created SIM override for ICCID %s — review it in the SIMs list below, then " +"Save & Apply." +msgstr "" +"Utworzono nadpisanie SIM dla ICCID %s — sprawdź je na liście SIM poniżej, a " +"następnie Zapisz i zastosuj." + +#: htdocs/luci-static/resources/wwand/netsel.js:32 +msgid "Current mode" +msgstr "Bieżący tryb" + +#: htdocs/luci-static/resources/wwand/logbox.js:363 +msgid "Daemon writes at" +msgstr "Demon zapisuje na poziomie" + +#: htdocs/luci-static/resources/view/wwand/status.js:130 +msgid "Data" +msgstr "Dane" + +#: htdocs/luci-static/resources/view/wwand/modems.js:164 +msgid "Data endpoint interface" +msgstr "Interfejs punktu końcowego danych" + +#: htdocs/luci-static/resources/view/wwand/modems.js:156 +msgid "Data endpoint type" +msgstr "Typ punktu końcowego danych" + +#: htdocs/luci-static/resources/wwand/modemopts.js:348 +msgid "Data multiplexing" +msgstr "Multipleksowanie danych" + +#: htdocs/luci-static/resources/view/wwand/status.js:439 +msgid "Datapath & muxing" +msgstr "Ścieżka danych i multipleksowanie" + +#: htdocs/luci-static/resources/view/wwand/status.js:434 +msgid "Datapath counters (parent)" +msgstr "Liczniki ścieżki danych (nadrzędne)" + +#: htdocs/luci-static/resources/wwand/modemopts.js:701 +msgid "Dead-bearer poll count" +msgstr "Liczba odpytań martwego nośnika" + +#: htdocs/luci-static/resources/wwand/modemopts.js:524 +msgid "" +"Default SIM PIN for this modem, entered on each start. Empty = unlocked SIM. " +"Per-card overrides live in the SIMs list." +msgstr "" +"Domyślny PIN karty SIM dla tego modemu, wprowadzany przy każdym " +"uruchomieniu. Puste = karta odblokowana. Nadpisania dla poszczególnych kart " +"znajdują się na liście SIM." + +#: htdocs/luci-static/resources/wwand/esim.js:311 +msgid "Delete" +msgstr "Usuń" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1098 +msgid "Delete %d selected" +msgstr "Usuń zaznaczone (%d)" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1067 +msgid "Delete %d selected message(s)?" +msgstr "Usunąć zaznaczone wiadomości (%d)?" + +#: htdocs/luci-static/resources/view/wwand/modems.js:643 +msgid "Delete modem \"%s\"?" +msgstr "Usunąć modem „%s”?" + +#: htdocs/luci-static/resources/view/wwand/modems.js:641 +msgid "" +"Delete modem \"%s\"? It is still used by: %s. Those interfaces lose their " +"modem and their connection when you Save & Apply." +msgstr "" +"Usunąć modem „%s”? Jest on nadal używany przez: %s. Te interfejsy utracą " +"modem i połączenie po Zapisz i zastosuj." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1087 +#: htdocs/luci-static/resources/view/wwand/settings.js:1099 +msgid "Delete selected" +msgstr "Usuń zaznaczone" + +#: htdocs/luci-static/resources/wwand/simlist.js:118 +msgid "Delete the override for SIM %s? Its PIN and APN settings are lost." +msgstr "" +"Usunąć nadpisanie dla karty SIM %s? Jej ustawienia PIN i APN zostaną " +"utracone." + +#: htdocs/luci-static/resources/view/wwand/settings.js:755 +msgid "Delete the saved list \"%s\"?" +msgstr "Usunąć zapisaną listę „%s”?" + +#: htdocs/luci-static/resources/wwand/simlist.js:119 +msgid "Delete this SIM override? Its PIN and APN settings are lost." +msgstr "Usunąć to nadpisanie SIM? Jego ustawienia PIN i APN zostaną utracone." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1081 +msgid "" +"Deleted %d of %d storage slots; %d could not be deleted. A message spanning " +"several slots may be partly left behind." +msgstr "" +"Usunięto %d z %d slotów pamięci; nie udało się usunąć %d. Wiadomość " +"zajmująca kilka slotów może zostać częściowo pozostawiona." + +#: htdocs/luci-static/resources/view/wwand/modems.js:494 +#: htdocs/luci-static/resources/view/wwand/modems.js:529 +msgid "Detach" +msgstr "Odłącz" + +#: htdocs/luci-static/resources/view/wwand/modems.js:491 +msgid "Detach failed: %s." +msgstr "Odłączenie nie powiodło się: %s." + +#: htdocs/luci-static/resources/view/wwand/modems.js:484 +msgid "" +"Detach modem \"%s\" from the network? Its connections stop and stay down " +"until you attach it again." +msgstr "" +"Odłączyć modem „%s” od sieci? Jego połączenia zostaną zatrzymane i pozostaną " +"wyłączone do ponownego dołączenia." + +#: htdocs/luci-static/resources/view/wwand/modems.js:671 +msgid "Detected modems (unconfigured)" +msgstr "Wykryte modemy (nieskonfigurowane)" + +#: htdocs/luci-static/resources/view/wwand/modems.js:191 +#: htdocs/luci-static/resources/view/wwand/modems.js:676 +#: htdocs/luci-static/resources/view/wwand/modems.js:791 +msgid "Device" +msgstr "Urządzenie" + +#: htdocs/luci-static/resources/wwand/modemopts.js:125 +msgid "Device path binding" +msgstr "Powiązanie ze ścieżką urządzenia" + +#: htdocs/luci-static/resources/view/wwand/status.js:304 +msgid "Dilution of precision" +msgstr "Rozmycie precyzji (DOP)" + +#: htdocs/luci-static/resources/wwand/esim.js:305 +msgid "Disable" +msgstr "Wyłącz" + +#: htdocs/luci-static/resources/wwand/esim.js:225 +msgid "Disable PIN" +msgstr "Wyłącz PIN" + +#: htdocs/luci-static/resources/wwand/esim.js:302 +msgid "Disable the active profile %s?" +msgstr "Wyłączyć aktywny profil %s?" + +#: htdocs/luci-static/resources/view/wwand/settings.js:71 +msgid "" +"Disable the currently enabled (test) profile first, then enable this profile." +msgstr "" +"Najpierw wyłącz aktualnie włączony profil (testowy), a następnie włącz ten " +"profil." + +#: htdocs/luci-static/resources/wwand/esim.js:386 +msgid "Done." +msgstr "Gotowe." + +#: htdocs/luci-static/resources/view/wwand/status.js:373 +msgid "Downlink NTB (aggregation buffer)" +msgstr "NTB łącza w dół (bufor agregacji)" + +#: htdocs/luci-static/resources/view/wwand/status.js:361 +msgid "Downlink aggregation (negotiated)" +msgstr "Agregacja łącza w dół (wynegocjowana)" + +#: htdocs/luci-static/resources/view/wwand/status.js:49 +msgid "Downlink centre frequency derived from the channel number" +msgstr "Częstotliwość środkowa łącza w dół wyznaczona z numeru kanału" + +#: htdocs/luci-static/resources/view/wwand/status.js:977 +msgid "Downlink centre frequency of the serving cell · channel bandwidth" +msgstr "" +"Częstotliwość środkowa łącza w dół komórki obsługującej · szerokość kanału" + +#: htdocs/luci-static/resources/view/wwand/status.js:421 +msgid "Downlink packets / frame" +msgstr "Pakiety łącza w dół / ramkę" + +#: htdocs/luci-static/resources/wwand/esim.js:471 +msgid "Download" +msgstr "Pobierz" + +#: htdocs/luci-static/resources/wwand/esim.js:455 +msgid "Download profile" +msgstr "Pobierz profil" + +#: htdocs/luci-static/resources/wwand/esim.js:384 +msgid "Downloading profile…" +msgstr "Pobieranie profilu…" + +#: htdocs/luci-static/resources/view/wwand/status.js:48 +msgid "" +"E-UTRA Absolute Radio Frequency Channel Number — the LTE (or, in the 5G " +"tables, NR-ARFCN) channel number of the carrier" +msgstr "" +"Numer bezwzględnego kanału radiowego E-UTRA (EARFCN) — numer kanału nośnej " +"LTE (lub, w tabelach 5G, NR-ARFCN)" + +#: htdocs/luci-static/resources/view/wwand/status.js:980 +msgid "" +"EARFCN = LTE channel number of the carrier; PCI = Physical Cell ID. " +"EARFCN:PCI identifies the exact cell (usable for the cell lock)" +msgstr "" +"EARFCN = numer kanału nośnej LTE; PCI = fizyczny identyfikator komórki. " +"EARFCN:PCI jednoznacznie wskazuje komórkę (użyteczne do blokady komórki)" + +#: htdocs/luci-static/resources/view/wwand/status.js:54 +msgid "EARFCN:PCI value to copy into the cell-lock field of the modem settings" +msgstr "" +"Wartość EARFCN:PCI do skopiowania do pola blokady komórki w ustawieniach " +"modemu" + +#: htdocs/luci-static/resources/view/wwand/status.js:936 +msgid "EPS attach" +msgstr "Dołączenie EPS" + +#: htdocs/luci-static/resources/view/wwand/settings.js:252 +msgid "Earlier" +msgstr "" + +#: htdocs/luci-static/resources/wwand/graph.js:156 +msgid "Ec/Io 3G" +msgstr "Ec/Io 3G" + +#: htdocs/luci-static/resources/view/wwand/sims.js:70 +msgid "Edit" +msgstr "Edytuj" + +#: htdocs/luci-static/resources/view/wwand/sims.js:66 +msgid "Edit the settings of this SIM card" +msgstr "Edytuj ustawienia tej karty SIM" + +#: htdocs/luci-static/resources/view/wwand/status.js:257 +msgid "Elevation" +msgstr "Wysokość" + +#: htdocs/luci-static/resources/wwand/esim.js:298 +msgid "Enable" +msgstr "Włącz" + +#: htdocs/luci-static/resources/wwand/esim.js:223 +msgid "Enable PIN" +msgstr "Włącz PIN" + +#: htdocs/luci-static/resources/wwand/esim.js:295 +msgid "Enable profile %s? The connection will re-establish." +msgstr "Włączyć profil %s? Połączenie zostanie nawiązane ponownie." + +#: htdocs/luci-static/resources/wwand/esim.js:228 +msgid "" +"Enabling or disabling the PIN query needs the current PIN. Disabling lets " +"the SIM boot without a PIN." +msgstr "" +"Włączenie lub wyłączenie zapytania o PIN wymaga podania bieżącego PIN-u. " +"Wyłączenie pozwala kartom SIM uruchamiać się bez PIN-u." + +#: htdocs/luci-static/resources/view/wwand/modems.js:466 +msgid "Enter PIN" +msgstr "Wprowadź PIN" + +#: htdocs/luci-static/resources/view/wwand/modems.js:437 +msgid "Enter PUK — SIM %s" +msgstr "Wprowadź PUK — SIM %s" + +#: htdocs/luci-static/resources/wwand/esim.js:461 +msgid "Enter an activation code first." +msgstr "Najpierw wprowadź kod aktywacyjny." + +#: htdocs/luci-static/resources/wwand/esim.js:204 +msgid "Enter the current SIM PIN first." +msgstr "Najpierw wprowadź bieżący PIN karty SIM." + +#: htdocs/luci-static/resources/view/wwand/status.js:132 +msgid "Errors / dropped" +msgstr "Błędy / odrzucone" + +#: htdocs/luci-static/resources/view/wwand/sims.js:124 +msgid "" +"Every SIM card wwand has seen and where it is: in a modem slot, as a profile " +"on an eUICC, or in a reader (remote SIM). A card that was taken out stays " +"listed as not present. On a modem with more than one slot, the cards in the " +"inactive slots appear once the slot list has been read." +msgstr "" +"Każda karta SIM, którą wwand widział, i jej położenie: w slocie modemu, jako " +"profil na eUICC lub w czytniku (zdalna SIM). Wyjęta karta pozostaje na " +"liście jako nieobecna. W modemie z więcej niż jednym slotem karty w " +"nieaktywnych slotach pojawiają się po odczytaniu listy slotów." + +#: htdocs/luci-static/resources/wwand/modemopts.js:430 +msgid "Extra AT init commands" +msgstr "Dodatkowe polecenia inicjalizacji AT" + +#: htdocs/luci-static/resources/view/wwand/status.js:874 +msgid "FCC lock" +msgstr "Blokada FCC" + +#: htdocs/luci-static/resources/wwand/modemopts.js:269 +msgid "FCC unlock" +msgstr "Odblokowanie FCC" + +#: htdocs/luci-static/resources/wwand/modemopts.js:651 +msgid "" +"Failed attempts at which the recovery ladder cycles the radio, resets the " +"modem and pulses the reset line / repowers it, e.g. \"2 6 12\". One or two " +"numbers replace only the first steps. Empty = 8 16 24. An earlier radio " +"cycle helps after a registration reject, which the modem may only retry " +"after a switch-off." +msgstr "" +"Liczby nieudanych prób, przy których procedura odzyskiwania wyłącza i włącza " +"radio, resetuje modem i wysyła impuls linii resetu / ponownie zasila modem, " +"np. „2 6 12”. Jedna lub dwie liczby zastępują tylko pierwsze kroki. Puste = " +"8 16 24. Wcześniejsze przełączenie radia pomaga po odrzuceniu rejestracji, " +"którą modem może ponowić dopiero po wyłączeniu." + +#: htdocs/luci-static/resources/view/wwand/settings.js:835 +#: htdocs/luci-static/resources/wwand/esim.js:507 +#: htdocs/luci-static/resources/wwand/netsel.js:51 +msgid "Failed:" +msgstr "Niepowodzenie:" + +#: htdocs/luci-static/resources/wwand/esim.js:32 +msgid "Fetching profile" +msgstr "Odbieranie profilu" + +#: htdocs/luci-static/resources/view/wwand/status.js:576 +msgid "Firmware" +msgstr "Firmware" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1394 +msgid "Firmware protocol mode" +msgstr "Tryb protokołu firmware" + +#: htdocs/luci-static/resources/view/wwand/status.js:576 +msgid "" +"Firmware version reported by the modem — relevant when comparing behaviour " +"or looking for carrier-specific builds" +msgstr "" +"Wersja firmware zgłaszana przez modem — istotna przy porównywaniu zachowania " +"lub poszukiwaniu wersji dla konkretnych operatorów" + +#: htdocs/luci-static/resources/view/wwand/status.js:239 +msgid "Fix" +msgstr "Fix" + +#: htdocs/luci-static/resources/view/wwand/status.js:273 +msgid "Fix age" +msgstr "Wiek fixa" + +#: htdocs/luci-static/resources/wwand/modemopts.js:749 +msgid "Fix only (less CPU)" +msgstr "" + +#: htdocs/luci-static/resources/view/wwand/settings.js:742 +msgid "Forbidden" +msgstr "Zabronione" + +#: htdocs/luci-static/resources/view/wwand/settings.js:599 +msgid "Forbidden list (FPLMN, EF 6F7B)" +msgstr "Lista zabronionych (FPLMN, EF 6F7B)" + +#: htdocs/luci-static/resources/view/wwand/modems.js:367 +msgid "" +"Force a network re-registration (deregister + re-attach) so automatic " +"selection re-scans. Use when the modem stays on a network you manually " +"selected earlier. The connection blips and recovers on its own; not a modem " +"reset." +msgstr "" +"Wymusza ponowną rejestrację w sieci (wyrejestrowanie + ponowne dołączenie), " +"aby automatyczny wybór ponownie przeskanował sieci. Użyj, gdy modem " +"pozostaje w sieci wybranej wcześniej ręcznie. Połączenie na chwilę zanika i " +"wraca samoczynnie; to nie jest reset modemu." + +#: htdocs/luci-static/resources/view/wwand/status.js:49 +#: htdocs/luci-static/resources/view/wwand/status.js:977 +msgid "Frequency" +msgstr "Częstotliwość" + +#: htdocs/luci-static/resources/view/wwand/status.js:1003 +msgid "Frequency range" +msgstr "Zakres częstotliwości" + +#: htdocs/luci-static/resources/view/wwand/status.js:323 +msgid "GNSS" +msgstr "GNSS" + +#: htdocs/luci-static/resources/wwand/modemopts.js:746 +msgid "GNSS NMEA output" +msgstr "" + +#: htdocs/luci-static/resources/wwand/modemopts.js:729 +msgid "GNSS receiver" +msgstr "Odbiornik GNSS" + +#: root/usr/share/rpcd/acl.d/luci-app-wwand.json:3 +msgid "Grant access to the wwand modem status page" +msgstr "Zezwól na dostęp do strony statusu modemu wwand" + +#: htdocs/luci-static/resources/view/wwand/status.js:264 +msgid "" +"Ground speed. NMEA reports knots; both are shown when both are known, so " +"neither unit has to be converted in your head." +msgstr "" +"Prędkość względem ziemi. NMEA podaje węzły; gdy znane są obie jednostki, " +"wyświetlane są obie, więc żadnej nie trzeba przeliczać w pamięci." + +#: htdocs/luci-static/resources/view/wwand/modems.js:349 +msgid "" +"Hard-repower modem \"%s\" now? This cuts and restores the modem's power/" +"reset line." +msgstr "" +"Twardo zrestartować zasilanie modemu „%s” teraz? Spowoduje to odcięcie i " +"przywrócenie linii zasilania/resetu modemu." + +#: htdocs/luci-static/resources/view/wwand/modems.js:346 +msgid "" +"Hardware repower: pulse the reset GPIO (or power-cycle the modem) — recovers " +"a hung or vanished modem. Needs a reset_gpio on multi-modem boxes." +msgstr "" +"Sprzętowe ponowne zasilenie: impuls na GPIO resetu (lub wyłączenie i " +"włączenie zasilania modemu) — przywraca zawieszony lub zniknięty modem. Na " +"płytach z wieloma modemami wymaga reset_gpio." + +#: htdocs/luci-static/resources/view/wwand/status.js:860 +msgid "Hardware step" +msgstr "Krok sprzętowy" + +#: htdocs/luci-static/resources/view/wwand/settings.js:797 +msgid "Home PLMN (6F62)" +msgstr "Macierzysty PLMN (6F62)" + +#: htdocs/luci-static/resources/wwand/modemopts.js:257 +msgid "" +"How SIM Application Toolkit is routed. A router has no screen to display " +"operator menus or texts on, so a modem advertising a phone-like profile can " +"leave the card waiting for a response nothing here will send. \"disabled\" " +"stops routing toolkit to a control point. Unset leaves the modem exactly as " +"the vendor configured it." +msgstr "" +"Sposób kierowania SIM Application Toolkit. Router nie ma ekranu do " +"wyświetlania menu ani tekstów operatora, więc modem deklarujący profil " +"zbliżony do telefonu może zostawić kartę czekającą na odpowiedź, której nic " +"tu nie wyśle. „disabled” wyłącza kierowanie toolkit do punktu sterowania. " +"Nieustawione pozostawia modem dokładnie tak, jak skonfigurował go producent." + +#: htdocs/luci-static/resources/wwand/graph.js:92 +msgid "" +"How far the wanted signal stands above noise and interference — this, not " +"RSRP, is what throughput follows. 0 dB is the edge of usable. LTE and 5G NR " +"share these boundaries in the published tables." +msgstr "" +"O ile pożądany sygnał przewyższa szum i zakłócenia — to, a nie RSRP, " +"determinuje przepustowość. 0 dB to granica użyteczności. LTE i 5G NR mają w " +"opublikowanych tabelach te same progi." + +#: htdocs/luci-static/resources/view/wwand/status.js:304 +msgid "" +"How favourably the satellites in use are spread: horizontal, positional and " +"vertical. Lower is better — under 2 is good, over 5 means the position is " +"only roughly right." +msgstr "" +"Jak korzystnie rozmieszczone są używane satelity: poziomo, pozycyjnie i " +"pionowo. Im niższa wartość, tym lepiej — poniżej 2 jest dobrze, powyżej 5 " +"pozycja jest tylko z grubsza poprawna." + +#: htdocs/luci-static/resources/view/wwand/modems.js:207 +msgid "" +"How many configured interfaces use this modem. Several can share one modem " +"through different mux channels." +msgstr "" +"Ile skonfigurowanych interfejsów używa tego modemu. Kilka może współdzielić " +"jeden modem przez różne kanały mux." + +#: htdocs/luci-static/resources/wwand/graph.js:112 +msgid "" +"How many downlink carriers the modem is aggregating. The lower line is the " +"LTE anchor; the upper one is the total including 5G, so the gap between them " +"is what the 5G leg contributes — under EN-DC both legs serve one link, and " +"stacking them says that better than two separate counts. A secondary carrier " +"that is configured but not activated is not counted: it carries no data. A " +"modem that reports no carrier list at all still shows its serving cell as " +"one carrier. The total line appears only while a 5G carrier is actually " +"serving, not merely visible." +msgstr "" +"Ile nośnych łącza w dół agreguje modem. Dolna linia to kotwica LTE; górna to " +"suma wraz z 5G, więc odstęp między nimi pokazuje wkład gałęzi 5G — w EN-DC " +"obie gałęzie obsługują jedno łącze, a ich nałożenie oddaje to lepiej niż dwa " +"osobne liczniki. Nośna wtórna, która jest skonfigurowana, ale nieaktywowana, " +"nie jest liczona: nie przenosi danych. Modem, który w ogóle nie raportuje " +"listy nośnych, nadal pokazuje swoją komórkę obsługującą jako jedną nośną. " +"Linia sumy pojawia się tylko wtedy, gdy nośna 5G faktycznie obsługuje ruch, " +"a nie tylko jest widoczna." + +#: htdocs/luci-static/resources/wwand/logbox.js:164 +msgid "How many wwand lines to show" +msgstr "Ile wierszy wwand wyświetlać" + +#: htdocs/luci-static/resources/view/wwand/status.js:869 +msgid "" +"How often the kernel found the modem's transmit queue stuck (its \"NETDEV " +"WATCHDOG\" message), counted since wwand started watching this modem. A " +"stall that lasts the TX stall timeout while a connection is up resets the " +"modem." +msgstr "" +"Ile razy jądro stwierdziło zablokowanie kolejki nadawczej modemu (komunikat " +"„NETDEV WATCHDOG”), liczone od chwili, gdy wwand zaczął obserwować ten " +"modem. Zastój trwający tyle, ile wynosi limit zastoju TX, przy aktywnym " +"połączeniu powoduje reset modemu." + +#: htdocs/luci-static/resources/view/wwand/sims.js:90 +#: htdocs/luci-static/resources/wwand/simlist.js:132 +msgid "ICCID" +msgstr "ICCID" + +#: htdocs/luci-static/resources/view/wwand/sims.js:93 +msgid "IMSI" +msgstr "IMSI" + +#: htdocs/luci-static/resources/view/wwand/status.js:123 +msgid "IP" +msgstr "IP" + +#: htdocs/luci-static/resources/wwand/simlist.js:217 +msgid "" +"IP version(s) requested for this card's data session, overriding the " +"interface setting." +msgstr "" +"Wersje IP żądane dla sesji danych tej karty, nadpisujące ustawienie " +"interfejsu." + +#: htdocs/luci-static/resources/wwand/format.js:500 +msgid "IPA" +msgstr "IPA" + +#: htdocs/luci-static/resources/view/wwand/status.js:103 +#: htdocs/luci-static/resources/wwand/simlist.js:221 +msgid "IPv4" +msgstr "IPv4" + +#: htdocs/luci-static/resources/wwand/simlist.js:220 +msgid "IPv4 + IPv6" +msgstr "IPv4 + IPv6" + +#: htdocs/luci-static/resources/view/wwand/status.js:105 +msgid "IPv4 DNS" +msgstr "DNS IPv4" + +#: htdocs/luci-static/resources/view/wwand/status.js:104 +msgid "IPv4 gateway" +msgstr "Brama IPv4" + +#: htdocs/luci-static/resources/view/wwand/status.js:111 +#: htdocs/luci-static/resources/view/wwand/status.js:117 +#: htdocs/luci-static/resources/wwand/simlist.js:222 +msgid "IPv6" +msgstr "IPv6" + +#: htdocs/luci-static/resources/view/wwand/status.js:114 +#: htdocs/luci-static/resources/view/wwand/status.js:119 +msgid "IPv6 DNS" +msgstr "DNS IPv6" + +#: htdocs/luci-static/resources/view/wwand/status.js:118 +msgid "IPv6 gateway" +msgstr "Brama IPv6" + +#: htdocs/luci-static/resources/view/wwand/settings.js:542 +msgid "Id" +msgstr "Id" + +#: htdocs/luci-static/resources/wwand/modemopts.js:665 +msgid "" +"If the modem has not answered since boot or since the outage began, pulse " +"its reset GPIO once after this many seconds. Only a modem with its own reset " +"GPIO set is touched, never another modem on the router. 0 = never." +msgstr "" +"Jeśli modem nie odpowiedział od startu lub od początku awarii, wyślij " +"jednorazowy impuls na jego GPIO resetu po tylu sekundach. Dotyczy tylko " +"modemu z ustawionym własnym GPIO resetu, nigdy innego modemu w routerze. 0 = " +"nigdy." + +#: htdocs/luci-static/resources/view/wwand/status.js:292 +msgid "" +"In use / in view. Nothing in view is usually the antenna, though a cold " +"receiver or a blocked sky looks the same; many in view with none in use " +"means it can hear them but not yet solve a position, which outdoors " +"typically takes a minute or two." +msgstr "" +"Używane / widoczne. Brak widocznych zwykle oznacza antenę, choć zimny " +"odbiornik lub zasłonięte niebo wyglądają tak samo; wiele widocznych bez " +"żadnego używanego oznacza, że odbiornik je słyszy, ale jeszcze nie wyznaczył " +"pozycji, co na zewnątrz zwykle trwa minutę lub dwie." + +#: htdocs/luci-static/resources/view/wwand/settings.js:108 +msgid "Incomplete request — a required value was missing." +msgstr "Niekompletne żądanie — brakowało wymaganej wartości." + +#: htdocs/luci-static/resources/view/wwand/status.js:1102 +msgid "" +"Inferred from how many logical slots are in use. That is a lower bound: a " +"modem with a second radio stack whose other slot is empty looks exactly like " +"a single-stack one, so no definite mode can be stated." +msgstr "" +"Wnioskowane na podstawie liczby używanych slotów logicznych. To dolne " +"ograniczenie: modem z drugim stosem radiowym, którego drugi slot jest pusty, " +"wygląda dokładnie jak modem z jednym stosem, więc nie można podać " +"jednoznacznego trybu." + +#: htdocs/luci-static/resources/wwand/esim.js:33 +msgid "Installing profile" +msgstr "Instalowanie profilu" + +#: htdocs/luci-static/resources/view/wwand/modems.js:789 +#: htdocs/luci-static/resources/view/wwand/status.js:95 +msgid "Interface" +msgstr "Interfejs" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1280 +msgid "Interfaces" +msgstr "Interfejsy" + +#: htdocs/luci-static/resources/view/wwand/modems.js:785 +msgid "" +"Interfaces still using a stock cellular protocol (qmi / mbim / ncm / " +"modemmanager) that wwand does not manage yet. Select the ones to migrate and " +"press \"Migrate selected\": each is converted in place to proto wwand (its " +"name, firewall zone and IP settings are kept) and a wwand modem section is " +"created and linked. When migrating a ModemManager interface, stop and remove " +"the ModemManager service afterwards — it would otherwise keep claiming the " +"modem's control port." +msgstr "" +"Interfejsy nadal używające standardowego protokołu komórkowego (qmi / mbim / " +"ncm / modemmanager), którymi wwand jeszcze nie zarządza. Zaznacz te do " +"migracji i naciśnij „Migruj zaznaczone”: każdy zostanie przekonwertowany w " +"miejscu na proto wwand (jego nazwa, strefa zapory i ustawienia IP zostają " +"zachowane), a sekcja modemu wwand zostanie utworzona i powiązana. Przy " +"migracji interfejsu ModemManager zatrzymaj, a następnie usuń usługę " +"ModemManager — w przeciwnym razie nadal będzie zajmować port sterujący " +"modemu." + +#: htdocs/luci-static/resources/view/wwand/status.js:710 +msgid "International Mobile Equipment Identity — the modem hardware serial" +msgstr "International Mobile Equipment Identity — numer seryjny sprzętu modemu" + +#: htdocs/luci-static/resources/wwand/modemopts.js:557 +msgid "Keep the modem's order" +msgstr "" + +#: htdocs/luci-static/resources/wwand/modemopts.js:349 +msgid "" +"Kernel datapath that carries this modem's data sessions. Leave on automatic " +"unless a modem misbehaves — under automatic an installed vendor datapath " +"that recognises this hardware is used on its own. Naming one pins it. Only " +"datapaths this modem's control protocol can use are offered. What automatic " +"settled on is shown as configured → actual under Datapath on the " +"status page: a single interface on an MBIM modem lands on untagged, " +"where the session rides the parent device with no 802.1q tag on every frame, " +"and a QMI modem that turns out not to carry QMAP lands on raw_ip." +msgstr "" +"Ścieżka danych jądra, która przenosi sesje danych tego modemu. Pozostaw " +"automatyczną, chyba że modem zachowuje się nieprawidłowo — w trybie " +"automatycznym używana jest zainstalowana ścieżka producenta, która " +"rozpoznaje ten sprzęt. Podanie nazwy przypina wybraną ścieżkę. Oferowane są " +"tylko ścieżki, których może użyć protokół sterowania tego modemu. To, na co " +"zdecydował się tryb automatyczny, jest pokazane jako skonfigurowana → " +"rzeczywista w polu Ścieżka danych na stronie statusu: pojedynczy " +"interfejs w modemie MBIM kończy na untagged, gdzie sesja idzie " +"przez urządzenie nadrzędne bez znacznika 802.1q na każdej ramce, a modem " +"QMI, który okazuje się nie obsługiwać QMAP, kończy na raw_ip." + +#: htdocs/luci-static/resources/wwand/graph.js:164 +msgid "LTE" +msgstr "LTE" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1303 +msgid "LTE bands" +msgstr "Pasma LTE" + +#: htdocs/luci-static/resources/wwand/graph.js:162 +msgid "LTE carriers" +msgstr "Nośne LTE" + +#: htdocs/luci-static/resources/view/wwand/settings.js:912 +#: htdocs/luci-static/resources/wwand/modemopts.js:607 +msgid "LTE cell lock" +msgstr "Blokada komórki LTE" + +#: htdocs/luci-static/resources/view/wwand/status.js:1204 +msgid "LTE neighbour cells (inter-frequency)" +msgstr "Komórki sąsiednie LTE (międzyczęstotliwościowe)" + +#: htdocs/luci-static/resources/view/wwand/status.js:1168 +msgid "LTE neighbour cells (intra-frequency)" +msgstr "Komórki sąsiednie LTE (wewnątrzczęstotliwościowe)" + +#: htdocs/luci-static/resources/view/wwand/status.js:144 +msgid "Last error" +msgstr "Ostatni błąd" + +#: htdocs/luci-static/resources/view/wwand/settings.js:257 +msgid "Later" +msgstr "" + +#: htdocs/luci-static/resources/view/wwand/status.js:243 +msgid "Latitude and longitude as the receiver last reported them." +msgstr "Szerokość i długość geograficzna według ostatniego raportu odbiornika." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1312 +msgid "" +"Leave every band unchecked (and the fallback empty) to let the modem use all " +"supported bands." +msgstr "" +"Pozostaw wszystkie pasma niezaznaczone (a zapasową listę pustą), aby modem " +"używał wszystkich obsługiwanych pasm." + +#: htdocs/luci-static/resources/wwand/modemopts.js:178 +#: htdocs/luci-static/resources/wwand/modemopts.js:748 +msgid "Leave modem setting unchanged" +msgstr "Pozostaw ustawienie modemu bez zmian" + +#: htdocs/luci-static/resources/wwand/modemopts.js:756 +msgid "" +"Let wwand reprogram mismatched modem-side settings (attach profile, etc.) " +"when it detects them." +msgstr "" +"Pozwól wwand przeprogramować niezgodne ustawienia po stronie modemu (profil " +"dołączania itp.), gdy je wykryje." + +#: htdocs/luci-static/resources/wwand/modemopts.js:735 +msgid "" +"Let wwand step the system clock from the GNSS time — only while the clock is " +"plainly unset, so it never fights the NTP client. Off by default. For " +"installs with no RTC where the modem is the only time source." +msgstr "" +"Pozwól wwand skokowo ustawić zegar systemowy według czasu GNSS — tylko gdy " +"zegar jest ewidentnie nieustawiony, aby nigdy nie walczyć z klientem NTP. " +"Domyślnie wyłączone. Dla instalacji bez RTC, w których modem jest jedynym " +"źródłem czasu." + +#: htdocs/luci-static/resources/wwand/esim.js:498 +msgid "List pending" +msgstr "Wyświetl oczekujące" + +#: htdocs/luci-static/resources/wwand/esim.js:487 +msgid "Listing pending notifications…" +msgstr "Pobieranie listy oczekujących powiadomień…" + +#: htdocs/luci-static/resources/view/wwand/status.js:1431 +msgid "" +"Live cellular signal and cell environment — updates about once per second. " +"Aim the antenna for the highest RSRP / SINR: the graphs keep the last few " +"minutes in the browser, so you can see what turning it did. The history is " +"not stored on the router and starts empty after a reload." +msgstr "" +"Sygnał komórkowy i otoczenie komórek na żywo — odświeżane około raz na " +"sekundę. Ustaw antenę na najwyższy RSRP / SINR: wykresy zachowują w " +"przeglądarce ostatnie kilka minut, więc widać, co dało obrócenie anteny. " +"Historia nie jest przechowywana na routerze i po przeładowaniu zaczyna się " +"od zera." + +#: htdocs/luci-static/resources/view/wwand/modems.js:609 +msgid "Live status page (signal, cells, connection) for this modem" +msgstr "Strona statusu na żywo (sygnał, komórki, połączenie) tego modemu" + +#: htdocs/luci-static/resources/view/wwand/settings.js:744 +#: htdocs/luci-static/resources/view/wwand/settings.js:1145 +msgid "Load" +msgstr "Wczytaj" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1124 +msgid "Loading…" +msgstr "Wczytywanie…" + +#: htdocs/luci-static/resources/view/wwand/status.js:54 +msgid "Lock" +msgstr "Blokada" + +#: htdocs/luci-static/resources/wwand/modemopts.js:608 +msgid "" +"Lock to LTE cells, one \"earfcn:pci\" per entry (several = a cell list)." +msgstr "" +"Blokada na komórki LTE, po jednym „earfcn:pci” na wpis (kilka = lista " +"komórek)." + +#: htdocs/luci-static/resources/wwand/modemopts.js:619 +msgid "" +"Lock to a 5G SA cell: \"pci:arfcn:scs:band\" — the subcarrier spacing is in " +"kHz (15, 30, 60 …). The \"Lock this 5G cell\" button fills in 30 as a " +"default; the modem does not report the actual spacing, so check it against " +"your band before saving." +msgstr "" +"Blokada na komórkę 5G SA: „pci:arfcn:scs:band” — odstęp podnośnych w kHz " +"(15, 30, 60 …). Przycisk „Lock this 5G cell” wstawia domyślnie 30; modem nie " +"raportuje rzeczywistego odstępu, więc sprawdź go dla swojego pasma przed " +"zapisem." + +#: htdocs/luci-static/resources/view/wwand/status.js:879 +msgid "Locked to" +msgstr "Zablokowano na" + +#: htdocs/luci-static/resources/wwand/esim.js:416 +msgid "Lost contact with the download — reload to see its result." +msgstr "Utracono kontakt z pobieraniem — przeładuj stronę, aby zobaczyć wynik." + +#: htdocs/luci-static/resources/wwand/esim.js:429 +msgid "Lost contact with the download: %s" +msgstr "Utracono kontakt z pobieraniem: %s" + +#: htdocs/luci-static/resources/wwand/modemopts.js:275 +msgid "MBIM Quectel radio state" +msgstr "Stan radia MBIM Quectel" + +#: htdocs/luci-static/resources/view/wwand/status.js:667 +msgid "MBIM, over the QMI passthrough" +msgstr "MBIM, przez przekazywanie QMI" + +#: htdocs/luci-static/resources/wwand/modemopts.js:581 +msgid "MCC" +msgstr "MCC" + +#: htdocs/luci-static/resources/wwand/modemopts.js:586 +msgid "MNC" +msgstr "MNC" + +#: htdocs/luci-static/resources/view/wwand/status.js:124 +msgid "MTU" +msgstr "MTU" + +#: htdocs/luci-static/resources/wwand/esim.js:276 +msgid "Managed card" +msgstr "Karta zarządzana" + +#: htdocs/luci-static/resources/wwand/netsel.js:111 +msgid "Manual network selection applied." +msgstr "Zastosowano ręczny wybór sieci." + +#: htdocs/luci-static/resources/view/wwand/status.js:573 +msgid "Manufacturer" +msgstr "Producent" + +#: htdocs/luci-static/resources/wwand/simlist.js:104 +msgid "" +"Match a specific card by its ICCID and give it a PIN — and optionally its " +"own APN/auth. Applies to whichever modem holds that card (leave Modem " +"empty). Handy for dual-SIM or eUICC profiles with different PINs." +msgstr "" +"Dopasuj konkretną kartę według ICCID i nadaj jej PIN — oraz opcjonalnie " +"własny APN/uwierzytelnianie. Dotyczy modemu, w którym znajduje się ta karta " +"(pole Modem pozostaw puste). Przydatne przy dwóch kartach SIM lub profilach " +"eUICC z różnymi PIN-ami." + +#: htdocs/luci-static/resources/wwand/modemopts.js:425 +msgid "" +"Max downlink QMAP aggregation datagram in bytes (0 = board/model default)." +msgstr "" +"Maksymalny datagram agregacji QMAP łącza w dół w bajtach (0 = domyślny dla " +"płyty/modelu)." + +#: htdocs/luci-static/resources/view/wwand/status.js:138 +msgid "Max rate" +msgstr "Maks. prędkość" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1111 +msgid "Message" +msgstr "Wiadomość" + +#: htdocs/luci-static/resources/view/wwand/modems.js:784 +msgid "Migratable interfaces" +msgstr "Interfejsy do migracji" + +#: htdocs/luci-static/resources/view/wwand/modems.js:769 +msgid "" +"Migrate %d interface(s) to proto wwand? Each is converted in place — its " +"name, firewall zone and IP settings are kept — and wwand takes over managing " +"it." +msgstr "" +"Zmigrować interfejsy (%d) do proto wwand? Każdy jest konwertowany w miejscu " +"— jego nazwa, strefa zapory i ustawienia IP zostają zachowane — a " +"zarządzanie nim przejmuje wwand." + +#: htdocs/luci-static/resources/view/wwand/modems.js:781 +msgid "Migrate selected" +msgstr "Migruj zaznaczone" + +#: htdocs/luci-static/resources/view/wwand/modems.js:774 +msgid "Migrated %d interface(s); reloading…" +msgstr "Zmigrowano interfejsy (%d); przeładowywanie…" + +#: htdocs/luci-static/resources/view/wwand/modems.js:778 +msgid "Migration failed: %s" +msgstr "Migracja nie powiodła się: %s" + +#: htdocs/luci-static/resources/wwand/logbox.js:166 +msgid "Minimum severity" +msgstr "Minimalna ważność" + +#: htdocs/luci-static/resources/wwand/modemopts.js:582 +msgid "Mobile Country Code for manual network selection." +msgstr "Mobile Country Code (kod kraju) do ręcznego wyboru sieci." + +#: htdocs/luci-static/resources/view/wwand/modems.js:119 +msgid "Mobile Modems" +msgstr "Modemy komórkowe" + +#: htdocs/luci-static/resources/wwand/modemopts.js:587 +msgid "Mobile Network Code (requires MCC)." +msgstr "Mobile Network Code (kod sieci; wymaga MCC)." + +#: htdocs/luci-static/resources/view/wwand/status.js:558 +msgid "Mode" +msgstr "Tryb" + +#: htdocs/luci-static/resources/view/wwand/status.js:574 +msgid "Model" +msgstr "Model" + +#: htdocs/luci-static/resources/view/wwand/modems.js:128 +#: htdocs/luci-static/resources/view/wwand/status.js:882 +#: htdocs/luci-static/resources/view/wwand/status.js:1365 +#: htdocs/luci-static/resources/wwand/simlist.js:190 +#: root/usr/share/luci/menu.d/luci-app-wwand.json:3 +msgid "Modem" +msgstr "Modem" + +#: htdocs/luci-static/resources/view/wwand/modems.js:130 +msgid "Modem & SIM" +msgstr "Modem i SIM" + +#: htdocs/luci-static/resources/view/wwand/status.js:1429 +msgid "Modem Status" +msgstr "Status modemu" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1247 +#: root/usr/share/luci/menu.d/luci-app-wwand.json:52 +msgid "Modem Tools" +msgstr "Narzędzia modemu" + +#: htdocs/luci-static/resources/view/wwand/modems.js:489 +msgid "Modem attached (%s)." +msgstr "Modem dołączony (%s)." + +#: htdocs/luci-static/resources/view/wwand/status.js:589 +msgid "" +"Modem baseband temperature. Whether the module is actually throttling is " +"reported separately below — the modem itself says so, no guessing from the " +"number needed." +msgstr "" +"Temperatura pasma podstawowego modemu. Czy moduł faktycznie ogranicza " +"wydajność, raportowane jest osobno poniżej — mówi o tym sam modem, nie " +"trzeba zgadywać na podstawie liczby." + +#: htdocs/luci-static/resources/view/wwand/settings.js:937 +msgid "Modem currently locked: %s — the live read-back of the lock editor." +msgstr "" +"Modem aktualnie zablokowany na: %s — odczyt zwrotny na żywo edytora blokady." + +#: htdocs/luci-static/resources/wwand/modemopts.js:535 +msgid "Modem default" +msgstr "Domyślne modemu" + +#: htdocs/luci-static/resources/view/wwand/modems.js:489 +msgid "Modem detached (%s)." +msgstr "Modem odłączony (%s)." + +#: htdocs/luci-static/resources/view/wwand/modems.js:137 +msgid "Modem device" +msgstr "Urządzenie modemu" + +#: htdocs/luci-static/resources/wwand/modemopts.js:635 +msgid "Modem init delay" +msgstr "Opóźnienie inicjalizacji modemu" + +#: htdocs/luci-static/resources/view/wwand/settings.js:50 +msgid "" +"Modem is restarting — the connection resumes automatically once it re-" +"registers." +msgstr "" +"Modem się restartuje — połączenie zostanie wznowione automatycznie po " +"ponownej rejestracji." + +#: htdocs/luci-static/resources/view/wwand/settings.js:961 +msgid "Modem memory" +msgstr "Pamięć modemu" + +#: htdocs/luci-static/resources/view/wwand/modems.js:353 +msgid "Modem repowered (%s)." +msgstr "Zasilanie modemu zrestartowane (%s)." + +#: htdocs/luci-static/resources/wwand/modemopts.js:143 +msgid "Modem reset GPIO" +msgstr "GPIO resetu modemu" + +#: htdocs/luci-static/resources/view/wwand/settings.js:47 +msgid "Modem reset failed: %s" +msgstr "Reset modemu nie powiódł się: %s" + +#: htdocs/luci-static/resources/view/wwand/modems.js:312 +#: htdocs/luci-static/resources/wwand/modemopts.js:194 +msgid "Modem reset triggered (%s)." +msgstr "Wywołano reset modemu (%s)." + +#: htdocs/luci-static/resources/view/wwand/settings.js:833 +msgid "Modem settings applied." +msgstr "Zastosowano ustawienia modemu." + +#: htdocs/luci-static/resources/view/wwand/modems.js:122 +#: htdocs/luci-static/resources/view/wwand/settings.js:1276 +#: root/usr/share/luci/menu.d/luci-app-wwand.json:39 +msgid "Modems" +msgstr "Modemy" + +#: htdocs/luci-static/resources/wwand/graph.js:97 +msgid "" +"Mostly a read on how busy the cell is: it falls as the cell fills, even with " +"the antenna untouched. Treat it as an indicator rather than a target — " +"published ladders disagree below -10 dB, and 5G NR is graded a little " +"tighter than the LTE steps drawn here." +msgstr "" +"W dużej mierze miara obciążenia komórki: spada, gdy komórka się zapełnia, " +"nawet bez ruszania anteny. Traktuj to jako wskaźnik, a nie cel — " +"opublikowane drabinki różnią się poniżej -10 dB, a 5G NR jest oceniane nieco " +"ostrzej niż pokazane tu stopnie LTE." + +#: htdocs/luci-static/resources/view/wwand/status.js:407 +msgid "Mux channel %d" +msgstr "Kanał mux %d" + +#: htdocs/luci-static/resources/view/wwand/settings.js:742 +msgid "NAS" +msgstr "NAS" + +#: htdocs/luci-static/resources/view/wwand/settings.js:787 +msgid "" +"NAS = QMI preferred-networks list; User = SIM EF 6F60 (preference, not a " +"lock); Forbidden = SIM EF 6F7B (FPLMN) — networks the modem must NOT use. " +"Editing here is temporary; save it as a list so the daemon re-applies it " +"before every radio-on (survives modem reboots)." +msgstr "" +"NAS = lista preferowanych sieci QMI; User = EF 6F60 na karcie SIM " +"(preferencja, nie blokada); Forbidden = EF 6F7B na karcie SIM (FPLMN) — " +"sieci, których modem NIE może używać. Edycja tutaj jest tymczasowa; zapisz " +"ją jako listę, aby demon stosował ją przed każdym włączeniem radia (przetrwa " +"restarty modemu)." + +#: htdocs/luci-static/resources/view/wwand/settings.js:597 +msgid "NAS preferred networks" +msgstr "Preferowane sieci NAS" + +#: htdocs/luci-static/resources/wwand/modemopts.js:215 +msgid "NCM (AT + ethernet data port)" +msgstr "NCM (AT + port danych Ethernet)" + +#: htdocs/luci-static/resources/wwand/modemopts.js:702 +msgid "" +"NCM modems only. How many consecutive status polls may list no data context " +"— with no received traffic in between — before wwand declares the bearer " +"dead and redials. One poll per telemetry interval, so 3 is about three " +"minutes. Lower recovers faster from a drop the modem does not announce; the " +"minimum is 2, because a single unparsed answer on an idle link must not be " +"enough to drop a working connection." +msgstr "" +"Tylko modemy NCM. Ile kolejnych odpytań stanu może nie wykazać żadnego " +"kontekstu danych — bez odebranego w międzyczasie ruchu — zanim wwand uzna " +"nośnik za martwy i wykona ponowne wybieranie. Jedno odpytanie na interwał " +"telemetrii, więc 3 to około trzy minuty. Niższa wartość szybciej odzyskuje " +"po zerwaniu, którego modem nie zgłasza; minimum to 2, ponieważ pojedyncza " +"nieprzetworzona odpowiedź na bezczynnym łączu nie może wystarczyć do " +"zerwania działającego połączenia." + +#: htdocs/luci-static/resources/view/wwand/status.js:222 +msgid "NMEA port" +msgstr "Port NMEA" + +#: htdocs/luci-static/resources/view/wwand/status.js:318 +msgid "NMEA stream" +msgstr "Strumień NMEA" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1309 +msgid "NR5G NSA bands" +msgstr "Pasma NR5G NSA" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1307 +msgid "NR5G SA bands" +msgstr "Pasma NR5G SA" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1305 +msgid "NR5G bands" +msgstr "Pasma NR5G" + +#: htdocs/luci-static/resources/view/wwand/settings.js:791 +#: htdocs/luci-static/resources/view/wwand/sims.js:89 +#: htdocs/luci-static/resources/wwand/simlist.js:144 +msgid "Name" +msgstr "Nazwa" + +#: htdocs/luci-static/resources/wwand/modemopts.js:161 +msgid "" +"Named GPIO on the modem RESET line (invert, wait 30 s, restore instead of a " +"USB power-cycle). This board provides the reset GPIO \"%s\", which explicit " +"resets use anyway; set it here as well to let wwand pulse it automatically " +"when the modem never answers after boot." +msgstr "" +"Nazwane GPIO na linii RESET modemu (odwróć, odczekaj 30 s, przywróć — " +"zamiast cyklu zasilania USB). Ta płyta udostępnia GPIO resetu „%s”, którego " +"używają i tak resety wywołane jawnie; ustaw je również tutaj, aby wwand " +"wysyłał impuls automatycznie, gdy modem nigdy nie odpowie po starcie." + +#: htdocs/luci-static/resources/wwand/modemopts.js:144 +msgid "" +"Named GPIO on the modem RESET line. When set, recovery pulses it (invert, " +"wait 30 s, restore) instead of cycling USB power." +msgstr "" +"Nazwane GPIO na linii RESET modemu. Po ustawieniu odzyskiwanie wysyła na nie " +"impuls (odwróć, odczekaj 30 s, przywróć) zamiast cyklu zasilania USB." + +#: htdocs/luci-static/resources/view/wwand/status.js:685 +msgid "Network" +msgstr "Sieć" + +#: htdocs/luci-static/resources/view/wwand/modems.js:138 +msgid "" +"Network device name (e.g. wwan0), a mux parent, or a control node (/dev/cdc-" +"wdm0). Leave empty and set only the USB path to bind purely by topology. " +"qrtr drives a Qualcomm modem on PCIe/MHI whose QMI is only on the " +"QRTR bus (no cdc-wdm); its data device is the interface's L3 device." +msgstr "" +"Nazwa urządzenia sieciowego (np. wwan0), urządzenie nadrzędne mux lub węzeł " +"sterujący (/dev/cdc-wdm0). Pozostaw puste i ustaw tylko ścieżkę USB, aby " +"powiązać wyłącznie według topologii. qrtr obsługuje modem Qualcomm " +"na PCIe/MHI, którego QMI jest dostępne tylko na magistrali QRTR (bez cdc-" +"wdm); jego urządzeniem danych jest urządzenie L3 interfejsu." + +#: htdocs/luci-static/resources/view/wwand/modems.js:374 +msgid "Network re-attach triggered (%s)." +msgstr "Wywołano ponowne dołączenie do sieci (%s)." + +#: htdocs/luci-static/resources/view/wwand/status.js:890 +msgid "Network registration state: home / roaming / searching / denied" +msgstr "" +"Stan rejestracji w sieci: macierzysta / roaming / wyszukiwanie / odmowa" + +#: htdocs/luci-static/resources/wwand/netsel.js:256 +msgid "Network selection" +msgstr "Wybór sieci" + +#: htdocs/luci-static/resources/wwand/esim.js:238 +msgid "Network → Modems" +msgstr "Sieć → Modemy" + +#: htdocs/luci-static/resources/view/wwand/modems.js:431 +msgid "New PIN (4-8 digits)" +msgstr "Nowy PIN (4–8 cyfr)" + +#: htdocs/luci-static/resources/view/wwand/sims.js:85 +msgid "No SIM card seen yet." +msgstr "Nie widziano jeszcze żadnej karty SIM." + +#: htdocs/luci-static/resources/view/wwand/settings.js:815 +msgid "No change — the modem already runs these settings." +msgstr "Bez zmian — modem już używa tych ustawień." + +#: htdocs/luci-static/resources/wwand/netsel.js:54 +msgid "No change — the modem already runs this selection." +msgstr "Bez zmian — modem już używa tego wyboru." + +#: htdocs/luci-static/resources/view/wwand/status.js:647 +msgid "No exchange has succeeded in the selected protocol yet." +msgstr "Żadna wymiana w wybranym protokole nie powiodła się jeszcze." + +#: htdocs/luci-static/resources/view/wwand/modems.js:767 +msgid "No interface selected." +msgstr "Nie wybrano interfejsu." + +#: htdocs/luci-static/resources/view/wwand/settings.js:987 +msgid "No messages stored." +msgstr "Brak zapisanych wiadomości." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1156 +msgid "No modem present." +msgstr "Brak modemu." + +#: htdocs/luci-static/resources/wwand/modemopts.js:59 +msgid "No multiplexing (MBIM session 0, untagged)" +msgstr "Bez multipleksowania (sesja MBIM 0, bez znaczników)" + +#: htdocs/luci-static/resources/wwand/modemopts.js:58 +msgid "No multiplexing (plain raw IP)" +msgstr "Bez multipleksowania (zwykły raw IP)" + +#: htdocs/luci-static/resources/wwand/netsel.js:88 +msgid "No operators found." +msgstr "Nie znaleziono operatorów." + +#: htdocs/luci-static/resources/wwand/netsel.js:87 +msgid "No operators returned — %s." +msgstr "Nie zwrócono operatorów — %s." + +#: htdocs/luci-static/resources/wwand/esim.js:492 +msgid "No pending notifications." +msgstr "Brak oczekujących powiadomień." + +#: htdocs/luci-static/resources/wwand/format.js:568 +msgid "No profiles installed" +msgstr "Brak zainstalowanych profili" + +#: htdocs/luci-static/resources/view/wwand/settings.js:775 +msgid "" +"No saved list is attached to this modem yet — edit above and \"Save as list " +"& attach\"." +msgstr "" +"Do tego modemu nie dołączono jeszcze żadnej zapisanej listy — edytuj powyżej " +"i użyj „Zapisz jako listę i dołącz”." + +#: htdocs/luci-static/resources/wwand/esim.js:186 +msgid "No slot information available." +msgstr "Brak informacji o slotach." + +#: htdocs/luci-static/resources/view/wwand/settings.js:101 +msgid "No such connection." +msgstr "Nie ma takiego połączenia." + +#: htdocs/luci-static/resources/view/wwand/settings.js:100 +msgid "No such modem — it may have been unplugged or renamed." +msgstr "Nie ma takiego modemu — mógł zostać odłączony lub przemianowany." + +#: htdocs/luci-static/resources/view/wwand/settings.js:106 +msgid "No usable channel to the SIM on this modem." +msgstr "Brak używalnego kanału do karty SIM w tym modemie." + +#: htdocs/luci-static/resources/view/wwand/settings.js:852 +msgid "No wwand interface found — the cell lock is stored on the modem." +msgstr "" +"Nie znaleziono interfejsu wwand — blokada komórki jest przechowywana w " +"modemie." + +#: htdocs/luci-static/resources/wwand/logbox.js:205 +msgid "No wwand lines matched." +msgstr "Żaden wiersz wwand nie pasuje." + +#: htdocs/luci-static/resources/wwand/simlist.js:200 +msgid "None" +msgstr "Brak" + +#: htdocs/luci-static/resources/view/wwand/settings.js:627 +msgid "" +"Not written: %d entries of this list are reported by the modem by name " +"without a complete MCC/MNC, and writing the list would drop them." +msgstr "" +"Nie zapisano: %d wpisów tej listy modem raportuje po nazwie, bez kompletnego " +"MCC/MNC, a zapis listy by je pominął." + +#: htdocs/luci-static/resources/wwand/simlist.js:149 +msgid "Now" +msgstr "Teraz" + +#: htdocs/luci-static/resources/wwand/modemopts.js:181 +msgid "Off" +msgstr "Wyłączone" + +#: htdocs/luci-static/resources/view/wwand/settings.js:795 +msgid "On the card" +msgstr "Na karcie" + +#: htdocs/luci-static/resources/wwand/modemopts.js:179 +msgid "On, card detected at high level" +msgstr "Włączone, karta wykrywana stanem wysokim" + +#: htdocs/luci-static/resources/wwand/modemopts.js:180 +msgid "On, card detected at low level" +msgstr "Włączone, karta wykrywana stanem niskim" + +#: htdocs/luci-static/resources/wwand/modemopts.js:656 +msgid "One to three whole numbers, rising, e.g. \"2 6 12\"" +msgstr "Od jednej do trzech liczb całkowitych, rosnąco, np. „2 6 12”" + +#: htdocs/luci-static/resources/wwand/logbox.js:168 +msgid "Only this connection" +msgstr "Tylko to połączenie" + +#: htdocs/luci-static/resources/wwand/logbox.js:167 +msgid "Only this modem" +msgstr "Tylko ten modem" + +#: htdocs/luci-static/resources/wwand/logbox.js:169 +msgid "Only this subsystem" +msgstr "Tylko ten podsystem" + +#: htdocs/luci-static/resources/view/wwand/settings.js:397 +#: htdocs/luci-static/resources/view/wwand/settings.js:603 +#: htdocs/luci-static/resources/view/wwand/status.js:948 +#: htdocs/luci-static/resources/wwand/format.js:480 +#: htdocs/luci-static/resources/wwand/netsel.js:130 +msgid "Operator" +msgstr "Operator" + +#: htdocs/luci-static/resources/view/wwand/settings.js:796 +msgid "Operator-controlled (6F61)" +msgstr "Kontrolowane przez operatora (6F61)" + +#: htdocs/luci-static/resources/wwand/simlist.js:187 +msgid "Optional APN for this card, overriding the interface/default APN." +msgstr "Opcjonalny APN dla tej karty, nadpisujący APN interfejsu/domyślny." + +#: htdocs/luci-static/resources/wwand/modemopts.js:594 +msgid "" +"Optional: a saved PLMN list (config wwand_plmnlist) the daemon restores " +"before every radio-on. Edit lists on the modem status page." +msgstr "" +"Opcjonalnie: zapisana lista PLMN (config wwand_plmnlist), którą demon " +"przywraca przed każdym włączeniem radia. Listy edytuje się na stronie " +"statusu modemu." + +#: htdocs/luci-static/resources/wwand/simlist.js:229 +msgid "" +"Optional: a saved PLMN list to restore for this SIM (type NAS or user is set " +"on the list). Wins over the modem's list." +msgstr "" +"Opcjonalnie: zapisana lista PLMN do przywrócenia dla tej karty SIM (typ NAS " +"lub user jest ustawiony na liście). Ma pierwszeństwo przed listą modemu." + +#: htdocs/luci-static/resources/wwand/simlist.js:191 +msgid "" +"Optional: bind this override to one modem (its wwand_modem name). Empty = " +"any modem holding this card." +msgstr "" +"Opcjonalnie: powiąż to nadpisanie z jednym modemem (jego nazwa wwand_modem). " +"Puste = dowolny modem z tą kartą." + +#: htdocs/luci-static/resources/view/wwand/modems.js:157 +msgid "" +"Override the QMI data endpoint type sent with the data format and mux " +"binding. Leave on automatic unless a modem refuses the derived endpoint." +msgstr "" +"Nadpisz typ punktu końcowego danych QMI wysyłany z formatem danych i " +"powiązaniem mux. Pozostaw automatyczny, chyba że modem odrzuca wyprowadzony " +"punkt końcowy." + +#: htdocs/luci-static/resources/wwand/modemopts.js:201 +msgid "Override the auto-detected AT serial port (e.g. /dev/ttyUSB2)." +msgstr "Nadpisz automatycznie wykryty port szeregowy AT (np. /dev/ttyUSB2)." + +#: htdocs/luci-static/resources/view/wwand/modems.js:165 +msgid "" +"Override the endpoint interface number (USB: the QMI function's interface, " +"PCIe: 4). Leave empty for automatic." +msgstr "" +"Nadpisz numer interfejsu punktu końcowego (USB: interfejs funkcji QMI, PCIe: " +"4). Pozostaw puste dla automatycznego." + +#: htdocs/luci-static/resources/wwand/modemopts.js:235 +msgid "PAP or CHAP" +msgstr "PAP lub CHAP" + +#: htdocs/luci-static/resources/wwand/simlist.js:210 +msgid "PAP/CHAP password" +msgstr "Hasło PAP/CHAP" + +#: htdocs/luci-static/resources/wwand/simlist.js:205 +msgid "PAP/CHAP username" +msgstr "Nazwa użytkownika PAP/CHAP" + +#: htdocs/luci-static/resources/wwand/simlist.js:216 +msgid "PDP type" +msgstr "Typ PDP" + +#: htdocs/luci-static/resources/view/wwand/modems.js:434 +#: htdocs/luci-static/resources/wwand/format.js:492 +#: htdocs/luci-static/resources/wwand/simlist.js:175 +msgid "PIN" +msgstr "PIN" + +#: htdocs/luci-static/resources/wwand/esim.js:209 +msgid "PIN change failed: %s" +msgstr "Zmiana PIN-u nie powiodła się: %s" + +#: htdocs/luci-static/resources/wwand/esim.js:219 +msgid "PIN query:" +msgstr "Zapytanie o PIN:" + +#: htdocs/luci-static/resources/view/wwand/modems.js:463 +msgid "PIN release requested — the modem restarts its bring-up." +msgstr "Zażądano zwolnienia PIN — modem ponownie rozpoczyna uruchamianie." + +#: htdocs/luci-static/resources/wwand/format.js:547 +msgid "PIN required" +msgstr "Wymagany PIN" + +#: htdocs/luci-static/resources/wwand/format.js:505 +msgid "PIN state" +msgstr "Stan PIN" + +#: htdocs/luci-static/resources/wwand/format.js:549 +msgid "PIN2 required" +msgstr "Wymagany PIN2" + +#: htdocs/luci-static/resources/wwand/netsel.js:131 +msgid "PLMN" +msgstr "PLMN" + +#: htdocs/luci-static/resources/view/wwand/modems.js:430 +msgid "PUK (8 digits)" +msgstr "PUK (8 cyfr)" + +#: htdocs/luci-static/resources/wwand/format.js:548 +msgid "PUK required — the PIN is blocked" +msgstr "Wymagany PUK — PIN jest zablokowany" + +#: htdocs/luci-static/resources/wwand/format.js:550 +msgid "PUK2 required" +msgstr "Wymagany PUK2" + +#: htdocs/luci-static/resources/view/wwand/status.js:354 +msgid "Parent device" +msgstr "Urządzenie nadrzędne" + +#: htdocs/luci-static/resources/wwand/modemopts.js:678 +msgid "Park the radio when idle" +msgstr "Usypiaj radio w stanie bezczynności" + +#: htdocs/luci-static/resources/wwand/simlist.js:211 +msgid "Password for the APN authentication." +msgstr "Hasło do uwierzytelniania APN." + +#: htdocs/luci-static/resources/wwand/modemopts.js:247 +msgid "Password for the attach bearer." +msgstr "Hasło nośnika dołączania." + +#: htdocs/luci-static/resources/view/wwand/modems.js:677 +msgid "Path" +msgstr "Ścieżka" + +#: htdocs/luci-static/resources/wwand/esim.js:492 +msgid "Pending notifications:" +msgstr "Oczekujące powiadomienia:" + +#: htdocs/luci-static/resources/view/wwand/status.js:311 +msgid "" +"Per satellite: the constellation, its number, and the carrier-to-noise it is " +"received at. A satellite listed without a figure is one the receiver can " +"place but not hear." +msgstr "" +"Dla każdego satelity: konstelacja, jego numer i stosunek nośnej do szumu, z " +"jakim jest odbierany. Satelita wymieniony bez wartości to taki, który " +"odbiornik potrafi umiejscowić, ale nie słyszy." + +#: htdocs/luci-static/resources/wwand/esim.js:237 +msgid "Per-card PIN/APN overrides (matched by ICCID or IMSI) are managed on" +msgstr "" +"Nadpisania PIN/APN dla poszczególnych kart (dopasowywane według ICCID lub " +"IMSI) są zarządzane w" + +#: htdocs/luci-static/resources/wwand/esim.js:308 +msgid "Permanently DELETE profile %s from the eUICC?" +msgstr "TRWALE USUNĄĆ profil %s z eUICC?" + +#: htdocs/luci-static/resources/view/wwand/settings.js:916 +msgid "Persist in modem" +msgstr "Zachowaj w modemie" + +#: htdocs/luci-static/resources/wwand/modemopts.js:623 +msgid "Persist lock in modem" +msgstr "Zachowaj blokadę w modemie" + +#: htdocs/luci-static/resources/view/wwand/status.js:51 +msgid "" +"Physical Cell ID — identifies the cell on this frequency; EARFCN:PCI " +"addresses one specific cell" +msgstr "" +"Physical Cell ID — identyfikuje komórkę na tej częstotliwości; EARFCN:PCI " +"wskazuje jedną konkretną komórkę" + +#: htdocs/luci-static/resources/wwand/modemopts.js:436 +msgid "" +"Physical SIM slot to activate on multi-slot / eSIM modems (0 = leave as-is). " +"The list shows the slots the modem reports." +msgstr "" +"Fizyczny slot SIM do aktywacji w modemach wieloslotowych / eSIM (0 = bez " +"zmian). Lista pokazuje sloty zgłaszane przez modem." + +#: htdocs/luci-static/resources/wwand/modemopts.js:104 +msgid "" +"Pin this modem by its IMEI — globally unique and verified after the modem " +"opens. A configured IMEI that does not match blocks bring-up, so the wrong " +"physical modem never gets this SIM/APN. Pick a detected modem below, or " +"leave empty." +msgstr "" +"Przypnij ten modem według jego IMEI — globalnie unikalnego i weryfikowanego " +"po otwarciu modemu. Skonfigurowany IMEI, który nie pasuje, blokuje " +"uruchomienie, aby niewłaściwy fizyczny modem nigdy nie dostał tej karty SIM/" +"APN. Wybierz wykryty modem poniżej lub pozostaw puste." + +#: htdocs/luci-static/resources/wwand/modemopts.js:86 +msgid "" +"Pin this modem by its USB iSerial — a stable identity that follows the modem " +"across renumbering and port changes (read before the modem is opened). Pick " +"a detected modem below, or leave empty." +msgstr "" +"Przypnij ten modem według jego USB iSerial — stabilnej tożsamości, która " +"podąża za modemem przy zmianie numeracji i portów (odczytywana przed " +"otwarciem modemu). Wybierz wykryty modem poniżej lub pozostaw puste." + +#: htdocs/luci-static/resources/view/wwand/status.js:243 +#: htdocs/luci-static/resources/view/wwand/status.js:281 +msgid "Position" +msgstr "Pozycja" + +#: htdocs/luci-static/resources/view/wwand/settings.js:784 +msgid "Preferred PLMN lists" +msgstr "Listy preferowanych PLMN" + +#: htdocs/luci-static/resources/view/wwand/settings.js:785 +msgid "Preferred-PLMN editor —" +msgstr "Edytor preferowanych PLMN —" + +#: htdocs/luci-static/resources/wwand/modemopts.js:593 +#: htdocs/luci-static/resources/wwand/simlist.js:228 +msgid "Preferred-PLMN list" +msgstr "Lista preferowanych PLMN" + +#: htdocs/luci-static/resources/wwand/esim.js:31 +msgid "Preparing download" +msgstr "Przygotowanie pobierania" + +#: htdocs/luci-static/resources/wwand/esim.js:176 +msgid "Primary SIM set to slot %d (persisted)." +msgstr "Główna karta SIM ustawiona na slot %d (zachowano)." + +#: htdocs/luci-static/resources/view/wwand/status.js:903 +msgid "Problem" +msgstr "Problem" + +#: htdocs/luci-static/resources/wwand/esim.js:95 +msgid "Profile" +msgstr "Profil" + +#: htdocs/luci-static/resources/wwand/esim.js:388 +msgid "Profile downloaded (install not yet confirmed to the operator)." +msgstr "Profil pobrany (instalacja jeszcze niepotwierdzona u operatora)." + +#: htdocs/luci-static/resources/wwand/esim.js:389 +msgid "Profile downloaded and confirmed — the eUICC will re-initialise." +msgstr "Profil pobrany i potwierdzony — eUICC zainicjuje się ponownie." + +#: htdocs/luci-static/resources/view/wwand/settings.js:76 +msgid "Profile switch failed: %s" +msgstr "Przełączenie profilu nie powiodło się: %s" + +#: htdocs/luci-static/resources/view/wwand/settings.js:82 +msgid "" +"Profile switched — the SIM was reset automatically, the connection re-" +"establishes in a few seconds." +msgstr "" +"Profil przełączony — karta SIM została zresetowana automatycznie, połączenie " +"zostanie nawiązane ponownie w ciągu kilku sekund." + +#: htdocs/luci-static/resources/wwand/format.js:590 +msgid "Profiles (%d)" +msgstr "Profile (%d)" + +#: htdocs/luci-static/resources/wwand/format.js:564 +msgid "Profiles are readable only while this slot is the active one" +msgstr "Profile można odczytać tylko wtedy, gdy ten slot jest aktywny" + +#: htdocs/luci-static/resources/wwand/format.js:532 +msgid "Profiles: kept by the card — its own IPA manages them with the eIM" +msgstr "" +"Profile: przechowywane przez kartę — jej własne IPA zarządza nimi z eIM" + +#: htdocs/luci-static/resources/wwand/format.js:563 +msgid "Profiles: not read" +msgstr "Profile: nie odczytano" + +#: htdocs/luci-static/resources/view/wwand/modems.js:790 +msgid "Protocol" +msgstr "Protokół" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1412 +msgid "Protocol switch failed: %s." +msgstr "Przełączenie protokołu nie powiodło się: %s." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1411 +msgid "Protocol switch to %s issued — the modem resets." +msgstr "Zlecono przełączenie protokołu na %s — modem się resetuje." + +#: htdocs/luci-static/resources/wwand/esim.js:329 +msgid "Provider" +msgstr "Dostawca" + +#: htdocs/luci-static/resources/wwand/esim.js:483 +msgid "Provider confirmations (notifications)" +msgstr "Potwierdzenia dostawcy (powiadomienia)" + +#: htdocs/luci-static/resources/wwand/modemopts.js:679 +msgid "" +"Put the modem's radio into low power once no interface on this modem is up — " +"for battery and solar installs, where an idle modem still spends a couple of " +"watts holding a registration nobody uses. Only on an administrative down, " +"never on a brief connection loss. A parked modem is NOT reachable from the " +"network until the interface is brought up again." +msgstr "" +"Przełącza radio modemu w stan niskiego poboru mocy, gdy żaden interfejs tego " +"modemu nie jest aktywny — dla instalacji bateryjnych i solarnych, gdzie " +"bezczynny modem nadal zużywa kilka watów na utrzymanie rejestracji, z której " +"nikt nie korzysta. Tylko przy administracyjnym wyłączeniu, nigdy przy " +"krótkiej utracie połączenia. Uśpiony modem NIE jest osiągalny z sieci, " +"dopóki interfejs nie zostanie ponownie włączony." + +#: htdocs/luci-static/resources/view/wwand/status.js:667 +msgid "QMI" +msgstr "QMI" + +#: htdocs/luci-static/resources/wwand/modemopts.js:273 +msgid "QMI DMS (Quectel EM1xx)" +msgstr "QMI DMS (Quectel EM1xx)" + +#: htdocs/luci-static/resources/wwand/modemopts.js:274 +msgid "QMI Foxconn (SDX55, DW5821e)" +msgstr "QMI Foxconn (SDX55, DW5821e)" + +#: htdocs/luci-static/resources/wwand/modemopts.js:550 +msgid "" +"QMI and MBIM modems: the order in which the modem searches the radio " +"technologies, most preferred first, e.g. nr5g,lte. wwand writes " +"it at every start when the modem holds a different one. A technology missing " +"from the list may never be searched for, even when \"Radio technology\" " +"allows it. Empty = leave the modem's order unchanged. Saving a change " +"restarts this modem's bring-up, so its connection drops briefly. The current " +"order is shown, and can be changed live, under Modem Tools." +msgstr "" +"Modemy QMI i MBIM: kolejność, w jakiej modem przeszukuje technologie " +"radiowe– najpierw te najbardziej preferowane, np. nr5g,lte. " +"wwand zapisuje to przy każdym uruchomieniu, gdy modem ma ustawioną inną " +"kolejność. Technologia, której brakuje na liście może nigdy nie zostać " +"wyszukana, nawet jeśli opcja \"Technologia radiowa\" na to pozwala. Puste " +"pole = pozostaw kolejność modemu bez zmian. Zapisanie zmiany powoduje " +"ponowne uruchomienie modemu, co powoduje krótkotrwałe przerwanie połączenia. " +"Aktualna kolejność jest wyświetlana i można ją zmieniać na bieżąco w sekcji " +"Narzędzia modemu." + +#: htdocs/luci-static/resources/view/wwand/status.js:213 +msgid "QMI location service" +msgstr "Usługa lokalizacji QMI" + +#: htdocs/luci-static/resources/view/wwand/modems.js:143 +msgid "QRTR node" +msgstr "Węzeł QRTR" + +#: htdocs/luci-static/resources/wwand/modemopts.js:747 +msgid "" +"Quectel and ASR modems: which NMEA sentences the receiver streams. Written " +"when wwand starts the receiver; saving a change restarts this modem's bring-" +"up, so its connection drops briefly. Fix only is GPS position, time " +"and DOP (GGA, RMC, GSA) with the other constellations' sentences off — far " +"less for wwand-gps to parse, the same fix, but no satellites-in-view list " +"and only GPS satellites counted as used. The modem keeps what was written " +"last: leaving this unset does not undo Fix only, choose All " +"sentences for that." +msgstr "" + +#: htdocs/luci-static/resources/wwand/modemopts.js:177 +msgid "" +"Quectel modems: let the modem notice a SIM card that is inserted or removed " +"while it runs. \"Inserted\" is the level of the SIM tray's detect pin with a " +"card in — it depends on the board; a wrong level makes the modem see no card." +msgstr "" +"Modemy Quectel: pozwól modemowi zauważać kartę SIM włożoną lub wyjętą " +"podczas pracy. „Włożona” to poziom styku wykrywania w tacce SIM przy obecnej " +"karcie — zależy od płyty; zły poziom powoduje, że modem nie widzi karty." + +#: htdocs/luci-static/resources/view/wwand/status.js:112 +msgid "RA/SLAAC on the netdev, run by the dhcpv6 subinterface" +msgstr "RA/SLAAC na netdev, obsługiwane przez podinterfejs dhcpv6" + +#: htdocs/luci-static/resources/wwand/netsel.js:132 +msgid "RAT" +msgstr "RAT" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1297 +#: htdocs/luci-static/resources/wwand/modemopts.js:549 +msgid "RAT priority" +msgstr "Priorytet RAT" + +#: htdocs/luci-static/resources/wwand/modemopts.js:270 +msgid "" +"RF unlock for laptop-SKU modems that boot radio-locked (Lenovo/Dell/HP " +"variants of Quectel EM1xx, Foxconn SDX55/SDX62). Default \"auto\" tries the " +"known QMI unlock messages when the modem stays in low-power after going " +"online; MBIM modems need the explicit \"quectel\" method. \"off\" disables " +"the unlock entirely." +msgstr "" +"Odblokowanie RF dla modemów w wersjach laptopowych, które uruchamiają się z " +"zablokowanym radiem (warianty Lenovo/Dell/HP Quectel EM1xx, Foxconn SDX55/" +"SDX62). Domyślne „auto” próbuje znanych komunikatów odblokowania QMI, gdy " +"modem po przejściu online pozostaje w trybie niskiego poboru; modemy MBIM " +"wymagają jawnej metody „quectel”. „off” całkowicie wyłącza odblokowanie." + +#: htdocs/luci-static/resources/wwand/graph.js:144 +msgid "RSCP 3G" +msgstr "RSCP 3G" + +#: htdocs/luci-static/resources/wwand/graph.js:143 +msgid "RSRP 5G" +msgstr "RSRP 5G" + +#: htdocs/luci-static/resources/wwand/graph.js:142 +msgid "RSRP LTE" +msgstr "RSRP LTE" + +#: htdocs/luci-static/resources/wwand/graph.js:155 +msgid "RSRQ 5G" +msgstr "RSRQ 5G" + +#: htdocs/luci-static/resources/wwand/graph.js:154 +msgid "RSRQ LTE" +msgstr "RSRQ LTE" + +#: htdocs/luci-static/resources/wwand/graph.js:151 +msgid "RSSI" +msgstr "RSSI" + +#: htdocs/luci-static/resources/wwand/graph.js:150 +msgid "RSSI 2G" +msgstr "RSSI 2G" + +#: htdocs/luci-static/resources/wwand/graph.js:149 +msgid "RSSI 3G" +msgstr "RSSI 3G" + +#: htdocs/luci-static/resources/wwand/graph.js:148 +msgid "RSSI LTE" +msgstr "RSSI LTE" + +#: htdocs/luci-static/resources/view/wwand/status.js:568 +#: htdocs/luci-static/resources/view/wwand/status.js:679 +msgid "Radio" +msgstr "Radio" + +#: htdocs/luci-static/resources/view/wwand/modems.js:131 +msgid "Radio & Cell" +msgstr "Radio i komórka" + +#: htdocs/luci-static/resources/view/wwand/status.js:581 +msgid "Radio access technologies this modem hardware reports to support" +msgstr "" +"Technologie dostępu radiowego, które według zgłoszenia obsługuje sprzęt tego " +"modemu" + +#: htdocs/luci-static/resources/view/wwand/status.js:952 +msgid "" +"Radio access technology of the current connection (LTE, 5G NSA = 5G carrier " +"on an LTE anchor, 5G SA = standalone 5G, NB-IoT/LTE-M = IoT modes)" +msgstr "" +"Technologia dostępu radiowego bieżącego połączenia (LTE, 5G NSA = nośna 5G " +"na kotwicy LTE, 5G SA = samodzielne 5G, NB-IoT/LTE-M = tryby IoT)" + +#: htdocs/luci-static/resources/wwand/format.js:512 +msgid "Radio stack" +msgstr "Stos radiowy" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1293 +msgid "Radio technologies" +msgstr "Technologie radiowe" + +#: htdocs/luci-static/resources/wwand/modemopts.js:533 +msgid "Radio technology" +msgstr "Technologia radiowa" + +#: htdocs/luci-static/resources/view/wwand/modems.js:131 +msgid "Radio technology, manual operator selection and cell lock." +msgstr "Technologia radiowa, ręczny wybór operatora i blokada komórki." + +#: htdocs/luci-static/resources/view/wwand/modems.js:370 +msgid "" +"Re-attach modem \"%s\" to the network now? It briefly deregisters and re-" +"registers; the connection recovers automatically." +msgstr "" +"Dołączyć modem „%s” do sieci ponownie teraz? Na chwilę wyrejestruje się i " +"zarejestruje ponownie; połączenie odzyska się automatycznie." + +#: htdocs/luci-static/resources/view/wwand/modems.js:376 +msgid "Re-attach unavailable: %s." +msgstr "Ponowne dołączenie niedostępne: %s." + +#: htdocs/luci-static/resources/view/wwand/modems.js:326 +msgid "" +"Re-initialise the SIM card in the active slot: power it off and on and read " +"it again, e.g. after swapping the card. The connection drops and recovers on " +"its own; not a modem reset." +msgstr "" +"Zainicjuj ponownie kartę SIM w aktywnym slocie: wyłącz ją i włącz zasilanie " +"oraz odczytaj ponownie, np. po wymianie karty. Połączenie zanika i odzyskuje " +"się samoczynnie; to nie jest reset modemu." + +#: htdocs/luci-static/resources/view/wwand/modems.js:329 +msgid "" +"Re-initialise the SIM of modem \"%s\" now? Its connection drops and recovers " +"automatically." +msgstr "" +"Zainicjować ponownie kartę SIM modemu „%s” teraz? Jego połączenie zanika i " +"odzyskuje się automatycznie." + +#: htdocs/luci-static/resources/wwand/esim.js:92 +msgid "Read from the modem directly (slot %d), bypassing ES10:" +msgstr "Odczyt bezpośrednio z modemu (slot %d), z pominięciem ES10:" + +#: htdocs/luci-static/resources/view/wwand/settings.js:239 +msgid "" +"Read only: the list has entries this interface cannot name (IoT " +"technologies). Change it with the modem vendor's command." +msgstr "" + +#: root/usr/share/rpcd/acl.d/luci-app-wwand.json:66 +msgid "" +"Read the system log for the wwand log viewer on the modem status page. " +"SEPARATE ROLE ON PURPOSE: rpcd cannot filter a method's arguments or its " +"result, so granting `log read` grants the WHOLE system log — kernel, auth, " +"dropbear, firewall — not just the wwand lines, which are picked out in the " +"browser afterwards. Keeping it out of the luci-app-wwand role lets an " +"operator be given the modem pages without the box's logs; the viewer says it " +"cannot read them and everything else on the page still works." +msgstr "" +"Odczyt dziennika systemowego dla przeglądarki dziennika wwand na stronie " +"statusu modemu. CELOWO OSOBNA ROLA: rpcd nie potrafi filtrować argumentów " +"metody ani jej wyniku, więc nadanie `log read` daje dostęp do CAŁEGO " +"dziennika systemowego — jądra, uwierzytelniania, dropbear, zapory — a nie " +"tylko do wierszy wwand, które są wybierane dopiero w przeglądarce. " +"Wyłączenie tego z roli luci-app-wwand pozwala nadać operatorowi strony " +"modemu bez logów urządzenia; przeglądarka dziennika zgłasza wtedy, że nie " +"może ich odczytać, a cała reszta strony nadal działa." + +#: htdocs/luci-static/resources/view/wwand/status.js:231 +msgid "Reader" +msgstr "Czytnik" + +#: htdocs/luci-static/resources/view/wwand/modems.js:379 +#: htdocs/luci-static/resources/view/wwand/modems.js:528 +msgid "Reattach" +msgstr "Dołącz ponownie" + +#: htdocs/luci-static/resources/view/wwand/modems.js:318 +#: htdocs/luci-static/resources/view/wwand/modems.js:530 +msgid "Reboot" +msgstr "Restart" + +#: htdocs/luci-static/resources/wwand/modemopts.js:641 +msgid "Reboot after N failures" +msgstr "Restart po N niepowodzeniach" + +#: htdocs/luci-static/resources/wwand/modemopts.js:670 +msgid "Reboot after N protocol errors" +msgstr "Restart po N błędach protokołu" + +#: htdocs/luci-static/resources/wwand/modemopts.js:671 +msgid "" +"Reboot after this many consecutive control-protocol errors. Gated by the " +"failure-reboot setting above — with reboot disabled it never fires." +msgstr "" +"Restart po tylu kolejnych błędach protokołu sterowania. Zależy od powyższego " +"ustawienia restartu po niepowodzeniach — przy wyłączonym restarcie nigdy nie " +"zadziała." + +#: htdocs/luci-static/resources/wwand/modemopts.js:642 +msgid "" +"Reboot the router after this many failed connection attempts. 0 = never " +"reboot — the hardware recovery rungs (op-mode cycle, modem reset, GPIO/" +"repower) still run and the ladder then keeps retrying." +msgstr "" +"Restart routera po tylu nieudanych próbach połączenia. 0 = nigdy nie " +"restartuj — sprzętowe etapy odzyskiwania (cykl trybu pracy, reset modemu, " +"GPIO/ponowne zasilenie) nadal działają, a procedura potem próbuje dalej." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1110 +msgid "Received" +msgstr "Odebrano" + +#: htdocs/luci-static/resources/view/wwand/status.js:215 +#: htdocs/luci-static/resources/view/wwand/status.js:225 +msgid "Receiver" +msgstr "Odbiornik" + +#: htdocs/luci-static/resources/view/wwand/status.js:643 +#: htdocs/luci-static/resources/view/wwand/status.js:774 +msgid "Recovery" +msgstr "Odzyskiwanie" + +#: htdocs/luci-static/resources/wwand/modemopts.js:650 +msgid "Recovery steps" +msgstr "Kroki odzyskiwania" + +#: htdocs/luci-static/resources/view/wwand/modems.js:132 +msgid "Recovery, watchdogs and telemetry cadence." +msgstr "Odzyskiwanie, watchdogi i częstotliwość telemetrii." + +#: htdocs/luci-static/resources/view/wwand/status.js:52 +msgid "" +"Reference Signal Received Power — signal strength of this cell in dBm " +"(closer to 0 = stronger; -80 excellent, -110 weak)" +msgstr "" +"Reference Signal Received Power — siła sygnału tej komórki w dBm (bliżej 0 = " +"silniej; -80 doskonały, -110 słaby)" + +#: htdocs/luci-static/resources/view/wwand/status.js:53 +msgid "" +"Reference Signal Received Quality in dB (higher = better; -10 good, -15 poor)" +msgstr "" +"Reference Signal Received Quality w dB (wyżej = lepiej; -10 dobra, -15 słaba)" + +#: htdocs/luci-static/resources/wwand/graph.js:96 +msgid "Reference signal quality (RSRQ)" +msgstr "Jakość sygnału referencyjnego (RSRQ)" + +#: htdocs/luci-static/resources/wwand/logbox.js:278 +msgid "Refresh" +msgstr "Odśwież" + +#: htdocs/luci-static/resources/wwand/netsel.js:107 +msgid "Register manually to %s (%s)? The connection may briefly drop." +msgstr "Zarejestrować ręcznie w %s (%s)? Połączenie może na chwilę zaniknąć." + +#: htdocs/luci-static/resources/wwand/netsel.js:36 +msgid "Registered operator" +msgstr "Zarejestrowany operator" + +#: htdocs/luci-static/resources/view/wwand/modems.js:212 +#: htdocs/luci-static/resources/view/wwand/status.js:890 +msgid "Registration" +msgstr "Rejestracja" + +#: htdocs/luci-static/resources/view/wwand/status.js:903 +msgid "" +"Registration problem reported by the network — the 3GPP reject cause " +"explains why the attach was refused" +msgstr "" +"Problem z rejestracją zgłoszony przez sieć — przyczyna odrzucenia 3GPP " +"wyjaśnia, dlaczego dołączenie zostało odmówione" + +#: htdocs/luci-static/resources/view/wwand/modems.js:338 +#: htdocs/luci-static/resources/view/wwand/modems.js:527 +msgid "Reinit SIM" +msgstr "Reinicjalizuj SIM" + +#: htdocs/luci-static/resources/view/wwand/modems.js:438 +msgid "Release PIN — SIM %s" +msgstr "Zwolnij PIN — SIM %s" + +#: htdocs/luci-static/resources/wwand/modemopts.js:279 +msgid "Release secondary AT port" +msgstr "Zwolnij dodatkowy port AT" + +#: htdocs/luci-static/resources/view/wwand/settings.js:731 +msgid "Reload from modem" +msgstr "Wczytaj ponownie z modemu" + +#: htdocs/luci-static/resources/view/wwand/status.js:671 +msgid "Remote SIM supported" +msgstr "Zdalna SIM obsługiwana" + +#: htdocs/luci-static/resources/view/wwand/status.js:697 +msgid "Reported by the optional package %s" +msgstr "Zgłoszone przez opcjonalny pakiet %s" + +#: htdocs/luci-static/resources/view/wwand/modems.js:359 +#: htdocs/luci-static/resources/view/wwand/modems.js:531 +msgid "Repower" +msgstr "Ponowne zasilenie" + +#: htdocs/luci-static/resources/wwand/modemopts.js:683 +msgid "Repower / reset duration" +msgstr "Czas ponownego zasilenia / resetu" + +#: htdocs/luci-static/resources/view/wwand/modems.js:355 +msgid "Repower unavailable: %s." +msgstr "Ponowne zasilenie niedostępne: %s." + +#: htdocs/luci-static/resources/wwand/netsel.js:112 +msgid "Reselect" +msgstr "Wybierz ponownie" + +#: htdocs/luci-static/resources/wwand/modemopts.js:280 +msgid "" +"Reserve the modem's second AT port for external tools (gpsd, own scripts): " +"wwand never opens it and runs telemetry over the control channel instead. " +"The modem status shows which port was released." +msgstr "" +"Zarezerwuj drugi port AT modemu dla zewnętrznych narzędzi (gpsd, własne " +"skrypty): wwand nigdy go nie otwiera, a telemetrię prowadzi przez kanał " +"sterujący. Status modemu pokazuje, który port został zwolniony." + +#: htdocs/luci-static/resources/wwand/modemopts.js:166 +msgid "Reset GPIO fallback" +msgstr "Zapasowe GPIO resetu" + +#: htdocs/luci-static/resources/wwand/modemopts.js:664 +msgid "Reset a never-answering modem after (s)" +msgstr "Resetuj nigdy nieodpowiadający modem po (s)" + +#: htdocs/luci-static/resources/wwand/modemopts.js:187 +msgid "Reset modem" +msgstr "Resetuj modem" + +#: htdocs/luci-static/resources/view/wwand/modems.js:308 +msgid "" +"Reset modem \"%s\" now? Its connection(s) will drop briefly and recover " +"automatically." +msgstr "" +"Zresetować modem „%s” teraz? Jego połączenia na chwilę zanikną i odzyskają " +"się automatycznie." + +#: htdocs/luci-static/resources/wwand/modemopts.js:185 +msgid "Reset modem now" +msgstr "Resetuj modem teraz" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1328 +msgid "Reset radio/band/usage/roaming settings to defaults?" +msgstr "Przywrócić domyślne ustawienia radia/pasm/użycia/roamingu?" + +#: htdocs/luci-static/resources/wwand/modemopts.js:696 +msgid "" +"Reset the modem when, with a connection up, the kernel reports its transmit " +"queue stuck for this many seconds, i.e. the modem has stopped taking packets " +"(the \"NETDEV WATCHDOG\" message in the system log). 0 only records the " +"stalls in the status. Default 120 s." +msgstr "" +"Resetuj modem, gdy przy aktywnym połączeniu jądro zgłasza zablokowaną " +"kolejkę nadawczą przez tyle sekund, tj. modem przestał przyjmować pakiety " +"(komunikat „NETDEV WATCHDOG” w dzienniku systemowym). 0 tylko zapisuje " +"zastoje w statusie. Domyślnie 120 s." + +#: htdocs/luci-static/resources/wwand/modemopts.js:186 +msgid "" +"Reset this modem: the control protocol's soft reset first (QMI/MBIM " +"offline+reset, AT+CFUN=1,1), so it can shut down cleanly; a dedicated reset " +"GPIO is pulsed only if the modem does not reboot. Recovers a modem that hung " +"or dropped off USB." +msgstr "" +"Resetuje ten modem: najpierw programowy reset protokołu sterowania (QMI/MBIM " +"offline+reset, AT+CFUN=1,1), aby mógł się czysto wyłączyć; dedykowane GPIO " +"resetu jest pulsowane tylko wtedy, gdy modem się nie zrestartuje. Przywraca " +"modem, który zawisł lub zniknął z USB." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1372 +msgid "Reset to defaults" +msgstr "Przywróć domyślne" + +#: htdocs/luci-static/resources/view/wwand/modems.js:314 +#: htdocs/luci-static/resources/wwand/modemopts.js:196 +msgid "Reset unavailable: %s." +msgstr "Reset niedostępny: %s." + +#: htdocs/luci-static/resources/view/wwand/modems.js:305 +msgid "" +"Reset/reboot this modem: its own soft reset first, so it can shut down " +"cleanly; a configured reset GPIO is pulsed only if the modem does not " +"reboot. Its connections drop briefly and recover on their own." +msgstr "" +"Reset/restart tego modemu: najpierw jego własny reset programowy, aby mógł " +"się czysto wyłączyć; skonfigurowane GPIO resetu jest pulsowane tylko wtedy, " +"gdy modem się nie zrestartuje. Jego połączenia na chwilę zanikają i " +"odzyskują się samoczynnie." + +#: htdocs/luci-static/resources/view/wwand/modems.js:132 +msgid "Resilience" +msgstr "Odporność" + +#: htdocs/luci-static/resources/view/wwand/settings.js:53 +msgid "Restart modem now" +msgstr "Uruchom modem ponownie teraz" + +#: htdocs/luci-static/resources/wwand/modemopts.js:690 +msgid "" +"Restart the connection after this many seconds without received packets (0 = " +"off)." +msgstr "" +"Uruchom połączenie ponownie po tylu sekundach bez odebranych pakietów (0 = " +"wyłączone)." + +#: htdocs/luci-static/resources/view/wwand/settings.js:772 +msgid "Restore failed: %s" +msgstr "Przywracanie nie powiodło się: %s" + +#: htdocs/luci-static/resources/view/wwand/settings.js:766 +msgid "Restore now" +msgstr "Przywróć teraz" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1304 +msgid "" +"Restrict LTE to these bands. Leave everything unchecked to let the modem use " +"all bands it supports — a narrowed list is a way to pin a known-good band, " +"not a way to improve a working link." +msgstr "" +"Ogranicz LTE do tych pasm. Pozostaw wszystko niezaznaczone, aby modem używał " +"wszystkich obsługiwanych pasm — zawężona lista służy do przypięcia " +"sprawdzonego pasma, a nie do poprawy działającego łącza." + +#: htdocs/luci-static/resources/wwand/modemopts.js:534 +msgid "Restrict the modem to certain radio access technologies." +msgstr "Ogranicz modem do wybranych technologii dostępu radiowego." + +#: htdocs/luci-static/resources/view/wwand/status.js:578 +msgid "Revision" +msgstr "Rewizja" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1417 +msgid "" +"Rewrites the modem firmware mode (QMI ↔ MBIM). The modem resets; use only " +"when the other mode is known to work on this module. The daemon clears a " +"contradicting \"Control protocol\" pin when the switch succeeds." +msgstr "" +"Przepisuje tryb firmware modemu (QMI ↔ MBIM). Modem się resetuje; używaj " +"tylko wtedy, gdy wiadomo, że drugi tryb działa w tym module. Demon czyści " +"sprzeczne przypięcie „Protokołu sterowania”, gdy przełączenie się powiedzie." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1301 +msgid "Roaming" +msgstr "Roaming" + +#: htdocs/luci-static/resources/wwand/format.js:278 +msgid "SGP.32 IoT eUICC" +msgstr "SGP.32 IoT eUICC" + +#: htdocs/luci-static/resources/wwand/format.js:278 +msgid "SGP.32 IoT eUICC (on %s)" +msgstr "SGP.32 IoT eUICC (na %s)" + +#: htdocs/luci-static/resources/view/wwand/modems.js:210 +#: htdocs/luci-static/resources/wwand/esim.js:185 +msgid "SIM" +msgstr "SIM" + +#: htdocs/luci-static/resources/wwand/esim.js:216 +#: htdocs/luci-static/resources/wwand/modemopts.js:523 +msgid "SIM PIN" +msgstr "PIN karty SIM" + +#: htdocs/luci-static/resources/wwand/simlist.js:176 +msgid "" +"SIM PIN for this card. Overrides the modem's default PIN; wwand never " +"retries a PIN when only one attempt is left." +msgstr "" +"PIN karty SIM dla tej karty. Nadpisuje domyślny PIN modemu; wwand nigdy nie " +"ponawia PIN-u, gdy została tylko jedna próba." + +#: htdocs/luci-static/resources/wwand/esim.js:211 +msgid "SIM PIN query disabled." +msgstr "Wyłączono zapytanie o PIN karty SIM." + +#: htdocs/luci-static/resources/wwand/esim.js:211 +msgid "SIM PIN query enabled." +msgstr "Włączono zapytanie o PIN karty SIM." + +#: htdocs/luci-static/resources/view/wwand/settings.js:960 +#: htdocs/luci-static/resources/view/wwand/status.js:653 +#: htdocs/luci-static/resources/wwand/format.js:456 +msgid "SIM card" +msgstr "Karta SIM" + +#: htdocs/luci-static/resources/view/wwand/sims.js:122 +#: root/usr/share/luci/menu.d/luci-app-wwand.json:21 +msgid "SIM cards" +msgstr "Karty SIM" + +#: htdocs/luci-static/resources/view/wwand/status.js:657 +msgid "SIM event" +msgstr "Zdarzenie SIM" + +#: htdocs/luci-static/resources/wwand/modemopts.js:176 +msgid "SIM hot-plug detection" +msgstr "Wykrywanie hot-plug karty SIM" + +#: htdocs/luci-static/resources/view/wwand/modems.js:333 +msgid "SIM in slot %d re-initialised." +msgstr "Karta SIM w slocie %d zainicjowana ponownie." + +#: htdocs/luci-static/resources/wwand/esim.js:235 +msgid "SIM overrides" +msgstr "Przypisane SIM" + +#: htdocs/luci-static/resources/view/wwand/modems.js:335 +msgid "SIM re-initialisation failed: %s." +msgstr "Ponowna inicjalizacja SIM nie powiodła się: %s." + +#: htdocs/luci-static/resources/wwand/modemopts.js:435 +msgid "SIM slot" +msgstr "Slot SIM" + +#: htdocs/luci-static/resources/view/wwand/status.js:1107 +msgid "SIM slots" +msgstr "Sloty SIM" + +#: htdocs/luci-static/resources/wwand/modemopts.js:256 +msgid "SIM toolkit" +msgstr "SIM Toolkit" + +#: htdocs/luci-static/resources/view/wwand/modems.js:458 +msgid "" +"SIM unblocked, new PIN set. Update the configured PIN in the modem settings!" +msgstr "" +"SIM odblokowana, ustawiono nowy PIN. Zaktualizuj skonfigurowany PIN w " +"ustawieniach modemu!" + +#: htdocs/luci-static/resources/wwand/simlist.js:103 +msgid "SIMs (per-ICCID overrides)" +msgstr "SIM (Przypisane według ICCID)" + +#: htdocs/luci-static/resources/wwand/graph.js:153 +msgid "SINR 5G" +msgstr "SINR 5G" + +#: htdocs/luci-static/resources/wwand/graph.js:152 +msgid "SINR LTE" +msgstr "SINR LTE" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1140 +msgid "SMS" +msgstr "SMS" + +#: htdocs/luci-static/resources/view/wwand/status.js:292 +msgid "Satellites" +msgstr "Satelity" + +#: htdocs/luci-static/resources/view/wwand/modems.js:411 +#: htdocs/luci-static/resources/view/wwand/modems.js:526 +msgid "Save SIM" +msgstr "Zapisz SIM" + +#: htdocs/luci-static/resources/view/wwand/settings.js:733 +msgid "Save as list & attach" +msgstr "Zapisz jako listę i dołącz" + +#: htdocs/luci-static/resources/view/wwand/settings.js:696 +msgid "Save as list — name:" +msgstr "Zapisz jako listę — nazwa:" + +#: htdocs/luci-static/resources/view/wwand/settings.js:941 +msgid "Save cell lock" +msgstr "Zapisz blokadę komórki" + +#: htdocs/luci-static/resources/view/wwand/settings.js:789 +msgid "Saved PLMN lists" +msgstr "Zapisane listy PLMN" + +#: htdocs/luci-static/resources/view/wwand/settings.js:724 +msgid "Saved list \"%s\" (%d record(s)) and attached it to this modem." +msgstr "Zapisano listę „%s” (rekordów: %d) i dołączono ją do tego modemu." + +#: htdocs/luci-static/resources/view/wwand/settings.js:40 +msgid "Saved — but this modem applies the change only after a modem restart." +msgstr "" +"Zapisano — ale ten modem zastosuje zmianę dopiero po ponownym uruchomieniu " +"modemu." + +#: htdocs/luci-static/resources/wwand/netsel.js:149 +msgid "Scan failed:" +msgstr "Skanowanie nie powiodło się:" + +#: htdocs/luci-static/resources/wwand/netsel.js:240 +msgid "Scan for operators" +msgstr "Skanuj operatorów" + +#: htdocs/luci-static/resources/wwand/netsel.js:156 +msgid "Scanning for operators — this can take several minutes…" +msgstr "Skanowanie operatorów — może to potrwać kilka minut…" + +#: htdocs/luci-static/resources/wwand/modemopts.js:167 +msgid "" +"Seconds a reset waits for the modem's own soft reset to take it off USB " +"before the reset GPIO is pulsed (default 30)." +msgstr "" +"Liczba sekund, przez które reset czeka, aż programowy reset modemu odłączy " +"go od USB, zanim pulsowane jest GPIO resetu (domyślnie 30)." + +#: htdocs/luci-static/resources/wwand/modemopts.js:708 +msgid "Seconds between throughput/signal telemetry samples while connected." +msgstr "" +"Liczba sekund między próbkami telemetrii przepustowości/sygnału podczas " +"połączenia." + +#: htdocs/luci-static/resources/view/wwand/status.js:273 +msgid "" +"Seconds since the last position was parsed. A receiver that has lost the sky " +"keeps reporting the last fix it had, so the age is what says whether it is " +"still current." +msgstr "" +"Sekundy od ostatniego przetworzenia pozycji. Odbiornik, który stracił widok " +"nieba, nadal raportuje ostatni posiadany fix, więc to wiek mówi, czy jest on " +"jeszcze aktualny." + +#: htdocs/luci-static/resources/wwand/modemopts.js:684 +msgid "" +"Seconds the modem is held de-powered during a recovery power-cycle (or held " +"in reset, when a reset GPIO is used). Default 30 s." +msgstr "" +"Sekundy, przez które modem jest pozbawiony zasilania podczas odzyskiwania " +"przez cykl zasilania (lub utrzymywany w resecie, gdy używane jest GPIO " +"resetu). Domyślnie 30 s." + +#: htdocs/luci-static/resources/wwand/modemopts.js:636 +msgid "Seconds to wait before initializing the modem." +msgstr "Liczba sekund oczekiwania przed zainicjowaniem modemu." + +#: htdocs/luci-static/resources/view/wwand/settings.js:525 +#: htdocs/luci-static/resources/wwand/netsel.js:112 +msgid "Select" +msgstr "Wybierz" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1039 +msgid "Select all messages" +msgstr "Zaznacz wszystkie wiadomości" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1019 +msgid "Select this message" +msgstr "Zaznacz tę wiadomość" + +#: htdocs/luci-static/resources/view/wwand/settings.js:520 +msgid "Selecting the carrier configuration failed." +msgstr "Wybór konfiguracji operatora nie powiódł się." + +#: htdocs/luci-static/resources/wwand/esim.js:502 +msgid "Send all pending notifications to the operator now?" +msgstr "Wysłać teraz wszystkie oczekujące powiadomienia do operatora?" + +#: htdocs/luci-static/resources/wwand/esim.js:511 +msgid "Send confirmations" +msgstr "Wyślij potwierdzenia" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1109 +msgid "Sender" +msgstr "Nadawca" + +#: htdocs/luci-static/resources/wwand/esim.js:504 +msgid "Sending confirmations…" +msgstr "Wysyłanie potwierdzeń…" + +#: htdocs/luci-static/resources/view/wwand/status.js:318 +msgid "" +"Sentences understood since the reader opened the port, and how many it could " +"not parse. A climbing \"unparsed\" means the port is carrying something that " +"is not NMEA." +msgstr "" +"Zdania zrozumiane od otwarcia portu przez czytnik oraz liczba tych, których " +"nie udało się przetworzyć. Rosnąca liczba „nieprzetworzonych” oznacza, że " +"port przenosi coś, co nie jest NMEA." + +#: htdocs/luci-static/resources/view/wwand/modems.js:678 +msgid "Serial" +msgstr "Numer seryjny" + +#: htdocs/luci-static/resources/wwand/format.js:506 +msgid "Service" +msgstr "Usługa" + +#: htdocs/luci-static/resources/view/wwand/status.js:1014 +msgid "Serving cell" +msgstr "Komórka obsługująca" + +#: htdocs/luci-static/resources/wwand/esim.js:178 +msgid "Set as primary" +msgstr "Ustaw jako główną" + +#: htdocs/luci-static/resources/wwand/netsel.js:245 +msgid "Set automatic" +msgstr "Ustaw automatyczny" + +#: htdocs/luci-static/resources/wwand/modemopts.js:734 +msgid "Set the clock from GNSS" +msgstr "Ustaw zegar z GNSS" + +#: htdocs/luci-static/resources/wwand/modemopts.js:716 +msgid "Set the clock from the network" +msgstr "Ustaw zegar z sieci" + +#: htdocs/luci-static/resources/view/wwand/sims.js:95 +msgid "Settings" +msgstr "Ustawienia" + +#: htdocs/luci-static/resources/wwand/esim.js:395 +msgid "Show raw lpac log" +msgstr "Pokaż surowy dziennik lpac" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1296 +msgid "" +"Shown, not set: this modem picks its technologies with its own RAT setting, " +"and a band edit keeps it as it is." +msgstr "" +"Tylko do wyświetlenia: ten modem wybiera swoje technologie własnym " +"ustawieniem RAT, a edycja pasm go nie zmienia." + +#: htdocs/luci-static/resources/view/wwand/modems.js:214 +msgid "Signal" +msgstr "Sygnał" + +#: htdocs/luci-static/resources/view/wwand/status.js:1312 +msgid "" +"Signal graphs unavailable: the graph canvas (wwand/signal.svg) could not be " +"loaded. Reload the page to try again." +msgstr "" +"Wykresy sygnału niedostępne: nie udało się wczytać płótna wykresów (wwand/" +"signal.svg). Przeładuj stronę, aby spróbować ponownie." + +#: htdocs/luci-static/resources/wwand/graph.js:91 +msgid "Signal quality (SINR)" +msgstr "Jakość sygnału (SINR)" + +#: htdocs/luci-static/resources/wwand/graph.js:81 +msgid "Signal strength (RSRP)" +msgstr "Siła sygnału (RSRP)" + +#: htdocs/luci-static/resources/wwand/format.js:458 +#: htdocs/luci-static/resources/wwand/format.js:625 +#: htdocs/luci-static/resources/wwand/modemopts.js:480 +msgid "Slot %d" +msgstr "Slot %d" + +#: htdocs/luci-static/resources/wwand/format.js:1261 +msgid "Slot %d/%d" +msgstr "Slot %d/%d" + +#: htdocs/luci-static/resources/view/wwand/status.js:212 +msgid "Source" +msgstr "Źródło" + +#: htdocs/luci-static/resources/view/wwand/settings.js:913 +msgid "" +"Space/comma separated \"earfcn:pci\" entries (several = a cell list). See " +"Status → Modem for the live cells and their lock values." +msgstr "" +"Wpisy „earfcn:pci” oddzielone spacjami/przecinkami (kilka = lista komórek). " +"Aktualne komórki i ich wartości blokady znajdziesz w Status → Modem." + +#: htdocs/luci-static/resources/view/wwand/status.js:264 +msgid "Speed" +msgstr "Prędkość" + +#: htdocs/luci-static/resources/wwand/modemopts.js:126 +msgid "" +"Stable device topology anchor (like a wifi-device path) — the " +"modem's sysfs path relative to /sys/devices, so it survives USB renumbering " +"and is PCIe/MHI-ready. Pick a detected modem below or type a path; empty = " +"bind by the modem device. Prefer Bind by USB serial/IMEI when " +"available." +msgstr "" +"Stabilna kotwica topologii urządzenia (jak path urządzenia " +"wifi) — ścieżka sysfs modemu względem /sys/devices, więc przetrwa zmianę " +"numeracji USB i jest gotowa na PCIe/MHI. Wybierz wykryty modem poniżej lub " +"wpisz ścieżkę; puste = powiązanie według urządzenia modemu. Gdy to możliwe, " +"preferuj Powiąż według numeru seryjnego USB/IMEI." + +#: htdocs/luci-static/resources/wwand/esim.js:464 +msgid "Starting download…" +msgstr "Rozpoczynanie pobierania…" + +#: htdocs/luci-static/resources/view/wwand/modems.js:208 +#: htdocs/luci-static/resources/view/wwand/settings.js:541 +#: htdocs/luci-static/resources/view/wwand/sims.js:92 +#: htdocs/luci-static/resources/view/wwand/status.js:100 +#: htdocs/luci-static/resources/view/wwand/status.js:556 +#: htdocs/luci-static/resources/wwand/esim.js:96 +#: htdocs/luci-static/resources/wwand/esim.js:330 +msgid "State" +msgstr "Stan" + +#: htdocs/luci-static/resources/view/wwand/modems.js:609 +#: htdocs/luci-static/resources/view/wwand/settings.js:1278 +#: htdocs/luci-static/resources/wwand/netsel.js:133 +msgid "Status" +msgstr "Status" + +#: htdocs/luci-static/resources/wwand/modemopts.js:717 +msgid "" +"Step the system clock to the network's time (NITZ) when it is more than two " +"minutes off. Smaller differences are left to NTP. A router without a real-" +"time clock otherwise keeps its boot-time date until an NTP server is " +"reachable. The time zone is not changed." +msgstr "" +"Skokowo ustaw zegar systemowy na czas sieci (NITZ), gdy różni się o więcej " +"niż dwie minuty. Mniejsze różnice zostawia się NTP. Router bez zegara czasu " +"rzeczywistego w przeciwnym razie zachowuje datę z czasu startu, dopóki " +"serwer NTP nie stanie się osiągalny. Strefa czasowa nie jest zmieniana." + +#: htdocs/luci-static/resources/view/wwand/modems.js:481 +msgid "" +"Stop the data sessions and detach the modem from the network (radio stays " +"on). It stays detached until you attach it again or the modem is reset." +msgstr "" +"Zatrzymuje sesje danych i odłącza modem od sieci (radio pozostaje włączone). " +"Pozostaje odłączony do ponownego dołączenia lub resetu modemu." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1142 +msgid "Storage" +msgstr "Pamięć" + +#: htdocs/luci-static/resources/wwand/modemopts.js:624 +msgid "Store the cell lock in modem non-volatile memory." +msgstr "Zapisz blokadę komórki w pamięci nieulotnej modemu." + +#: htdocs/luci-static/resources/view/wwand/settings.js:917 +msgid "Store the lock in modem non-volatile memory" +msgstr "Zapisz blokadę w pamięci nieulotnej modemu" + +#: htdocs/luci-static/resources/view/wwand/status.js:311 +msgid "Strongest signals" +msgstr "Najsilniejsze sygnały" + +#: htdocs/luci-static/resources/view/wwand/status.js:1074 +#: htdocs/luci-static/resources/wwand/format.js:650 +msgid "Switch now" +msgstr "Przełącz teraz" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1415 +msgid "Switch protocol" +msgstr "Przełącz protokół" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1406 +msgid "" +"Switch the modem firmware to %s? The modem resets and re-enumerates — its " +"connections come back on their own. A \"Control protocol\" pinned on the " +"modem page is cleared back to \"detect\", because a pin the driver " +"contradicts disables hardware recovery for this modem." +msgstr "" +"Przełączyć firmware modemu na %s? Modem się resetuje i ponownie enumeruje — " +"jego połączenia wracają samoczynnie. „Protokół sterowania” przypięty na " +"stronie modemu zostaje przywrócony do „wykrywaj”, ponieważ przypięcie " +"sprzeczne ze sterownikiem wyłącza sprzętowe odzyskiwanie tego modemu." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1405 +msgid "" +"Switch the modem firmware to MBIM? The modem resets and re-enumerates. Some " +"firmwares reject MBIM even though the switch itself succeeds — if the modem " +"does not come back, switch it to QMI again. A \"Control protocol\" pinned on " +"the modem page is cleared back to \"detect\", because a pin the driver " +"contradicts disables hardware recovery for this modem." +msgstr "" +"Przełączyć firmware modemu na MBIM? Modem się resetuje i ponownie enumeruje. " +"Niektóre firmware odrzucają MBIM, mimo że samo przełączenie się powiodło — " +"jeśli modem nie wróci, przełącz go ponownie na QMI. „Protokół sterowania” " +"przypięty na stronie modemu zostaje przywrócony do „wykrywaj”, ponieważ " +"przypięcie sprzeczne ze sterownikiem wyłącza sprzętowe odzyskiwanie tego " +"modemu." + +#: htdocs/luci-static/resources/wwand/modemopts.js:730 +msgid "" +"Switch the modem's GNSS on. With wwand-gps installed wwand reads " +"the position itself: from the modem's NMEA port (reported as gps_port; the receiver is started with the vendor AT command, the path that works " +"on Quectel and ASR modems), or — on a modem without an NMEA port — the same " +"NMEA over the QMI location service, natively or over the MBIM passthrough." +msgstr "" +"Włącza GNSS modemu. Gdy zainstalowano wwand-gps, wwand odczytuje " +"pozycję samodzielnie: z portu NMEA modemu (raportowanego jako gps_port; odbiornik jest uruchamiany poleceniem AT producenta, ścieżką działającą " +"w modemach Quectel i ASR) lub — w modemie bez portu NMEA — z tego samego " +"NMEA przez usługę lokalizacji QMI, natywnie lub przez przekazywanie MBIM." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1396 +msgid "Switch to %s" +msgstr "Przełącz na %s" + +#: htdocs/luci-static/resources/view/wwand/status.js:1071 +#: htdocs/luci-static/resources/wwand/format.js:647 +msgid "Switch to SIM slot %d? The connection will drop and re-establish." +msgstr "" +"Przełączyć na slot SIM %d? Połączenie zostanie przerwane i nawiązane " +"ponownie." + +#: htdocs/luci-static/resources/view/wwand/status.js:982 +msgid "TAC / Cell ID" +msgstr "TAC / Cell ID" + +#: htdocs/luci-static/resources/view/wwand/status.js:982 +msgid "" +"TAC = Tracking Area Code (paging area); Cell ID = the network-wide unique " +"identifier of this cell" +msgstr "" +"TAC = Tracking Area Code (obszar przywoławczy); Cell ID = unikalny w całej " +"sieci identyfikator tej komórki" + +#: htdocs/luci-static/resources/wwand/modemopts.js:695 +msgid "TX stall timeout" +msgstr "Limit zastoju TX" + +#: htdocs/luci-static/resources/view/wwand/status.js:869 +msgid "TX stalls" +msgstr "Zastoje TX" + +#: htdocs/luci-static/resources/view/wwand/status.js:952 +msgid "Technology" +msgstr "Technologia" + +#: htdocs/luci-static/resources/wwand/modemopts.js:707 +msgid "Telemetry interval" +msgstr "Interwał telemetrii" + +#: htdocs/luci-static/resources/view/wwand/status.js:589 +msgid "Temperature" +msgstr "Temperatura" + +#: htdocs/luci-static/resources/wwand/graph.js:120 +msgid "" +"The 3G counterpart to SINR: pilot energy against everything else in the " +"carrier. Above roughly -7 dB a connection starts to suffer; the steps drawn " +"here are the common operator convention around that figure, which is less " +"firmly published than the LTE and 5G ladders. On a modem that normally runs " +"LTE, this canvas appearing at all is the news." +msgstr "" +"Odpowiednik SINR dla 3G: energia pilota względem wszystkiego innego w " +"nośnej. Powyżej około -7 dB połączenie zaczyna cierpieć; pokazane tu stopnie " +"to powszechna konwencja operatorów wokół tej wartości, mniej ugruntowana w " +"publikacjach niż drabinki LTE i 5G. W modemie, który zwykle pracuje w LTE, " +"sam fakt pojawienia się tego płótna jest wiadomością." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1300 +msgid "" +"The 3GPP usage setting the modem reports to the network: whether this device " +"is here for data or for voice. It is a preference, not a command — what the " +"network then does about voice continuity is its decision. Data centric is " +"what a router wants." +msgstr "" +"Ustawienie użycia 3GPP raportowane przez modem do sieci: czy to urządzenie " +"jest tu do danych czy do głosu. To preferencja, nie polecenie — to, co sieć " +"zrobi z ciągłością głosu, jest jej decyzją. Router potrzebuje trybu " +"skoncentrowanego na danych." + +#: htdocs/luci-static/resources/view/wwand/status.js:996 +msgid "" +"The 5G NR serving cell: band · centre frequency · bandwidth · Physical Cell " +"ID" +msgstr "" +"Komórka obsługująca 5G NR: pasmo · częstotliwość środkowa · szerokość pasma " +"· Physical Cell ID" + +#: htdocs/luci-static/resources/view/wwand/status.js:986 +msgid "" +"The Cell ID split into the base station (eNB = eNodeB, ID = Cell ID / 256) " +"and its cell, often one sector of the site (Cell ID % 256). Cells with the " +"same eNB ID belong to the same base station, which may drive radios at more " +"than one site" +msgstr "" +"Cell ID rozbity na stację bazową (eNB = eNodeB, ID = Cell ID / 256) i jej " +"komórkę, często jeden sektor lokalizacji (Cell ID % 256). Komórki o tym " +"samym eNB ID należą do tej samej stacji bazowej, która może obsługiwać radia " +"w więcej niż jednej lokalizacji" + +#: htdocs/luci-static/resources/view/wwand/status.js:936 +msgid "" +"The LTE/5G attach the modem performed, and the 3GPP cause when the network " +"refused it. Reported separately from registration because the two can fail " +"independently — a wrong attach APN registers fine and never attaches" +msgstr "" +"Dołączenie LTE/5G wykonane przez modem oraz przyczyna 3GPP, gdy sieć je " +"odrzuciła. Raportowane osobno od rejestracji, ponieważ oba mogą zawieść " +"niezależnie — zły APN dołączania rejestruje się poprawnie i nigdy się nie " +"dołącza" + +#: htdocs/luci-static/resources/view/wwand/modems.js:455 +msgid "The PUK must be exactly 8 digits." +msgstr "PUK musi mieć dokładnie 8 cyfr." + +#: htdocs/luci-static/resources/view/wwand/status.js:215 +msgid "" +"The QMI LOC position session. Started when the modem registers and stopped " +"before its client is released; a firmware may accept it and still send " +"nothing, which shows as no sentences below." +msgstr "" +"Sesja pozycji QMI LOC. Uruchamiana przy rejestracji modemu i zatrzymywana " +"przed zwolnieniem jego klienta; firmware może ją zaakceptować i mimo to nic " +"nie wysyłać, co objawia się brakiem zdań poniżej." + +#: htdocs/luci-static/resources/view/wwand/modems.js:144 +msgid "" +"The QRTR node of this modem, for a box with more than one QRTR modem. Leave " +"empty to use the first node that serves QMI DMS and WDS." +msgstr "" +"Węzeł QRTR tego modemu, dla urządzenia z więcej niż jednym modemem QRTR. " +"Pozostaw puste, aby użyć pierwszego węzła obsługującego QMI DMS i WDS." + +#: htdocs/luci-static/resources/view/wwand/settings.js:822 +msgid "" +"The RAT priority did not take either: the modem accepted it and kept its own " +"order." +msgstr "" + +#: htdocs/luci-static/resources/view/wwand/status.js:653 +msgid "" +"The card reports itself busy. Reads of ICCID, IMSI and the PIN state will " +"fail until it clears, which is why those rows may be missing." +msgstr "" +"Karta zgłasza zajętość. Odczyty ICCID, IMSI i stanu PIN będą się nie udawać, " +"dopóki zajętość nie ustąpi, dlatego tych wierszy może brakować." + +#: htdocs/luci-static/resources/view/wwand/modems.js:211 +msgid "" +"The card the modem is using: its readiness, which physical slot is live, and " +"whether that slot holds an eUICC (eSIM)." +msgstr "" +"Karta używana przez modem: jej gotowość, który slot fizyczny jest aktywny i " +"czy ten slot zawiera eUICC (eSIM)." + +#: htdocs/luci-static/resources/wwand/simlist.js:133 +msgid "The card's ICCID (printed on the SIM / shown on the modem status page)." +msgstr "" +"ICCID karty (wydrukowany na karcie SIM / pokazany na stronie statusu modemu)." + +#: htdocs/luci-static/resources/view/wwand/settings.js:535 +msgid "" +"The carrier profile (MBN) the modem applies: APN defaults, IMS settings, " +"band and roaming policy for a given network. The wrong one leaves the modem " +"technically working but subtly wrong on that network — a rejected attach, no " +"IMS, a missing band." +msgstr "" +"Profil operatora (MBN) stosowany przez modem: domyślne APN, ustawienia IMS, " +"polityka pasm i roamingu dla danej sieci. Zły profil sprawia, że modem " +"technicznie działa, ale subtelnie źle w tej sieci — odrzucone dołączenie, " +"brak IMS, brakujące pasmo." + +#: htdocs/luci-static/resources/view/wwand/modems.js:201 +msgid "" +"The control protocol wwand speaks to this modem: QMI, MBIM or NCM. Set by " +"`option protocol`, or detected from the control device." +msgstr "" +"Protokół sterowania, którym wwand komunikuje się z tym modemem: QMI, MBIM " +"lub NCM. Ustawiany przez `option protocol` lub wykrywany z urządzenia " +"sterującego." + +#: htdocs/luci-static/resources/view/wwand/modems.js:215 +msgid "" +"The current signal, per radio technology. RSRP is reception strength, RSRQ " +"quality, SNR the noise margin — RSRP alone does not say whether the link is " +"good." +msgstr "" +"Bieżący sygnał według technologii radiowej. RSRP to siła odbioru, RSRQ " +"jakość, SNR margines szumu — sam RSRP nie mówi, czy łącze jest dobre." + +#: htdocs/luci-static/resources/view/wwand/modems.js:433 +msgid "" +"The daemon refuses to auto-enter the PIN (%s) to protect the last attempts. " +"Enter the PIN to release it manually." +msgstr "" +"Demon odmawia automatycznego wprowadzenia PIN-u (%s), aby chronić ostatnie " +"próby. Wprowadź PIN, aby zwolnić go ręcznie." + +#: htdocs/luci-static/resources/wwand/graph.js:115 +msgid "" +"The downlink bandwidth of the aggregated carriers added up, per radio " +"technology — the width of the pipe, which is what carrier aggregation is " +"for. It moves independently of the carrier count: three 10 MHz carriers are " +"less than one 20 MHz carrier. Drawn only where the modem reports a width per " +"carrier. The 5G line is usually absent for that reason: the 5G rows of the " +"carrier list carry a bandwidth whose unit this tree has not verified against " +"hardware, and a confidently wrong 100 MHz is worse than a gap." +msgstr "" +"Szerokość pasma łącza w dół zagregowanych nośnych zsumowana, dla każdej " +"technologii radiowej — szerokość „rury”, czemu służy agregacja nośnych. " +"Zmienia się niezależnie od liczby nośnych: trzy nośne po 10 MHz to mniej niż " +"jedna nośna 20 MHz. Rysowana tylko tam, gdzie modem raportuje szerokość dla " +"każdej nośnej. Linia 5G zwykle z tego powodu nie występuje: wiersze 5G na " +"liście nośnych niosą szerokość, której jednostki to drzewo nie zweryfikowało " +"na sprzęcie, a pewne siebie, błędne 100 MHz jest gorsze niż luka." + +#: htdocs/luci-static/resources/view/wwand/settings.js:70 +msgid "" +"The eUICC refused to enable this profile: while a test profile is enabled, " +"only the profile that was active before it can be re-enabled (SGP.22 " +"\"wrongProfileReenabling\")." +msgstr "" +"eUICC odmówiło włączenia tego profilu: gdy włączony jest profil testowy, " +"można ponownie włączyć tylko profil, który był aktywny przed nim (SGP.22 " +"„wrongProfileReenabling”)." + +#: htdocs/luci-static/resources/view/wwand/modems.js:196 +msgid "" +"The kernel network device the modem's data runs on, with the model behind " +"it. A muxed connection names the child device (wwan0m1), not the parent." +msgstr "" +"Urządzenie sieciowe jądra, na którym działają dane modemu, wraz z modelem za " +"nim. Połączenie z mux wskazuje urządzenie potomne (wwan0m1), a nie nadrzędne." + +#: htdocs/luci-static/resources/view/wwand/status.js:657 +msgid "" +"The last thing the card said about itself: a session it closed and why, an " +"internal recovery, or an activation that did not complete." +msgstr "" +"Ostatnie, co karta powiedziała o sobie: zamknięta sesja i dlaczego, " +"wewnętrzne odzyskiwanie lub aktywacja, która nie została ukończona." + +#: htdocs/luci-static/resources/wwand/logbox.js:366 +msgid "" +"The level the wwand PROCESS logs at — not a display filter. Takes effect at " +"once and is NOT written to the configuration: a restart or a config reload " +"returns to the configured level." +msgstr "" +"Poziom, na którym loguje PROCES wwand — nie filtr wyświetlania. Działa " +"natychmiast i NIE jest zapisywane w konfiguracji: restart lub przeładowanie " +"konfiguracji wraca do skonfigurowanego poziomu." + +#: htdocs/luci-static/resources/wwand/graph.js:82 +msgid "" +"The measure to aim an antenna by, and the same steps the router's signal " +"LEDs use: the serving cell's own power, RSRP on LTE and 5G, RSCP on 3G. LTE " +"and 5G NR are graded alike, because published tables give SSB-RSRP the same " +"boundaries as LTE RSRP below 6 GHz. RSSI is a different measure on its own " +"graph." +msgstr "" +"Miara do celowania anteny, i te same stopnie, których używają diody sygnału " +"routera: własna moc komórki obsługującej, RSRP w LTE i 5G, RSCP w 3G. LTE i " +"5G NR są oceniane tak samo, ponieważ opublikowane tabele nadają SSB-RSRP te " +"same progi co RSRP LTE poniżej 6 GHz. RSSI to inna miara i ma własny wykres." + +#: htdocs/luci-static/resources/view/wwand/settings.js:103 +msgid "The modem did not answer in time." +msgstr "Modem nie odpowiedział na czas." + +#: htdocs/luci-static/resources/view/wwand/settings.js:99 +msgid "The modem does not support that operation." +msgstr "Modem nie obsługuje tej operacji." + +#: htdocs/luci-static/resources/wwand/esim.js:439 +msgid "" +"The modem downloads over its own network attach — no router data path or APN " +"needed." +msgstr "" +"Modem pobiera przez własne dołączenie do sieci — nie potrzeba ścieżki danych " +"routera ani APN." + +#: htdocs/luci-static/resources/view/wwand/status.js:643 +msgid "" +"The modem has not yet answered in the control protocol wwand is using, so no " +"hardware recovery step will run — repowering a modem that was never broken " +"only adds outages. Check the control protocol setting and the bound driver." +msgstr "" +"Modem nie odpowiedział jeszcze w używanym przez wwand protokole sterowania, " +"więc żaden sprzętowy krok odzyskiwania nie zostanie wykonany — ponowne " +"zasilanie modemu, który nigdy nie był zepsuty, tylko dodaje przerw. Sprawdź " +"ustawienie protokołu sterowania i powiązany sterownik." + +#: htdocs/luci-static/resources/view/wwand/status.js:568 +msgid "" +"The modem has two independent radio switches. Software is the one wwand " +"controls and switches on at start-up; hardware is a physical switch or an " +"airplane-mode toggle on the host, which wwand cannot override — a modem held " +"off by it will not register no matter how often it is reset." +msgstr "" +"Modem ma dwa niezależne przełączniki radia. Programowy to ten, który " +"kontroluje wwand i włącza przy starcie; sprzętowy to fizyczny przełącznik " +"lub przełącznik trybu samolotowego na hoście, którego wwand nie może " +"nadpisać — modem przez niego wyłączony nie zarejestruje się, niezależnie od " +"liczby resetów." + +#: htdocs/luci-static/resources/view/wwand/settings.js:102 +msgid "The modem is busy with another operation. Try again in a moment." +msgstr "Modem jest zajęty inną operacją. Spróbuj ponownie za chwilę." + +#: htdocs/luci-static/resources/view/wwand/status.js:602 +msgid "" +"The modem is holding its own radio back — usually reduced transmit power. " +"This is the explanation for throughput that drops while the signal stays " +"good." +msgstr "" +"Modem sam ogranicza swoje radio — zwykle przez zmniejszenie mocy nadawania. " +"To wyjaśnia przepustowość, która spada, gdy sygnał pozostaje dobry." + +#: htdocs/luci-static/resources/view/wwand/settings.js:105 +msgid "The modem is not ready yet." +msgstr "Modem nie jest jeszcze gotowy." + +#: htdocs/luci-static/resources/wwand/esim.js:87 +msgid "The modem returned no profiles for this slot." +msgstr "Modem nie zwrócił profili dla tego slotu." + +#: htdocs/luci-static/resources/view/wwand/status.js:222 +msgid "" +"The modem serial port that carries NMEA sentences. wwand finds it during " +"enumeration; with wwand-gps installed it also reads it." +msgstr "" +"Port szeregowy modemu przenoszący zdania NMEA. wwand znajduje go podczas " +"enumeracji; z zainstalowanym wwand-gps również go odczytuje." + +#: htdocs/luci-static/resources/view/wwand/status.js:685 +msgid "" +"The modem was detached from the network by hand. Its interfaces stay down " +"until it is attached again (modem list → Attach, or wwandctl attach); a " +"modem reset attaches it too." +msgstr "" +"Modem został odłączony od sieci ręcznie. Jego interfejsy pozostają wyłączone " +"do ponownego dołączenia (lista modemów → Dołącz lub wwandctl attach); reset " +"modemu również go dołącza." + +#: htdocs/luci-static/resources/view/wwand/settings.js:107 +msgid "" +"The modem's current radio-technology setting does not allow a band list to " +"be written." +msgstr "" +"Bieżące ustawienie technologii radiowej modemu nie pozwala na zapis listy " +"pasm." + +#: htdocs/luci-static/resources/view/wwand/status.js:948 +msgid "" +"The network currently serving the modem, as name (MCC/MNC). MCC = country, " +"MNC = network within it" +msgstr "" +"Sieć aktualnie obsługująca modem, jako nazwa (MCC/MNC). MCC = kraj, MNC = " +"sieć w nim" + +#: htdocs/luci-static/resources/view/wwand/modems.js:213 +msgid "" +"The network the modem is registered on, or why it is not. A reject cause " +"here is the network refusing, not the modem failing." +msgstr "" +"Sieć, w której modem jest zarejestrowany, lub dlaczego nie jest. Przyczyna " +"odrzucenia tutaj oznacza odmowę sieci, a nie awarię modemu." + +#: htdocs/luci-static/resources/view/wwand/modems.js:456 +msgid "The new PIN must be 4-8 digits." +msgstr "Nowy PIN musi mieć 4–8 cyfr." + +#: htdocs/luci-static/resources/wwand/esim.js:391 +msgid "The operation failed." +msgstr "Operacja nie powiodła się." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1298 +msgid "" +"The order in which the modem searches the radio technologies, first row " +"first. Only ticked ones are searched: a technology allowed above but " +"unticked here may never be used — a modem with 5G enabled and NR5G missing " +"from this list may never try 5G standalone. Applying a change re-attaches " +"the modem, so its connections drop briefly. Kept in the modem's own memory; " +"to have wwand restore it on every start, set \"RAT priority\" on the modem " +"page." +msgstr "" +"Kolejność, w jakiej modem przeszukuje technologie radiowe – zaczynając od " +"pierwszego wiersza. Przeszukiwane są tylko zaznaczone pozycje: technologia " +"dozwolona powyżej, ale niezaznaczona tutaj, może nigdy nie zostać " +"wykorzystana — modem z włączoną obsługą 5G, w przypadku którego na tej " +"liście brakuje pozycji NR5G, może nigdy nie próbować połączenia 5G w trybie " +"autonomicznym. Zastosowanie zmiany powoduje ponowne podłączenie/uruchomienie " +"modemu, co skutkuje krótkotrwałym zerwaniem połączenia. Ustawienia są " +"przechowywane w pamięci modemu, aby program wwand przywracał je przy każdym " +"uruchomieniu należy ustawić opcję \"Priorytet RAT\" na stronie modemu." + +#: htdocs/luci-static/resources/view/wwand/status.js:712 +msgid "The phone number stored on the SIM (often empty on data SIMs)" +msgstr "Numer telefonu zapisany na karcie SIM (często pusty na kartach danych)" + +#: htdocs/luci-static/resources/view/wwand/status.js:1101 +msgid "" +"The radio-stack figures come from the modem itself (MBIM SYS_CAPS); the slot " +"count is how many slots it enumerated. A modem can advertise more slots than " +"the product actually gives you a card reader for, and may then report the " +"extra one as empty — these protocols carry no signal that separates that " +"from a reader you simply left empty." +msgstr "" +"Dane o stosach radiowych pochodzą z samego modemu (MBIM SYS_CAPS); liczba " +"slotów to liczba slotów, które wyliczył. Modem może deklarować więcej " +"slotów, niż produkt faktycznie daje czytników kart, i wtedy zgłaszać " +"dodatkowy jako pusty — te protokoły nie niosą sygnału odróżniającego to od " +"czytnika, który po prostu zostawiono pustym." + +#: htdocs/luci-static/resources/view/wwand/status.js:602 +#: htdocs/luci-static/resources/view/wwand/status.js:611 +msgid "Thermal" +msgstr "Termika" + +#: htdocs/luci-static/resources/view/wwand/modems.js:429 +msgid "" +"This SIM is PUK-locked (%s). A WRONG PUK consumes one of ~10 attempts — " +"after the last one the SIM is permanently destroyed. The PUK is printed on " +"the SIM carrier or available from the provider." +msgstr "" +"Ta karta SIM jest zablokowana PUK-iem (%s). BŁĘDNY PUK zużywa jedną z ok. 10 " +"prób — po ostatniej karta SIM zostaje trwale zniszczona. PUK jest " +"wydrukowany na nośniku karty SIM lub dostępny u dostawcy." + +#: htdocs/luci-static/resources/view/wwand/status.js:1050 +msgid "This backend does not report whether a PIN is set." +msgstr "Ten backend nie raportuje, czy ustawiono PIN." + +#: htdocs/luci-static/resources/wwand/esim.js:278 +msgid "This card is managed by %s. Profile changes are made there, not here." +msgstr "" +"Ta karta jest zarządzana przez %s. Zmiany profili wprowadza się tam, a nie " +"tutaj." + +#: htdocs/luci-static/resources/wwand/esim.js:252 +msgid "" +"This is the normal behaviour of M2M eUICCs (SGP.02) and of vendor-locked " +"cards: profiles are managed over-the-air by the SIM provider (SM-SR), so " +"downloading or switching profiles from this router is not possible. Use the " +"provider’s portal to manage the card — the router only needs to keep the " +"active profile registered and online." +msgstr "" +"To normalne zachowanie eUICC M2M (SGP.02) i kart zablokowanych przez " +"producenta: profilami zarządza zdalnie dostawca SIM (SM-SR), więc pobieranie " +"ani przełączanie profili z tego routera nie jest możliwe. Do zarządzania " +"kartą użyj portalu dostawcy — router musi tylko utrzymywać aktywny profil " +"zarejestrowany i online." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1129 +msgid "This modem does not expose SMS access." +msgstr "Ten modem nie udostępnia dostępu do SMS." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1316 +msgid "" +"This modem forgets its bands when it is power-cycled, so they are kept in " +"the router configuration (band_lte / band_nr of the modem) and applied again " +"at every start." +msgstr "" +"Ten modem zapomina pasma po wyłączeniu i włączeniu zasilania, więc są one " +"przechowywane w konfiguracji routera (band_lte / band_nr modemu) i stosowane " +"ponownie przy każdym starcie." + +#: htdocs/luci-static/resources/view/wwand/status.js:212 +msgid "" +"This modem has no NMEA port of its own, so wwand-gps asks its QMI location " +"service (LOC) for the same NMEA sentences — natively on a QMI modem, over " +"the QMI passthrough on an MBIM one." +msgstr "" +"Ten modem nie ma własnego portu NMEA, więc wwand-gps prosi jego usługę " +"lokalizacji QMI (LOC) o te same zdania NMEA — natywnie w modemie QMI, przez " +"przekazywanie QMI w modemie MBIM." + +#: htdocs/luci-static/resources/wwand/esim.js:86 +msgid "" +"This modem has no native eUICC interface either, so the card cannot be " +"enumerated from the router at all." +msgstr "" +"Ten modem nie ma też natywnego interfejsu eUICC, więc karty nie można w " +"ogóle wyliczyć z routera." + +#: htdocs/luci-static/resources/view/wwand/settings.js:765 +msgid "This modem restores \"%s\" before every radio-on." +msgstr "Ten modem przywraca „%s” przed każdym włączeniem radia." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1291 +msgid "This modem's bands cannot be changed from here right now." +msgstr "Pasm tego modemu nie można teraz zmieniać stąd." + +#: htdocs/luci-static/resources/view/wwand/settings.js:98 +msgid "This modem's control backend does not offer that operation." +msgstr "Backend sterowania tego modemu nie oferuje tej operacji." + +#: htdocs/luci-static/resources/view/wwand/status.js:874 +msgid "" +"This module boots radio-locked (laptop-SKU) and the modem will not register " +"while the lock is armed — set fcc_auth on the modem configuration to unlock " +"at boot" +msgstr "" +"Ten moduł uruchamia się z zablokowanym radiem (wersja laptopowa) i modem nie " +"zarejestruje się, dopóki blokada jest uzbrojona — ustaw fcc_auth w " +"konfiguracji modemu, aby odblokować przy starcie" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1118 +msgid "Tick messages and delete them in one step; shift-click selects a range." +msgstr "" +"Zaznacz wiadomości i usuń je jednym krokiem; Shift+klik zaznacza zakres." + +#: htdocs/luci-static/resources/view/wwand/modems.js:522 +#: htdocs/luci-static/resources/view/wwand/modems.js:523 +msgid "Tools" +msgstr "Narzędzia" + +#: htdocs/luci-static/resources/wwand/graph.js:87 +msgid "" +"Total received power in the band — the serving cell plus its neighbours plus " +"interference. Coarser than RSRP and graded 20 dB higher (-65/-75/-85 dBm), " +"which is why it has its own graph: a strong signal reaches -46 dBm, off the " +"top of any scale drawn for RSRP. On 2G it is the only measure there is, and " +"on a modem that reports nothing better it is all you get. Each line carries " +"the radio that measured it; the plain amber one appears only when the reply " +"is untagged." +msgstr "" +"Całkowita moc odebrana w paśmie — komórka obsługująca plus jej sąsiedzi plus " +"zakłócenia. Grubsza niż RSRP i oceniana o 20 dB wyżej (-65/-75/-85 dBm), " +"dlatego ma własny wykres: silny sygnał sięga -46 dBm, poza skalą narysowaną " +"dla RSRP. W 2G to jedyna dostępna miara, a w modemie, który nie raportuje " +"niczego lepszego, to wszystko, co dostajesz. Każda linia niesie radio, które " +"ją zmierzyło; zwykła bursztynowa pojawia się tylko wtedy, gdy odpowiedź nie " +"ma znacznika." + +#: htdocs/luci-static/resources/view/wwand/status.js:1009 +msgid "" +"Tracking Area Identity recorded at attach: PLMN and Tracking Area Code. " +"Differs from the serving cell TAC when the modem moved without re-attaching" +msgstr "" +"Tracking Area Identity zapisane przy dołączeniu: PLMN i Tracking Area Code. " +"Różni się od TAC komórki obsługującej, gdy modem się przemieścił bez " +"ponownego dołączenia" + +#: htdocs/luci-static/resources/view/wwand/modems.js:675 +#: htdocs/luci-static/resources/view/wwand/status.js:46 +msgid "Type" +msgstr "Typ" + +#: htdocs/luci-static/resources/view/wwand/settings.js:792 +msgid "Type · entries" +msgstr "Typ · wpisy" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1299 +msgid "UE usage" +msgstr "Użycie UE" + +#: htdocs/luci-static/resources/view/wwand/status.js:357 +msgid "URB / frame size" +msgstr "Rozmiar URB / ramki" + +#: htdocs/luci-static/resources/view/wwand/modems.js:466 +msgid "Unblock SIM" +msgstr "Odblokuj SIM (PUK)" + +#: htdocs/luci-static/resources/wwand/modemopts.js:572 +msgid "Unknown technology \"%s\" — use %s" +msgstr "" + +#: htdocs/luci-static/resources/view/wwand/modems.js:469 +#: htdocs/luci-static/resources/view/wwand/modems.js:525 +msgid "Unlock SIM" +msgstr "Odblokuj SIM" + +#: htdocs/luci-static/resources/view/wwand/modems.js:450 +msgid "Unlock failed: %s" +msgstr "Odblokowanie nie powiodło się: %s" + +#: htdocs/luci-static/resources/view/wwand/modems.js:421 +msgid "Unlock the blocked SIM (%s)" +msgstr "Odblokuj zablokowaną kartę SIM (%s)" + +#: htdocs/luci-static/resources/view/wwand/status.js:375 +msgid "Uplink NTB" +msgstr "NTB łącza w górę" + +#: htdocs/luci-static/resources/view/wwand/status.js:365 +msgid "Uplink aggregation (negotiated)" +msgstr "Agregacja łącza w górę (wynegocjowana)" + +#: htdocs/luci-static/resources/view/wwand/status.js:379 +msgid "Uplink coalescing timer" +msgstr "Licznik łączenia łącza w górę" + +#: htdocs/luci-static/resources/view/wwand/status.js:431 +msgid "Uplink packets / frame" +msgstr "Pakiety łącza w górę / ramkę" + +#: htdocs/luci-static/resources/view/wwand/status.js:127 +msgid "Uptime" +msgstr "Czas działania" + +#: htdocs/luci-static/resources/view/wwand/settings.js:748 +msgid "Use here" +msgstr "Użyj tutaj" + +#: htdocs/luci-static/resources/view/wwand/settings.js:742 +msgid "User" +msgstr "Użytkownik" + +#: htdocs/luci-static/resources/view/wwand/settings.js:598 +msgid "User list (SIM EF 6F60)" +msgstr "Lista użytkownika (SIM EF 6F60)" + +#: htdocs/luci-static/resources/wwand/simlist.js:206 +msgid "Username for the APN authentication." +msgstr "Nazwa użytkownika do uwierzytelniania APN." + +#: htdocs/luci-static/resources/wwand/modemopts.js:241 +msgid "" +"Username for the attach bearer. Ignored without an attach APN — it would " +"otherwise apply to whatever APN the modem's attach profile already held." +msgstr "" +"Nazwa użytkownika nośnika dołączania. Ignorowana bez APN dołączania — w " +"przeciwnym razie dotyczyłaby dowolnego APN, który miał już profil dołączania " +"modemu." + +#: htdocs/luci-static/resources/wwand/modemopts.js:431 +msgid "" +"Vendor AT commands sent once after the modem is detected, before " +"registration." +msgstr "" +"Polecenia AT producenta wysyłane raz po wykryciu modemu, przed rejestracją." + +#: htdocs/luci-static/resources/wwand/esim.js:29 +msgid "Verifying server" +msgstr "Weryfikowanie serwera" + +#: htdocs/luci-static/resources/view/wwand/status.js:231 +msgid "" +"What parses the NMEA and publishes a position. Not reading is a different " +"thing from having no fix — the receiver may be perfectly happy and nobody " +"listening." +msgstr "" +"Co przetwarza NMEA i publikuje pozycję. Brak odczytu to coś innego niż brak " +"fixa — odbiornik może być w pełni zadowolony, tylko nikt nie słucha." + +#: htdocs/luci-static/resources/view/wwand/status.js:860 +msgid "" +"What the hardware step of the recovery ladder would actually do on this box " +"for this modem — asked of the same code that performs it, not inferred." +msgstr "" +"Co faktycznie zrobiłby sprzętowy krok procedury odzyskiwania na tym " +"urządzeniu dla tego modemu — zapytane tego samego kodu, który go wykonuje, a " +"nie wnioskowane." + +#: htdocs/luci-static/resources/view/wwand/sims.js:91 +msgid "Where" +msgstr "Gdzie" + +#: htdocs/luci-static/resources/view/wwand/modems.js:209 +msgid "" +"Where the modem is in its bring-up: from ABSENT through SIM_UNLOCK and " +"REGISTERING to READY. A state that does not advance is what the log explains." +msgstr "" +"Na jakim etapie uruchamiania jest modem: od ABSENT przez SIM_UNLOCK i " +"REGISTERING do READY. Stan, który nie postępuje, jest tym, co wyjaśnia " +"dziennik." + +#: htdocs/luci-static/resources/view/wwand/status.js:671 +msgid "" +"Whether the modem offers QMI UIM Remote, the service a modem needs to run on " +"a SIM card that is not in its own slot (a card in a reader, a phone, or " +"another modem — the wwand-rsim package). Read from the services the modem " +"lists itself; a Quectel lists it even while its switch for it is off " +"(wwandctl rsim MODEM switch)." +msgstr "" +"Czy modem oferuje QMI UIM Remote — usługę potrzebną modemowi do pracy na " +"karcie SIM spoza własnego slotu (karta w czytniku, telefonie lub innym " +"modemie — pakiet wwand-rsim). Odczytane z usług, które modem sam wymienia; " +"Quectel wymienia ją nawet wtedy, gdy jego przełącznik jest wyłączony " +"(wwandctl rsim MODEM switch)." + +#: htdocs/luci-static/resources/view/wwand/status.js:225 +msgid "" +"Whether the modem's own GNSS engine has been switched on. wwand starts it " +"with the vendor AT command when `option gnss` is set; without that the port " +"exists and nothing is sent on it." +msgstr "" +"Czy włączono własny silnik GNSS modemu. wwand uruchamia go poleceniem AT " +"producenta, gdy ustawiono `option gnss`; bez tego port istnieje i nic się na " +"nim nie wysyła." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1295 +msgid "" +"Which generations the modem may use. Unchecking one stops it being used at " +"all — a modem restricted to 5G will not fall back to LTE where 5G is absent, " +"so leave everything the network offers checked unless you are deliberately " +"pinning a technology." +msgstr "" +"Z jakich generacji modem może korzystać. Odznaczenie jednej oznacza, że nie " +"będzie w ogóle używana — modem ograniczony do 5G nie wróci do LTE tam, gdzie " +"5G nie ma, więc zostaw zaznaczone wszystko, co oferuje sieć, chyba że celowo " +"przypinasz technologię." + +#: htdocs/luci-static/resources/view/wwand/modems.js:130 +msgid "Which modem, and its SIM." +msgstr "Który modem i jego SIM." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1302 +msgid "" +"Which networks the modem may register on. \"Home only\" refuses roaming " +"partners outright — useful against accidental cross-border charges, and the " +"reason a SIM that works elsewhere may show \"no service\" here." +msgstr "" +"W jakich sieciach modem może się rejestrować. „Home only” wprost odmawia " +"partnerom roamingowym — przydatne przeciw przypadkowym opłatom za granicą i " +"powód, dla którego karta SIM działająca gdzie indziej może tu pokazywać " +"„brak usługi”." + +#: htdocs/luci-static/resources/wwand/modemopts.js:211 +msgid "" +"Which protocol wwand uses to drive this modem. Leave on \"detect\" — this " +"exists for a control device whose driver wwand cannot classify, and it will " +"say so in the log. Pinning a protocol that contradicts a driver wwand does " +"recognise disables hardware recovery for this modem, because an answering AT " +"port would then prove nothing." +msgstr "" +"Którego protokołu wwand używa do sterowania tym modemem. Pozostaw „wykrywaj” " +"— istnieje to dla urządzenia sterującego, którego sterownika wwand nie " +"potrafi sklasyfikować, i zapisze to w dzienniku. Przypięcie protokołu " +"sprzecznego ze sterownikiem, który wwand rozpoznaje, wyłącza sprzętowe " +"odzyskiwanie tego modemu, ponieważ odpowiadający port AT niczego by wtedy " +"nie dowodził." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1147 +msgid "" +"Which store to list and delete from — the SIM card or the modem's own " +"memory. They are read one at a time, so a message you expect and cannot see " +"may simply be in the other one. A full store silently stops accepting new " +"messages." +msgstr "" +"Z którego magazynu wyświetlać i usuwać — z karty SIM czy z własnej pamięci " +"modemu. Są odczytywane po jednym, więc wiadomość, której się spodziewasz, a " +"której nie widać, może po prostu być w drugim. Pełny magazyn po cichu " +"przestaje przyjmować nowe wiadomości." + +#: htdocs/luci-static/resources/view/wwand/settings.js:918 +msgid "" +"Without this the lock is lost on the next modem reset — which is the safer " +"default: a persisted lock survives a reboot, so a cell that disappears " +"leaves the modem searching for it with no obvious way back short of clearing " +"the lock by hand." +msgstr "" +"Bez tego blokada zostaje utracona przy następnym resecie modemu — co jest " +"bezpieczniejszą wartością domyślną: utrwalona blokada przetrwa restart, więc " +"komórka, która zniknie, zostawia modem szukający jej bez oczywistej drogi " +"powrotu poza ręcznym usunięciem blokady." + +#: htdocs/luci-static/resources/view/wwand/settings.js:674 +msgid "Write %d record(s) to the modem's %s list now? (not saved to config)" +msgstr "" +"Zapisać rekordy (%d) na liście %s modemu teraz? (nie zapisano w konfiguracji)" + +#: htdocs/luci-static/resources/view/wwand/settings.js:687 +msgid "Write failed: %s" +msgstr "Zapis nie powiódł się: %s" + +#: htdocs/luci-static/resources/view/wwand/settings.js:684 +msgid "Write failed: %s. The SIM/modem may reject it." +msgstr "Zapis nie powiódł się: %s. Karta SIM/modem mogą go odrzucać." + +#: htdocs/luci-static/resources/view/wwand/settings.js:732 +msgid "Write to modem now" +msgstr "Zapisz do modemu teraz" + +#: htdocs/luci-static/resources/view/wwand/settings.js:681 +msgid "Written to the modem (%d record(s))." +msgstr "Zapisano do modemu (rekordów: %d)." + +#: htdocs/luci-static/resources/wwand/modemopts.js:689 +msgid "Zero-RX timeout" +msgstr "Limit czasu zerowego RX" + +#: htdocs/luci-static/resources/view/wwand/status.js:194 +msgid "`option gnss` is not set, so the receiver was never started" +msgstr "" +"`option gnss` nie jest ustawione, więc odbiornik nigdy nie został uruchomiony" + +#: htdocs/luci-static/resources/view/wwand/settings.js:508 +#: htdocs/luci-static/resources/wwand/esim.js:103 +#: htdocs/luci-static/resources/wwand/format.js:471 +msgid "active" +msgstr "aktywny" + +#: htdocs/luci-static/resources/view/wwand/status.js:875 +msgid "active (mode %d)" +msgstr "aktywny (tryb %d)" + +#: htdocs/luci-static/resources/wwand/format.js:1071 +msgid "actual" +msgstr "rzeczywista" + +#: htdocs/luci-static/resources/wwand/modemopts.js:63 +msgid "add-on" +msgstr "dodatek" + +#: htdocs/luci-static/resources/view/wwand/settings.js:197 +msgid "additional (non-listed) band numbers, comma/space separated" +msgstr "dodatkowe numery pasm (spoza listy), oddzielone przecinkami/spacjami" + +#: htdocs/luci-static/resources/wwand/logbox.js:164 +msgid "all" +msgstr "wszystkie" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1194 +msgid "any" +msgstr "dowolne" + +#: htdocs/luci-static/resources/wwand/logbox.js:168 +#: htdocs/luci-static/resources/wwand/logbox.js:264 +msgid "any connection" +msgstr "dowolne połączenie" + +#: htdocs/luci-static/resources/wwand/logbox.js:165 +msgid "any level" +msgstr "dowolny poziom" + +#: htdocs/luci-static/resources/wwand/logbox.js:167 +#: htdocs/luci-static/resources/wwand/logbox.js:263 +msgid "any modem" +msgstr "dowolny modem" + +#: htdocs/luci-static/resources/wwand/logbox.js:169 +#: htdocs/luci-static/resources/wwand/logbox.js:265 +msgid "any subsystem" +msgstr "dowolny podsystem" + +#: htdocs/luci-static/resources/wwand/logbox.js:342 +msgid "applying…" +msgstr "stosowanie…" + +#: htdocs/luci-static/resources/view/wwand/status.js:737 +#: htdocs/luci-static/resources/wwand/format.js:1350 +msgid "armed" +msgstr "uzbrojone" + +#: htdocs/luci-static/resources/view/wwand/status.js:735 +msgid "armed · %d/%d" +msgstr "uzbrojone · %d/%d" + +#: htdocs/luci-static/resources/wwand/format.js:390 +msgid "at least %s" +msgstr "co najmniej %s" + +#: htdocs/luci-static/resources/wwand/modemopts.js:271 +msgid "auto (QMI only)" +msgstr "auto (tylko QMI)" + +#: htdocs/luci-static/resources/wwand/logbox.js:360 +msgid "auto-refresh" +msgstr "automatyczne odświeżanie" + +#: htdocs/luci-static/resources/view/wwand/modems.js:158 +#: htdocs/luci-static/resources/wwand/netsel.js:33 +msgid "automatic" +msgstr "automatyczny" + +#: htdocs/luci-static/resources/wwand/netsel.js:67 +msgid "available" +msgstr "dostępna" + +#: htdocs/luci-static/resources/wwand/format.js:1274 +msgid "blocked (%s)" +msgstr "zablokowana — %s" + +#: htdocs/luci-static/resources/view/wwand/status.js:1044 +msgid "blocked — %s" +msgstr "zablokowana — %s" + +#: htdocs/luci-static/resources/wwand/esim.js:193 +msgid "blocked — PUK required" +msgstr "zablokowana — wymagany PUK" + +#: htdocs/luci-static/resources/view/wwand/status.js:792 +msgid "board default" +msgstr "domyślne płyty" + +#: htdocs/luci-static/resources/wwand/format.js:380 +msgid "both registered, one carries data" +msgstr "obie zarejestrowane, jedna przenosi dane" + +#: htdocs/luci-static/resources/wwand/format.js:381 +msgid "both usable at once" +msgstr "obie używalne jednocześnie" + +#: htdocs/luci-static/resources/wwand/format.js:1285 +msgid "busy (reads failing)" +msgstr "zajęta — odczyty zawodzą" + +#: htdocs/luci-static/resources/view/wwand/status.js:654 +msgid "busy — reads failing" +msgstr "zajęta — odczyty zawodzą" + +#: htdocs/luci-static/resources/wwand/esim.js:194 +msgid "card busy — cannot be read right now" +msgstr "karta zajęta — nie można jej teraz odczytać" + +#: htdocs/luci-static/resources/wwand/format.js:613 +msgid "card error" +msgstr "błąd karty" + +#: htdocs/luci-static/resources/wwand/format.js:611 +msgid "card present" +msgstr "karta obecna" + +#: htdocs/luci-static/resources/view/wwand/status.js:922 +msgid "cause %d" +msgstr "przyczyna %d" + +#: htdocs/luci-static/resources/view/wwand/status.js:145 +msgid "code" +msgstr "kod" + +#: htdocs/luci-static/resources/view/wwand/settings.js:116 +msgid "code %d" +msgstr "kod %d" + +#: htdocs/luci-static/resources/view/wwand/modems.js:704 +msgid "configuring…" +msgstr "konfigurowanie…" + +#: htdocs/luci-static/resources/wwand/esim.js:451 +msgid "confirmation code (optional)" +msgstr "kod potwierdzenia (opcjonalnie)" + +#: htdocs/luci-static/resources/wwand/netsel.js:65 +msgid "current" +msgstr "bieżąca" + +#: htdocs/luci-static/resources/wwand/esim.js:200 +msgid "current PIN" +msgstr "bieżący PIN" + +#: htdocs/luci-static/resources/wwand/modemopts.js:265 +msgid "custom (decoded)" +msgstr "niestandardowe (zdekodowane)" + +#: htdocs/luci-static/resources/wwand/modemopts.js:264 +msgid "custom (raw)" +msgstr "niestandardowe (surowe)" + +#: htdocs/luci-static/resources/view/wwand/status.js:472 +msgid "cycle the radio" +msgstr "wyłącz i włącz radio" + +#: htdocs/luci-static/resources/wwand/logbox.js:333 +msgid "daemon not answering" +msgstr "demon nie odpowiada" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1189 +msgid "data centric" +msgstr "skoncentrowane na danych" + +#: htdocs/luci-static/resources/wwand/modemopts.js:262 +msgid "decoded" +msgstr "zdekodowane" + +#: htdocs/luci-static/resources/wwand/modemopts.js:263 +msgid "decoded, pull-only" +msgstr "zdekodowane, tylko pobieranie" + +#: htdocs/luci-static/resources/wwand/format.js:46 +msgid "deconfigured" +msgstr "zdekonfigurowany" + +#: htdocs/luci-static/resources/wwand/esim.js:107 +msgid "delete not allowed" +msgstr "usuwanie niedozwolone" + +#: htdocs/luci-static/resources/view/wwand/status.js:686 +msgid "detached by the operator" +msgstr "odłączony ręcznie" + +#: htdocs/luci-static/resources/wwand/modemopts.js:212 +msgid "detect (recommended)" +msgstr "wykrywaj (zalecane)" + +#: htdocs/luci-static/resources/wwand/modemopts.js:259 +msgid "disabled" +msgstr "wyłączone" + +#: htdocs/luci-static/resources/wwand/esim.js:195 +msgid "disabled (SIM boots without PIN)" +msgstr "wyłączone (SIM uruchamia się bez PIN-u)" + +#: htdocs/luci-static/resources/view/wwand/status.js:986 +msgid "eNB / Sector" +msgstr "eNB / sektor" + +#: htdocs/luci-static/resources/view/wwand/sims.js:94 +#: htdocs/luci-static/resources/wwand/esim.js:248 +#: htdocs/luci-static/resources/wwand/simlist.js:41 +msgid "eSIM" +msgstr "eSIM" + +#: htdocs/luci-static/resources/wwand/simlist.js:41 +msgid "eSIM %s" +msgstr "eSIM %s" + +#: htdocs/luci-static/resources/wwand/esim.js:245 +msgid "eSIM management: package wwand-esim is not installed." +msgstr "Zarządzanie eSIM: pakiet wwand-esim nie jest zainstalowany." + +#: htdocs/luci-static/resources/wwand/esim.js:325 +msgid "eSIM profiles" +msgstr "Profile eSIM" + +#: htdocs/luci-static/resources/view/wwand/sims.js:51 +#: htdocs/luci-static/resources/wwand/format.js:497 +msgid "eUICC" +msgstr "eUICC" + +#: htdocs/luci-static/resources/wwand/format.js:456 +msgid "eUICC (eSIM)" +msgstr "eUICC (eSIM)" + +#: htdocs/luci-static/resources/wwand/esim.js:27 +msgid "eUICC challenge" +msgstr "Wyzwanie eUICC" + +#: htdocs/luci-static/resources/wwand/esim.js:250 +msgid "" +"eUICC detected, but the card refuses local eSIM management (ES10 rejected, " +"SW %s)." +msgstr "" +"Wykryto eUICC, ale karta odmawia lokalnego zarządzania eSIM (ES10 odrzucone, " +"SW %s)." + +#: htdocs/luci-static/resources/wwand/format.js:552 +msgid "eUICC with no profile installed" +msgstr "eUICC bez zainstalowanego profilu" + +#: htdocs/luci-static/resources/wwand/format.js:612 +#: htdocs/luci-static/resources/wwand/modemopts.js:485 +msgid "empty" +msgstr "pusty" + +#: htdocs/luci-static/resources/wwand/esim.js:196 +#: htdocs/luci-static/resources/wwand/format.js:582 +msgid "enabled" +msgstr "włączony" + +#: htdocs/luci-static/resources/wwand/graph.js:80 +#: htdocs/luci-static/resources/wwand/graph.js:85 +#: htdocs/luci-static/resources/wwand/graph.js:90 +#: htdocs/luci-static/resources/wwand/graph.js:95 +#: htdocs/luci-static/resources/wwand/graph.js:118 +msgid "excellent" +msgstr "doskonały" + +#: htdocs/luci-static/resources/wwand/format.js:1070 +msgid "expected" +msgstr "oczekiwana" + +#: htdocs/luci-static/resources/wwand/graph.js:80 +#: htdocs/luci-static/resources/wwand/graph.js:85 +#: htdocs/luci-static/resources/wwand/graph.js:90 +#: htdocs/luci-static/resources/wwand/graph.js:95 +#: htdocs/luci-static/resources/wwand/graph.js:118 +msgid "fair" +msgstr "przeciętny" + +#: htdocs/luci-static/resources/wwand/netsel.js:66 +msgid "forbidden" +msgstr "zabroniona" + +#: htdocs/luci-static/resources/view/wwand/status.js:792 +msgid "from this modem's configuration" +msgstr "z konfiguracji tego modemu" + +#: htdocs/luci-static/resources/wwand/graph.js:80 +#: htdocs/luci-static/resources/wwand/graph.js:85 +#: htdocs/luci-static/resources/wwand/graph.js:90 +#: htdocs/luci-static/resources/wwand/graph.js:95 +#: htdocs/luci-static/resources/wwand/graph.js:118 +msgid "good" +msgstr "dobry" + +#: htdocs/luci-static/resources/wwand/format.js:291 +msgid "" +"in the card (IPAe) — the card and its eIM manage the profiles; the router " +"has no ES10 access" +msgstr "" +"w karcie (IPAe) — karta i jej eIM zarządzają profilami; router nie ma " +"dostępu do ES10" + +#: htdocs/luci-static/resources/view/wwand/sims.js:42 +#: htdocs/luci-static/resources/wwand/simlist.js:44 +msgid "in use" +msgstr "w użyciu" + +#: htdocs/luci-static/resources/wwand/format.js:46 +msgid "inactive" +msgstr "nieaktywny" + +#: htdocs/luci-static/resources/wwand/format.js:395 +msgid "inferred" +msgstr "wywnioskowane" + +#: htdocs/luci-static/resources/view/wwand/status.js:897 +#: htdocs/luci-static/resources/view/wwand/status.js:899 +msgid "limited service" +msgstr "usługa ograniczona" + +#: htdocs/luci-static/resources/view/wwand/status.js:1281 +msgid "loading…" +msgstr "wczytywanie…" + +#: htdocs/luci-static/resources/wwand/esim.js:440 +msgid "" +"lpac downloads on the router over your uplink and relays the eUICC APDUs " +"through wwand — no dedicated modem APN needed." +msgstr "" +"lpac pobiera na routerze przez Twoje łącze wychodzące i przekazuje APDU " +"eUICC przez wwand — dedykowany APN modemu nie jest potrzebny." + +#: htdocs/luci-static/resources/wwand/esim.js:289 +msgid "managed by %s" +msgstr "zarządzane przez %s" + +#: htdocs/luci-static/resources/wwand/netsel.js:33 +msgid "manual" +msgstr "ręczny" + +#: htdocs/luci-static/resources/wwand/format.js:1179 +msgid "mmWave, 24 GHz and above" +msgstr "fale milimetrowe, 24 GHz i wyżej" + +#: htdocs/luci-static/resources/wwand/modemopts.js:231 +msgid "modem default" +msgstr "domyślne modemu" + +#: htdocs/luci-static/resources/wwand/modemopts.js:258 +msgid "modem default (do not change)" +msgstr "domyślne modemu (nie zmieniaj)" + +#: htdocs/luci-static/resources/wwand/format.js:310 +msgid "" +"modem reset, power cycle and reboot held for %d min — the card is settling " +"its own profile change" +msgstr "" +"reset modemu, cykl zasilania i restart wstrzymane na %d min — karta dokańcza " +"własną zmianę profilu" + +#: htdocs/luci-static/resources/view/wwand/status.js:1170 +#: htdocs/luci-static/resources/view/wwand/status.js:1191 +#: htdocs/luci-static/resources/view/wwand/status.js:1213 +msgid "neighbour" +msgstr "sąsiednia" + +#: htdocs/luci-static/resources/wwand/format.js:551 +msgid "network lock (SIM not accepted by this modem)" +msgstr "blokada sieciowa (SIM nieakceptowana przez ten modem)" + +#: htdocs/luci-static/resources/view/wwand/status.js:766 +msgid "next: %s in %d attempts" +msgstr "następny: %s za %d prób" + +#: htdocs/luci-static/resources/view/wwand/status.js:767 +msgid "next: %s, due now" +msgstr "następny: %s, teraz" + +#: htdocs/luci-static/resources/view/wwand/status.js:400 +#: htdocs/luci-static/resources/view/wwand/status.js:668 +msgid "no" +msgstr "nie" + +#: htdocs/luci-static/resources/view/wwand/status.js:857 +msgid "no GPIO — nothing. %s." +msgstr "brak GPIO — nic. %s." + +#: htdocs/luci-static/resources/view/wwand/status.js:855 +msgid "" +"no GPIO — software only (%s), and no reboot either, so the ladder keeps " +"retrying. %s." +msgstr "" +"brak GPIO — tylko programowo (%s), a restartu też nie będzie, więc procedura " +"próbuje dalej. %s." + +#: htdocs/luci-static/resources/view/wwand/status.js:853 +msgid "no GPIO — software only (%s), then a router reboot. %s." +msgstr "brak GPIO — tylko programowo (%s), a następnie restart routera. %s." + +#: htdocs/luci-static/resources/view/wwand/status.js:847 +msgid "no board profile for this device, so no power or reset line is known" +msgstr "" +"brak profilu płyty dla tego urządzenia, więc nie jest znana linia zasilania " +"ani resetu" + +#: htdocs/luci-static/resources/wwand/format.js:553 +msgid "no card" +msgstr "brak karty" + +#: htdocs/luci-static/resources/view/wwand/status.js:283 +msgid "no current fix — last one %d s ago" +msgstr "brak bieżącego fixa — ostatni %d s temu" + +#: htdocs/luci-static/resources/view/wwand/status.js:284 +msgid "no fix yet" +msgstr "jeszcze brak fixa" + +#: htdocs/luci-static/resources/view/wwand/status.js:796 +msgid "no power line" +msgstr "brak linii zasilania" + +#: htdocs/luci-static/resources/view/wwand/modems.js:356 +msgid "no power/reset control" +msgstr "brak sterowania zasilaniem/resetem" + +#: htdocs/luci-static/resources/wwand/esim.js:334 +msgid "no profiles" +msgstr "brak profili" + +#: htdocs/luci-static/resources/view/wwand/modems.js:315 +#: htdocs/luci-static/resources/wwand/modemopts.js:196 +msgid "no reset control" +msgstr "brak sterowania resetem" + +#: htdocs/luci-static/resources/view/wwand/modems.js:292 +msgid "no reset control for this modem" +msgstr "brak sterowania resetem dla tego modemu" + +#: htdocs/luci-static/resources/wwand/format.js:1032 +msgid "no signal (modem not registered)" +msgstr "brak sygnału (modem niezarejestrowany)" + +#: htdocs/luci-static/resources/wwand/modemopts.js:232 +msgid "none" +msgstr "brak" + +#: htdocs/luci-static/resources/view/wwand/status.js:223 +msgid "none reported" +msgstr "nie zgłoszono" + +#: htdocs/luci-static/resources/view/wwand/settings.js:793 +msgid "none yet" +msgstr "jeszcze brak" + +#: htdocs/luci-static/resources/view/wwand/status.js:646 +msgid "not armed" +msgstr "nieuzbrojone" + +#: htdocs/luci-static/resources/view/wwand/status.js:735 +msgid "not armed · %d/%d" +msgstr "nieuzbrojone · %d/%d" + +#: htdocs/luci-static/resources/view/wwand/status.js:738 +msgid "not armed — no exchange has succeeded in the selected protocol yet" +msgstr "" +"nieuzbrojone — żadna wymiana w wybranym protokole jeszcze się nie powiodła" + +#: htdocs/luci-static/resources/view/wwand/status.js:218 +msgid "not available (%s)" +msgstr "niedostępne (%s)" + +#: htdocs/luci-static/resources/view/wwand/status.js:123 +msgid "not connected" +msgstr "niepołączony" + +#: htdocs/luci-static/resources/view/wwand/sims.js:40 +#: htdocs/luci-static/resources/wwand/simlist.js:32 +msgid "not present" +msgstr "nieobecna" + +#: htdocs/luci-static/resources/view/wwand/settings.js:368 +msgid "not present on this SIM" +msgstr "nieobecne na tej karcie SIM" + +#: htdocs/luci-static/resources/wwand/simlist.js:29 +msgid "not present, %s" +msgstr "nieobecna, %s" + +#: htdocs/luci-static/resources/view/wwand/sims.js:39 +msgid "not present, last seen %s" +msgstr "nieobecna, ostatnio widziana %s" + +#: htdocs/luci-static/resources/wwand/format.js:614 +msgid "not read" +msgstr "nie odczytano" + +#: htdocs/luci-static/resources/view/wwand/status.js:234 +msgid "not reading" +msgstr "nie odczytuje" + +#: htdocs/luci-static/resources/view/wwand/status.js:787 +#: htdocs/luci-static/resources/wwand/graph.js:565 +msgid "not reported" +msgstr "nie zgłoszono" + +#: htdocs/luci-static/resources/view/wwand/status.js:788 +msgid "not reported by this wwand version" +msgstr "niezgłaszane przez tę wersję wwand" + +#: htdocs/luci-static/resources/view/wwand/status.js:1049 +msgid "not required" +msgstr "niewymagany" + +#: htdocs/luci-static/resources/view/wwand/modems.js:64 +msgid "not running" +msgstr "nie działa" + +#: htdocs/luci-static/resources/wwand/simlist.js:169 +msgid "not seen" +msgstr "niewidziana" + +#: htdocs/luci-static/resources/view/wwand/status.js:615 +msgid "not throttling" +msgstr "bez ograniczania" + +#: htdocs/luci-static/resources/view/wwand/status.js:850 +msgid "nothing" +msgstr "nic" + +#: htdocs/luci-static/resources/view/wwand/status.js:195 +msgid "nothing is reading the port" +msgstr "nic nie odczytuje portu" + +#: htdocs/luci-static/resources/view/wwand/status.js:763 +msgid "nothing physical until the modem answers" +msgstr "nic fizycznego, dopóki modem nie odpowie" + +#: htdocs/luci-static/resources/view/wwand/status.js:754 +msgid "nothing physical until the modem answers (automatic reset switched off)" +msgstr "" +"nic fizycznego, dopóki modem nie odpowie (automatyczny reset wyłączony)" + +#: htdocs/luci-static/resources/view/wwand/status.js:762 +msgid "nothing physical until the modem answers (no reset GPIO assigned to it)" +msgstr "" +"nic fizycznego, dopóki modem nie odpowie (brak przypisanego GPIO resetu)" + +#: htdocs/luci-static/resources/view/wwand/status.js:760 +msgid "" +"nothing physical until the modem answers (the board's reset line %s is used " +"only by an explicit reset — set this modem's reset GPIO for the automatic " +"pulse)" +msgstr "" +"nic fizycznego, dopóki modem nie odpowie (linia resetu płyty %s jest używana " +"tylko przez jawny reset — ustaw GPIO resetu tego modemu, aby uzyskać " +"automatyczny impuls)" + +#: htdocs/luci-static/resources/view/wwand/status.js:808 +msgid "" +"nothing — \"%s\" is not in wwand's board profile table, so its modem power " +"and reset lines are unknown. They may well exist." +msgstr "" +"nic — „%s” nie występuje w tabeli profili płyt wwand, więc linie zasilania i " +"resetu modemu są nieznane. Mogą jednak istnieć." + +#: htdocs/luci-static/resources/view/wwand/settings.js:1193 +#: htdocs/luci-static/resources/view/wwand/status.js:227 +#: htdocs/luci-static/resources/wwand/modemopts.js:272 +msgid "off" +msgstr "wyłączone" + +#: htdocs/luci-static/resources/view/wwand/status.js:570 +msgid "off (hardware switch)" +msgstr "wyłączone (przełącznik sprzętowy)" + +#: htdocs/luci-static/resources/view/wwand/status.js:570 +msgid "off (software)" +msgstr "wyłączone (programowo)" + +#: htdocs/luci-static/resources/view/wwand/status.js:680 +msgid "off — %s" +msgstr "wyłączone — %s" + +#: htdocs/luci-static/resources/wwand/format.js:296 +msgid "on the device (IPAd)" +msgstr "na urządzeniu (IPAd)" + +#: htdocs/luci-static/resources/wwand/format.js:295 +msgid "on the device (IPAd); the card could also run its own" +msgstr "na urządzeniu (IPAd); karta mogłaby też uruchamiać własne" + +#: htdocs/luci-static/resources/wwand/format.js:379 +msgid "one SIM active at a time (switching)" +msgstr "jedna SIM aktywna naraz (przełączanie)" + +#: htdocs/luci-static/resources/view/wwand/status.js:250 +msgid "open in map" +msgstr "otwórz na mapie" + +#: htdocs/luci-static/resources/view/wwand/settings.js:509 +msgid "pending — takes effect after a modem reset" +msgstr "oczekuje — zadziała po resecie modemu" + +#: htdocs/luci-static/resources/view/wwand/status.js:795 +msgid "power cycle" +msgstr "cykl zasilania" + +#: htdocs/luci-static/resources/view/wwand/status.js:798 +msgid "power cycle the modem" +msgstr "wyłącz i włącz zasilanie modemu" + +#: htdocs/luci-static/resources/view/wwand/status.js:810 +msgid "power cycle — but this board has no modem power line" +msgstr "cykl zasilania — ale ta płyta nie ma linii zasilania modemu" + +#: htdocs/luci-static/resources/view/wwand/sims.js:43 +msgid "present" +msgstr "obecna" + +#: htdocs/luci-static/resources/view/wwand/sims.js:52 +msgid "profile %s" +msgstr "profil %s" + +#: htdocs/luci-static/resources/view/wwand/modems.js:139 +msgid "qrtr — QMI over the QRTR bus (PCIe/MHI)" +msgstr "qrtr — QMI przez magistralę QRTR (PCIe/MHI)" + +#: htdocs/luci-static/resources/wwand/netsel.js:73 +msgid "radio link failure during the scan" +msgstr "awaria łącza radiowego podczas skanowania" + +#: htdocs/luci-static/resources/view/wwand/sims.js:19 +msgid "reader %s" +msgstr "czytnik %s" + +#: htdocs/luci-static/resources/view/wwand/settings.js:649 +msgid "reading…" +msgstr "odczytywanie…" + +#: htdocs/luci-static/resources/wwand/format.js:1291 +msgid "ready" +msgstr "gotowa" + +#: htdocs/luci-static/resources/wwand/format.js:1291 +msgid "ready, PIN enabled" +msgstr "gotowa, PIN włączony" + +#: htdocs/luci-static/resources/wwand/logbox.js:351 +msgid "refused" +msgstr "odmówiono" + +#: htdocs/luci-static/resources/wwand/format.js:1040 +#: htdocs/luci-static/resources/wwand/format.js:1113 +msgid "registered" +msgstr "zarejestrowany" + +#: htdocs/luci-static/resources/wwand/format.js:1031 +msgid "registered — this modem reports no signal detail" +msgstr "zarejestrowany — ten modem nie raportuje szczegółów sygnału" + +#: htdocs/luci-static/resources/view/wwand/status.js:897 +msgid "reject cause %d" +msgstr "przyczyna odrzucenia %d" + +#: htdocs/luci-static/resources/wwand/format.js:1120 +msgid "rejected: %s" +msgstr "odrzucono: %s" + +#: htdocs/luci-static/resources/view/wwand/status.js:227 +msgid "requested, not started" +msgstr "zażądano, nie uruchomiono" + +#: htdocs/luci-static/resources/view/wwand/status.js:1047 +msgid "required (%d attempts left)" +msgstr "wymagany (pozostało prób: %d)" + +#: htdocs/luci-static/resources/view/wwand/status.js:791 +msgid "reset line %s" +msgstr "linia resetu %s" + +#: htdocs/luci-static/resources/view/wwand/status.js:473 +msgid "reset the modem" +msgstr "zresetuj modem" + +#: htdocs/luci-static/resources/view/wwand/status.js:752 +msgid "reset-line pulse already used this outage" +msgstr "impuls linii resetu już użyty w tej awarii" + +#: htdocs/luci-static/resources/view/wwand/status.js:747 +msgid "reset-line pulse available" +msgstr "impuls linii resetu dostępny" + +#: htdocs/luci-static/resources/view/wwand/status.js:749 +msgid "reset-line pulse in %d s" +msgstr "impuls linii resetu za %d s" + +#: htdocs/luci-static/resources/view/wwand/status.js:750 +msgid "reset-line pulse on the next failed attempt" +msgstr "impuls linii resetu przy następnej nieudanej próbie" + +#: htdocs/luci-static/resources/view/wwand/settings.js:767 +msgid "restoring…" +msgstr "przywracanie…" + +#: htdocs/luci-static/resources/view/wwand/settings.js:115 +msgid "result %d" +msgstr "wynik %d" + +#: htdocs/luci-static/resources/wwand/format.js:147 +#: htdocs/luci-static/resources/wwand/netsel.js:123 +msgid "roaming" +msgstr "roaming" + +#: htdocs/luci-static/resources/wwand/simlist.js:36 +msgid "rsim %s" +msgstr "rsim %s" + +#: htdocs/luci-static/resources/view/wwand/status.js:216 +#: htdocs/luci-static/resources/view/wwand/status.js:226 +msgid "running" +msgstr "działa" + +#: htdocs/luci-static/resources/wwand/modemopts.js:225 +msgid "same as the interface" +msgstr "tak jak interfejs" + +#: htdocs/luci-static/resources/wwand/netsel.js:189 +msgid "scan vanished (daemon restarted?)" +msgstr "skan zniknął (demon uruchomiony ponownie?)" + +#: htdocs/luci-static/resources/wwand/format.js:1040 +msgid "searching" +msgstr "wyszukiwanie…" + +#: htdocs/luci-static/resources/wwand/format.js:1122 +msgid "searching…" +msgstr "wyszukiwanie…" + +#: htdocs/luci-static/resources/view/wwand/status.js:850 +msgid "software only" +msgstr "tylko programowo" + +#: htdocs/luci-static/resources/view/wwand/status.js:219 +msgid "starting" +msgstr "uruchamianie" + +#: htdocs/luci-static/resources/view/wwand/status.js:871 +msgid "stuck for %d s (%d timeouts on %s)" +msgstr "zablokowane od %d s (przekroczeń limitu czasu: %d na %s)" + +#: htdocs/luci-static/resources/wwand/format.js:1178 +msgid "sub-6 GHz" +msgstr "sub-6 GHz" + +#: htdocs/luci-static/resources/view/wwand/modems.js:289 +msgid "the board profile has no modem power line" +msgstr "profil płyty nie ma linii zasilania modemu" + +#: htdocs/luci-static/resources/view/wwand/modems.js:290 +msgid "the configured reset GPIO could not be driven" +msgstr "nie udało się wysterować skonfigurowanego GPIO resetu" + +#: htdocs/luci-static/resources/view/wwand/modems.js:291 +msgid "the daemon does not know this modem" +msgstr "demon nie zna tego modemu" + +#: htdocs/luci-static/resources/wwand/netsel.js:72 +msgid "the modem aborted the scan" +msgstr "modem przerwał skanowanie" + +#: htdocs/luci-static/resources/view/wwand/status.js:193 +msgid "the modem reports no NMEA port" +msgstr "modem nie raportuje portu NMEA" + +#: htdocs/luci-static/resources/view/wwand/status.js:848 +msgid "this board exposes no modem power or reset line" +msgstr "ta płyta nie udostępnia linii zasilania ani resetu modemu" + +#: htdocs/luci-static/resources/view/wwand/modems.js:288 +msgid "" +"this board is not in wwand's profile table, so it has no known power or " +"reset line" +msgstr "" +"ta płyta nie występuje w tabeli profili wwand, więc nie ma znanej linii " +"zasilania ani resetu" + +#: htdocs/luci-static/resources/view/wwand/modems.js:287 +msgid "" +"this board powers both modems from one line, so cutting it would drop the " +"other modem too — give this modem its own \"Modem reset GPIO\" to repower it " +"alone" +msgstr "" +"ta płyta zasila oba modemy z jednej linii, więc jej odcięcie wyłączyłoby " +"także drugi modem — nadaj temu modemowi własne „GPIO resetu modemu”, aby " +"zasilić go ponownie osobno" + +#: htdocs/luci-static/resources/view/wwand/status.js:845 +msgid "" +"this box has more than one modem and the board lines would hit the wrong one " +"— set \"Modem reset GPIO\" on this modem to give the step something to do" +msgstr "" +"to urządzenie ma więcej niż jeden modem, a linie płyty trafiłyby w " +"niewłaściwy — ustaw „GPIO resetu modemu” dla tego modemu, aby krok miał co " +"robić" + +#: htdocs/luci-static/resources/view/wwand/status.js:604 +msgid "throttling, level %d" +msgstr "ograniczanie, poziom %d" + +#: htdocs/luci-static/resources/wwand/graph.js:163 +#: htdocs/luci-static/resources/wwand/graph.js:165 +msgid "total (LTE + 5G)" +msgstr "razem (LTE + 5G)" + +#: htdocs/luci-static/resources/view/wwand/status.js:233 +msgid "ugps is not answering" +msgstr "ugps nie odpowiada" + +#: htdocs/luci-static/resources/view/wwand/modems.js:778 +#: htdocs/luci-static/resources/view/wwand/status.js:668 +#: htdocs/luci-static/resources/wwand/esim.js:198 +#: htdocs/luci-static/resources/wwand/format.js:298 +#: htdocs/luci-static/resources/wwand/netsel.js:37 +msgid "unknown" +msgstr "nieznany" + +#: htdocs/luci-static/resources/wwand/format.js:298 +msgid "unknown (the card supports an IPAe)" +msgstr "nieznane (karta obsługuje IPAe)" + +#: htdocs/luci-static/resources/view/wwand/status.js:796 +msgid "unknown board" +msgstr "nieznana płyta" + +#: htdocs/luci-static/resources/view/wwand/status.js:1050 +#: htdocs/luci-static/resources/wwand/format.js:546 +msgid "unlocked" +msgstr "odblokowany" + +#: htdocs/luci-static/resources/view/wwand/status.js:111 +msgid "unmanaged" +msgstr "niezarządzany" + +#: htdocs/luci-static/resources/view/wwand/settings.js:1188 +msgid "voice centric" +msgstr "skoncentrowane na głosie" + +#: htdocs/luci-static/resources/view/wwand/settings.js:677 +msgid "writing…" +msgstr "zapisywanie…" + +#: htdocs/luci-static/resources/view/wwand/status.js:774 +msgid "" +"wwand escalates a failing modem in steps: cycle the operating mode, reset " +"the modem, then the board's power or reset line, and a reboot beyond that. " +"Each step fires once per outage. The ladder stays disarmed until one " +"exchange has succeeded in the selected control protocol, so a misdetected " +"modem is never repowered." +msgstr "" +"wwand eskaluje działania wobec zawodzącego modemu w krokach: cykl trybu " +"pracy, reset modemu, następnie linia zasilania lub resetu płyty, a dalej " +"restart. Każdy krok jest wykonywany raz na awarię. Procedura pozostaje " +"nieuzbrojona, dopóki jedna wymiana nie powiedzie się w wybranym protokole " +"sterowania, więc błędnie wykryty modem nigdy nie zostanie wyłączony i " +"włączony." + +#: htdocs/luci-static/resources/view/wwand/status.js:1392 +msgid "wwand is not running or no modem present yet." +msgstr "wwand nie działa lub nie ma jeszcze żadnego modemu." + +#: htdocs/luci-static/resources/wwand/modemopts.js:334 +msgid "wwand leaves %s untouched and polls telemetry over the control channel." +msgstr "wwand nie dotyka %s i odpytuje telemetrię przez kanał sterujący." + +#: htdocs/luci-static/resources/wwand/logbox.js:356 +msgid "wwand log" +msgstr "Dziennik wwand" + +#: htdocs/luci-static/resources/view/wwand/status.js:556 +msgid "" +"wwand state for this modem: READY = usable, REGISTERING = searching for a " +"network, WAITING_MODEM/ABSENT = control device not present, SIM_BLOCKED = " +"PIN/PUK required" +msgstr "" +"Stan wwand dla tego modemu: READY = używalny, REGISTERING = wyszukiwanie " +"sieci, WAITING_MODEM/ABSENT = brak urządzenia sterującego, SIM_BLOCKED = " +"wymagany PIN/PUK" + +#: htdocs/luci-static/resources/view/wwand/status.js:400 +#: htdocs/luci-static/resources/view/wwand/status.js:666 +msgid "yes" +msgstr "tak" + +#: htdocs/luci-static/resources/view/wwand/status.js:377 +msgid "· up to %d datagrams" +msgstr "· do %d datagramów" + +#: htdocs/luci-static/resources/view/wwand/status.js:424 +msgid "… based on" +msgstr "… na podstawie" diff --git a/luci-theme-argon/Makefile b/luci-theme-argon/Makefile index 746234b8b..a66e3e271 100644 --- a/luci-theme-argon/Makefile +++ b/luci-theme-argon/Makefile @@ -11,8 +11,8 @@ LUCI_TITLE:=Argon Theme LUCI_DEPENDS:=+luci-base +USE_APK:wget-any +!USE_APK:wget +jsonfilter +ucode-mod-math +flock +coreutils +coreutils-timeout # Keep existing translation package names and the argon-config message domain. LUCI_BASENAME:=argon-config -PKG_VERSION:=3.1.1 -PKG_RELEASE:=40 +PKG_VERSION:=3.1.2 +PKG_RELEASE:=41 PKG_LICENSE:=Apache-2.0 MIT # LuCI derives translations independently of the theme version. Keep release diff --git a/luci-theme-argon/README.md b/luci-theme-argon/README.md index 371af74e9..b3ef06f03 100644 --- a/luci-theme-argon/README.md +++ b/luci-theme-argon/README.md @@ -41,7 +41,7 @@ It also supports automatic and manual switching between light and dark modes. - **Layout fixes.** Improve dashboard actions, overview layouts, dynamic lists and custom IP/MAC inputs across narrow screens, light/dark modes and RTL layouts. - **Compatibility improvements.** Improve compatibility with stable OpenWrt firmware. -Users on v3.0.0 or earlier must install v3.1.1 manually once to get online updates. Users on v3.1.0 can upgrade through Theme updates. See the [release notes](RELEASE.md). +Users on v3.0.0 or earlier must install v3.1.2 manually once to get online updates. Users on v3.1.0 or later can upgrade through Theme updates. See the [release notes](RELEASE.md). ## 3.0 highlights diff --git a/luci-theme-argon/README_ZH.md b/luci-theme-argon/README_ZH.md index e3a7de665..f184402fe 100644 --- a/luci-theme-argon/README_ZH.md +++ b/luci-theme-argon/README_ZH.md @@ -41,7 +41,7 @@ Argon 是**一款干净整洁的 OpenWrt LuCI 主题**, - **布局修复**:优化 Dashboard 操作区、概览布局、动态列表和自定义 IP/MAC 输入框,兼顾窄屏、亮暗色和 RTL 布局。 - **兼容性改进**:改善稳定版 OpenWrt 固件上的兼容性。 -v3.0.0 及更早版本用户需先手动安装一次 v3.1.1,之后即可使用在线更新。v3.1.0 用户可直接通过主题更新升级。详见[更新日志](RELEASE_ZH.md)。 +v3.0.0 及更早版本用户需先手动安装一次 v3.1.2,之后即可使用在线更新。v3.1.0 及以上用户可直接通过主题更新升级。详见[更新日志](RELEASE_ZH.md)。 ## 3.0 主要特性 diff --git a/luci-theme-argon/RELEASE.md b/luci-theme-argon/RELEASE.md index bb61129f4..7e8be352c 100644 --- a/luci-theme-argon/RELEASE.md +++ b/luci-theme-argon/RELEASE.md @@ -1,5 +1,11 @@ # Update log for master branch +## v3.1.2 [ 2026.11.02 ] + +- Fix port card wrapping and alignment, including ImmortalWrt. +- Fix invisible OpenList2 button text and overflowing OpenClash status logs. +- Improve Dockerman, Passwall and system notes layouts. + ## v3.1.1 [ 2026.10.28 ] ### Fixed / 修复 diff --git a/luci-theme-argon/RELEASE_ZH.md b/luci-theme-argon/RELEASE_ZH.md index 63b1f10ae..8372c414b 100644 --- a/luci-theme-argon/RELEASE_ZH.md +++ b/luci-theme-argon/RELEASE_ZH.md @@ -1,5 +1,11 @@ # Master 分支的更新日志 +## v3.1.2 [ 2026.11.02 ] + +- 修复端口卡片换行和高度不一致,兼容 ImmortalWrt。 +- 修复 OpenList2 按钮文字不可见和 OpenClash 状态日志溢出。 +- 优化 Dockerman、Passwall 和系统备注框布局。 + ## v3.1.1 [ 2026.10.28 ] ### 修复 diff --git a/luci-theme-argon/htdocs/luci-static/argon/css/cascade.css b/luci-theme-argon/htdocs/luci-static/argon/css/cascade.css index 6c9a869c8..3dda33796 100644 --- a/luci-theme-argon/htdocs/luci-static/argon/css/cascade.css +++ b/luci-theme-argon/htdocs/luci-static/argon/css/cascade.css @@ -1068,14 +1068,14 @@ select { background-repeat: no-repeat; appearance: none; } -input:not(.cbi-button) { +input:not(.cbi-button, [type="button"], [type="submit"], [type="reset"]) { background-color: var(--input-bg); } .cbi-dropdown:not(.btn):not(.cbi-button) { background-color: var(--dropdown-control-bg); } select:not([multiple="multiple"]):focus, -input:not(.cbi-button):focus, +input:not(.cbi-button, [type="button"], [type="submit"], [type="reset"]):focus, textarea:focus, .cbi-dropdown:focus { --control-border: var(--control-focus); @@ -2492,6 +2492,7 @@ td > table > tbody > tr > td, } .btn, .cbi-button, +input:where([type="button"], [type="submit"], [type="reset"]), .item::after { font-size: 0.875rem; line-height: 1.25rem; @@ -2523,36 +2524,42 @@ td > table > tbody > tr > td, .btn:not(button) ul:not(.dropdown) li { padding: 0; } -.cbi-button:not(select) { +.cbi-button:not(select), +input:where([type="button"], [type="submit"], [type="reset"]) { appearance: none !important; } @media (hover: hover) { :is(.btn, .cbi-button):not(:disabled):not([disabled]):not([aria-disabled="true"]):not([disabled] *, [aria-disabled="true"] *):hover, + input:where([type="button"], [type="submit"], [type="reset"]):not(:disabled):not([disabled]):not([aria-disabled="true"]):not([disabled] *, [aria-disabled="true"] *):hover, .cbi-dynlist:not([disabled]):not([aria-disabled="true"]) > .item:hover::after { box-shadow: var(--button-hover-shadow); } } .btn:disabled, -.cbi-button:disabled { +.cbi-button:disabled, +input:where([type="button"], [type="submit"], [type="reset"]):disabled { cursor: not-allowed; pointer-events: none; opacity: 0.5; box-shadow: none; } .btn:focus-visible, -.cbi-button:focus-visible { +.cbi-button:focus-visible, +input:where([type="button"], [type="submit"], [type="reset"]):focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; } @media (prefers-reduced-motion: no-preference) { button.btn, button.cbi-button, - input.cbi-button { + input.cbi-button, + input:where([type="button"], [type="submit"], [type="reset"]) { transition: scale 0.15s ease-out, box-shadow 0.15s ease-out; } button.btn:not(:disabled):not([aria-disabled="true"]):not([data-static]):active, button.cbi-button:not(:disabled):not([aria-disabled="true"]):not([data-static]):active, - input.cbi-button:not(:disabled):not([aria-disabled="true"]):not([data-static]):active { + input.cbi-button:not(:disabled):not([aria-disabled="true"]):not([data-static]):active, + input:where([type="button"], [type="submit"], [type="reset"]):not(:disabled):not([aria-disabled="true"]):not([data-static]):active { scale: 0.96; } } @@ -6734,12 +6741,15 @@ pre.command-output { [data-page="admin-status-overview"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) { display: grid !important; grid-template-columns: repeat(auto-fit, minmax(min(100%, 8.5rem), 1fr)) !important; + align-items: stretch !important; + justify-items: stretch !important; gap: var(--field-gap); margin: 0 !important; padding: var(--field-gap) var(--section-inset); } [data-page="admin-status"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) > .ifacebox, [data-page="admin-status-overview"] #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) > .ifacebox { + width: auto !important; min-width: 0 !important; max-width: 100% !important; margin: 0 !important; @@ -7866,10 +7876,6 @@ pre.command-output { [data-page^="admin-services-openclash"] .oc .plugin-toggle-container { margin-left: inherit !important; } -[data-page^="admin-services-openclash"] .oc .core-status-toggle { - flex: 0 !important; - min-width: auto !important; -} [data-page^="admin-services-openclash"] .cbi-tabmenu > li { border-right: none !important; margin: 0 0.4rem 0 0 !important; diff --git a/luci-theme-argon/less/components/buttons.less b/luci-theme-argon/less/components/buttons.less index 571c4a967..b9ada55ea 100644 --- a/luci-theme-argon/less/components/buttons.less +++ b/luci-theme-argon/less/components/buttons.less @@ -1,7 +1,9 @@ // out: false +// Keep type selectors weak so standalone .cbi-button-* variants set the fill. .btn, .cbi-button, +input:where([type="button"], [type="submit"], [type="reset"]), .item::after { font-size: .875rem; line-height: 1.25rem; @@ -36,19 +38,22 @@ padding: 0; } -.cbi-button:not(select) { +.cbi-button:not(select), +input:where([type="button"], [type="submit"], [type="reset"]) { appearance: none !important; } @media (hover: hover) { :is(.btn, .cbi-button):not(:disabled):not([disabled]):not([aria-disabled="true"]):not([disabled] *, [aria-disabled="true"] *):hover, + input:where([type="button"], [type="submit"], [type="reset"]):not(:disabled):not([disabled]):not([aria-disabled="true"]):not([disabled] *, [aria-disabled="true"] *):hover, .cbi-dynlist:not([disabled]):not([aria-disabled="true"]) > .item:hover::after { box-shadow: var(--button-hover-shadow); } } .btn:disabled, -.cbi-button:disabled { +.cbi-button:disabled, +input:where([type="button"], [type="submit"], [type="reset"]):disabled { cursor: not-allowed; pointer-events: none; opacity: .5; @@ -56,7 +61,8 @@ } .btn:focus-visible, -.cbi-button:focus-visible { +.cbi-button:focus-visible, +input:where([type="button"], [type="submit"], [type="reset"]):focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; } @@ -64,7 +70,8 @@ @media (prefers-reduced-motion: no-preference) { button.btn, button.cbi-button, - input.cbi-button { + input.cbi-button, + input:where([type="button"], [type="submit"], [type="reset"]) { transition: scale .15s ease-out, box-shadow .15s ease-out; &:not(:disabled):not([aria-disabled="true"]):not([data-static]):active { diff --git a/luci-theme-argon/less/components/foundation.less b/luci-theme-argon/less/components/foundation.less index d7ed8fc76..aea566e93 100644 --- a/luci-theme-argon/less/components/foundation.less +++ b/luci-theme-argon/less/components/foundation.less @@ -28,7 +28,8 @@ select { appearance: none; } -input:not(.cbi-button) { +// Some LuCI apps use button variants without the .cbi-button base class. +input:not(.cbi-button, [type="button"], [type="submit"], [type="reset"]) { background-color: var(--input-bg); } @@ -37,7 +38,7 @@ input:not(.cbi-button) { } select:not([multiple="multiple"]):focus, -input:not(.cbi-button):focus, +input:not(.cbi-button, [type="button"], [type="submit"], [type="reset"]):focus, textarea:focus, .cbi-dropdown:focus { // Page-specific field borders also consume this locally focused role. diff --git a/luci-theme-argon/less/pages/openclash.less b/luci-theme-argon/less/pages/openclash.less index 64e86f442..a0a6518cd 100644 --- a/luci-theme-argon/less/pages/openclash.less +++ b/luci-theme-argon/less/pages/openclash.less @@ -455,11 +455,6 @@ margin-left: inherit !important; } - .oc .core-status-toggle { - flex: 0 !important; - min-width: auto !important; - } - .cbi-tabmenu>li { border-right: none !important; margin: 0 0.4rem 0 0 !important; diff --git a/luci-theme-argon/less/pages/overview.less b/luci-theme-argon/less/pages/overview.less index 17b362e71..23ceaab86 100644 --- a/luci-theme-argon/less/pages/overview.less +++ b/luci-theme-argon/less/pages/overview.less @@ -79,11 +79,15 @@ #view div:has(> .ifacebox > .ifacebox-body > img[src*="/icons/port_"]) { display: grid !important; grid-template-columns: repeat(auto-fit, minmax(~"min(100%, 8.5rem)", 1fr)) !important; + // ImmortalWrt centers fixed-width cards; let both native variants fill a row. + align-items: stretch !important; + justify-items: stretch !important; gap: var(--field-gap); margin: 0 !important; padding: var(--field-gap) var(--section-inset); > .ifacebox { + width: auto !important; min-width: 0 !important; max-width: 100% !important; margin: 0 !important; diff --git a/nps/Makefile b/nps/Makefile index 146cd4d4e..01fe9774f 100644 --- a/nps/Makefile +++ b/nps/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=nps -PKG_VERSION:=0.26.39 -PKG_RELEASE:=14 +PKG_VERSION:=0.26.40 +PKG_RELEASE:=15 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/yisier/nps/tar.gz/v$(PKG_VERSION)?