diff --git a/7z/Makefile b/7z/Makefile index b667cb2d3..2693c9421 100644 --- a/7z/Makefile +++ b/7z/Makefile @@ -1,11 +1,11 @@ include $(TOPDIR)/rules.mk PKG_NAME:=7z -PKG_VERSION:=26.04 -PKG_RELEASE:=5 +PKG_VERSION:=26.05 +PKG_RELEASE:=6 PKG_SOURCE:=$(PKG_NAME)$(subst .,,$(PKG_VERSION))-src.tar.xz -PKG_SOURCE_URL:=https://7-zip.org/a/ +PKG_SOURCE_URL:=https://github.com/ip7z/7zip/releases/download/$(PKG_VERSION)/ PKG_HASH:=skip PKG_MAINTAINER:=Tianling Shen diff --git a/7z/patches/010-flags.patch b/7z/patches/010-flags.patch index 8fe3f2a81..ac57333df 100644 --- a/7z/patches/010-flags.patch +++ b/7z/patches/010-flags.patch @@ -24,7 +24,7 @@ $(CFLAGS_DEBUG) -D_REENTRANT -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE \ -fPIC -@@ -169,7 +169,7 @@ endif +@@ -168,7 +168,7 @@ endif @@ -33,7 +33,7 @@ ifdef IS_MINGW -@@ -1361,7 +1361,7 @@ predef_cc: +@@ -1367,7 +1367,7 @@ predef_cc: $(CC) $(CFLAGS) -E $(SHOW_PREDEF) ../../../../C/CpuArch.c > predef_cc_log # $(CC) $(CFLAGS) -E -dM - < /dev/null predef_cxx: diff --git a/gately/Makefile b/gately/Makefile index f92386a14..99fcb8b28 100644 --- a/gately/Makefile +++ b/gately/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=gately -PKG_VERSION:=0.6.0.4 -PKG_RELEASE:=17 +PKG_VERSION:=0.6.1 +PKG_RELEASE:=18 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/luci-app-open-box/Makefile b/luci-app-open-box/Makefile index d1aec236a..3d1fe22eb 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.371 -PKG_RELEASE:=51 +PKG_VERSION:=0.1.374 +PKG_RELEASE:=52 PKG_MAINTAINER:=liandu2024 PKG_LICENSE:=GPL-3.0-or-later diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index 2be356d96..947dd5fda 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall PKG_VERSION:=26.10.29 -PKG_RELEASE:=357 +PKG_RELEASE:=358 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/luasrc/passwall/util_xray.lua b/luci-app-passwall/luasrc/passwall/util_xray.lua index 0d8f7d959..50af41781 100644 --- a/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -303,9 +303,13 @@ function gen_outbound(flag, node, tag, proxy_table) local finalmask = {} local TP = node.transport if TP == "mkcp" then + local s = { type = "mkcp-legacy" } + if node.mkcp_seed and node.mkcp_seed ~= "" then + s.settings = { value = node.mkcp_seed } + end + local udp = { s } local map = {none = "none", srtp = "srtp", utp = "utp", ["wechat-video"] = "wechat", dtls = "dtls", wireguard = "wireguard", dns = "dns"} - local udp = {} if node.mkcp_guise and node.mkcp_guise ~= "none" then local g = { type = "mkcp-legacy" } g.settings = { header = map[node.mkcp_guise] } @@ -314,11 +318,6 @@ function gen_outbound(flag, node, tag, proxy_table) end udp[#udp+1] = g end - local s = { type = "mkcp-legacy" } - if node.mkcp_seed and node.mkcp_seed ~= "" then - s.settings = { value = node.mkcp_seed } - end - udp[#udp+1] = s finalmask.udp = udp elseif TP == "hysteria" then local udp = {} @@ -807,9 +806,13 @@ function gen_config_server(node) finalmask = (function() local finalmask = {} if node.transport == "mkcp" then + local s = { type = "mkcp-legacy" } + if node.mkcp_seed and node.mkcp_seed ~= "" then + s.settings = { value = node.mkcp_seed } + end + local udp = { s } local map = {none = "none", srtp = "srtp", utp = "utp", ["wechat-video"] = "wechat", dtls = "dtls", wireguard = "wireguard", dns = "dns"} - local udp = {} if node.mkcp_guise and node.mkcp_guise ~= "none" then local g = { type = "mkcp-legacy" } g.settings = { header = map[node.mkcp_guise] } @@ -818,11 +821,6 @@ function gen_config_server(node) end udp[#udp+1] = g end - local s = { type = "mkcp-legacy" } - if node.mkcp_seed and node.mkcp_seed ~= "" then - s.settings = { value = node.mkcp_seed } - end - udp[#udp+1] = s finalmask.udp = udp elseif node.transport == "hysteria" then local udp = {} diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index 6f228e6a2..db85f5f9e 100644 --- a/luci-app-passwall2/Makefile +++ b/luci-app-passwall2/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 PKG_VERSION:=26.10.29 -PKG_RELEASE:=158 +PKG_RELEASE:=159 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/luci-app-passwall2/luasrc/passwall2/util_xray.lua index f400785bc..bf3d63707 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_xray.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_xray.lua @@ -312,9 +312,13 @@ function gen_outbound(flag, node, tag, proxy_table) local finalmask = {} local TP = node.transport if TP == "mkcp" then + local s = { type = "mkcp-legacy" } + if node.mkcp_seed and node.mkcp_seed ~= "" then + s.settings = { value = node.mkcp_seed } + end + local udp = { s } local map = {none = "none", srtp = "srtp", utp = "utp", ["wechat-video"] = "wechat", dtls = "dtls", wireguard = "wireguard", dns = "dns"} - local udp = {} if node.mkcp_guise and node.mkcp_guise ~= "none" then local g = { type = "mkcp-legacy" } g.settings = { header = map[node.mkcp_guise] } @@ -323,11 +327,6 @@ function gen_outbound(flag, node, tag, proxy_table) end udp[#udp+1] = g end - local s = { type = "mkcp-legacy" } - if node.mkcp_seed and node.mkcp_seed ~= "" then - s.settings = { value = node.mkcp_seed } - end - udp[#udp+1] = s finalmask.udp = udp elseif TP == "hysteria" then local udp = {} @@ -812,9 +811,13 @@ function gen_config_server(node) finalmask = (function() local finalmask = {} if node.transport == "mkcp" then + local s = { type = "mkcp-legacy" } + if node.mkcp_seed and node.mkcp_seed ~= "" then + s.settings = { value = node.mkcp_seed } + end + local udp = { s } local map = {none = "none", srtp = "srtp", utp = "utp", ["wechat-video"] = "wechat", dtls = "dtls", wireguard = "wireguard", dns = "dns"} - local udp = {} if node.mkcp_guise and node.mkcp_guise ~= "none" then local g = { type = "mkcp-legacy" } g.settings = { header = map[node.mkcp_guise] } @@ -823,11 +826,6 @@ function gen_config_server(node) end udp[#udp+1] = g end - local s = { type = "mkcp-legacy" } - if node.mkcp_seed and node.mkcp_seed ~= "" then - s.settings = { value = node.mkcp_seed } - end - udp[#udp+1] = s finalmask.udp = udp elseif node.transport == "hysteria" then local udp = {} diff --git a/luci-app-smartsafehub/CHANGELOG.md b/luci-app-smartsafehub/CHANGELOG.md index a9d050ba4..f8185671a 100644 --- a/luci-app-smartsafehub/CHANGELOG.md +++ b/luci-app-smartsafehub/CHANGELOG.md @@ -1,5 +1,534 @@ # 변경 기록 +## [0.2.31-r1] - 2026-10-30 + +### 추가 + +- SmartSafeHub Apps 출시하였습니다. 더 많은 앱들이 추가될 예정입니다. + +### 개선 +- 디스크 사용량을 대시보드에서 표시하여 파악할 수 있도록 하였습니다. +- 간헐적으로 느려지는 성능 문제를 해결하였습니다. +- 시스템 안정성을 개선하였습니다. + + +## [0.2.30-r40] - 2026-10-30 + +### 개선 +- Apps 메뉴를 별도의 Explore 구역으로 분리해 더 잘 보이도록 정리했습니다. +- Apps 메뉴에 `앱으로 확장` 안내 문구를 추가해 어떤 기능인지 더 쉽게 이해할 수 있습니다. +- 기존 Apps 아이콘은 그대로 유지하면서 메뉴 강조를 더 자연스럽게 보이도록 다듬었습니다. + +### 테스트 +- Apps 메뉴가 Explore 구역에 단독으로 표시되는지 자동 검사에 추가했습니다. +- Apps 보조 문구와 System 메뉴 순서가 유지되는지 함께 확인합니다. + +### 호환성 +- Apps 화면, 앱 설치 방식, 기존 메뉴 링크 주소는 변경하지 않았습니다. + + +## [0.2.30-r39] - 2026-10-29 + +### 개선 +- 기능이 많아져도 화면이 느려지지 않도록, Apps와 계정의 오래 걸리는 작업을 공유기 내부 백그라운드에서 처리합니다. +- 앱 목록과 설치 상태를 빠르게 표시하고, 필요한 데이터는 자동으로 새로 확인합니다. +- 화면 이동 후 불필요한 요청을 중단하고, 중요한 사용자 작업이 먼저 응답하도록 개선했습니다. +- 사용자 규칙은 일시적인 통신 오류가 나도 마지막으로 확인한 목록을 유지합니다. +- 추천 규칙 해제는 요청 접수와 실제 적용 결과를 구분해 안내합니다. + +### 테스트 +- 동시에 여러 화면을 열거나 작업을 반복할 때 중복 실행을 막는지 확인합니다. +- 느린 작업, 연결 실패, 작업 취소, 캐시 재사용 및 OpenWrt 기본 명령어 호환성을 검사합니다. + +### 호환성 +- 기존 앱 설치·제거 및 사용자 규칙 저장 방식은 유지합니다. + + +## [0.2.30-r38] - 2026-10-29 + +### 개선 +- Apps의 '도구' 아이콘을 전원 버튼에서 렌치 모양으로 변경했습니다. +- '기타' 아이콘을 원 안에 점 3개가 있는 모양으로 변경해 네트워크와 쉽게 구분되도록 했습니다. +- 분류되지 않은 앱도 '기타'에 표시하며, 공유기 체험 화면에도 같은 아이콘을 사용합니다. + +### 테스트 +- 도구·기타 아이콘과 기존 앱 카테고리 표시가 유지되는지 확인합니다. +- 공유기와 체험 화면에서 같은 카테고리 아이콘을 사용하는지 검사합니다. + +### 호환성 +- 앱 설치·제거 및 기존 앱 카테고리 데이터는 변경하지 않았습니다. + + +## [0.2.30-r37] - 2026-10-29 + +### 추가 +- SmartSafeHub Apps에서 Wake-on-LAN 앱을 설치하고, 설치 후 컴퓨터 전원 켜기 화면을 열 수 있습니다. +- Apps에 '도구' 카테고리를 추가하여 Wake-on-LAN 같은 유틸리티 앱을 쉽게 찾을 수 있습니다. + +### 개선 +- Wake-on-LAN에 필요한 두 패키지가 모두 승인된 경우에만 설치하도록 확인합니다. +- 설치 또는 제거 후 LuCI 메뉴를 갱신하여 Wake-on-LAN 화면이 바로 반영되도록 개선합니다. +- 공유기 체험 화면에서 Wake-on-LAN 앱 설치 전 상태를 확인할 수 있습니다. + +### 테스트 +- 두 패키지 설치·제거, 승인 목록 위변조 차단 및 실행 화면 확인을 점검합니다. +- 앱 카테고리와 체험 화면의 읽기 전용 동작을 확인합니다. + +### 호환성 +- 기존 Apps 설치기와 OpenWrt 25.12 패키지 관리 방식을 그대로 사용합니다. + + +## [0.2.30-r36] - 2026-10-29 + +### 개선 +- 공유기 체험 화면에서 레트로 아케이드의 업데이트 가능 상태를 확인할 수 있습니다. +- 네트워크 사용량 모니터도 설치된 예시로 표시하고, 두 앱의 설치 용량을 보여줍니다. +- 대시보드와 Apps 화면에서 같은 가상 저장 공간 데이터를 사용합니다. + +### 테스트 +- 체험 화면의 설치·업데이트 상태, 앱별 용량과 읽기 전용 동작을 확인합니다. + + +## [0.2.30-r35] - 2026-10-29 + +### 수정 +- 앱 목록을 새로고침할 때 일시적인 오류로 기존 앱이 제공 중단된 것으로 표시되던 문제를 수정했습니다. + +### 개선 +- 목록을 잠시 불러오지 못하면 마지막으로 확인된 앱 목록을 유지합니다. +- 실제로 제공이 중단된 앱은 공유기에 남아 있는 관리 기록과 설치 상태를 확인한 후 안내합니다. + +### 테스트 +- 일시적인 목록 오류, 수동 APK 설치, 제공 중단 앱의 구분 및 기존 앱 작업을 확인합니다. + + +## [0.2.30-r34] - 2026-10-29 + +### 추가 +- 대시보드 리소스 사용량에서 디스크 사용률과 사용 중인 용량을 확인할 수 있습니다. + +### 개선 +- 리소스 카드를 메모리, 디스크, 시스템 부하, 실행 시간 순서로 보기 좋게 배치했습니다. +- Apps 화면과 같은 저장 공간 기준을 사용하며, 용량 정보를 읽을 수 없더라도 다른 상태는 정상 표시합니다. + +### 테스트 +- 디스크 용량 계산, 카드 표시 순서, 저장 공간 조회 실패 시 화면 표시를 검증합니다. +- 추가 조회 없이 기존 시스템 상태 정보를 재사용하는지 검사합니다. + + +## [0.2.30-r33] - 2026-10-29 + +### 추가 +- README에서 공유기 웹사이트 체험 페이지를 바로 방문할 수 있습니다. +- 공유기 체험 페이지에 SmartSafeHub 계정 및 Apps 메뉴를 추가했습니다. + +### 개선 +- 계정 연결과 앱 설치 상태를 예시로 볼 수 있으며, 체험 중에는 실제 계정이나 공유기의 정보가 변경되지 않습니다. + +### 테스트 +- 데모 메뉴와 가상 데이터, 실제 서버 요청 및 앱 실행 차단을 자동 검사합니다. + + +## [0.2.30-r32] - 2026-10-29 + +### 개선 +- 앱 카드에 네트워크, 보안, 스마트홈, 엔터테인먼트 등 카테고리별 기본 아이콘을 표시합니다. +- 카테고리 선택 버튼에도 같은 아이콘을 추가해 앱 종류를 쉽게 구분할 수 있습니다. +- 새 카테고리의 앱도 기본 아이콘으로 표시되도록 했습니다. + +### 테스트 +- 앱 카드와 카테고리 버튼의 아이콘 표시, 카테고리별 구분 및 기존 필터 동작을 확인합니다. + + +## [0.2.30-r31] - 2026-10-29 + +### 추가 +- SmartSafeHub Apps에서 앱 업데이트가 성공하면 설치 건수와 별도로 업데이트 성공 건수를 보고합니다. + +### 개선 +- 보고에 실패해도 앱 업데이트는 정상 완료되며, 나중에 다시 전송합니다. +- 자동 복원과 실패한 업데이트는 업데이트 건수에 포함하지 않습니다. + +### 테스트 +- 설치·업데이트 성공 건수 분리와 중복 전송, 실패 시 재시도 동작을 검사합니다. + + +## [0.2.30-r30] - 2026-10-29 + +### 수정 +- 앱 업데이트 후 설치 용량이 표시되지 않을 수 있던 문제를 수정했습니다. + +### 개선 +- 저장 공간의 도넛 차트를 조금 키워 가운데 숫자가 더 편안하게 보이도록 했습니다. + +### 테스트 +- 앱 업데이트 전후의 서로 다른 용량 정보가 함께 반환되는 경우를 검사합니다. +- 저장 공간 차트의 크기와 기존 앱 기능을 확인합니다. + + +## [0.2.30-r29] - 2026-10-29 + +### 개선 +- Apps에서 설치 버전과 제공 버전, 설치 용량을 더 간결하게 보여줍니다. +- 저장 공간 카드에서 남은 용량과 함께 사용 중이거나 예약된 공간도 확인할 수 있습니다. + +### 테스트 +- 앱 정보가 중복되지 않고 좁은 화면에서도 표시되는지 확인합니다. +- 저장 공간 사용량 계산과 잘못된 용량 정보 처리를 검사합니다. + + +## [0.2.30-r28] - 2026-10-29 + +### 개선 +- Apps 화면의 저장 공간 카드를 내용에 맞게 줄여 더 간결하게 표시합니다. +- 앱 설치 용량을 정상적으로 표시하므로 임시 용량 진단 파일을 더 이상 만들지 않습니다. + +### 테스트 +- 저장 공간 카드의 화면 너비와 기존 용량 표시를 확인합니다. +- 임시 진단 파일 없이도 앱 설치 용량을 정확하게 계산하는지 검사합니다. + + +## [0.2.30-r27] - 2026-10-29 + +### 수정 +- 여러 패키지로 구성된 앱의 설치 용량이 표시되지 않을 수 있던 문제를 수정했습니다. + +### 개선 +- 설치된 앱의 용량을 각각 확인해 합산하고, 앱 카드에서 보기 쉽게 표시합니다. + +### 테스트 +- 여러 패키지의 용량 합산과 용량 정보를 확인할 수 없는 경우를 검사합니다. + + +## [0.2.30-r26] - 2026-10-29 + +### 수정 +- Apps 화면에서 저장 공간과 앱 설치 용량이 표시되지 않던 문제를 수정했습니다. +- 업데이트가 있을 때 설치된 버전 대신 새 버전이 표시되던 문제를 수정했습니다. + +### 개선 +- 설치 여부는 앱 카드의 오른쪽 위에, 업데이트 알림은 설치 버전 옆에 표시합니다. +- 설치 상태와 업데이트 버튼의 색상을 구분해 알아보기 쉽게 바꿨습니다. + +### 테스트 +- 저장 공간 조회 권한, 실제 설치 버전 표시, 업데이트 상태와 화면 배치를 확인합니다. + + +## [0.2.30-r25] - 2026-10-29 + +### 추가 +- 설치된 앱의 패키지 용량을 앱 카드에서 확인할 수 있습니다. + +### 개선 +- 여러 패키지로 구성된 앱도 용량을 합쳐서 보여줍니다. 용량을 확인할 수 없으면 잘못된 숫자를 표시하지 않습니다. + +### 테스트 +- 패키지 용량 계산, 정보 누락 및 설치 상태별 표시를 확인합니다. + + +## [0.2.30-r24] - 2026-10-29 + +### 추가 +- Apps 화면에서 앱을 설치할 수 있는 저장 공간과 남은 용량을 도넛 차트로 확인할 수 있습니다. + +### 개선 +- 검증된 앱 안내 영역을 줄이고 저장 공간 정보를 오른쪽에 배치했습니다. 모바일에서는 위아래로 표시합니다. +- 앱을 설치·업데이트·제거한 후 저장 공간 정보도 함께 새로고침됩니다. + +### 테스트 +- 남은 용량 계산, 저장 공간 정보가 없을 때의 표시, 작은 화면 및 다크 모드 동작을 확인합니다. + + +## [0.2.30-r23] - 2026-10-28 + +### 추가 +- SmartSafeHub Apps에서 설치한 앱의 새 승인 버전이 나오면 `업데이트` 버튼으로 직접 업데이트할 수 있습니다. + +### 개선 +- 업데이트 전 설치된 버전과 앱 소유권을 확인하고, 승인된 앱 패키지만 변경하도록 보호합니다. +- 업데이트 후 기존 앱 제거 및 펌웨어 업데이트 복원 기록을 새 버전에 맞게 유지합니다. +- 앱 업데이트는 신규 설치 건수에 포함하지 않습니다. + +### 테스트 +- 새 버전 표시, 다른 패키지 변경 방지, 검증 실패 차단 및 설치 기록 유지 여부를 확인합니다. + + +## [0.2.30-r22] - 2026-10-28 + +### 수정 +- Apps에서 설치 또는 제거 버튼을 눌러도 작업이 시작되지 않는 일부 공유기 문제를 수정했습니다. +- 앱 작업의 완료 상태와 오류 알림이 정확하게 표시되도록 개선했습니다. + +### 테스트 +- 일부 공유기에 없는 명령을 사용하지 않고도 앱 설치·제거가 시작되는지 확인합니다. +- 기존 작업 알림과 새로운 설치·제거 작업 ID를 모두 인식하는지 확인합니다. + + +## [0.2.30-r21] - 2026-10-28 + +### 추가 +- SmartSafeHub Apps에서 앱을 정상 설치하면 내부 다운로드 통계에 자동으로 기록합니다. + +### 개선 +- 인터넷 연결이 끊겨도 설치 기록을 안전하게 보관하고 다시 전송합니다. 설치 작업 자체에는 영향을 주지 않습니다. +- 펌웨어 업데이트 후 자동 복원, 설치 실패, 제거는 다운로드 횟수에 포함하지 않습니다. +- 앱 다운로드 수는 아직 공유기 화면에 표시하지 않습니다. + +### 테스트 +- 설치 성공·실패·자동 복원별 집계, 전송 재시도, 중복 방지를 확인합니다. + + +## [0.2.30-r20] - 2026-10-28 + +### 개선 +- Apps에서 작업 오류 알림을 닫으면 새로고침하거나 화면을 다시 열어도 같은 알림이 나타나지 않습니다. +- 새로운 앱 설치·제거 작업에서 오류가 발생하면 이전과 같은 오류 코드라도 다시 알려줍니다. + +### 테스트 +- 앱 작업 알림의 닫기 기록 보존과 새 작업 구별, 브라우저 저장소 사용 제한 상황을 확인합니다. + + +## [0.2.30-r19] - 2026-10-28 + +### 수정 +- Apps에 여러 앱이 등록된 경우 두 번째 이후 앱의 설치가 E22 오류로 실패하던 문제를 수정했습니다. +- 설치된 앱을 확인할 때 첫 번째 앱만 표시되어 다른 앱이 설치되지 않은 것처럼 보이던 문제를 수정했습니다. + +### 테스트 +- Retro Arcade와 네트워크 사용량 모니터가 함께 등록되어도 설치 상태와 설치·제거가 정확하게 처리되는지 확인합니다. + + +## [0.2.30-r18] - 2026-10-28 + +### 개선 +- 오래된 화면 배치와 이전 성능 개선 단계를 확인하던 중복 테스트를 정리했습니다. +- 변경된 기능을 확인하는 테스트가 자동 검사에서 빠지지 않도록 점검 기능을 추가했습니다. + +### 테스트 +- 기기 등록, 관리자 비밀번호 복구, 네트워크 설정, 펌웨어 등 주요 검증을 자동 검사에 포함했습니다. +- 사용자 규칙, 세션 만료, 화면 새로고침 등 브라우저 동작 테스트 전체를 실행하도록 개선했습니다. +- 이미 실행한 브라우저 동작 테스트를 중복 실행하지 않도록 정리했습니다. + +### 호환성 +- 공유기의 실제 기능과 화면 구성은 변경하지 않았습니다. + + +## [0.2.30-r17] - 2026-10-28 + +### 개선 +- 내부에서 사용하는 패키지를 업데이트 하였습니다. + + +## [0.2.30-r16] - 2026-10-28 + +### 추가 +- SmartSafeHub Apps에 엔터테인먼트 분류를 추가하고 Retro Arcade를 설치한 후 새 탭으로 실행할 수 있습니다. + +### 개선 +- 공유기 내부에서 실행되는 Retro Arcade 주소만 허용하고, 임의의 외부 주소는 열리지 않도록 보호합니다. + +### 테스트 +- Retro Arcade APK 1개 설치·제거, 웹 파일 확인, 잘못된 패키지 목록과 실행 주소 차단을 검증합니다. + +### 호환성 +- 네트워크 사용량 모니터의 기존 실행 방식은 그대로 유지합니다. + + +## [0.2.30-r15] - 2026-10-28 + +### 수정 +- Apps 설치 전 패키지 확인 과정에서 저장소 목록을 찾지 못해 E34 오류가 발생하던 문제를 수정했습니다. +- 검증한 패키지 파일과 서명된 목록만 사용하도록 설치 과정을 개선했습니다. + +### 테스트 +- 패키지 목록 준비, 설치 사전 확인, 서명 검증 유지와 실패 시 안전한 중단을 확인했습니다. + + +## [0.2.30-r14] - 2026-10-27 + +### 수정 +- 공식 인증 앱이 기존 SmartSafeHub 패키지 저장소 주소에서 승인된 릴리스를 다운로드하도록 수정했습니다. + +### 테스트 +- 네트워크 사용량 모니터의 설치와 제거, 패키지 해시 검사를 다시 확인했습니다. + + +## [0.2.30-r13] - 2026-10-27 + +### 개선[text](../../Downloads/retro-r.patch) +- 공식 인증 앱을 SmartSafeHub가 직접 빌드한 패키지로만 설치하도록 변경했습니다. +- 네트워크 사용량 모니터의 세 패키지를 검증된 동일 릴리스에서 설치하도록 단순화했습니다. + +### 수정 +- 외부 OpenWrt 저장소 백업과 자동 대체 설치 경로를 제거했습니다. + +### 테스트 +- 고정 릴리스에서 세 패키지의 설치·제거 및 변조된 해시 차단을 확인합니다. + +### 호환성 +- 기존 외부 저장소 승인과 이전 설치 상태는 승계하지 않습니다. 새로운 앱 승인 이후 테스트 기기에서 재설치해야 합니다. + + +## [0.2.30-r12] - 2026-10-27 + +### 개선 +- 공식 인증 앱 설치 시 SmartSafeHub에 보관한 OpenWrt 패키지가 있으면 보관본을 우선 사용합니다. +- 보관본이 없으면 기존 공식 저장소를 사용하며, 확인된 패키지 해시가 다르면 설치하지 않습니다. + +### 호환성 +- 기존 승인 정보와 앱 설치 기록은 그대로 유지됩니다. 보관본에도 OpenWrt 원본 서명을 사용합니다. + +### 테스트 +- 보관본을 이용한 설치, 원본 저장소 대체, 손상된 보관본 차단을 검사합니다. + + +## [0.2.30-r11] - 2026-10-26 + +### 추가 +- 펌웨어 업데이트 뒤 기존에 설치했던 Apps가 사라지면 인터넷 연결과 새 펌웨어의 승인 정보를 확인해 자동으로 복원합니다. +- 기존 펌웨어 관리 서비스에서 복원을 한 번만 시작하므로 별도의 daemon이나 init 서비스를 추가하지 않습니다. +- Apps 화면에서 자동 복원 진행 상태와 실패 원인을 확인할 수 있습니다. + +### 개선 +- 사용자가 제거한 앱이나 다른 방법으로 설치한 패키지는 자동 복원하지 않습니다. +- 연결이 잠시 끊긴 경우 재시도하지만, 검증된 패키지와 저장소 정보가 맞지 않으면 안전하게 중단합니다. + +### 호환성 +- 펌웨어 업데이트 시 앱 설치 기록을 보존해야 하며, 새 펌웨어에 유효한 Apps 승인이 있어야 복원할 수 있습니다. +- 공식 OpenWrt 저장소 버전이 바뀌면 재승인 없이 자동 설치하지 않습니다. + +### 테스트 +- 앱 설치 기록 보존, 자동 복원·재시도·승인 실패·중복 설치 방지를 검증합니다. + + +## [0.2.30-r10] - 2026-10-26 + +### 추가 +- 검증된 네트워크 사용량 모니터 앱에서 공식 OpenWrt 웹 화면을 함께 설치할 수 있도록 지원합니다. +- 기존에 사용량 수집 앱만 설치했다면 Apps의 `설치 마무리`로 웹 화면을 추가할 수 있습니다. + +### 개선 +- 이미 다른 앱이 사용하는 공통 라이브러리는 유지하고, SmartSafeHub가 직접 설치한 패키지만 제거합니다. + +### 호환성 +- 공식 웹 화면 설치에는 현재 펌웨어와 일치하는 OpenWrt LuCI 저장소 및 관리자가 검증한 패키지 승인이 필요합니다. +- 공식 저장소가 변경되었거나 다른 시스템 패키지를 업그레이드해야 한다면 설치하지 않습니다. + +### 테스트 +- 공식 패키지 검증, 설치 마무리, 소유권 보호, 시스템 패키지 교체 차단을 확인합니다. + + +## [0.2.30-r9] - 2026-10-25 + +### 추가 +- 설치된 앱에 웹 화면이 있으면 `열기` 버튼으로 새 탭에서 사용할 수 있습니다. 기존 SmartSafeHub 화면은 그대로 유지됩니다. + +### 개선 +- 앱 작업 완료 알림은 7초 후 자동으로 닫히며, 직접 닫을 수도 있습니다. 실패 알림은 확인할 때까지 유지됩니다. +- 네트워크 사용량 모니터에 공식 LuCI 화면을 연결할 수 있도록 설치 상태를 확인합니다. +- 검증된 앱에 새 패키지가 추가되면, 기존 설치를 안전하게 유지하면서 추가 패키지를 설치할 수 있습니다. + +### 호환성 +- 네트워크 사용량 모니터의 웹 화면은 `luci-app-nlbwmon` 패키지가 설치되어야 열 수 있습니다. 함께 설치하려면 두 패키지 모두 SmartSafeHub 승인 목록에 등록되어 있어야 합니다. + +### 테스트 +- 새 탭 실행 경로 검증, 앱 알림 표시, 승인된 패키지 추가 설치 및 기존 패키지 보호를 확인합니다. + + +## [0.2.30-r8] - 2026-10-25 + +### 수정 +- Apps 설치 시 공식 저장소에 같은 앱이 있더라도 SmartSafeHub에서 검증한 패키지만 선택하도록 수정했습니다. + +### 개선 +- 앱 설치에 별도의 로컬 저장소를 만들지 않고 SmartSafeHub 서명 저장소를 직접 사용합니다. +- 의존성이 부족해 설치하지 못한 경우 패키지 관리자의 오류 로그를 확인할 수 있도록 개선했습니다. + +### 테스트 +- 원격 저장소에서만 설치하는지, 같은 버전의 다른 패키지가 선택되지 않는지, 의존성이 없을 때 안전하게 중단하는지 검증합니다. + + +## [0.2.30-r7] - 2026-10-25 + +### 개선 +- Apps에서 승인된 패키지를 SmartSafeHub 저장소에서 직접 내려받아 설치할 수 있도록 연결했습니다. 공유기 펌웨어에 저장소 인덱스를 별도로 복사할 필요가 없습니다. +- 다른 방법으로 설치한 앱은 계속 보호하고, 설치 오류를 더 쉽게 이해할 수 있도록 안내합니다. + +### 수정 +- 앱을 제거할 때 네트워크 및 시스템 필수 패키지가 함께 제거될 위험이 있으면 작업을 차단합니다. + +### 테스트 +- 승인된 저장소·APK의 해시 확인, 설치·제거, 위험한 제거 방지 기능을 검증합니다. + + +## [0.2.30-r6] - 2026-10-23 + +### 테스트 +- 테스트를 위해서 revision을 올렸습니다. + + +## [0.2.30-r5] - 2026-10-23 + +### 개선 +- 사용자 규칙에서 검색과 추천 규칙 분류를 더 효율적으로 처리해 화면이 불필요하게 다시 그려지는 일을 줄였습니다. +- 추천 규칙 상태를 자동 확인할 때 목록 내용이 같다면 기존 화면을 유지합니다. + +### 추가 +- 공유기 웹사이트에서 간헐적으로 느린 요청을 확인할 수 있도록 RPC 처리 시간 진단을 추가했습니다. 진단 정보는 브라우저에만 일시 보관됩니다. + +### 테스트 +- 규칙 검색 및 분류, 추천 규칙 상태 갱신, RPC 지연 기록과 개인정보 비수집을 검증합니다. + + +## [0.2.30-r4] - 2026-10-20 + +### 추가 +- Apps에서 검증된 앱을 설치하고 제거할 수 있습니다. +- 설치 진행 상태와 실패 원인을 확인할 수 있습니다. + +### 개선 +- 공유기와 펌웨어에 맞는 앱만 설치되도록 확인합니다. +- 앱 제거 시 사용자가 별도로 설치한 패키지는 보호합니다. + +### 호환성 +- 현재는 OpenWrt APK 앱을 지원합니다. 스마트홈 및 로컬 AI 앱은 향후 추가될 예정입니다. +- 앱을 설치하려면 해당 펌웨어에 승인된 패키지와 고정된 패키지 저장소가 필요합니다. + +### 테스트 +- 설치·제거, 승인 철회, 패키지 검증, 사용자 설치 패키지 보호를 확인합니다. + + +## [0.2.30-r3] - 2026-10-20 + +### 추가 +- 공유기 메뉴의 '업데이트' 바로 위에 **Apps**를 추가했습니다. +- SmartSafeHub에서 검증한 앱만 현재 공유기의 펌웨어에 맞춰 표시하는 화면을 추가했습니다. + +### 개선 +- 앱 목록이 비어 있거나 서버에 연결할 수 없을 때 이유를 구분해 안내합니다. + +### 호환성 +- 이번 버전은 검증된 앱 목록 조회만 지원합니다. 실제 앱 설치·삭제는 아직 지원하지 않습니다. +- 앱을 표시하려면 Hub의 앱 카탈로그 API와 공유기의 펌웨어 식별 정보가 필요합니다. + +### 테스트 +- PC·모바일 메뉴 순서와 화면 이동을 확인합니다. +- 다른 기기나 펌웨어에 승인된 목록이 나타나지 않도록 확인합니다. + + +## [0.2.30-r2] - 2026-10-20 + +### 개선 +- 추천 사용자 규칙을 처리할 때 공유기의 불필요한 작업과 설정 저장 횟수를 줄여 화면이 더 빠르게 열리도록 개선했습니다. +- 추천 규칙 적용 상태만 바뀌면 사용자 규칙 목록을 다시 불러오지 않도록 했습니다. + +### 수정 +- 추천 규칙 동기화와 SafeShield 목록 갱신이 겹칠 때 발생할 수 있는 지연을 줄였습니다. + +### 테스트 +- 추천 규칙을 추가하거나 해제할 때 기존 사용자 규칙이 보존되는지와 중복 조회가 줄었는지 확인합니다. + + ## [0.2.30-r1] - 2026-10-18 ### 개선 @@ -8,6 +537,7 @@ ### 수정 - 추천 사용자 규칙 해제가 안되던 문제를 수정하였습니다. + ## [0.2.29-r14] - 2026-10-18 ### 개선 @@ -45,6 +575,7 @@ - Cloud 설정이 활동 목록 위에 있는지, 접기·펼치기와 활동 더 보기가 동작하는지 확인합니다. - 계정 연결 안내, 전송 오류, 로컬 활동 기록 유지 안내가 사라지지 않았는지 확인합니다. + ## [0.2.29-r11] - 2026-10-18 ### 개선 @@ -58,6 +589,7 @@ - 메뉴별 제목 배치와 기존 조작 버튼, 설정 저장 안내가 유지되는지 확인합니다. + ## [0.2.29-r10] - 2026-10-18 ### 개선 @@ -69,6 +601,7 @@ - 허용 목록 확인 결과의 전송, 최신 규칙 비교 및 중복 보고 방지를 검증합니다. + ## [0.2.29-r9] - 2026-10-18 ### 수정 @@ -87,6 +620,7 @@ - SafeShield 갱신 중 규칙 변경 보류, 재시도, 기존 사용자 규칙 보존 및 상태 표시를 검증합니다. + ## [0.2.29-r8] - 2026-10-17 ### 수정 @@ -98,6 +632,7 @@ - 갱신 실패 후 재할당, 기존 직접 허용 규칙 보존, 동기화 확인서가 사라진 경우의 복구를 검증합니다. + ## [0.2.29-r7] - 2026-10-17 ### 추가 @@ -112,6 +647,7 @@ - 해제 확인, 기기 인증, 공유기 동기화와 데모 접근 차단을 확인합니다. + ## [0.2.29-r6] - 2026-10-17 ### 수정 @@ -130,6 +666,7 @@ - 다크모드 추천 규칙, 비밀번호 자동 완성, 설정 탭의 카드 배치를 점검합니다. + ## [0.2.29-r5] - 2026-10-17 ### 개선 @@ -142,6 +679,7 @@ - 기존 사용자 규칙과 추천 규칙의 구분, 삭제 권한, 접기·펼치기 및 데모 격리를 검증합니다. + ## [0.2.29-r4] - 2026-10-17 ### 개선 @@ -179,6 +717,7 @@ - 추천 사용자 규칙을 적용하지 못하는 문제를 수정하였습니다. + ## [0.2.29-r1] - 2026-10-17 ### 추가 @@ -190,6 +729,7 @@ - 펌웨어 정보에 관리 소프트웨어의 버전을 표시합니다. + ## [0.2.28-r9] - 2026-10-17 ### 추가 @@ -207,6 +747,7 @@ - 기존 허용 규칙 판정, 동기화 실패·재시도 및 잘못된 확인 응답을 검증합니다. + ## [0.2.28-r8] - 2026-10-16 ### 추가 @@ -228,6 +769,7 @@ - 추천 규칙 추가·해제, 직접 등록한 허용 규칙 보존, 동기화 실패 및 잘못된 응답 처리를 확인합니다. + ## [0.2.28-r7] - 2026-10-16 ### 개선 @@ -258,6 +800,7 @@ - 기본 탭, 주소 직접 접속, 이전 주소 호환성, 입력 상태 보존과 모바일 탭 구조를 확인합니다. + ## [0.2.28-r5] - 2026-10-16 ### 추가 @@ -274,6 +817,7 @@ - 펌웨어의 포함 버전 전달과 동일·신규·이전 버전 비교 및 정보 누락 처리를 확인합니다. + ## [0.2.28-r4] - 2026-10-13 ### 수정 @@ -293,6 +837,7 @@ - DNS 적용을 기다리는 동안에도 입력창을 초기화하고, 저장 실패 시 입력을 유지하는 동작을 확인합니다. - 여러 규칙을 연속 등록해도 진행 안내가 뒤섞이지 않는지 확인합니다. + ## [0.2.28-r3] - 2026-10-11 ### 수정 @@ -313,6 +858,7 @@ - 추가한 데모 메뉴와 데이터 및 실제 공유기 통신 차단을 확인합니다. + ## [0.2.28-r2] - 2026-10-10 ### 추가 @@ -329,6 +875,7 @@ - 데모 화면에 실제 로그인 및 공유기 통신 코드가 포함되지 않도록 검사합니다. - 데모를 새로 빌드했을 때 저장소에 등록된 파일과 일치하는지 확인합니다. + ## [0.2.28-r1] - 2026-10-10 ### 추가 @@ -346,12 +893,14 @@ - SafeShield의 최소 버전을 0.3.25 이상으로 강제합니다. + ## [0.2.27-r27] - 2026-10-10 ### 수정 - SafeShield의 최소 버전을 0.3.25 이상으로 강제합니다. + ## [0.2.27-r26] - 2026-10-09 ### 수정 @@ -364,6 +913,7 @@ - PRO 라이선스 할당·해제 및 변경 없는 상태에서의 중복 갱신 방지를 확인합니다. - 일시적인 갱신 실패 후 재시도와 잘못된 요금제 응답 처리를 검증합니다. + ## [0.2.27-r25] - 2026-10-09 ### 수정 @@ -375,6 +925,7 @@ - 실패한 단계가 남아 있어도 다시 갱신할 수 있는지 확인하는 테스트를 추가했습니다. + ## [0.2.27-r24] - 2026-10-09 ### 추가 @@ -391,6 +942,7 @@ - 게스트 Wi-Fi의 기본 설정, 네트워크 격리, API 권한 및 화면 연결을 확인하는 테스트를 추가했습니다. + ## [0.2.27-r23] - 2026-10-09 ### 개선 @@ -402,6 +954,7 @@ - 라이브러리 버전과 변경된 타입 사용 방식이 일치하는지 확인하는 테스트를 추가했습니다. + ## [0.2.27-r22] - 2026-10-09 ### 개선 @@ -414,6 +967,7 @@ - 토큰 다운로드, 잘못된 토큰 거부, 기존 다운로드 링크 사용을 검증하는 테스트를 추가했습니다. + ## [0.2.27-r21] - 2026-10-09 ### 개선 @@ -426,6 +980,7 @@ - 유휴 상태의 조회 간격, 활동 알림 처리 및 오류 재시도 동작을 확인하는 테스트를 추가했습니다. + ## [0.2.27-r20] - 2026-10-09 ### 개선 @@ -442,6 +997,7 @@ - 실행 진단 정보 기록 및 CLI 호환성 테스트를 추가했습니다. + ## [0.2.27-r19] - 2026-10-09 ### 추가 @@ -456,6 +1012,7 @@ - QR 코드 다운로드 및 파일명 생성 기능이 유지되는지 확인합니다. + ## [0.2.27-r18] - 2026-10-09 ### 추가 @@ -471,6 +1028,7 @@ - QR 코드 생성과 관리자 인증, 일반 Wi-Fi 조회의 비밀번호 비노출을 확인합니다. + ## [0.2.27-r17] - 2026-10-09 ### 개선 @@ -481,6 +1039,7 @@ - SafeShield 원형 진행 표시의 선 두께가 의도한 값으로 유지되는지 계약 테스트로 확인합니다. + ## [0.2.27-r16] - 2026-10-09 ### 수정 @@ -497,6 +1056,7 @@ - 계정 상태 요청 순서, 캐시 성공 시각, 업데이트 복귀·재시도 계약을 검증합니다. + ## [0.2.27-r15] - 2026-10-09 ### 개선 @@ -507,6 +1067,7 @@ - 계정 메뉴의 접근성 문구와 시각적 알림의 중복 읽기 방지를 확인합니다. + ## [0.2.27-r14] - 2026-10-09 ### 개선 @@ -518,6 +1079,7 @@ - 최근 활동 전용 RPC 권한과 개발용 지연 기록 조건을 검증합니다. + ## [0.2.27-r13] - 2026-10-09 ### 개선 @@ -530,6 +1092,7 @@ - 상태 갱신, 캐시 재사용, 전역 업데이트 조회 계약을 검증합니다. + ## [0.2.27-r12] - 2026-10-09 ### 추가 @@ -545,6 +1108,7 @@ - 미연결 안내, 계정 화면 이동 및 불필요한 서버 동기화 방지 계약을 검증합니다. + ## [0.2.27-r11] - 2026-10-08 ### 개선 @@ -558,6 +1122,7 @@ - 중복 조회 방지, 화면별 폴링 조건 및 기존 데이터 유지 동작에 대한 회귀 테스트를 추가했습니다. + ## [0.2.27-r10] - 2026-10-08 ### 개선 @@ -574,6 +1139,7 @@ - SafeShield `0.3.24-r4`의 `status.entitlement` 스키마를 사용합니다. `status.license` 호환 경로는 유지하지 않습니다. + ## [0.2.27-r9] - 2026-10-08 ### 개선 @@ -587,6 +1153,7 @@ - 연결된 기기의 웹사이트 등록 해제를 감지하면 계정 상태와 Cloud 활동 credential을 정리하고 SafeShield 갱신을 한 번 요청하는지 검증하는 회귀 테스트를 추가했습니다. - 계정 화면이 백그라운드 로컬 상태 변경을 주기적으로 반영하는 UI 계약을 추가했습니다. + ## [0.2.27-r8] - 2026-10-08 ### 개선 @@ -599,6 +1166,7 @@ - 계정에 연결된 Free 기기의 7일 Cloud 활동 업로드, Pro의 90일 보관, 계정 미연결 기기의 전송 차단을 검증하는 테스트를 보강했습니다. + ## [0.2.27-r7] - 2026-10-08 ### 개선 @@ -610,6 +1178,7 @@ - 연결 코드 유효 시간 강조와 SafeShield 플랜 배지 스타일 재사용을 검증하는 계정 UI 계약 테스트를 보강했습니다. + ## [0.2.27-r6] - 2026-10-08 ### 개선 @@ -626,6 +1195,7 @@ - 로컬 상태 우선 표시, 백그라운드 동기화, 중복 요청 방지와 연결된 기기의 지연 안내 문구를 검증하는 계정 UI 계약 테스트를 보강했습니다. + ## [0.2.27-r5] - 2026-10-08 ### 개선 @@ -637,6 +1207,7 @@ - 활성 연결 코드가 계정 등록으로 소비되면 SafeShield 갱신을 한 번만 요청하고, 이후 기기 동기화에서는 같은 갱신을 반복하지 않는 회귀 테스트를 추가했습니다. + ## [0.2.27-r4] - 2026-10-08 ### 추가 @@ -653,6 +1224,7 @@ - 계정 메뉴 라우팅, 연결 코드 polling, 등록 완료 후 코드 숨김, device registration RPC 동기화와 SafeShield 화면 분리를 검증하는 계약 테스트를 추가·보강했습니다. + ## [0.2.27-r3] - 2026-10-08 ### 개선 @@ -664,6 +1236,7 @@ - 연결 코드 강조 표시와 복사 기능이 SafeShield 화면에 유지되는지 UI 계약 테스트를 보강했습니다. + ## [0.2.27-r2] - 2026-10-03 ### 추가 @@ -689,6 +1262,7 @@ - 서버의 기존 라이선스 API는 전환 기간 동안 그대로 유지되므로 이전 버전 기기와 새 기기 등록 방식을 함께 운영할 수 있습니다. + ## [0.2.27-r1] - 2026-10-03 ### 개선 @@ -696,6 +1270,7 @@ - 일부 UI를 개선하여 더 명확하게 정보를 표현하도록 하였습니다. - 첫 설치 시 브라우저의 시간대를 기본 시간대로 사용할 수 있도록 개선하였습니다. + ## [0.2.26-r4] - 2026-10-01 ### 개선 @@ -711,6 +1286,7 @@ - 보호 데이터 카드가 보호 도메인, 보호 수준, 마지막 업데이트, 상태 안내와 접힌 세부 정보를 제공하고 기존 영어 내부 필드를 기본 화면에 직접 노출하지 않는지 UI 계약 테스트를 추가했습니다. - 패키지 revision과 로그인/프론트엔드 asset version이 `0.2.26-r4`로 일치하는지 기존 패키지 계약 테스트로 함께 검증합니다. + ## [0.2.26-r3] - 2026-10-01 ### 개선 @@ -724,6 +1300,7 @@ - 원격 상태 보고 화면에 개인정보 보호 설명과 전송/비전송 정보 분류가 유지되는지 UI 계약 테스트를 보강했습니다. - DNS 요청 내용과 시스템 로그 원문이 서버에 전송되지 않는다는 안내가 계속 표시되는지 Health 계약 테스트를 추가했습니다. + ## [0.2.26-r2] - 2026-09-30 ### 추가 @@ -739,12 +1316,14 @@ - 신규 설치의 1회 시간대 자동 설정, 기존 장치 시간대 보존, 지원하지 않는 브라우저 시간대 처리와 `SmartRouter` hostname 강제 적용을 검증하는 회귀 테스트를 보강했습니다. + ## [0.2.26-r1] - 2026-09-30 ### 수정 - 펌웨어를 업데이트한 뒤에도 이전 펌웨어 버전으로 인식되어 같은 업데이트가 다시 표시되던 문제를 수정했습니다. + ## [0.2.25-r2] - 2026-09-29 ### 수정 @@ -765,6 +1344,7 @@ - 이 버전부터 생성한 설정 백업에는 SmartSafeHub 기기 코드가 `/etc/config/smartsafehub`에 포함됩니다. 기기 코드가 없는 이전 백업은 다른 모델 백업의 오적용을 막기 위해 복원이 거부될 수 있습니다. + ## [0.2.25-r1] - 2026-09-29 ### 개선 @@ -772,6 +1352,7 @@ - 관리자 비밀번호 변경 UI를 개선하였습니다. - 내부 모듈을 정리하였습니다. + ## [0.2.24-r5] - 2026-09-29 ### 개선 @@ -782,6 +1363,7 @@ - 비밀번호 요구 사항 안내 다음의 확인 입력 영역에 추가 여백이 유지되는지 UI 계약 테스트를 보강했습니다. + ## [0.2.24-r4] - 2026-09-29 ### 수정 @@ -793,6 +1375,7 @@ - 패키지 설치 과정에서 Cloud 전송을 강제로 켜는 레거시 migration이 남아 있지 않은지 검증하고, RPC·이벤트 기록·동기화·라이선스 경로 모두 명시적으로 ON인 경우에만 Cloud 기능을 사용하도록 계약 테스트를 보강했습니다. + ## [0.2.24-r3] - 2026-09-29 ### 수정 @@ -806,6 +1389,7 @@ - 기존 사용자 system/NTP 설정 보존, 새 설치 기본값 적용, `compat_version` 비변경을 검증하는 system-defaults 회귀 테스트를 보강했습니다. - 같은 `device_code` 백업 허용, 다른 장치 및 식별 metadata 없는 백업 거부, 성공/실패 복원 후 현재 `firmware.json` 보존을 검증하는 백업 회귀 테스트를 추가했습니다. + ## [0.2.24-r2] - 2026-09-29 ### 개선 @@ -817,12 +1401,14 @@ - 사용 가능한 펌웨어 요약에 OpenWrt 기반 버전이 다시 노출되지 않고 SmartSafeHub 펌웨어 버전과 이미지 크기가 유지되는지 UI contract 테스트를 추가했습니다. + ## [0.2.24-r1] - 2026-09-28 ### 수정 - 내부 설정 충돌을 해결하였습니다. + ## [0.2.23-r2] - 2026-09-28 ### 수정 @@ -834,6 +1420,7 @@ - system/NTP 섹션이 이미 존재할 때 섹션을 재생성하지 않는지, `compat_version`을 수정하거나 삭제하지 않는지, 누락된 섹션만 방어적으로 생성하는지 검증하는 회귀 테스트를 추가했습니다. + ## [0.2.23-r1] - 2026-09-28 ### 개선 @@ -845,6 +1432,7 @@ - 다크 모드에서 일부 UI의 가독성이 떨어진 부분을 수정하였습니다. + ## [0.2.22-r14] - 2026-09-28 ### 개선 @@ -856,6 +1444,7 @@ - 관리 소프트웨어 업데이트 수가 여러 개여도 사이드바 badge에서는 1개 유형으로만 계산하고 펌웨어와 합산해 최대 2가 되는 계약을 검증하도록 업데이트 UI 회귀 테스트를 보강했습니다. + ## [0.2.22-r13] - 2026-09-28 ### 개선 @@ -868,6 +1457,7 @@ - recovery bridge의 marker/빈 root 비밀번호 조건, 제한 RPC grant, 일반 로그인보다 우선하는 복구 진입 순서와 production asset version 계약을 검증하는 회귀 테스트를 추가했습니다. + ## [0.2.22-r12] - 2026-09-28 ### 추가 @@ -886,6 +1476,7 @@ - Reset 5~9초/10초 경계, 구형 펌웨어 업그레이드 차단 계약, 비밀번호 복구 marker, Dropbear 보호/복원, 실패 rollback과 복구 전용 UI를 검증하는 회귀 테스트를 추가했습니다. + ## [0.2.22-r11] - 2026-09-28 ### 추가 @@ -904,6 +1495,7 @@ - 관리자 비밀번호 변경 RPC, ACL, 현재 비밀번호 검증, 세션 폐기, 공통 비밀번호 정책과 설정 UI 재로그인 흐름을 검증하는 회귀 테스트를 추가했습니다. + ## [0.2.22-r10] - 2026-09-28 ### 수정 @@ -915,6 +1507,7 @@ - 업데이트 UI 계약 테스트에 설치 진행 패널의 `text-sky-800` 및 `text-sky-950` 다크 모드 매핑 회귀 검증을 추가했습니다. + ## [0.2.22-r9] - 2026-09-28 ### 개선 @@ -927,6 +1520,7 @@ - 사이드바 업데이트 badge가 관리 소프트웨어 업데이트 수와 펌웨어 `updateAvailable` 상태를 합산하는지, 펌웨어 상태 조회가 전역에서 활성화되는지 검증하는 회귀 조건을 추가했습니다. + ## [0.2.22-r8] - 2026-09-28 ### 개선 @@ -940,6 +1534,7 @@ - WAN 고정 IPv4의 주소/게이트웨이/DNS가 공용 octet 입력을 사용하는지, LAN도 같은 컴포넌트를 재사용하는지와 `네트워크` 메뉴명을 검증하는 회귀 조건을 추가했습니다. + ## [0.2.22-r7] - 2026-09-28 ### 개선 @@ -954,6 +1549,7 @@ - 네트워크 설정 단일 route, 기존 WAN/LAN hash 호환, 메뉴 통합, 대시보드 링크 및 WAN/LAN 동시 새로고침을 검증하는 회귀 테스트를 추가했습니다. - 기존 WAN, LAN, IPTV와 내비게이션 계약 테스트를 새 통합 메뉴 구조에 맞게 갱신했습니다. + ## [0.2.22-r6] - 2026-09-28 ### 추가 @@ -974,6 +1570,7 @@ - WAN RPC/ACL, UCI 저장, 입력 검증, PPPoE 비밀번호 비노출/보존, WAN-only 재연결, 메뉴/화면/최근 활동 연결을 검증하는 계약 테스트를 추가했습니다. - OpenWrt ucode 런타임 fixture에서 PPPoE `/32` 주소, gateway/DNS 상태 해석과 기존 비밀번호를 노출하지 않는 무변경 저장을 검증합니다. + ## [0.2.22-r5] - 2026-09-28 ### 정리 @@ -985,6 +1582,7 @@ - 이벤트 테스트가 새 SafeShield observer 상태 파일명을 사용하도록 갱신하고, 패키지/Cloud activity 계약에서 존재하지 않는 legacy 서비스의 stop/disable migration 요구사항을 제거했습니다. + ## [0.2.22-r4] - 2026-09-28 ### 개선 @@ -996,6 +1594,7 @@ - 대시보드 계약 테스트에 최근 활동 액션의 제목 행 배치, 모바일 줄바꿈 방지, SafeShield 집계 상대 시간과 1분 주기 갱신 연동을 검증하는 회귀 조건을 추가했습니다. + ## [0.2.22-r3] - 2026-09-28 ### 수정 @@ -1009,6 +1608,7 @@ - Health Reporter에서 `pro` + `ACTIVE`가 `PRO` + `active`로 정규화되고 유료 권한으로 판정되는지 검증합니다. - Cloud 최근 활동 동기화가 POSIX 문자 클래스 기반 `tr` 대소문자 변환을 다시 사용하지 않는지 검증합니다. + ## [0.2.22-r2] - 2026-09-28 ### 수정 @@ -1021,6 +1621,7 @@ - LAN 계약 테스트에 모바일 IPv4 4열 그리드와 입력 폭 제한 회귀 검증을 추가했습니다. - IPTV 계약 테스트에 공통 스위치 geometry 사용과 모바일 제목 행 배치 회귀 검증을 추가했습니다. + ## [0.2.22-r1] - 2026-09-27 ### 개선 @@ -1028,6 +1629,7 @@ - 신규 기기 지원을 위해서 내부 모듈을 업데이트 하였습니다. - 가독성을 위해서 최근 활동의 UI를 개선하였습니다. + ## [0.2.21-r3] - 2026-09-27 ### 개선 @@ -1041,6 +1643,7 @@ - A3004T와 AX3000SE가 펌웨어 메타데이터와 board-name fallback 양쪽에서 올바른 장치 코드로 식별되는지 검증하는 회귀 테스트를 추가했습니다. - rpcd 펌웨어 모듈이 두 신규 장치 코드와 OpenWrt board name 매핑을 포함하는지 계약 테스트를 추가했습니다. + ## [0.2.21-r2] - 2026-09-25 ### 개선 @@ -1051,12 +1654,14 @@ - 최근 활동 UI 계약 테스트에 대시보드 compact 타임라인의 항목 간격을 검증하는 회귀 테스트를 추가했습니다. + ## [0.2.21-r1] - 2026-09-25 ### 수정 - 모바일 기기에서 자동 업데이트 시간 설정의 UI가 깨지는 문제를 수정하였습니다. + ## [0.2.20-r5] - 2026-09-25 ### 수정 @@ -1069,6 +1674,7 @@ - 업데이트 UI와 설정 UI 계약 테스트에서 native `type="time"` 재도입을 금지하고 공통 `TimeSelect` 사용 및 실제 배포 `app.js`/`app.css` 반영 여부를 검증하도록 보강했습니다. + ## [0.2.20-r4] - 2026-09-25 ### 수정 @@ -1080,6 +1686,7 @@ - 업데이트 UI 계약 테스트가 소스 코드뿐 아니라 실제 패키지에 포함되는 `app.js`/`app.css`에도 iPad 레이아웃 보정이 들어갔는지 확인하도록 확장했습니다. + ## [0.2.20-r3] - 2026-09-25 ### 수정 @@ -1092,6 +1699,7 @@ - 업데이트 UI 계약 테스트에 자동 업데이트 설정의 container-width 반응형 Grid와 iPadOS/WebKit 시간 입력 폭 제한을 검증하는 회귀 테스트를 추가했습니다. + ## [0.2.20-r2] - 2026-09-23 ### 개선 @@ -1108,6 +1716,7 @@ - 펌웨어 helper/RPC 계약 테스트에 "패키지 저장소가 Beta여도 설치된 펌웨어 채널은 Stable로 유지된다"는 회귀 검증과 "펌웨어 채널은 저장소 파일이 아니라 펌웨어 metadata에서 읽는다"는 계약 검증을 추가했습니다. - README의 업데이트 섹션에 펌웨어 채널과 패키지 채널을 분리한 규칙을 문서화했습니다. + ## [0.2.20-r1] - 2026-09-22 ### 기능 @@ -1123,6 +1732,7 @@ - SafeShield refresh 로딩바를 다듬었습니다. + ## [0.2.19-r21] - 2026-09-22 ### 수정 @@ -1135,6 +1745,7 @@ - SafeShield UI contract에 refresh donut의 반지름과 container 크기를 고정하는 회귀 검증을 추가해 이후 스타일 수정으로 전체 ring 크기가 다시 커지지 않도록 했습니다. - README의 SafeShield 갱신 단계 설명을 현재의 약간 더 컴팩트한 ring 규칙에 맞게 갱신했습니다. + ## [0.2.19-r20] - 2026-09-22 ### 수정 @@ -1147,6 +1758,7 @@ - 첫 확인 실패 후 두 번째 확인에서 복구되는 transient dnsmasq 상태는 critical/event를 만들지 않고, 3회 연속 실패는 실제 critical 및 `health.issue.started`로 기록되는 회귀 테스트를 추가했습니다. - README의 로컬 Health 진단 설명에 dnsmasq 연속 확인/debounce 정책을 추가했습니다. + ## [0.2.19-r19] - 2026-09-21 ### 기능 @@ -1168,6 +1780,7 @@ - Cloud OFF에서 local history만 남고 outbox/wake/credential/network 호출이 생기지 않는지, license status-sync가 Cloud OFF에서도 라이선스 상태를 유지하면서 activity credential을 저장하지 않는지 회귀 테스트를 추가했습니다. - UI/RPC/ACL 계약에 유료 entitlement, 접근 가능한 switch semantics, 신규 설치 기본 OFF와 구버전 upgrade 호환 정책을 고정하고 README의 Cloud 활동 기록 데이터 흐름을 갱신했습니다. + ## [0.2.19-r18] - 2026-09-21 ### 수정 @@ -1181,6 +1794,7 @@ - SafeShield UI contract에 track/arc stroke 폭과 라이트/다크 모드 shadow 강도를 고정하는 회귀 검증을 추가해 이후 Tailwind border/shadow 변경이 진행 ring의 시각 두께를 다시 키우지 않도록 했습니다. - README의 SafeShield 갱신 단계 설명에 얇은 progress ring 시각 규칙을 추가했습니다. + ## [0.2.19-r17] - 2026-09-21 ### 수정 @@ -1196,6 +1810,7 @@ - license status-sync가 paid 응답의 activity credential을 실제 runtime cache에 저장하고 revoke/clear 시 제거하는 계약을 추가했습니다. Activity Sync는 license API를 직접 호출하지 않고 `smartsafehub-license status-sync` 경계만 사용하며, cached credential 재사용·paid credential 누락·Free/revoked 처리·Cloud upload/ack·backoff가 유지되는지 회귀 테스트로 고정했습니다. - README의 Cloud 활동 기록 구조를 `license = entitlement/credential owner`, `activity-sync = upload/ack consumer` 책임 분리와 향후 agent module 경계에 맞게 갱신했습니다. + ## [0.2.19-r16] - 2026-09-21 ### 수정 @@ -1211,6 +1826,7 @@ - Activity Sync가 `/licenses/status`만 호출하고 `/licenses/resolve`를 호출하지 않는 계약, status body가 최소 identity만 포함하는 계약, 구 Hub paid 응답의 outbox 보존, revoked/Free outbox 정리, status 장애 backoff를 회귀 테스트로 고정했습니다. - README의 Cloud 활동 기록 동기화 설명을 Hub 1.4.86의 조회 전용 status credential 구조와 단계적 배포 호환 정책에 맞게 갱신했습니다. + ## [0.2.19-r15] - 2026-09-21 ### 수정 @@ -1224,6 +1840,7 @@ - status RPC가 선택적 limit 인자에 의존하지 않고 최대 128건을 반환하는 계약과 Cloud resolve 실패의 15/30/60분 bounded backoff를 검증하는 회귀 테스트를 추가했습니다. - README에 Cloud Activity API 장애/미배포 시 재시도 정책과 로컬 이벤트 기록이 Cloud 통신과 독립적이라는 점을 보강했습니다. + ## [0.2.19-r14] - 2026-09-21 ### 수정 @@ -1237,6 +1854,7 @@ - PID 파일이 아직 없는 초기화 중 lock을 경쟁 writer가 탈취하지 않는지 검증하는 회귀 테스트를 추가했습니다. 기존 동시 writer 및 local history/outbox 분리 테스트와 함께 여러 producer가 겹쳐도 이벤트가 덮어써지지 않는 계약을 검증합니다. - README에 lock owner 게시 전 구간을 stale로 간주하지 않는 소유권 규칙과 event emit 실패 로그 정책을 문서화했습니다. + ## [0.2.19-r13] - 2026-09-21 ### 수정 @@ -1251,6 +1869,7 @@ - Hub API 미배포/통신 실패, legacy paid resolve, plan 미확인 응답에서 outbox가 삭제되지 않고, 명시적인 Free 응답에서만 정리되는 계약을 추가했습니다. - README에 로컬 history와 Cloud 전송 실패의 독립성, bounded lock wait, Cloud API rollout 이전의 retry 정책을 문서화했습니다. + ## [0.2.19-r12] - 2026-09-21 ### 기능 @@ -1272,6 +1891,7 @@ - 유료 Cloud batch 업로드/ack, Free entitlement, 유료 credential 누락 시 outbox 보존, 직접/관찰 이벤트 소유권을 검증하는 `tests/test-activity-cloud-sync.sh`를 추가하고 ShellSpec/패키지/static validation 계약에 연결했습니다. - README의 이벤트 생산 경로를 direct mutation과 observer transition으로 구분하고 Cloud 활동 기록 동기화/유료 entitlement/로컬 history 보존 정책을 문서화했습니다. + ## [0.2.19-r11] - 2026-09-21 ### 수정 @@ -1287,6 +1907,7 @@ - 최근 활동 UI 계약 테스트에 부팅, 인터넷 연결, SafeShield 보호, 라이선스, Health 상태의 사용자 문구와 관측 기반 장애 시간 표현을 고정하고, 오해를 유발하던 기존 문구가 다시 들어오지 않는 회귀 검사를 추가했습니다. - README에 최근 활동은 관측 데이터가 실제로 보장하는 범위만 표현하고 오류 코드는 사용자용 설명 뒤의 진단 정보로 노출한다는 정책을 문서화했습니다. + ## [0.2.19-r10] - 2026-09-21 ### 수정 @@ -1306,6 +1927,7 @@ - 최근 활동 계약 테스트에 `status` RPC가 인자 없이 유지되는지, 프론트엔드가 신규 status 인자를 전송하지 않는지, 이전 handler 응답을 안전하게 처리하는지, 생략된 limit이 `NaN`으로 흐르지 않는지 검증을 추가했습니다. - README의 최근 활동 조회 구조를 인자 없는 기존 `status` RPC를 사용하는 현재 동작에 맞게 갱신했습니다. + ## [0.2.19-r9] - 2026-09-21 ### 기능 @@ -1326,6 +1948,7 @@ - 기존 event queue 테스트를 확장해 outbox와 local history가 함께 bounded retention을 적용하고, Cloud ack가 로컬 표시 history를 제거하지 않는지 검증합니다. - README의 활동 이벤트 문서를 공유기 `최근 활동` UI와 Cloud outbox/local history 분리 구조에 맞게 갱신했습니다. + ## [0.2.19-r8] - 2026-09-21 ### 기능 @@ -1348,6 +1971,7 @@ - Health 회귀 테스트에 첫 관측 무이벤트, WAN down 반복 polling 중복 방지, 인터넷 복구 중단 시간, SafeShield `last_success` 기반 갱신 이벤트를 검증하는 실행형 시나리오를 추가했습니다. - 패키지 계약에 event helper/init script 실행 권한과 postinst 강제 enable을 추가하고 README에 이벤트 schema, 현재 source 범위, 휘발성 보관 정책과 향후 Cloud 연결 경계를 문서화했습니다. + ## [0.2.19-r7] - 2026-09-21 ### 개선 @@ -1360,6 +1984,7 @@ - 대시보드와 SafeShield 통계 계약 테스트에 최근 24시간 차단 수만 `text-teal-700` 강조를 사용하고, DNS 요청 및 차단율은 기본 텍스트 색상을 유지하는 회귀 검증을 추가했습니다. - README에 대시보드/SafeShield 차단 수치 강조 정책을 문서화했습니다. + ## [0.2.19-r6] - 2026-09-20 ### 수정 @@ -1371,6 +1996,7 @@ - IPTV/내비게이션 계약 테스트에 접힌 사이드바가 반드시 단일 `β` 문자를 사용하고 `20x20px` 배지와 `13px` 글꼴을 유지하는지 검증하는 회귀 테스트를 추가했습니다. 기존의 `8px` 크기로 다시 작아지는 회귀도 차단합니다. - README의 IPTV Beta 배지 설명을 접힌 사이드바의 단일 `β` 원형 배지 동작에 맞게 갱신했습니다. + ## [0.2.19-r5] - 2026-09-20 ### 수정 @@ -1382,6 +2008,7 @@ - IPTV/내비게이션 계약 테스트에 접힌 사이드바의 Beta 배지가 더 이상 `8px` 단일 문자 스타일을 사용하지 않고, 더 큰 pill 크기를 유지하는지 검증하는 회귀 테스트를 추가했습니다. - README의 IPTV Beta 설명을 접힌 사이드바에서도 읽기 쉬운 Beta 배지를 유지하는 동작에 맞게 갱신했습니다. + ## [0.2.19-r4] - 2026-09-20 ### 개선 @@ -1395,6 +2022,7 @@ - IPTV UI 계약 테스트에 `enabled`와 `provider`를 모두 포함한 dirty-state 판정, amber 경고 아이콘/`저장되지 않음` 표시, 저장된 런타임 기준 안내, SKB/LG U+ 공통 프로파일 설명을 고정하는 회귀 검증을 추가했습니다. - README의 IPTV Beta 설명을 현재 provider/profile UI와 저장되지 않은 설정 경고 동작에 맞게 갱신했습니다. + ## [0.2.19-r3] - 2026-09-20 ### 수정 @@ -1408,6 +2036,7 @@ - 패키지 계약 테스트에 rpcd reconcile helper 사용을 고정하고 ShellSpec 전체 계약에 새 회귀 테스트를 연결했습니다. - README에 패키지 업그레이드 시 rpcd self-heal 정책을 문서화했습니다. + ## [0.2.19-r2] - 2026-09-20 ### 기능 @@ -1428,6 +2057,7 @@ - IPTV RPC/ACL, IGMP Proxy·IGMP snooping 구성, rollback, 서비스 lifecycle, Beta UI, SKB/LG U+ 선택 제한을 검증하는 회귀 테스트를 추가했습니다. - README에 IPTV Beta 기능, 의존성, 적용 방식과 현재 지원 범위를 문서화했습니다. + ## [0.2.19-r1] - 2026-09-20 ### 개선 @@ -1438,6 +2068,7 @@ - 예약 재부팅 기능이 시작되지 않던 버그를 수정하였습니다. + ## [0.2.18-r4] - 2026-09-20 ### 수정 @@ -1450,6 +2081,7 @@ - 패키지 계약 테스트에 `smartsafehub-maintenance enable`이 항상 포함되고 `PKG_UPGRADE` 또는 기존 enabled 상태에 따라 조건부로 생략되지 않는지 검증하는 회귀 테스트를 추가했습니다. - README에 maintenance 서비스의 업그레이드 복구 정책과 postinst 서비스 enable 동작을 문서화했습니다. + ## [0.2.18-r3] - 2026-09-20 ### 개선 @@ -1464,6 +2096,7 @@ - 기존 Wi-Fi, 시스템 시간, 업데이트 UI 계약을 native select 스타일 검사 대신 공통 `CustomSelect` 사용 여부를 검증하도록 갱신했습니다. - README의 선택 컨트롤 설명을 공통 커스텀 드롭다운 동작에 맞게 갱신했습니다. + ## [0.2.18-r2] - 2026-09-20 ### 수정 @@ -1474,6 +2107,7 @@ - 다시 border 관련 문제가 발생하지 않도록 회귀 테스트 코드를 추가하였습니다. + ## [0.2.18-r1] - 2026-09-20 ### 개선 @@ -1485,6 +2119,7 @@ - Updater가 시작되지 않던 버그를 수정하였습니다. + ## [0.2.17-r9] - 2026-09-20 ### 개선 @@ -1498,6 +2133,7 @@ - `tests/test-safeshield-page-contract.sh`에 정적 ULTIMATE/PRO/기타 유료 플랜 surface, glow·shine 제거, 다크 모드 ULTIMATE 규칙 단일 정의를 검증하는 소스 스타일 계약을 추가했습니다. - README의 유료 플랜 표시 설명을 정적인 premium chip 디자인에 맞게 갱신했습니다. + ## [0.2.17-r8] - 2026-09-20 ### 개선 @@ -1511,6 +2147,7 @@ - `tests/test-navigation-contract.sh`에 데스크톱 제품 헤더와 사이드바 브랜드 영역이 정확히 `72px`를 사용하고, 사이드바 토글 기준선도 같은 `72px`에 맞는지 검증하는 회귀 계약을 추가했습니다. - README에 데스크톱 application shell의 공통 `72px` 헤더 규칙과 유지되는 시각적 강조 요소를 문서화했습니다. + ## [0.2.17-r7] - 2026-09-20 ### 안정성 @@ -1523,6 +2160,7 @@ - 패키지 계약 테스트에 `SMARTSAFEHUB_DEV_ROUTER`가 `defineConfig` callback 안에서만 읽히는지, production build base가 기존 `/luci-static/smartsafehub/`로 유지되는지 검증하는 회귀 계약을 추가했습니다. - README에 production build에서는 개발용 router 환경 변수를 읽거나 검증하지 않는다는 점을 명시했습니다. + ## [0.2.17-r6] - 2026-09-20 ### 수정 @@ -1536,6 +2174,7 @@ - 패키지 계약 테스트에 Vite serve/build base 분리, 개발용 stylesheet root 경로, `/cgi-bin` proxy 계약을 추가했습니다. - README의 로컬 개발 접속 주소를 `http://localhost:5173/`로 갱신하고 production base가 유지된다는 점을 명시했습니다. + ## [0.2.17-r5] - 2026-09-20 ### 개발 환경 @@ -1549,6 +2188,7 @@ - 패키지 계약 테스트에 개발용 Shadow DOM stylesheet 경로, `SMARTSAFEHUB_DEV_ROUTER` 기반 proxy, `/cgi-bin` 전달, production base 유지 계약을 추가했습니다. - README에 실제 공유기 API를 사용한 로컬 Vite 개발 방법과 최종 장치 검증이 필요한 범위를 문서화했습니다. + ## [0.2.17-r4] - 2026-09-20 ### 수정 @@ -1562,6 +2202,7 @@ - `tests/test-navigation-contract.sh`에서 라이트/다크 및 hover 상태의 상단 액션 테두리 색상이 Cloud Console 토큰과 일치하는지 검증합니다. - README에 공유기 웹사이트와 Cloud Console의 상단 액션 테두리 색상 통일 정책을 반영했습니다. + ## [0.2.17-r3] - 2026-09-20 ### 개선 @@ -1573,6 +2214,7 @@ - 내비게이션 계약에 본문 최대 폭 유지/대화면 왼쪽 정렬, 헤더 전체 폭 사용, 오른쪽 액션 정렬, Cloud Console형 버튼 크기와 다크 모드 대비가 유지되는지 검증하는 회귀 테스트를 추가했습니다. + ## [0.2.17-r2] - 2026-09-20 ### 수정 @@ -1586,6 +2228,7 @@ - 대시보드와 업데이트 페이지가 동일한 관리 소프트웨어 freshness helper를 사용하는지, 지연 상태에서 초록색 최신 상태를 표시하지 않는지 검증하는 UI 계약을 추가했습니다. - 패키지 post-install hook이 `smartsafehub-updater enable`을 항상 수행하고 자기 업데이트 중 서비스 강제 재시작을 하지 않는지 검증하는 패키지 계약을 추가했습니다. + ## [0.2.17-r1] - 2026-09-19 ### 개선 @@ -1597,12 +2240,14 @@ - SafeShield의 최소 버전을 0.3.24 이상으로 올렸습니다. + ## [0.2.16-r15] - 2026-09-19 ### 수정 - SafeShield의 최소 버전을 0.3.24 이상으로 강제합니다. + ## [0.2.16-r14] - 2026-09-19 ### 개선 @@ -1617,6 +2262,7 @@ - `tests/test-settings-ui-contract.sh`에 백업 카드의 전체 폭 제거, compact 세로 레이아웃, 통합 시스템 도구 카드와 재부팅/고급 도구 접근성 계약을 추가했습니다. - README의 설정 화면 설명을 새 시스템 관리 2열 레이아웃에 맞게 업데이트했습니다. + ## [0.2.16-r13] - 2026-09-19 ### 개선 @@ -1628,6 +2274,7 @@ - `tests/test-update-ui-contract.sh`에 수동 펌웨어 펼침 본문의 컴팩트한 상단 여백 계약을 추가했습니다. - README의 수동 펌웨어 설치 UI 설명에 펼친 상태의 간격 정책을 반영했습니다. + ## [0.2.16-r12] - 2026-09-19 ### 개선 @@ -1639,6 +2286,7 @@ - `tests/test-update-ui-contract.sh`에 수동 펌웨어 안전 안내의 데스크톱 한 줄 표시와 불필요한 `max-w-3xl` 제한 제거 계약을 추가했습니다. - README의 수동 펌웨어 설치 UI 설명에 넓은 화면 한 줄 표시 동작을 반영했습니다. + ## [0.2.16-r11] - 2026-09-19 ### 개선 @@ -1651,6 +2299,7 @@ - `tests/test-update-ui-contract.sh`에 두 업데이트 카드의 간결한 문구와 데스크톱 한 줄 표시 계약을 추가했습니다. - README의 업데이트 UI 설명에 넓은 화면에서 소개 문구를 한 줄로 유지하는 동작을 반영했습니다. + ## [0.2.16-r10] - 2026-09-19 ### 개선 @@ -1665,6 +2314,7 @@ - 자동 업데이트, LAN/DHCP, Wi-Fi의 저장 전 변경 상태·원래 값 복원·저장 버튼 활성 조건·접근 가능한 경고 표시를 검증하는 계약 테스트를 보강했습니다. - README의 LAN/DHCP, Wi-Fi, 업데이트 설명에 저장되지 않은 변경사항 표시 동작을 반영했습니다. + ## [0.2.16-r9] - 2026-09-19 ### 개선 @@ -1678,6 +2328,7 @@ - 예약 재부팅 dirty state와 경고 아이콘/문구가 연결되어 있는지, 접근성 상태와 다크 모드 amber 토큰 계약이 유지되는지 테스트를 보강했습니다. - README에 저장되지 않은 예약 재부팅 변경사항의 표시 동작을 추가했습니다. + ## [0.2.16-r8] - 2026-09-19 ### 개선 @@ -1691,6 +2342,7 @@ - `tests/test-settings-ui-contract.sh`와 `tests/test-scheduled-reboot.sh`에 예약 재부팅이 독립 시스템 관리 카드가 아니라 시간 및 시간대 카드 내부의 접근 가능한 하위 섹션으로 렌더링되는 계약을 추가했습니다. - README에 예약 재부팅 설정의 새 배치와 반응형 동작을 반영했습니다. + ## [0.2.16-r7] - 2026-09-18 ### 기기별 통계 TOP 3 인지성 개선 @@ -1704,6 +2356,7 @@ - `tests/test-statistics-ui-contract.sh`에 TOP 3 배지와 차단 수 기준 안내 계약을 추가했습니다. - README의 기기별 통계 설명에 TOP 3 표시와 선정 기준 안내를 반영했습니다. + ## [0.2.16-r6] - 2026-09-18 ### 기기별 통계 목록 압축 @@ -1717,6 +2370,7 @@ - `tests/test-statistics-ui-contract.sh`에 기본 3개 미리보기, 전체 보기 토글, 접근성 속성, 펼친 상태에서만 페이지네이션을 제공하는 계약을 추가했습니다. - README에 기기별 통계의 미리보기/전체 보기 동작을 반영했습니다. + ## [0.2.16-r5] - 2026-09-18 ### 라이선스 daemon 종료 안정성 @@ -1735,6 +2389,7 @@ - `tests/test-license.sh`에 daemon 장기 sleep 중 SIGTERM 종료, Hub HTTP 진단 상태, activation 결과/오류 보존 계약을 추가했습니다. - README와 아키텍처 문서에 daemon 종료 경계와 새 진단 필드 의미를 반영했습니다. + ## [0.2.16-r4] - 2026-09-18 ### 라이선스 활성화 안정성 @@ -1754,6 +2409,7 @@ - `tests/test-license.sh`에 rpcd 경로에서 SafeShield 동기 호출 금지, detached helper의 device payload 구성, identity 조회 실패 시 fail-closed 동작, 카드 내부 feedback routing과 완화된 polling 계약을 추가했습니다. - README와 아키텍처 문서에 비동기 activation 경계와 라이선스 카드 피드백 동작을 반영했습니다. + ## [0.2.16-r3] - 2026-09-18 ### 테스트 안정성 @@ -1761,6 +2417,7 @@ - `tests/test-license.sh`의 라이선스 mock 상태를 각 시나리오 시작 전에 명시적으로 초기화하도록 변경해 macOS `/bin/sh`를 포함한 셸 구현 차이로 이전 실패 시나리오의 `MOCK_*` 값이 다음 stale-lock 복구 테스트에 영향을 주지 않도록 했습니다. - stale activation lock 복구 시나리오는 `license_get`과 Hub status mock을 정상 상태로 명시한 뒤 lock 제거, 임시 activation request 정리, `/licenses/status` 재개까지 독립적으로 검증합니다. + ## [0.2.16-r2] - 2026-09-18 ### 라이선스 lifecycle 분리 @@ -1781,6 +2438,7 @@ - Hub activate 성공/거부, active/revoked status, `clear_license`, 서버 장애 fail-open, SafeShield 로컬 조회 실패, 미설정 상태와 activate/status 경쟁 조건을 검증하는 `test-license.sh`를 추가했습니다. - RPC/ACL, 패키지 설치 서비스, shell 정적 검증 계약에 새 라이선스 모듈을 포함하고 README 및 아키텍처 문서를 갱신했습니다. + ## [0.2.16-r1] - 2026-09-18 ### 기능 @@ -1794,6 +2452,7 @@ - 대시보드, 펌웨어 페이지의 UI를 개선하였습니다. - 재부팅 후 진단 기능이 준비가 된 이후에 확인하도록 개선하였습니다. + ## [0.2.15-r29] - 2026-09-18 ### Health Reporter 상태 UI 개선 @@ -1808,6 +2467,7 @@ - Health 계약 테스트에 즉시 optimistic 상태 반영, 실패 시 rollback, 짧은 확인 polling, stale `disabled/never` 상태의 `첫 보고 준비 중` 표시와 명확한 켜짐/꺼짐 안내를 검증하는 회귀 계약을 추가했습니다. - README와 아키텍처 문서에 Reporter 토글의 즉시 UI 반영과 첫 보고 확인 흐름을 반영했습니다. + ## [0.2.15-r28] - 2026-09-18 ### Health Reporter 라이선스 인증 수정 @@ -1820,6 +2480,7 @@ - Health 테스트의 `license_get` mock을 실제 SafeShield 중첩 응답과 동일하게 변경하고, 중첩 키가 `X-SafeShield-License-Key` 인증 헤더까지 전달되는 기존 통합 시나리오와 실제 응답 경로 계약을 회귀 검증합니다. - README와 아키텍처 문서에 `license_get` 응답 구조와 Health Reporter 인증 경로를 반영했습니다. + ## [0.2.15-r27] - 2026-09-18 ### 새로고침 경로 유지 @@ -1833,6 +2494,7 @@ - 내비게이션 계약 테스트에 `sessionStorage` 기반 유효 route 저장, Navigation Timing의 reload 판별, History API 복원, 일반 navigation 초기화 계약을 추가했습니다. - README와 아키텍처 문서에 hash route 새로고침 보존 정책을 반영했습니다. + ## [0.2.15-r26] - 2026-09-18 ### 루트 URL 진입 @@ -1847,6 +2509,7 @@ - exact-root rewrite JSON 계약, `index_page` 비변경, 기존 `json_script` 보존, 중복 설치/제거 idempotency, 선택적 uHTTPd reload를 검증하는 `test-root-url-rewrite.sh`를 추가했습니다. - 패키지 설치/제거 hook과 공식 public URL 정규화에 대한 회귀 계약을 보강하고 README/아키텍처 문서를 갱신했습니다. + ## [0.2.15-r25] - 2026-09-17 ### UI 개선 @@ -1867,6 +2530,7 @@ - 테스트용 `jsonfilter` stub이 JSON boolean `false`를 누락하지 않도록 수정해 실제 OpenWrt jsonfilter 동작에 더 가깝게 만들었습니다. - 대시보드 계약 테스트에 정상 `자세히 보기` 문구와 장치 진단 `준비 중` 렌더링 계약을 추가하고 README/아키텍처 문서를 갱신했습니다. + ## [0.2.15-r24] - 2026-09-17 ### 수정 @@ -1877,6 +2541,7 @@ - 대시보드 계약 테스트에 사설 WAN 판별이 undefined-safe octet 변수를 사용하고 직접 배열 인덱스 비교를 다시 도입하지 않는 회귀 검사를 추가했습니다. 프런트엔드 CI의 기존 `npm run build`(`tsc --noEmit` 포함)와 함께 strict TypeScript 타입 오류를 차단합니다. + ## [0.2.15-r23] - 2026-09-17 ### UI 개선 @@ -1890,6 +2555,7 @@ - 대시보드 계약 테스트에 LAN 상태 조회/전달/새로고침, 정상·충돌 Internet 카드 문구, 상위/LAN 네트워크 상세, 사설 WAN 표시와 LAN 설정 이동 계약을 추가했습니다. - README와 아키텍처 문서에 대시보드 Internet/LAN 상태 통합 방식을 반영했습니다. + ## [0.2.15-r22] - 2026-09-17 ### 개선 @@ -1903,6 +2569,7 @@ - LAN 계약 테스트에 4-octet 공유기 주소 입력, 각 octet 3자리 제한, DHCP prefix 잠금, 공유기 주소 변경 시 DHCP 시작/종료 prefix 자동 동기화 계약을 추가했습니다. - README와 아키텍처 문서에 LAN/DHCP 입력 UX와 동일 prefix 유지 원칙을 반영했습니다. + ## [0.2.15-r21] - 2026-09-17 ### 수정 @@ -1919,6 +2586,7 @@ - GitHub Actions에서는 기존 `SMARTSAFEHUB_REQUIRE_UCODE=1` 계약에 의해 이 런타임 호환성 테스트도 ucode 없이 skip할 수 없도록 했습니다. - README와 아키텍처 문서에 OpenWrt 25.12의 LAN list/CIDR 저장 형식과 호환성 테스트를 반영했습니다. + ## [0.2.15-r20] - 2026-09-17 ### 수정 @@ -1937,6 +2605,7 @@ - 정적 검증에 회귀 계약을 추가해 다른 기능별 테스트가 다시 raw `ucode -c`를 추가하지 못하도록 했습니다. - README의 ucode 진단 및 배포 전 검사 절차를 새 CI 컴파일 계약에 맞게 갱신했습니다. + ## [0.2.15-r19] - 2026-09-17 ### 수정 @@ -1952,6 +2621,7 @@ - 프런트엔드가 LAN 전용 ubus 객체를 직접 사용하는지, LAN 객체 자체에 관리자 비밀번호 gate와 최소 ACL이 적용되는지 검증하도록 테스트를 보강했습니다. - README와 아키텍처 문서의 LAN RPC 호출 흐름 및 진단 명령을 실제 구현과 일치하도록 갱신했습니다. + ## [0.2.15-r18] - 2026-09-17 ### 수정 @@ -1965,6 +2635,7 @@ - LAN 구현 모듈이 공개 RPC 진입점에 직접 import되지 않는지, 내부 LAN RPC가 기존 canonical 모듈 경로를 사용하는지, 내부 객체가 브라우저 ACL에 노출되지 않는지 회귀 테스트를 보강했습니다. - LAN RPC 격리 구조와 `ucode -c` 진단 방법을 README 및 아키텍처 문서에 반영했습니다. + ## [0.2.15-r16] - 2026-09-17 ### 기능 @@ -1985,6 +2656,7 @@ - LAN RPC 등록/ACL, UCI 변경·롤백·지연 재적용, WAN/LAN 충돌 거부, 추천 대역 적용, 프런트엔드 route/API/hook/UI 계약을 검증하는 `test-lan-settings.sh`를 추가했습니다. - 내비게이션/RPC/정적 검증 계약을 새 LAN 기능에 맞게 보강하고 README와 아키텍처 문서를 최신화했습니다. + ## [0.2.15-r15] - 2026-09-17 ### 수정 @@ -1996,6 +2668,7 @@ - 대시보드 계약 테스트에 정상/주의/이상 장치 진단 배경과 세부 항목 배경·테두리의 다크 테마 매핑이 유지되는지 확인하는 회귀 검사를 추가했습니다. + ## [0.2.15-r14] - 2026-09-17 ### 수정 @@ -2008,6 +2681,7 @@ - 연결 기기 목록 확인 시각이 오래되어도 `metaWarning`이나 stale 기반 warning 상태를 적용하지 않는 계약을 추가했습니다. - 연결 기기의 최근 확인 시각은 `목록 확인: 상대 시간` 형식으로 계속 표시되는지 검증합니다. + ## [0.2.15-r13] - 2026-09-17 ### UI 개선 @@ -2022,6 +2696,7 @@ - 정상/주의·이상/대기 상태의 장치 진단 요약과 최대 2건의 이상 항목, 설정 페이지 상세 링크가 유지되는지 검증합니다. - 수정한 대시보드 계약 테스트의 설명과 실패 메시지를 가능한 범위에서 한글화했습니다. + ## [0.2.15-r12] - 2026-09-17 ### 수정 @@ -2033,6 +2708,7 @@ - `test-health.sh`에 GNU awk 예약 이름을 다시 `-v load=` 형태로 사용하지 못하도록 회귀 검사를 추가했습니다. - GitHub Actions와 ShellSpec 출력에서 테스트 목적을 바로 확인할 수 있도록 ShellSpec 계약 테스트 이름을 가능한 범위에서 한글 설명으로 통일했고, Health 계약 테스트의 실패/성공 메시지도 한글화했습니다. + ## [0.2.15-r11] - 2026-09-17 ### 수정 @@ -2046,6 +2722,7 @@ - 새 RPC 권한이 아직 반영되지 않은 기존 세션과 실제 만료 세션을 구분하는 런타임 테스트를 보강했습니다. - 패키지 postinst가 LuCI 캐시 정리와 `rpcd reload`를 수행하는 계약 테스트를 추가했습니다. + ## [0.2.15-r10] - 2026-09-17 ### 기능 @@ -2067,6 +2744,7 @@ - 새 Health RPC/ACL, UCI 기본값, procd 서비스, 프런트엔드 API/hook/UI와 패키지 post-install 활성화 계약을 기존 정적/패키지/RPC/UI contract에 추가했습니다. - Health helper가 내부적으로 다른 ubus 객체를 호출하므로 rpcd handler에서 동기 실행하지 않고 detached process로 시작하는 회귀 계약을 추가해 rpcd 이벤트 루프 교착을 방지했습니다. + ## [0.2.15-r9] - 2026-09-17 ### UI 개선 @@ -2080,6 +2758,7 @@ - 설정 페이지가 SmartSafeHub 제품 펌웨어 버전/build ID를 우선 표시하고 OpenWrt fallback 및 runtime kernel을 유지하는지 검증하는 UI contract를 추가했습니다. - 설정 페이지의 전역 새로고침이 펌웨어 상태도 함께 갱신하는지 검증합니다. + ## [0.2.15-r8] - 2026-09-17 ### UI 개선 @@ -2093,6 +2772,7 @@ - Dashboard가 펌웨어 상태 RPC의 커스텀 firmware metadata를 함께 읽고 SmartSafeHub 제품 버전/build ID를 우선 사용하며 OpenWrt fallback과 실제 runtime kernel을 유지하는지 contract를 추가했습니다. - freshness label과 상대 시간이 콜론으로 분리되는 UI 계약을 추가했습니다. + ## [0.2.15-r7] - 2026-09-17 ### UI 개선 @@ -2106,6 +2786,7 @@ - 대시보드에 별도 freshness 섹션이 다시 추가되지 않는지, 세 개의 개요 카드가 상대 시간 메타데이터를 사용하고 stale 상태를 개별 경고하는지 검증하도록 dashboard UI contract를 보강했습니다. - 상대 시간 formatter가 `방금 전`/분/시간/일 단위를 제공하는 계약을 추가했습니다. + ## [0.2.15-r6] - 2026-09-17 ### 개선 @@ -2119,6 +2800,7 @@ - production 소스에 `/tmp/smartsafehub-*` 형태의 최상위 런타임 경로가 다시 추가되지 않는지, 각 helper/init script가 `/tmp/smartsafehub/`를 생성하는지, rpcd/ACL/프런트엔드 업로드 경로가 동일한 디렉터리를 사용하는지 검증하는 runtime path contract를 추가했습니다. - helper를 직접 실행하는 경우에도 지정한 runtime directory가 자동 생성되고 상태 파일이 그 아래에 기록되는지 검증합니다. + ## [0.2.15-r5] - 2026-09-17 ### 개선 @@ -2131,6 +2813,7 @@ - 펌웨어 helper/config/API에 `check_enabled` 의존성이 다시 추가되지 않는지 검증하고, 동시에 관리 소프트웨어의 `check_enabled`가 유지되는지 확인하는 회귀 테스트를 추가했습니다. + ## [0.2.15-r4] - 2026-09-17 ### 수정 @@ -2142,6 +2825,7 @@ - package contract에 runtime post-install hook이 `smartsafehub-firmware enable`을 포함하고, `PKG_UPGRADE`나 기존 enabled 상태에 따라 실행을 건너뛰지 않는지 검증하는 회귀 테스트를 추가했습니다. + ## [0.2.15-r3] - 2026-09-17 ### UI 개선 @@ -2153,6 +2837,7 @@ - 사이드바 토글이 지정된 좌/우 chevron SVG 경로와 동일한 크기를 유지하고, 기존 패널 외곽선 SVG가 다시 사용되지 않는지 검증하는 navigation contract를 추가했습니다. + ## [0.2.15-r2] - 2026-09-17 ### 개선 @@ -2167,6 +2852,7 @@ - 펌웨어와 관리 소프트웨어의 부팅 초기 확인이 두 번째 시도에서 성공하면 즉시 중단되고, 연속 실패 시 정확히 3회에서 멈추는 회귀 테스트를 추가했습니다. - 관리 소프트웨어 확인 실패 후 `last_attempt_at`이 기록되고, 일반 확인 주기가 지나기 전에는 추가 저장소 갱신을 실행하지 않는 회귀 테스트를 추가했습니다. + ## [0.2.15-r1] - 2026-09-15 ### 기능 @@ -2180,6 +2866,7 @@ - 일부 성능이 개선되었습니다. + ## [0.2.14-r16] - 2026-09-15 ### 수정 @@ -2191,6 +2878,7 @@ - 초기 비밀번호 설정 RPC가 `username`/`password`를 전달하고 OpenWrt 25.12에 없는 `oldpassword`/`rpcd` 인자를 다시 추가하지 않는 호환성 회귀 테스트를 추가했습니다. + ## [0.2.14-r15] - 2026-09-15 ### UI 개선 @@ -2202,6 +2890,7 @@ - 초기 설정 제목의 고정 2줄 구조와 보안 안내 문구에서 `root` 계정명이 노출되지 않는 계약을 회귀 테스트에 추가했습니다. + ## [0.2.14-r14] - 2026-09-15 ### 기능 @@ -2221,6 +2910,7 @@ - `/etc/shadow` 기반 상태 확인, 서버/프런트엔드 비밀번호 정책, 이미 설정된 비밀번호 덮어쓰기 방지, LuCI `setPassword` 위임과 결과 재검증, 일반 RPC 보안 게이트, 빈 root 로그인 진입, 설정 완료 후 세션 종료/재로그인 계약을 검증하는 전용 회귀 테스트를 추가했습니다. - 새 RPC/ACL, package executable 목록, shell syntax/JSON validation 및 ShellSpec contract suite에 초기 비밀번호 설정 테스트를 연결했습니다. + ## [0.2.14-r13] - 2026-09-15 ### 기능 @@ -2240,6 +2930,7 @@ - 정상 OpenWrt 백업 검증, 잘못된 gzip/설정 없는 archive 거부, `sysupgrade --restore-backup` 위임, 펌웨어 build ID 재동기화, 복원 후 재부팅, 업데이트/펌웨어 busy 차단, 실패 시 백업 보존과 재부팅 금지를 검증하는 shell 회귀 테스트를 추가했습니다. - backup/restore RPC·ACL, `cgi-backup`/`cgi-upload` 경로, 프런트엔드 업로드→검증→최종 확인 계약과 설정 화면 노출을 기존 contract 테스트에 추가했습니다. + ## [0.2.14-r12] - 2026-09-15 ### 기능 @@ -2256,6 +2947,7 @@ - firmware resolve 응답의 `current_version`/`release.version` 보존, RPC의 현재 릴리즈 버전 매핑, UI의 제품 버전 우선 표시와 `firmware.json`에 release version을 의존하지 않는 계약을 회귀 테스트에 추가했습니다. + ## [0.2.14-r11] - 2026-09-14 ### 수정 @@ -2273,6 +2965,7 @@ - 수동 업로드 검증이 단발성 400ms refresh에 의존하지 않고 `ready`/`error` terminal state까지 추적하는지, stale error grace와 prepared image 확인 후 성공 처리 정책이 유지되는지 update UI contract 테스트를 보강했습니다. + ## [0.2.14-r10] - 2026-09-14 ### 수정 @@ -2288,6 +2981,7 @@ - 수동 펌웨어 업로드가 `luciUrl('/cgi-upload')`를 다시 사용하지 않는지, `/cgi-bin` CGI base를 통해 업로드하는지와 업로드 실패 진단 로그가 유지되는지 update UI contract 테스트를 보강했습니다. + ## [0.2.14-r9] - 2026-09-14 ### 기능 @@ -2308,6 +3002,7 @@ - 예약 재부팅 비활성화 기본값, due schedule 실행, 동일 minute 중복 실행 방지, 최근 부팅 보호, 관리 소프트웨어/펌웨어 작업 중 연기, 연기 후 재시도, 2시간 timeout, UCI 입력 검증을 mock 기반 shell 테스트로 추가했습니다. - scheduled reboot RPC/ACL, frontend hook/API, 시스템 관리 UI, maintenance init/helper 실행 권한과 시간대 변경 연동을 기존 contract 테스트에 추가했습니다. + ## [0.2.14-r8] - 2026-09-14 ### 기능 @@ -2325,6 +3020,7 @@ - `system_time_sync` RPC/ACL, NTP 비활성화 차단, `sysntpd restart`, 동기화 후 장치 시간 재조회와 UI 동작을 검증하도록 시스템 시간 contract를 확장했습니다. - 공통 select 스타일이 native appearance를 제거하고 일관된 우측 inset 화살표와 padding을 유지하는지 네트워크/시간 설정 contract에 회귀 검증을 추가했습니다. + ## [0.2.14-r7] - 2026-09-14 ### 기능 @@ -2345,6 +3041,7 @@ - 시간대 목록 조회, 지원하지 않는 시간대 차단, `zonename`/`timezone` 동시 저장, 시스템 reload, 실패 시 rollback, 자동 업데이트 일정 marker 초기화와 updater 재시작을 검증하는 전용 contract 테스트를 추가했습니다. - 설정 페이지에서 중복 업데이트 진입점이 제거되고 시간대/진단/재부팅/고급 설정 영역이 유지되는지 검증하도록 UI contract와 RPC/ACL 검증을 보강했습니다. + ## [0.2.14-r6] - 2026-09-14 ### 개선 @@ -2357,6 +3054,7 @@ - 수동 펌웨어 입력이 시각적으로 숨겨진 실제 file input과 별도의 SmartSafeHub 파일 선택 컨트롤을 사용하는지, 한국어 파일 선택 상태와 업로드 검증 동작이 유지되는지 UI contract 테스트를 추가했습니다. + ## [0.2.14-r5] - 2026-09-14 ### 개선 @@ -2371,6 +3069,7 @@ - 관리 소프트웨어가 하나의 카드로 렌더링되는지, 현재 상태와 자동 업데이트 설정이 그 카드 내부에 함께 존재하는지, 이전 범위 경고와 `SmartSafeHub 자동 업데이트` 명칭이 다시 노출되지 않는지 contract 테스트를 보강했습니다. - 펌웨어와 관리 소프트웨어의 고객용 제목 및 업데이트 페이지 설명 용어를 회귀 검증하도록 관련 UI contract를 갱신했습니다. + ## [0.2.14-r4] - 2026-09-14 ### 개선 @@ -2384,6 +3083,7 @@ - 수동 펌웨어 설치가 펌웨어 카드 내부의 inset subsection으로 유지되는지, SmartSafeHub 자동 업데이트 범위 문구가 존재하는지, 최신/미확인 상태 안내가 SmartSafeHub 업데이트 카드 밖으로 분리되지 않는지 contract 테스트를 보강했습니다. + ## [0.2.14-r3] - 2026-09-14 ### 개선 @@ -2403,6 +3103,7 @@ - 펌웨어 카드가 소프트웨어 카드보다 먼저 렌더링되는지, 수동 펌웨어 설치가 같은 카드 내부의 접이식 영역인지, 넓은 화면에서 소프트웨어 상태와 자동 업데이트 설정이 2열로 구성되는지 contract 테스트를 추가했습니다. - 고객용 펌웨어 제목에 `OpenWrt 펌웨어` 표현이 다시 노출되지 않고, build ID 누락 시 기술 상세가 disclosure 안에 유지되는지 회귀 검증을 추가했습니다. + ## [0.2.14-r2] - 2026-09-13 ### 기능 @@ -2424,6 +3125,7 @@ - Hub resolve 요청, 온라인 다운로드 무결성 검증, OpenWrt 이미지 검증, 수동 업로드, 준비 파일 정리와 강제 업그레이드 금지 동작을 mock 기반 shell 테스트로 추가했습니다. - firmware RPC/ACL, 패키지 의존성, 업데이트 UI, 재부팅 후 reconnect/reload 안전성, Stable/Beta 자동 설치 기본값을 contract 테스트에 추가했습니다. + ## [0.2.14-r1] - 2026-09-13 ### 개선 @@ -2435,6 +3137,7 @@ - 업그레이드 시에 잘못된 로직을 수정하였습니다. + ## [0.2.13-r8] - 2026-09-13 ### 개선 @@ -2447,6 +3150,7 @@ - SafeShield 페이지와 체크인된 `app.js`/`app.css`에서 유료 플랜의 `멤버십 활성` 문구와 관련 status-caption 스타일이 다시 노출되지 않는지 회귀 검증을 추가했습니다. + ## [0.2.13-r7] - 2026-09-13 ### 개선 @@ -2460,6 +3164,7 @@ - SafeShield 페이지 계약 테스트에 refined 라이선스 summary/editor 클래스와 정적 자산 반영 여부를 추가하고, `unlicensed`/`active` 같은 raw 상태 텍스트가 다시 노출되지 않도록 회귀 검증을 강화했습니다. + ## [0.2.13-r6] - 2026-09-12 ### 개선 @@ -2472,6 +3177,7 @@ - SafeShield 페이지 계약 테스트에 premium badge의 high-contrast 색상, ULTIMATE shine keyframe, tier별 활성 caption 및 reduced-motion 대응이 소스와 체크인된 CSS에 유지되는지 검증을 추가했습니다. + ## [0.2.13-r5] - 2026-09-12 ### 수정 @@ -2485,6 +3191,7 @@ - SmartSafeHub와 SafeShield가 각각 로컬 APK identity pin 상태여도 `apk add --upgrade --latest`를 호출하지 않고 world 항목만 정규화한 뒤 target-only 업그레이드가 수행되는지 검증합니다. - unrelated package의 버전 constraint와 identity pin이 그대로 보존되는지, updater 소스에 광범위한 `--latest`/`--available` 업그레이드 경로가 다시 추가되지 않는지 회귀 테스트를 강화했습니다. + ## [0.2.13-r4] - 2026-09-12 ### 개선 @@ -2498,6 +3205,7 @@ - SafeShield UI 계약 테스트에 전용 플랜 컴포넌트, FREE 전용 CTA 조건, pricing URL, 안전한 새 탭 링크 속성, 체크인된 `app.js`/`app.css` 멤버십 UI 포함 여부를 추가했습니다. + ## [0.2.13-r3] - 2026-09-12 ### 수정 @@ -2511,6 +3219,7 @@ - SafeShield UI 계약 테스트에 상태/통계 timer 독립성, 성공 메시지 자동 닫기, 오류 지속 정책, README 최소 버전 일치 여부를 추가했습니다. + ## [0.2.13-r2] - 2026-09-12 ### 수정 @@ -2522,6 +3231,7 @@ - SafeShield UI 계약 테스트에서 수동 갱신의 일시적인 시작/진행 안내 문구가 소스와 배포 `app.js`에 다시 포함되지 않는지 검증합니다. + ## [0.2.13-r1] - 2026-09-10 ### 개선 @@ -2530,6 +3240,7 @@ - 로그인 세션이 만료 되었을때, 사용자에게 안내를 하고 로그인 페이지로 이동하도록 하였습니다. - 일부 UI에 대한 시인성을 개선하였습니다. + ## [0.2.12-r13] - 2026-09-10 ### 개선 @@ -2542,6 +3253,7 @@ - SafeShield 페이지 계약 테스트를 보강해 외곽 진행 띠 마크업, 회전 애니메이션 스타일, reduced-motion 대응이 체크인된 소스와 `app.css`에 모두 존재하는지 검증합니다. + ## [0.2.12-r12] - 2026-09-10 ### 수정 @@ -2554,6 +3266,7 @@ - SafeShield가 identity pin된 상태에서 최신 SmartSafeHub가 더 새로운 SafeShield를 요구하는 실제 실패 조건을 회귀 테스트로 추가했습니다. pin이 제거되고 SafeShield 의존성이 갱신된 뒤 SmartSafeHub 설치가 완료되는지 검증합니다. - SafeShield identity pin 해제가 실패하면 `UPDATES_INSTALL_FAILED`로 남고 SmartSafeHub 설치를 진행하지 않는지, 다른 패키지의 world constraint 및 identity pin은 변경되지 않는지 검증합니다. + ## [0.2.12-r11] - 2026-09-10 ### 테스트 @@ -2565,6 +3278,7 @@ - LuCI session endpoint의 401/403, `X-LuCI-Login-Required`, plain `Access denied`, HTML 로그인 응답, redirect, 올바른 session ID, HTTP 500 및 잘못된 payload를 각각 실행해 인증/만료/통신 오류가 혼동되지 않도록 검증합니다. 같은 세션에서 동시에 여러 RPC가 실패해도 session-expired 이벤트가 한 번만 발생하고, 새 로그인 lifecycle에서는 다시 정상적으로 발생할 수 있는지도 실제 `sessionEvents.ts` 모듈로 검증합니다. - ShellSpec 계약에 reload/session 안전 회귀 테스트를 추가하고, 정적 검증 및 패키지 계약에서 새 테스트 실행 파일의 존재와 실행 권한도 확인하도록 보강했습니다. + ## [0.2.12-r10] - 2026-09-10 ### 개선 @@ -2576,6 +3290,7 @@ - 상단 상태 배지가 유지되고 하단 요약에는 중복 보호 상태 대신 SafeShield 버전이 표시되며, 설명 영역에 중복 버전 텍스트가 남지 않는지 UI contract를 보강했습니다. + ## [0.2.12-r9] - 2026-09-10 ### 수정 @@ -2587,6 +3302,7 @@ - 초기 `data=null`이 self-update reload 기준값으로 사용되지 않는지, 첫 실제 update state는 기준값만 설정하는지, 이후 새로운 `lastInstallAt` 변화에만 자동 새로고침 조건이 열리는지 update UI contract를 보강했습니다. + ## [0.2.12-r8] - 2026-09-10 ### 수정 @@ -2600,6 +3316,7 @@ - RPC `Access denied`가 추가 session probe 없이 즉시 `SESSION_EXPIRED`로 전환되는지 contract를 보강했습니다. - 초기 로드의 단순 asset/version mismatch는 reload하지 않고, 새 `lastInstallAt`을 관찰한 updater 완료 시점에만 한 번 reload하는 contract를 추가했습니다. + ## [0.2.12-r7] - 2026-09-10 ### 수정 @@ -2613,6 +3330,7 @@ - Access denied 처리 시 probe된 session ID와 bootstrap session ID를 정확히 비교하는지, 동시 probe deduplication과 stale-session race guard가 유지되는지 로그인 UI contract를 보강했습니다. + ## [0.2.12-r6] - 2026-09-10 ### 수정 @@ -2625,6 +3343,7 @@ - 6단계 갱신 stage 매핑, 사용자용 진행 도넛, 내부 stage 비노출, 갱신 중 보호 상태 분리, 오류 코드의 사용자용 설명 contract를 추가했습니다. + ## [0.2.12-r5] - 2026-09-10 ### 수정 @@ -2636,6 +3355,7 @@ - 모바일 `button` 최소 높이 규칙보다 switch 전용 geometry selector가 우선하도록 CSS contract를 보강하고, track/thumb에 최대 크기 제한까지 존재하는지 검증합니다. + ## [0.2.12-r4] - 2026-09-10 ### 수정 @@ -2649,6 +3369,7 @@ - RPC 접근 거부 시 세션을 재확인한 뒤에만 만료 이벤트를 발생시키는지, 로그인 화면 전환 및 toast 표시가 연결되는지 UI contract를 추가했습니다. - 업데이트 polling이 더 이상 `Access denied`를 별도로 reload하지 않고 전역 세션 만료 처리에 맡기는지 검증합니다. + ## [0.2.12-r3] - 2026-09-09 ### 수정 @@ -2660,6 +3381,7 @@ - SafeShield 통계 및 업데이트 설정 토글이 공용 고정 geometry와 theme-safe thumb 스타일을 사용하는지 UI contract를 추가했습니다. + ## [0.2.12-r2] - 2026-09-09 ### 수정 @@ -2671,6 +3393,7 @@ - 기기별 통계가 10개 단위로 분할되고 이전/다음 페이지 컨트롤을 제공하는 UI contract를 추가했습니다. + ## [0.2.12-r1] - 2026-09-09 ### 수정 @@ -2678,6 +3401,7 @@ - 업데이트 기능을 개선하였습니다. - 성능 개선이 포함되었습니다. + ## [0.2.11-r3] - 2026-09-09 ### 추가 @@ -2690,6 +3414,7 @@ - `updates_status`가 SmartSafeHub 저장소 파일에서 업데이트 채널을 읽어 반환하는 contract를 추가했습니다. - 업데이트 화면이 `Stable`/`Beta` 채널을 표시하면서 저장소 hostname 같은 내부 구현 정보는 노출하지 않는지 UI contract로 검증합니다. + ## [0.2.11-r2] - 2026-09-08 ### 수정 @@ -2703,18 +3428,21 @@ - self-update 완료 경로가 `rpcd restart`를 사용하지 않고 `rpcd reload`만 예약하는지 검증합니다. - 업데이트 설치 중 `Access denied`가 발생하면 페이지 reload 복구 경로가 존재하고, 설치 완료 후 package/asset version 불일치에서도 새 frontend asset을 위한 reload가 수행되는지 UI contract로 검증합니다. + ## [0.2.11-r1] - 2026-09-08 ### 수정 - SafeShield의 최소 버전을 0.3.20 이상으로 설정하였습니다. + ## [0.2.10-r5] - 2026-09-08 ### 수정 - 내부에서 사용하는 패키지를 업데이트 하였습니다. + ## [0.2.10-r4] - 2026-09-08 ### 수정 @@ -2730,6 +3458,7 @@ - 로컬 APK identity pin이 있는 경우 SmartSafeHub 하나에 대해서만 `apk add --upgrade --latest`를 사용해 pin을 제거하고 목표 버전으로 올라가는지 검증합니다. - updater가 `apk upgrade --available`을 사용하지 않는 contract를 추가해 다른 OpenWrt 패키지의 광범위한 업그레이드가 다시 도입되지 않도록 방지합니다. + ## [0.2.10-r3] - 2026-09-08 ### 수정 @@ -2741,6 +3470,7 @@ - 업데이트 확인 단계가 1초 polling을 사용하고 설치 단계의 3초 polling 및 idle 상태의 5분 polling과 구분되는지 UI contract에서 검증합니다. + ## [0.2.10-r2] - 2026-09-08 ### 수정 @@ -2758,6 +3488,7 @@ - updater가 자동 설치 성공 시에만 날짜 marker를 기록하고 실패 재시도에는 15분 cooldown marker를 사용하는 contract를 검증합니다. - 자동 설치 최대 시도 횟수(`3`), 15분 retry cooldown, lock 경합 제외, 성공 시 retry marker 정리, 날짜 변경 시 retry count 초기화 contract를 검증합니다. + ## [0.2.10-r1] - 2026-09-07 ### 변경 @@ -2765,6 +3496,7 @@ - 새로고침 아이콘을 개선하고 전체적으로 통일성 있게 적용하였습니다. - 업데이트를 확인하는 로직을 수정하였습니다. + ## [0.2.9-r9] - 2026-09-07 ### 수정 @@ -2779,6 +3511,7 @@ - 마지막 업데이트 확인 이후 로컬 패키지 버전이 변경된 상태에서 APK 인덱스 갱신이 실패해도 `INSTALLED`가 실제 로컬 버전으로 갱신되고 이전 릴리즈 노트 캐시는 제거되는지 검증합니다. - 새로고침/로딩 상태가 공통 `ReloadIcon`을 사용하고 기존 `LoaderIcon`/`RefreshIcon` 변형이 다시 섞이지 않는지 UI contract에서 검증합니다. + ## [0.2.9-r8] - 2026-09-07 ### 수정 @@ -2791,6 +3524,7 @@ - APK 인덱스 갱신 실패 경로에서도 기존 package state로 beta/stable 릴리즈 노트를 다시 생성할 수 있는지 검증합니다. - 릴리즈 노트 서버의 일시적인 장애에서는 같은 버전 범위의 정상 캐시가 유지되고, 버전 범위가 달라진 캐시는 제거되는지 검증합니다. + ## [0.2.9-r7] - 2026-09-07 ### 개선 @@ -2803,6 +3537,7 @@ - 설치 중 spinner/indeterminate progress, 자동 갱신 안내, repository 오류 요약/상세 정보 분리와 기존 수동 새로고침 문구 제거를 update UI contract에서 검증합니다. + ## [0.2.9-r6] - 2026-09-07 ### 수정 @@ -2814,6 +3549,7 @@ - 첫 업데이트 확인 전에는 최신 상태 success UI가 표시되지 않고 `AVAILABLE` 값이 `미확인`으로 유지되는 contract를 추가했습니다. + ## [0.2.9-r5] - 2026-09-07 ### 수정 @@ -2825,6 +3561,7 @@ - 대시보드 contract에서 메모리 progress bar가 메모리 카드 내부에 존재하고 별도의 `메모리 사용률` 블록이 다시 추가되지 않는지 검증합니다. + ## [0.2.9-r4] - 2026-09-07 ### 수정 @@ -2836,6 +3573,7 @@ - 별도의 repository 파일에 stable URL이 먼저 존재하고 `smartsafehub.list`는 beta를 가리키는 전환 시나리오에서 beta 릴리즈 노트 경로만 사용하는 회귀 테스트를 추가했습니다. + ## [0.2.9-r3] - 2026-09-06 ### 변경 @@ -2848,6 +3586,7 @@ - 업데이트 polling 주기, 설치 단계 전용 3초 polling, visibility/focus 갱신과 이벤트 listener cleanup을 update UI contract에서 검증합니다. + ## [0.2.9-r2] - 2026-09-06 ### 테스트 @@ -2857,6 +3596,7 @@ - 기존 package, navigation, document, login, dashboard, network input, update, settings, ucode import, RPC, rules, SafeShield, statistics, updater contract를 개별 ShellSpec example로 유지합니다. - GitHub Actions에서는 ShellSpec 0.28.1을 고정 설치해 테스트 프레임워크 업데이트에 따른 비결정적 실패를 방지합니다. + ## [0.2.9-r1] - 2026-09-03 ### 변경 @@ -2864,6 +3604,7 @@ - 전체 레이아웃 구조를 변경하여 사용성과 메뉴 접근성을 향상시켰습니다. - 다크 모드에 대한 지원을 추가하였습니다. + ## [0.2.8-r16] - 2026-09-03 ### 변경 @@ -2877,6 +3618,7 @@ - 업데이트/설정 route 분리, 메뉴 순서, 시스템 상태/관리 이동, 사이드바의 독립 고급 설정 링크 제거와 설정 화면 내부 LuCI fallback을 검증하는 UI contract를 보강했습니다. + ## [0.2.8-r15] - 2026-09-03 ### 변경 @@ -2889,6 +3631,7 @@ - 데스크톱/모바일 새로고침 버튼의 위치, 아이콘 전용 표현, loading/refreshing 비활성화와 회전 상태를 navigation contract에서 검증합니다. + ## [0.2.8-r14] - 2026-09-03 ### 변경 @@ -2901,6 +3644,7 @@ - 데스크톱 헤더와 모바일 상단 내비게이션의 테마 토글 위치, 사이드바/모바일 drawer 내부의 중복 테마 액션 제거를 navigation contract에서 검증합니다. + ## [0.2.8-r13] - 2026-09-03 ### 변경 @@ -2915,6 +3659,7 @@ - 사용자 이름/비밀번호 필드, autocomplete, form submit, 비밀번호 표시 토글, 공통 theme 저장/복원, 로그인 Dark Mode와 form-control 스타일을 검증하는 로그인 UI contract 테스트를 추가했습니다. + ## [0.2.8-r12] - 2026-09-03 ### 추가 @@ -2931,6 +3676,7 @@ - Dashboard 차트 재사용, SafeShield statistics one-shot 조회, 통합 새로고침과 polling 비활성화 계약을 검증하도록 Dashboard UI contract 테스트를 보강했습니다. + ## [0.2.8-r11] - 2026-09-03 ### 변경 @@ -2945,6 +3691,7 @@ - Dashboard의 SafeShield/연결 기기/업데이트 요약, 일회성 기기 조회와 통합 새로고침 동작을 검증하는 UI contract 테스트를 추가했습니다. + ## [0.2.8-r10] - 2026-09-03 ### 추가 @@ -2962,6 +3709,7 @@ - LoadingPanel의 compact layout과 필수 document metadata 및 theme-color 동기화를 contract 테스트로 검증합니다. + ## [0.2.8-r9] - 2026-09-03 ### 변경 @@ -2976,6 +3724,7 @@ - 업데이트 요약 카드, 명확한 update action, form control, switch와 페이지 정보 계층을 검증하는 UI contract 테스트를 추가했습니다. + ## [0.2.8-r8] - 2026-09-03 ### 수정 @@ -2987,6 +3736,7 @@ - 허용/차단 목록 검색창의 돋보기 아이콘을 transform 기반 위치 계산 대신 고정 폭 flex 래퍼로 수직 중앙 정렬해 LuCI 환경에서 아이콘이 비뚤어져 보이던 문제를 수정했습니다. - 검색 입력 필드도 새 도메인 입력과 동일한 제품형 input surface로 통일했습니다. + ## [0.2.8-r7] - 2026-09-02 ### 변경 @@ -3001,6 +3751,7 @@ - SafeShield 제품형 페이지 계층과 최근 24시간 통계 표시 계약, 사이드바 경계 토글의 위치와 저강도 기본 스타일을 검증합니다. + ## [0.2.8-r6] - 2026-09-02 ### 수정 @@ -3008,6 +3759,7 @@ - 데스크톱 사이드바 접기/펼치기 버튼을 화면 중앙에서 브랜드 헤더 하단과 사이드바 오른쪽 경계선이 만나는 위치로 이동했습니다. - 토글 버튼에 SmartSafeHub 포인트 컬러 배경과 흰색 아이콘, 강조 shadow를 적용해 라이트/다크 모드 모두에서 더 쉽게 식별할 수 있도록 개선했습니다. + ## [0.2.8-r5] - 2026-09-02 ### 수정 @@ -3015,6 +3767,7 @@ - 데스크톱 사이드바 접기/펼치기 버튼을 확장/축소 상태와 관계없이 오른쪽 경계선 중앙에 걸쳐 표시하도록 변경했습니다. - 경계형 토글을 원형 버튼으로 통일해 사이드바 너비 전환 동작을 더 명확하게 표시합니다. + ## [0.2.8-r4] - 2026-09-02 ### 추가 @@ -3025,6 +3778,7 @@ - 저장된 테마가 없으면 브라우저의 `prefers-color-scheme` 설정을 초기값으로 사용합니다. - 모바일 메뉴에도 동일한 테마 전환 기능을 제공합니다. + ## [0.2.8-r3] - 2026-09-02 ### 추가 @@ -3035,6 +3789,7 @@ - 사용자가 선택한 사이드바 상태를 브라우저 `localStorage`에 저장해 페이지 이동과 다음 접속에서도 유지합니다. - 모바일 navigation drawer는 기존 동작을 그대로 유지합니다. + ## [0.2.8-r2] - 2026-09-02 ### 수정 @@ -3043,6 +3798,7 @@ - ProductNavigation이 콘텐츠 `
` 내부에서 전체 너비를 차지해 Dashboard가 아래로 밀리던 레이아웃 회귀를 수정했습니다. - 헤더와 Dashboard를 우측 workspace에 함께 배치해 사이드바가 화면 왼쪽에 고정되는 구조를 복원했습니다. + ## [0.2.8-r1] - 2026-09-02 ### 변경 @@ -3054,10 +3810,12 @@ - Dashboard를 KPI 중심 System overview, System health, Device details 구조로 재설계했습니다. - 기존 상태/업데이트 RPC 계약은 변경하지 않고 현재 로드되는 데이터만 재구성해 저사양 장비의 추가 호출을 만들지 않습니다. + ## [0.2.7-r1] - 2026-09-02 safeshield의 최소 버전을 0.3.19 이상으로 설정하였습니다. + ## [0.2.6-r3] - 2026-08-30 ### 변경 @@ -3065,14 +3823,17 @@ safeshield의 최소 버전을 0.3.19 이상으로 설정하였습니다. - backend CI에서 개별적으로 실행하던 shell syntax, JSON, package/RPC/ucode/statistics/updater 검증을 `tests/run.sh` 하나로 통합했습니다. - 로컬에서도 `./tests/run.sh`로 GitHub Actions backend job과 동일한 테스트 흐름을 실행할 수 있습니다. + ## [0.2.6-r2] - 2026-08-30 safeshield의 최소 버전을 0.3.17 이상으로 설정하였습니다. + ## [0.2.6-r1] - 2026-08-30 safeshield의 최소 버전을 0.3.15 이상으로 설정하였습니다. + ## [0.2.5-r5] - 2026-08-30 ### 변경 @@ -3087,6 +3848,7 @@ safeshield의 최소 버전을 0.3.15 이상으로 설정하였습니다. - statistics reconciliation 응답, spinner/wait cursor, 목표 상태 표시와 statistics-only 후속 polling 계약을 테스트합니다. + ## [0.2.5-r4] - 2026-08-29 ### 추가 @@ -3102,6 +3864,7 @@ safeshield의 최소 버전을 0.3.15 이상으로 설정하였습니다. - 통계 토글의 ACL, RPC payload, collector 상태 정규화와 접근성 switch 계약을 테스트에 추가합니다. + ## [0.2.5-r3] - 2026-08-29 ### 추가 @@ -3118,6 +3881,7 @@ safeshield의 최소 버전을 0.3.15 이상으로 설정하였습니다. - 통계 RPC의 `devices`, `device_limit`, `devices_truncated` 정규화와 기기별 통계 UI 연결을 계약 테스트에 추가합니다. + ## [0.2.5-r2] - 2026-08-29 ### 변경 @@ -3128,6 +3892,7 @@ safeshield의 최소 버전을 0.3.15 이상으로 설정하였습니다. - 운영체제의 `prefers-reduced-motion` 설정을 존중하여 모션 감소 사용자는 차트 애니메이션을 사용하지 않습니다. - 기존 24시간 범위, 3시간 간격 시간 라벨과 차단/DNS 요청 tooltip 정보를 유지하면서 Y축 눈금을 추가했습니다. + ## [0.2.5-r1] - 2026-08-29 ### 추가 @@ -3142,10 +3907,12 @@ safeshield의 최소 버전을 0.3.15 이상으로 설정하였습니다. - 통계 collector lifecycle 수정이 포함된 SafeShield `0.3.14-r2` 이상을 최소 의존성으로 요구합니다. - APK dependency 문법에 맞게 `EXTRA_DEPENDS`의 버전 조건에서 연산자 뒤 공백을 제거합니다. + ## [0.2.4-r2] - 2026-08-30 빌드 오류를 수정하였습니다. + ## [0.2.4-r1] - 2026-08-28 safeshield의 성능 개선 버전인 0.3.13 버전을 기본 버전으로 설정하였습니다. @@ -3154,6 +3921,7 @@ safeshield의 성능 개선 버전인 0.3.13 버전을 기본 버전으로 설 - safeshield의 최소 버전을 0.3.13로 올렸습니다. + ## [0.2.3-r2] - 2026-08-28 설치된 SmartSafeHub가 여러 릴리즈를 건너뛰어 업데이트될 때 그 사이의 릴리즈 노트를 함께 확인할 수 있도록 누적 릴리즈 노트 표시를 추가했습니다. @@ -3176,6 +3944,7 @@ safeshield의 성능 개선 버전인 0.3.13 버전을 기본 버전으로 설 - mock release index를 기준으로 `0.2.1-r1`에서 최신 버전으로 업데이트할 때 중간 릴리즈 노트를 모두 선택하는지 검증합니다. - 릴리즈 메타데이터 전체 다운로드 실패가 업데이트 확인과 설치를 막지 않는 기존 fail-open 계약을 유지합니다. + ## [0.2.3-r1] - 2026-08-28 SmartSafeHub 업데이트 화면에서 새 버전의 릴리즈 노트를 함께 확인할 수 있도록 저장소 릴리즈 메타데이터 연동을 추가했습니다. @@ -3200,6 +3969,7 @@ SmartSafeHub 업데이트 화면에서 새 버전의 릴리즈 노트를 함께 - 릴리즈 노트 다운로드 실패가 업데이트 확인 실패로 전파되지 않는 fail-open 동작을 검증합니다. - 설치 완료 후 이전 릴리즈 노트 cache가 제거되는지 검증합니다. + ## [0.2.2-r1] - 2026-08-28 GitHub Actions CI에서 SmartSafeHub 패키지 계약과 배포 산출물 회귀를 더 일찍 감지하도록 자동 검증 범위를 확장했습니다. @@ -3218,6 +3988,7 @@ GitHub Actions CI에서 SmartSafeHub 패키지 계약과 배포 산출물 회귀 - `npm run build`가 이미 TypeScript typecheck를 포함하므로 중복된 별도 typecheck step을 제거했습니다. - Vite production output에 `app.js`와 `app.css`가 존재하고 배포용 `index.html`이 생성되지 않는 계약을 검증합니다. + ## [0.2.1-r1] - 2026-08-28 `luci-app-smartsafehub` 업데이트를 감지하고, 사용자가 선택한 경우 공유기에서 예약 자동 설치할 수 있는 SmartSafeHub 업데이트 관리 기능을 추가했습니다. @@ -3242,6 +4013,7 @@ GitHub Actions CI에서 SmartSafeHub 패키지 계약과 배포 산출물 회귀 - 자동 설치는 기본적으로 꺼져 있으며 기본 예약 시각은 `03:00`, 자동 확인 기본 주기는 6시간입니다. - OpenWrt 펌웨어 업그레이드는 기존 LuCI 펌웨어 관리 화면에 계속 위임합니다. + ## [0.2.0-r1] - 2026-08-21 SmartSafeHub 전용 사용자 화면과 rpcd 백엔드를 처음 정식 배포하는 릴리스입니다. `0.2.0` 개발 과정에서 사용한 중간 package revision은 정식 배포 기준점인 `r1`으로 squash했습니다. diff --git a/luci-app-smartsafehub/Makefile b/luci-app-smartsafehub/Makefile index 138cb6abc..e0714d3cf 100644 --- a/luci-app-smartsafehub/Makefile +++ b/luci-app-smartsafehub/Makefile @@ -6,8 +6,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-smartsafehub -PKG_VERSION:=0.2.30 -PKG_RELEASE:=10 +PKG_VERSION:=0.2.31 +PKG_RELEASE:=11 PKG_MAINTAINER:=Beomjun Kang PKG_LICENSE:=GPL-3.0-or-later @@ -46,6 +46,8 @@ define Package/luci-app-smartsafehub/postinst #!/bin/sh if [ -z "$${IPKG_INSTROOT}" ]; then mkdir -p /tmp/smartsafehub + # Drop the temporary diagnostic created by older Apps size queries. + rm -f /tmp/smartsafehub/apps-size-diagnostics.tsv uci -q delete smartsafehub.firmware.check_enabled >/dev/null 2>&1 || true uci -q commit smartsafehub >/dev/null 2>&1 || true if [ -x /etc/init.d/smartsafehub-events ]; then diff --git a/luci-app-smartsafehub/README.md b/luci-app-smartsafehub/README.md index 84c785c40..b984aa0ad 100644 --- a/luci-app-smartsafehub/README.md +++ b/luci-app-smartsafehub/README.md @@ -7,6 +7,8 @@ SmartSafeHub는 OpenWrt 공유기를 위한 통합 홈 게이트웨이 관리 UI입니다. 네트워크, Wi-Fi, 연결 기기, SafeShield 보안, 업데이트와 시스템 상태를 하나의 사용자 친화적인 인터페이스에서 관리할 수 있습니다. +**[SmartSafeHub 공유기 체험하기](https://www.smartsafehub.com/router-demo/)** + diff --git a/luci-app-smartsafehub/docs/APPS.md b/luci-app-smartsafehub/docs/APPS.md new file mode 100644 index 000000000..b9346357a --- /dev/null +++ b/luci-app-smartsafehub/docs/APPS.md @@ -0,0 +1,105 @@ +# SmartSafeHub Apps — 공유기 웹사이트 + +## 메뉴 및 아키텍처 + +- `System` 영역: `SmartSafeHub 계정 → Apps → 업데이트 → 설정` / 해시 URL `#apps`. +- 앱 목록: `smartsafehub.apps_catalog` RPC → `/usr/libexec/smartsafehub-apps` → Hub 승인 카탈로그. +- 설치 상태: `smartsafehub.apps_status` RPC → 독립된 `smartsafehub-apps-manager`의 로컬 APK 상태와 설치 기록. +- 앱 설치 저장 공간과 설치된 APK의 용량은 별도 `smartsafehub.apps_metrics` 읽기 RPC로 조회합니다. LuCI ACL의 `read.ubus.smartsafehub`에도 `apps_metrics`가 있어야 합니다. 실패하더라도 설치·제거 상태에는 영향을 주지 않습니다. +- 설치된 버전은 `apk list -I`에서 조회한 대표 승인 APK의 실제 설치 버전입니다. Wake-on-LAN은 백엔드 `etherwake`가 아닌 `luci-app-wol` 버전을 표시합니다. 카탈로그의 `version`은 현재 **제공/승인된 앱 버전**이므로 둘을 구분해 표시합니다. 복수 APK 앱은 대표 패키지 버전이 앱 릴리스 버전과 다를 수 있습니다. +- 설치·업데이트·제거: `smartsafehub.apps_install`, `smartsafehub.apps_update`, `smartsafehub.apps_remove` RPC. 앱 ID만 받으며, 실제 작업은 root 전용 백그라운드 매니저에서 진행합니다. 다른 앱 작업이 진행 중이면 거절합니다. +- Phase 2의 IoT/로컬 AI 설치기는 추후 `runtime`에 따라 별도 어댑터로 추가합니다. 현재는 `router + apk`만 가능하며 Hub의 패키지명·URL·명령을 브라우저가 보낼 수 없습니다. + +## 앱 카테고리 아이콘 + +- **네트워크**: 연결된 노드 아이콘 / **도구**(`utilities`): 렌치 아이콘 / **기타**(`other`): 원 안의 점 3개 아이콘. +- 분류되지 않은 앱은 기타로 묶고, 같은 아이콘을 실제 공유기와 읽기 전용 체험 화면에서 사용합니다. 앱 설치·승인 정보와 정렬 순서는 변경하지 않습니다. + +## 설치를 활성화하기 위한 **필수 운영 조건** + +현재 버전은 APK 엔진까지 구현했지만 **아무 OpenWrt 미러에서나 바로 설치하지 않습니다.** + +1. Hub에 `store.patch`의 앱 카탈로그가 배포되고 펌웨어가 `device_code`와 `build_id`로 등록되어 있어야 합니다. +2. 관리자 승인이 특정 펌웨어 빌드, 채널, 패키지 아키텍처 및 **정확한 APK 버전과 SHA-256**에 대해 완료되어야 합니다. +3. 공유기에는 OpenWrt 공식 패키지 관리자(`apk`)와 필요한 시스템 의존성이 설치되어 있어야 합니다. SmartSafeHub가 사용하는 APK 서명 공개 키도 신뢰 저장소에 등록되어 있어야 합니다. +4. 설치기는 고정된 릴리스 ID, 승인된 채널(`beta`/`stable`)과 공유기 아키텍처로 `https://repo.smartsafehub.com/<채널>/packages/<아키텍처>/smartsafehub/<릴리스 ID>/packages.adb`를 조회합니다. 이 인덱스를 새로 다운로드한 뒤 Hub 승인 SHA-256과 비교합니다. 펌웨어에 별도 인덱스 파일을 복사할 필요가 없습니다. +5. 실제 APK는 위 고정 도메인/경로에서 이름과 버전으로만 내려받아 SHA-256을 검증합니다. 임의 URL 입력이나 `--allow-untrusted`는 허용되지 않습니다. +6. `apk`, `sha256sum`, `start-stop-daemon`, `jsonfilter`, `uclient-fetch`가 공유기에 있어야 하며 RAM/overlay 용량이 충분해야 합니다. + +**주의:** 모든 승인은 고정된 SmartSafeHub 릴리스에서 설치합니다. 릴리스 파일 또는 인덱스의 SHA-256이 승인값과 다르면 설치를 차단합니다. + +**의존성 주의:** `nlbwmon`, `luci-app-nlbwmon`, `luci-lib-chartjs`는 동일한 SmartSafeHub 서명 저장소에서만 설치합니다. 이외의 의존성을 자동으로 가져오지 않으며, 새로운 커널 모듈·라이브러리는 별도 검증이 필요합니다. + +OpenWrt 25.12의 패키지 인덱스는 `packages.adb` 형식일 수 있으며, 이전의 `APKINDEX.tar.gz` 형식이라고 가정하면 안 됩니다. + +## Wake-on-LAN 앱 (0.2.30-r37) + +- Hub에서 `wake-on-lan` 앱을 현재 펌웨어와 아키텍처에 승인하면 Apps에 표시됩니다. `utilities` 카테고리는 **도구** 탭에 표시합니다. +- 서명된 고정 릴리스에서 `etherwake`, `luci-app-wol` **두 APK만** 함께 설치합니다. 다른 경로로 설치된 패키지는 Apps가 자동으로 소유하지 않습니다. +- `luci-app-wol`까지 설치된 경우에만 **열기**를 표시하고, 새 탭에서 공유기의 `/cgi-bin/luci/admin/services/wol`로 이동합니다. LuCI 로그인을 추가로 요구할 수 있습니다. +- 지원되는 유선 컴퓨터의 BIOS/UEFI 및 운영체제에서 Wake-on-LAN 기능을 켜야 합니다. 원격 인터넷에서 임의로 전원을 켜는 기능이나 별도 포트 개방을 제공하지 않습니다. +- 패키지를 설치·제거하면 LuCI 메뉴 캐시를 갱신합니다. 공유기 체험 화면의 카드는 예시이며 실제 설치·신호 전송을 하지 않습니다. + +## 설치된 앱 열기 (0.2.30-r9) + +- 웹 화면을 제공하는 앱은 설치 후 `열기`로 **새 탭**에서 공식 LuCI UI를 표시합니다. SmartSafeHub 탭은 그대로 유지합니다. +- 웹 UI는 `web_ui: {"type":"luci","path":"/cgi-bin/luci/admin/…","package":"…"}`로 승인된 카탈로그에서 선언할 수 있습니다. 경로는 **같은 공유기의 LuCI 관리자 화면**으로 제한됩니다. 외부 사이트, 임의 포트, 실행 명령은 허용되지 않습니다. +- `network-usage-monitor`는 기존 카탈로그에도 `admin/services/nlbw/display` 경로를 사용할 수 있지만, **`luci-app-nlbwmon`이 설치되어 있어야만** 열기 버튼이 표시됩니다. `nlbwmon`만 설치된 공유기에는 `웹 UI 미설치`가 표시됩니다. +- 세 APK와 인덱스의 해시를 모두 확인하며, 설치 관리자는 승인된 릴리스 ID를 필수로 요구합니다. +- `luci-app-nlbwmon`은 OpenWrt LuCI 인증을 다시 요구할 수 있습니다. + +## 동작과 제한 + +- 설치 시작 시 **Hub에서 승인 목록을 다시 조회**합니다. 인터넷이 끊겼거나 승인이 취소된 앱은 신규 설치가 불가능합니다. +- SmartSafeHub 저장소에서 승인된 인덱스와 APK를 내려받아 SHA-256을 확인합니다. 확인된 파일만 임시 로컬 저장소로 구성하며, APK의 서명 검증은 그대로 적용합니다. +- 모든 앱 APK는 SmartSafeHub가 빌드하고 서명한 하나의 불변 릴리스에서 설치합니다. `apk --simulate`는 저장소 인덱스를 자동 갱신하지 않으므로, 검증된 임시 로컬 저장소에 대해 `apk update`를 수행한 후 설치 계획을 검사하고 설치합니다. 승인되지 않은 패키지는 자동 설치하지 않습니다. +- `--allow-untrusted`, 전체 `apk upgrade`, 사용자 입력 셸 명령 및 공유기의 영구 저장소 설정 변경은 사용하지 않습니다. 임시 저장소는 검증된 파일로만 구성하며 작업 종료 시 삭제합니다. +- 핵심 시스템 패키지 및 SafeShield 관련 패키지는 앱 패키지 대상에서 제외합니다. 의존성 해석과 설치 스크립트 실행은 APK가 담당하므로, 앱마다 실제 환경 테스트가 필요합니다. +- `/etc/smartsafehub/apps-installed/<앱 ID>`에 **Apps가 직접 설치한 패키지 소유권**을 기록합니다. 기존 Software/SSH로 설치된 동일 패키지는 Apps에서 제거하지 않습니다. +- 테스트 기기의 이전 설치·승인 정보는 승계하지 않습니다. 새 승인으로 설치한 앱만 이 버전에서 관리합니다. +- 펌웨어 변경으로 앱 바이너리가 사라진 경우 소유권 기록과 새 펌웨어 승인에 따라 자동 복원하며, 조건이 일치하지 않으면 안전하게 중단합니다. +- 설치 작업 중 공유기를 재부팅하거나 전원을 끊지 말아야 합니다. APK 실패 후 트랜잭션의 수동 복구가 필요할 수 있으며 **완전한 자동 롤백은 제공하지 않습니다**. +- Apps에서 직접 설치한 앱은 새 펌웨어·기기 모델·아키텍처에 맞게 승인된 **더 높은 버전**이 나타날 때 `업데이트` 버튼을 표시합니다. 외부에서 설치한 앱은 업데이트하거나 자동 인수하지 않습니다. +- 사용자가 명시적으로 업데이트를 선택해야 하며, 업데이트 전 카탈로그와 서명된 불변 저장소·파일 해시를 다시 검증합니다. +- 앱별 APK의 **같은 패키지 집합**에서 버전만 올릴 수 있습니다. 패키지 추가·제거, 다운그레이드, 같은 버전의 해시 변경, 기존 설치 버전 불일치, 다른 패키지 변경을 요구하는 계획은 거부합니다. +- OpenWrt 전체 `apk upgrade`를 실행하지 않습니다. 기존 패키지를 대상으로 `apk add --upgrade`를 정확한 버전으로 한정하고 사전 계획을 검증합니다. OpenWrt는 펌웨어의 시스템 패키지를 무분별하게 업그레이드하는 것을 권장하지 않습니다. +- 업데이트가 성공해야 소유권 파일의 버전·해시를 교체합니다. 실패 시 원본 소유권 기록은 유지되며, 부분적으로 변경된 APK를 자동으로 롤백하지 않습니다. 이 경우 관리자 확인이 필요할 수 있습니다. +- 업데이트는 새로운 설치 성공 다운로드 통계에 포함하지 않습니다. 펌웨어 갱신 후 자동 복원은 기존대로 소유권 기록을 활용하며 업데이트를 자동 수행하지 않습니다. + +## 장애 확인 + +- `E21`: 서버에서 새 카탈로그를 조회하지 못함 / `E22`: 승인 앱 없음 / `E23`, `E24`: 런타임 또는 기기 아키텍처 불일치. +- `E25`: SmartSafeHub 배포 인덱스 다운로드 실패 또는 SHA 불일치 / `E26`: 리소스 부족 / `E27`: 승인 패키지 정보 검증 실패. +- `E51`~`E55`: 소유권 없는 앱 업데이트, 승인 버전 불일치, 변경 범위 초과, APK 업데이트 또는 기록 저장 실패. +- `E29`: 기존 외부 설치 패키지 충돌 / `E31`~`E34`: 다운로드·해시·설치 시뮬레이션 실패 / `E35`: 설치 실패. +- `E41`: Apps가 설치한 앱이 아니라서 제거 불가 / `E44`, `E45`: 제거 시뮬레이션·실행 실패. +- `/tmp/smartsafehub/apps-operation.state`: 최근 작업 상태 및 코드 (재부팅 시 초기화). +- `/etc/smartsafehub/apps-installed/`: Apps가 설치한 패키지 소유권 기록 (설치 바이너리는 포함하지 않음). +- `apk` 작업 로그는 작업 후 임시 디렉터리에서 정리되며, 실패한 경우 최신 패키지 관리자 오류만 최대 4KB까지 `/tmp/smartsafehub/apps-last-error.log`에 보존합니다 (재부팅 시 초기화). + +## 테스트 + +```sh +sh tests/test-apps-contract.sh +sh tests/test-apps-manager.sh +sh tests/test-apps-restore.sh +sh tests/test-apps-ui.sh +sh tests/test-ucode-imports.sh +# 개발 환경에서 (프론트엔드 빌드 결과는 패치 미포함) +cd frontend && npm ci && npm run typecheck && npm run build +``` + + +## 펌웨어 업데이트 후 Apps 복원 + +- `sysupgrade`는 `/etc/smartsafehub/apps-installed/`의 **소유권 기록**을 보관합니다. APK 바이너리 자체는 새 펌웨어에 포함되지 않으면 사라질 수 있습니다. +- 기존 `smartsafehub-firmware` daemon이 부팅 시 **일회성 복원 스크립트**를 실행하여 누락된, SmartSafeHub가 관리한 앱만 복원합니다. 별도의 procd/init 서비스나 상시 실행 daemon은 추가하지 않습니다. 기록이 없거나 이미 설치되어 있으면 아무것도 설치하지 않습니다. +- 복원은 현재 펌웨어의 `device_code`·`build_id`에 일치하는 Hub 카탈로그를 새로 받아 기존 Apps manager의 **서명, SHA-256, 의존성 및 소유권 검사**를 통과해야 합니다. 지난 펌웨어의 승인이나 캐시된 카탈로그로 설치하지 않습니다. +- 네트워크 오류는 제한적으로 다시 시도합니다. 앱 승인이 철회되거나, APK 해시·SmartSafeHub 서명 인덱스·아키텍처·소유권 기록이 맞지 않으면 중단합니다. 다른 방법으로 설치된 패키지를 인수하지 않습니다. +- 공유기에서 `/tmp/smartsafehub/apps-restore.state`와 `logread -e smartsafehub-apps-restore`로 결과를 확인할 수 있습니다. 실패 시 Apps 화면의 **설치 마무리**로 다시 검증할 수 있습니다. 수동 재시도는 `/usr/libexec/smartsafehub-apps-restore run`을 사용합니다. +- 인터넷이 오래 끊겨 재시도 횟수를 넘기면 해당 실행은 종료됩니다. 이후 WAN이 다시 연결되면 hotplug이 네트워크 오류에 한하여 일회성 복원을 재시도하며, 수동 실행이나 다음 부팅으로도 다시 시도할 수 있습니다. +- 주의: 새로운 펌웨어에서 승인된 패키지 pin이 변경되었으면 이전 manifest를 무단으로 덮어쓰지 않고 중단합니다. 이 경우 호환성 확인과 승인 후 별도 마이그레이션이 필요합니다. + +## 설치 성공 건수 수집 (2026-10-28) + +Apps에서 사용자가 새로 설치에 성공하면, 앱 ID·릴리스 버전·작업 ID만 로컬 outbox에 보관했다가 기존 Device 자격증명으로 Hub에 전송합니다. 실패한 설치, 제거, 펌웨어 업데이트에 따른 자동 복원은 집계하지 않습니다. 최대 500건을 보관하고 네트워크 장애 시 5분 간격으로 최대 3건씩 재시도하며, 해당 전송은 앱 설치의 성공 여부에 영향을 주지 않습니다. 수치는 공개 Apps 화면에 표시하지 않습니다. diff --git a/luci-app-smartsafehub/docs/DEVELOPMENT.md b/luci-app-smartsafehub/docs/DEVELOPMENT.md index 584b67f2e..c18e1dc53 100644 --- a/luci-app-smartsafehub/docs/DEVELOPMENT.md +++ b/luci-app-smartsafehub/docs/DEVELOPMENT.md @@ -239,8 +239,30 @@ Health 계약 테스트의 mock 환경은 각 시나리오를 subshell에서 실 sh tests/test-lan-settings.sh SMARTSAFEHUB_REQUIRE_UCODE=1 sh tests/test-ucode-syntax.sh shellspec spec/contracts_spec.sh +# 새 테스트 파일이 ShellSpec 실행 목록에 누락되지 않았는지 확인 +sh tests/test-suite-integrity.sh +# 브라우저 동작 테스트 전체 (Node.js 24+) +node --experimental-strip-types --test tests/runtime/*.test.mjs ``` +### OpenWrt 25.12 셸 명령 성능 검사 + +`sh tests/test-openwrt-commands.sh`는 BusyBox 기본 명령의 존재 여부와 함께 공유기에 배포하는 셸 스크립트의 고비용 패턴을 검사합니다. `root/`의 새 셸 진입점도 자동 포함되고 Backend CI 및 ShellSpec에서 동일한 검사기가 실행됩니다. 다음 패턴은 실패합니다. + +- `find /`, `du /` 또는 `du -a`로 전체 파일시스템/파일 목록을 순회 +- 재귀적 `grep -r/-R/--recursive`, `ls -R`로 넓은 범위를 반복 검색 +- `dd if=/dev/zero` 또는 `dd if=/dev/urandom`에 `count=`가 없는 무제한 읽기 +- `xargs -P0` 또는 `xargs -P 0`의 무제한 병렬 작업 +- `sleep 0`, 명시적 대기·종료 경로가 없는 `while true`/`while :` 무한 루프 + +`grep`, `awk`, `find`, `sort` 자체를 금지하지 않습니다. 가능한 한 처리 디렉터리와 파일 크기, 반복 횟수, 병렬도에 상한을 두세요. 위험 동작이 실제로 필요한 경우 무조건 허용 목록에 넣기보다는 호출 범위를 제한하도록 코드를 수정하고 회귀 테스트를 추가합니다. 이 검사는 *정적 안티패턴 게이트*이며 변수로 전달된 경로, 런타임 실행 빈도, BusyBox 명령 옵션, 실제 CPU/메모리 부하까지 보증하지 않으므로 실기기 프로파일링을 대체하지 않습니다. + +셸 테스트(`tests/**/*.sh`)는 `spec/*_spec.sh`에 각각 한 번씩 등록해야 합니다. GitHub Actions의 Backend 작업은 ShellSpec과 ucode를 설치하기 **전에** `test-suite-integrity.sh`를 독립적으로 실행하므로, 등록되지 않은 테스트가 있으면 즉시 실패합니다. 동일한 파일을 중복 등록하거나 이미 삭제한 테스트를 참조해도 실패합니다. CI가 `shellspec` 전체 실행을 제거하거나 조건부 실행/실패 무시로 변경해도 검사합니다. + +브라우저 동작 테스트는 `tests/runtime/*.test.mjs` 이름으로 추가하면 Frontend CI에서 자동 실행됩니다. CI는 고정된 테스트 파일 목록을 사용하지 않으며, 자동 검색되지 않는 다른 확장자(`*.test.js`, `*.test.ts` 등)나 중첩 폴더에 테스트를 추가하면 누락 검사에서 실패합니다. 새로운 테스트 실행 형식이 필요하면 CI 실행 규칙과 검사기를 함께 수정해야 합니다. 변경하지 않은 배포 파일을 새로 생성하지 않고도 런타임 테스트를 로컬에서 실행할 수 있습니다. + +누락 검사 자체에도 **미등록/중복/삭제된 테스트 참조, 잘못된 파일 확장자, CI 실행 단계 제거·조건부 실행·실패 무시**를 실제로 재현하는 회귀 검증을 포함했습니다. 화면 소스의 특정 공백이나 Tailwind 클래스 이름만 고정하는 일회성 테스트보다는 **보안 경계, 설정 보존, 버전 비교, RPC 계약, 사용자 동작**을 확인하는 테스트를 우선합니다. + ```bash cd frontend npm ci diff --git a/luci-app-smartsafehub/docs/OPERATIONS.md b/luci-app-smartsafehub/docs/OPERATIONS.md index 12c90fd48..70335833e 100644 --- a/luci-app-smartsafehub/docs/OPERATIONS.md +++ b/luci-app-smartsafehub/docs/OPERATIONS.md @@ -176,3 +176,20 @@ rm -f /tmp/luci-indexcache ``` 브라우저에서는 강력 새로고침을 수행하거나 기존 SmartSafeHub 탭을 닫고 다시 접속합니다. 통합 진입 템플릿의 `app.js?v=...`와 Shadow DOM용 `app.css?v=...`에는 현재 패키지의 `PKG_VERSION-rPKG_RELEASE` 값이 사용되므로 패키지 릴리스 변경 시 브라우저 캐시가 함께 무효화됩니다. + +### 공유기 웹사이트 RPC 로딩 지연 확인 + +사용자 규칙 등 화면이 간헐적으로 느릴 때 공유기 웹사이트에 로그인한 브라우저의 +개발자 도구(F12 → Console)에서 아래 명령어를 실행합니다. 느려지는 상황을 재현한 +후 실행하면 가장 오래 걸린 RPC와 실패 요청을 구분할 수 있습니다. + +```js +window.__SMARTHUB_RPC_DIAGNOSTICS__?.snapshot() +// 처음부터 다시 측정할 때 +window.__SMARTHUB_RPC_DIAGNOSTICS__?.reset() +``` + +- `summary`: 메서드별 호출 수(`calls`), 느린 호출(`slow`), 오류(`errors`), 마지막/최대/평균 처리 시간(ms). +- `recent`: **1.5초 이상 걸린 요청** 또는 실패한 요청 최근 최대 60건. `responseMs`는 응답 헤더까지 걸린 시간, `decodeMs`는 본문 JSON 처리 시간입니다. +- 기록은 현재 브라우저 탭의 메모리에만 유지되며 새로고침 시 사라집니다. 서버 업로드나 영구 저장은 하지 않으며, 세션 ID·도메인 규칙·호출 인자·응답 내용·오류 메시지는 기록하지 않습니다. +- `responseMs`가 긴 경우 네트워크/uhttpd/rpcd 경로를, `decodeMs`가 긴 경우 큰 응답 또는 브라우저 메인 스레드 부하를 우선 확인합니다. 수치만으로 원인을 단정할 수는 없습니다. diff --git a/luci-app-smartsafehub/docs/ROUTER_DEMO.md b/luci-app-smartsafehub/docs/ROUTER_DEMO.md index d5ca6543f..19ac71b25 100644 --- a/luci-app-smartsafehub/docs/ROUTER_DEMO.md +++ b/luci-app-smartsafehub/docs/ROUTER_DEMO.md @@ -18,7 +18,8 @@ git add frontend/demo-dist ## 공개 데모 화면 -- 대시보드, 최근 활동, 네트워크(WAN/LAN), Wi-Fi/게스트 Wi-Fi, 연결된 기기, SafeShield, 사용자 규칙, 업데이트를 체험할 수 있습니다. +- 대시보드, 최근 활동, 네트워크(WAN/LAN), Wi-Fi/게스트 Wi-Fi, 연결된 기기, SafeShield, 사용자 규칙, **SmartSafeHub 계정 및 Apps**, 업데이트를 체험할 수 있습니다. +- 계정 연결 상태/플랜, 승인 앱 목록 및 설치 상태는 **가상 데이터**로만 표시합니다. 계정 관리 페이지 열기와 앱 설치·제거·업데이트·실행은 안내만 표시하고 실제 서버나 공유기에 요청하지 않습니다. - 저장, 차단 규칙 수정, WAN 재연결, 업데이트 확인/설치 등은 **실제 요청을 보내지 않고** 안내 메시지만 보여줍니다. - QR 코드도 실제 자격 정보 요청 대신 **데모 전용 예시 Wi-Fi 정보**로 생성합니다. 실제 공유기에서는 기존 인증된 RPC를 계속 사용합니다. - `frontend/src/app/routes.ts`의 `DEMO_ROUTES`는 데모 메뉴 및 허용 경로의 단일 기준입니다. diff --git a/luci-app-smartsafehub/docs/assets/dashboard-dark.png b/luci-app-smartsafehub/docs/assets/dashboard-dark.png index 8fb66b7ca..bfbe3578c 100644 Binary files a/luci-app-smartsafehub/docs/assets/dashboard-dark.png and b/luci-app-smartsafehub/docs/assets/dashboard-dark.png differ diff --git a/luci-app-smartsafehub/docs/assets/dashboard-white.png b/luci-app-smartsafehub/docs/assets/dashboard-white.png index 47840bcaa..fb906ce3d 100644 Binary files a/luci-app-smartsafehub/docs/assets/dashboard-white.png and b/luci-app-smartsafehub/docs/assets/dashboard-white.png differ diff --git a/luci-app-smartsafehub/frontend/demo-dist/demo.css b/luci-app-smartsafehub/frontend/demo-dist/demo.css index 33ff16ce1..4958fa0ef 100644 --- a/luci-app-smartsafehub/frontend/demo-dist/demo.css +++ b/luci-app-smartsafehub/frontend/demo-dist/demo.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-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-outline-style:solid;--tw-ease:initial}::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-outline-style:solid;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:#fef2f2;--color-red-100:#ffe2e2;--color-red-200:#ffcaca;--color-red-300:#ffa3a3;--color-red-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-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)}.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)}.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-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-4{column-gap:calc(var(--spacing) * 4)}.gap-x-5{column-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-700{border-color:var(--color-red-700)}.border-rose-200{border-color:var(--color-rose-200)}.border-rose-300{border-color:var(--color-rose-300)}.border-sky-100{border-color:var(--color-sky-100)}.border-sky-200{border-color:var(--color-sky-200)}.border-slate-100{border-color:var(--color-slate-100)}.border-slate-200{border-color:var(--color-slate-200)}.border-slate-300{border-color:var(--color-slate-300)}.border-teal-200{border-color:var(--color-teal-200)}.border-teal-300{border-color:var(--color-teal-300)}.border-teal-600{border-color:var(--color-teal-600)}.border-teal-700{border-color:var(--color-teal-700)}.border-white\/80{border-color:#fffc}@supports (color:color-mix(in lab, red, red)){.border-white\/80{border-color:color-mix(in oklab, var(--color-white) 80%, transparent)}}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-50\/70{background-color:#fffbebb3}@supports (color:color-mix(in lab, red, red)){.bg-amber-50\/70{background-color:color-mix(in oklab, var(--color-amber-50) 70%, transparent)}}.bg-amber-100{background-color:var(--color-amber-100)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-amber-600{background-color:var(--color-amber-600)}.bg-amber-700{background-color:var(--color-amber-700)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-emerald-50\/70{background-color:#ecfdf5b3}@supports (color:color-mix(in lab, red, red)){.bg-emerald-50\/70{background-color:color-mix(in oklab, var(--color-emerald-50) 70%, transparent)}}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-emerald-700{background-color:var(--color-emerald-700)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-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)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-10{padding-block:calc(var(--spacing) * 10)}.pt-1{padding-top:var(--spacing)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pr-12{padding-right:calc(var(--spacing) * 12)}.pb-1{padding-bottom:var(--spacing)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.pb-8{padding-bottom:calc(var(--spacing) * 8)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-9{padding-left:calc(var(--spacing) * 9)}.pl-11{padding-left:calc(var(--spacing) * 11)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.7rem\]{font-size:.7rem}.text-\[0\.65rem\]{font-size:.65rem}.text-\[0\.68rem\]{font-size:.68rem}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-6{--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6)}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.08em\]{--tw-tracking:.08em;letter-spacing:.08em}.tracking-\[0\.12em\]{--tw-tracking:.12em;letter-spacing:.12em}.tracking-\[0\.14em\]{--tw-tracking:.14em;letter-spacing:.14em}.tracking-\[0\.16em\]{--tw-tracking:.16em;letter-spacing:.16em}.tracking-\[0\.18em\]{--tw-tracking:.18em;letter-spacing:.18em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-amber-900{color:var(--color-amber-900)}.text-amber-900\/80{color:#7b3306cc}@supports (color:color-mix(in lab, red, red)){.text-amber-900\/80{color:color-mix(in oklab, var(--color-amber-900) 80%, transparent)}}.text-amber-950{color:var(--color-amber-950)}.text-blue-700{color:var(--color-blue-700)}.text-current{color:currentColor}.text-emerald-700{color:var(--color-emerald-700)}.text-emerald-800{color:var(--color-emerald-800)}.text-emerald-900{color:var(--color-emerald-900)}.text-emerald-950{color:var(--color-emerald-950)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-red-900{color:var(--color-red-900)}.text-red-950{color:var(--color-red-950)}.text-rose-600{color:var(--color-rose-600)}.text-rose-700{color:var(--color-rose-700)}.text-rose-800{color:var(--color-rose-800)}.text-rose-900{color:var(--color-rose-900)}.text-rose-950{color:var(--color-rose-950)}.text-sky-700{color:var(--color-sky-700)}.text-sky-800{color:var(--color-sky-800)}.text-sky-950{color:var(--color-sky-950)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-slate-700{color:var(--color-slate-700)}.text-slate-800{color:var(--color-slate-800)}.text-slate-900{color:var(--color-slate-900)}.text-slate-950{color:var(--color-slate-950)}.text-teal-300{color:var(--color-teal-300)}.text-teal-600{color:var(--color-teal-600)}.text-teal-700{color:var(--color-teal-700)}.text-teal-800{color:var(--color-teal-800)}.text-teal-950{color:var(--color-teal-950)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.accent-teal-700{accent-color:var(--color-teal-700)}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-slate-900\/5{--tw-shadow-color:#0f172b0d}@supports (color:color-mix(in lab, red, red)){.shadow-slate-900\/5{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-slate-900) 5%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-slate-950\/10{--tw-shadow-color:#0206181a}@supports (color:color-mix(in lab, red, red)){.shadow-slate-950\/10{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-slate-950) 10%, transparent) var(--tw-shadow-alpha), transparent)}}.ring-amber-200{--tw-ring-color:var(--color-amber-200)}.ring-blue-200{--tw-ring-color:var(--color-blue-200)}.ring-emerald-200{--tw-ring-color:var(--color-emerald-200)}.ring-red-200{--tw-ring-color:var(--color-red-200)}.ring-rose-200{--tw-ring-color:var(--color-rose-200)}.ring-sky-100{--tw-ring-color:var(--color-sky-100)}.ring-sky-200{--tw-ring-color:var(--color-sky-200)}.ring-slate-100{--tw-ring-color:var(--color-slate-100)}.ring-slate-200{--tw-ring-color:var(--color-slate-200)}.ring-teal-100{--tw-ring-color:var(--color-teal-100)}.ring-teal-200{--tw-ring-color:var(--color-teal-200)}.ring-white{--tw-ring-color:var(--color-white)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[left\]{transition-property:left;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.outline-none{--tw-outline-style:none;outline-style:none}.select-all{-webkit-user-select:all;user-select:all}.ring-inset{--tw-ring-inset:inset}.group-open\:hidden:-webkit-any(:where(.group):-webkit-any([open],:popover-open,:open) *){display:none}.group-open\:hidden:is(:where(.group):is([open],:popover-open,:open) *){display:none}.group-open\:inline-block:-webkit-any(:where(.group):-webkit-any([open],:popover-open,:open) *){display:inline-block}.group-open\:inline-block:is(:where(.group):is([open],:popover-open,:open) *){display:inline-block}.group-open\:inline-flex:-webkit-any(:where(.group):-webkit-any([open],:popover-open,:open) *){display:inline-flex}.group-open\:inline-flex:is(:where(.group):is([open],:popover-open,:open) *){display:inline-flex}.placeholder\:text-slate-400::placeholder{color:var(--color-slate-400)}.first\:mt-0:first-child{margin-top:0}.first\:pt-0:first-child{padding-top:0}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.last\:pb-0:last-child{padding-bottom:0}@media (hover:hover){.hover\:border-emerald-800:hover{border-color:var(--color-emerald-800)}.hover\:border-red-200:hover{border-color:var(--color-red-200)}.hover\:border-red-300:hover{border-color:var(--color-red-300)}.hover\:border-red-800:hover{border-color:var(--color-red-800)}.hover\:border-slate-300:hover{border-color:var(--color-slate-300)}.hover\:border-teal-200:hover{border-color:var(--color-teal-200)}.hover\:border-teal-300:hover{border-color:var(--color-teal-300)}.hover\:border-teal-400:hover{border-color:var(--color-teal-400)}.hover\:border-teal-800:hover{border-color:var(--color-teal-800)}.hover\:bg-amber-50:hover{background-color:var(--color-amber-50)}.hover\:bg-amber-100:hover{background-color:var(--color-amber-100)}.hover\:bg-amber-700:hover{background-color:var(--color-amber-700)}.hover\:bg-amber-800:hover{background-color:var(--color-amber-800)}.hover\:bg-black\/5:hover{background-color:#0000000d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-black\/5:hover{background-color:color-mix(in oklab, var(--color-black) 5%, transparent)}}.hover\:bg-emerald-800:hover{background-color:var(--color-emerald-800)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:bg-red-800:hover{background-color:var(--color-red-800)}.hover\:bg-rose-50:hover{background-color:var(--color-rose-50)}.hover\:bg-rose-100:hover{background-color:var(--color-rose-100)}.hover\:bg-rose-800:hover{background-color:var(--color-rose-800)}.hover\:bg-sky-800:hover{background-color:var(--color-sky-800)}.hover\:bg-slate-50:hover{background-color:var(--color-slate-50)}.hover\:bg-slate-100:hover{background-color:var(--color-slate-100)}.hover\:bg-slate-800:hover{background-color:var(--color-slate-800)}.hover\:bg-teal-50:hover{background-color:var(--color-teal-50)}.hover\:bg-teal-100:hover{background-color:var(--color-teal-100)}.hover\:bg-teal-800:hover{background-color:var(--color-teal-800)}.hover\:text-red-700:hover{color:var(--color-red-700)}.hover\:text-rose-800:hover{color:var(--color-rose-800)}.hover\:text-slate-800:hover{color:var(--color-slate-800)}.hover\:text-slate-950:hover{color:var(--color-slate-950)}.hover\:text-teal-700:hover{color:var(--color-teal-700)}.hover\:text-teal-800:hover{color:var(--color-teal-800)}.hover\:text-teal-900:hover{color:var(--color-teal-900)}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:border-teal-500:focus{border-color:var(--color-teal-500)}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-4:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-teal-100:focus{--tw-ring-color:var(--color-teal-100)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\: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-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-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\: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-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-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-950]{color:#5eead4}.ssh-app[data-theme=dark] [class~=border-teal-200],.ssh-app[data-theme=dark] [class~=border-teal-300]{border-color:#115e59}.ssh-app[data-theme=dark] [class~=ring-teal-100],.ssh-app[data-theme=dark] [class~=ring-teal-200]{--tw-ring-color:#134e4a}.ssh-app[data-theme=dark] [class~=bg-sky-50],.ssh-app[data-theme=dark] [class~=bg-sky-50\/60]{background-color:#0ea5e91f}.ssh-app[data-theme=dark] [class~=text-sky-700],.ssh-app[data-theme=dark] [class~=text-sky-800],.ssh-app[data-theme=dark] [class~=text-sky-950]{color:#7dd3fc}.ssh-app[data-theme=dark] [class~=border-sky-200]{border-color:#075985}.ssh-app[data-theme=dark] [class~=ring-sky-100],.ssh-app[data-theme=dark] [class~=ring-sky-200]{--tw-ring-color:#0c4a6e}.ssh-app[data-theme=dark] [class~=bg-emerald-50],.ssh-app[data-theme=dark] [class~=bg-emerald-100]{background-color:#10b98121}.ssh-app[data-theme=dark] [class~=bg-emerald-50\/70]{background-color:#10b9811a}.ssh-app[data-theme=dark] [class~=text-emerald-700],.ssh-app[data-theme=dark] [class~=text-emerald-800],.ssh-app[data-theme=dark] [class~=text-emerald-900],.ssh-app[data-theme=dark] [class~=text-emerald-950]{color:#6ee7b7}.ssh-app[data-theme=dark] [class~=border-emerald-200]{border-color:#065f46}.ssh-app[data-theme=dark] [class~=ring-emerald-100],.ssh-app[data-theme=dark] [class~=ring-emerald-200]{--tw-ring-color:#064e3b}.ssh-app[data-theme=dark] [class~=bg-amber-50],.ssh-app[data-theme=dark] [class~=bg-amber-50\/60],.ssh-app[data-theme=dark] [class~=bg-amber-100],.ssh-app[data-theme=dark] [class~=bg-amber-100\/70]{background-color:#f59e0b21}.ssh-app[data-theme=dark] [class~=bg-amber-50\/70]{background-color:#f59e0b1a}.ssh-app[data-theme=dark] [class~=text-amber-700],.ssh-app[data-theme=dark] [class~=text-amber-800],.ssh-app[data-theme=dark] [class~=text-amber-900],.ssh-app[data-theme=dark] [class~=text-amber-900\/80],.ssh-app[data-theme=dark] [class~=text-amber-950]{color:#fcd34d}.ssh-app[data-theme=dark] [class~=border-amber-200],.ssh-app[data-theme=dark] [class~=border-amber-300]{border-color:#92400e}.ssh-app[data-theme=dark] [class~=ring-amber-100],.ssh-app[data-theme=dark] [class~=ring-amber-200]{--tw-ring-color:#78350f}.ssh-app[data-theme=dark] [class~=bg-red-50],.ssh-app[data-theme=dark] [class~=bg-red-100]{background-color:#ef44441f}.ssh-app[data-theme=dark] [class~=text-red-700],.ssh-app[data-theme=dark] [class~=text-red-800],.ssh-app[data-theme=dark] [class~=text-red-900],.ssh-app[data-theme=dark] [class~=text-red-950]{color:#fca5a5}.ssh-app[data-theme=dark] [class~=border-red-200],.ssh-app[data-theme=dark] [class~=border-red-300]{border-color:#991b1b}.ssh-app[data-theme=dark] [class~=ring-red-100],.ssh-app[data-theme=dark] [class~=ring-red-200]{--tw-ring-color:#7f1d1d}.ssh-app[data-theme=dark] [class~=bg-rose-50]{background-color:#f43f5e1f}.ssh-app[data-theme=dark] [class~=bg-rose-50\/70]{background-color:#f43f5e1a}.ssh-app[data-theme=dark] [class~=text-rose-600],.ssh-app[data-theme=dark] [class~=text-rose-700],.ssh-app[data-theme=dark] [class~=text-rose-800],.ssh-app[data-theme=dark] [class~=text-rose-900],.ssh-app[data-theme=dark] [class~=text-rose-950]{color:#fda4af}.ssh-app[data-theme=dark] [class~=border-rose-200],.ssh-app[data-theme=dark] [class~=border-rose-300]{border-color:#9f1239}.ssh-app[data-theme=dark] [class~=ring-rose-200]{--tw-ring-color:#881337}.ssh-app[data-theme=dark] [class~=bg-blue-50]{background-color:#3b82f61f}.ssh-app[data-theme=dark] [class~=text-blue-700]{color:#93c5fd}.ssh-app[data-theme=dark] [class~=ring-blue-200]{--tw-ring-color:#1e3a8a}.ssh-app[data-theme=dark] .hover\:bg-slate-50:hover{background-color:#1e293b}.ssh-app[data-theme=dark] .hover\:bg-rose-50:hover{background-color:#f43f5e2e}.ssh-app[data-theme=dark] .hover\:text-slate-950:hover{color:#f8fafc}.ssh-app[data-theme=dark] .hover\:text-slate-700:hover{color:#e2e8f0}.ssh-app[data-theme=dark] .hover\:text-rose-800:hover{color:#fecdd3}.ssh-app[data-theme=dark] .hover\:border-slate-300:hover{border-color:#475569}.ssh-app[data-theme=dark] .ssh-product-header-action{color:#cbd5e1;background-color:#0f172a;border-color:#334155!important}.ssh-app[data-theme=dark] .ssh-product-header-action:hover{color:#5eead4;border-color:#0f766e!important}.ssh-app[data-theme=dark] .ssh-product-header-action:focus-visible{--tw-ring-color:#134e4a}.ssh-initial-setup-probe{color:#0f172a;background:#f8fafc;place-items:center;min-height:100dvh;padding:1.5rem;display:grid}.ssh-initial-setup-probe-card{text-align:center;background:#fff;border:1px solid #e2e8f0;border-radius:1.25rem;justify-items:center;gap:.7rem;width:min(100%,28rem);padding:2.25rem;display:grid;box-shadow:0 20px 55px #0f172a14}.ssh-initial-setup-probe-card>svg{color:#0f766e;width:2rem;height:2rem}.ssh-initial-setup-probe-card strong{font-size:1rem;font-weight:900}.ssh-initial-setup-probe-card span{color:#64748b;font-size:.84rem;line-height:1.55}.ssh-initial-setup-probe-card button{color:#fff;cursor:pointer;min-height:2.75rem;font:inherit;background:#0f766e;border:0;border-radius:.8rem;margin-top:.4rem;padding:0 1rem;font-size:.84rem;font-weight:900}.smartsafehub-login-shadow-root{background:#f8fafc;min-height:100dvh}.ssh-login-page,.ssh-login-page *,.ssh-login-page :before,.ssh-login-page :after{box-sizing:border-box}.ssh-login-page{--ssh-login-bg:#f8fafc;--ssh-login-surface:#fff;--ssh-login-surface-soft:#f8fafc;--ssh-login-border:#e2e8f0;--ssh-login-border-strong:#cbd5e1;--ssh-login-text:#0f172a;--ssh-login-muted:#64748b;--ssh-login-muted-soft:#94a3b8;--ssh-login-accent:#0f766e;--ssh-login-accent-hover:#115e59;--ssh-login-accent-soft:#f0fdfa;--ssh-login-accent-ring:#14b8a624;--ssh-login-card-shadow:0 24px 70px #0f172a14, 0 2px 10px #0f172a0a;background:var(--ssh-login-bg);min-height:100dvh;color:var(--ssh-login-text);grid-template-columns:minmax(0,1.08fr) minmax(420px,.92fr);display:grid}.ssh-login-page[data-theme=dark]{--ssh-login-bg:#020617;--ssh-login-surface:#0f172a;--ssh-login-surface-soft:#111827;--ssh-login-border:#1e293b;--ssh-login-border-strong:#334155;--ssh-login-text:#f8fafc;--ssh-login-muted:#94a3b8;--ssh-login-muted-soft:#64748b;--ssh-login-accent:#2dd4bf;--ssh-login-accent-hover:#5eead4;--ssh-login-accent-soft:#14b8a61a;--ssh-login-accent-ring:#2dd4bf2e;--ssh-login-card-shadow:0 28px 72px #0000003d, 0 0 0 1px #94a3b80a}.ssh-login-toast{top:max(1rem, env(safe-area-inset-top));z-index:20;color:#115e59;background:#f0fdfa;border:1px solid #99f6e4;border-radius:1rem;align-items:center;gap:.75rem;width:min(100% - 2rem,34rem);padding:.9rem 1rem;font-size:.88rem;font-weight:800;line-height:1.45;display:flex;position:fixed;left:50%;transform:translate(-50%);box-shadow:0 18px 45px #0f172a29}.ssh-login-toast svg{flex:none;width:1.25rem;height:1.25rem}.ssh-login-page[data-theme=dark] .ssh-login-toast{color:#99f6e4;background:#0f2f2e;border-color:#2dd4bf57;box-shadow:0 18px 45px #00000052}.ssh-login-brand{color:#f8fafc;background:radial-gradient(circle at 16% 14%,#2dd4bf29,#0000 28rem),linear-gradient(145deg,#020617 0%,#0f172a 56%,#042f2e 140%);justify-content:center;align-items:center;min-height:100dvh;padding:clamp(3rem,7vw,7rem);display:flex;position:relative;overflow:hidden}.ssh-login-brand:after{content:"";pointer-events:none;background-image:linear-gradient(#94a3b80e 1px,#0000 1px),linear-gradient(90deg,#94a3b80e 1px,#0000 1px);background-size:44px 44px;position:absolute;top:0;bottom:0;left:0;right:0;-webkit-mask-image:linear-gradient(to bottom right,#000,#0000 78%);mask-image:linear-gradient(to bottom right,#000,#0000 78%)}.ssh-login-brand-orb{filter:blur(1px);pointer-events:none;border-radius:9999px;position:absolute}.ssh-login-brand-orb-one{border:1px solid #2dd4bf29;width:30rem;height:30rem;top:-10rem;right:-8rem;box-shadow:inset 0 0 6rem #14b8a60d}.ssh-login-brand-orb-two{border:1px solid #5eead41c;width:36rem;height:36rem;bottom:-13rem;left:-10rem}.ssh-login-brand-content{z-index:1;width:min(100%,680px);position:relative}.ssh-login-brand-mark,.ssh-login-mobile-mark{color:#5eead4;background:#14b8a621;place-items:center;display:grid;box-shadow:inset 0 0 0 1px #5eead42e}.ssh-login-brand-mark{border-radius:1.5rem;width:4.5rem;height:4.5rem;margin-bottom:2rem}.ssh-login-brand-mark svg{width:2.25rem;height:2.25rem}.ssh-login-eyebrow,.ssh-login-kicker{color:#5eead4;letter-spacing:.22em;text-transform:uppercase;margin:0;font-size:.72rem;font-weight:900}.ssh-login-brand h1{letter-spacing:-.055em;margin:.7rem 0 0;font-size:clamp(2.7rem,5vw,5rem);font-weight:900;line-height:.98}.ssh-password-setup-brand-title-line{display:block}.ssh-login-brand-description{color:#cbd5e1;max-width:39rem;margin:1.6rem 0 0;font-size:clamp(1rem,1.5vw,1.16rem);line-height:1.85}.ssh-login-features{gap:.85rem;max-width:40rem;margin-top:3rem;display:grid}.ssh-login-feature{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background:#0f172a75;border:1px solid #94a3b81c;border-radius:1rem;align-items:center;gap:1rem;padding:.95rem 1rem;display:flex}.ssh-login-feature-icon{color:#5eead4;background:#14b8a61c;border-radius:.8rem;flex:none;place-items:center;width:2.65rem;height:2.65rem;display:grid}.ssh-login-feature-icon svg{width:1.25rem;height:1.25rem}.ssh-login-feature-copy{gap:.15rem;min-width:0;display:grid}.ssh-login-feature-copy strong{color:#f8fafc;font-size:.92rem;font-weight:800}.ssh-login-feature-copy>span{color:#94a3b8;font-size:.8rem;line-height:1.45}.ssh-login-panel{background:var(--ssh-login-bg);justify-content:center;align-items:center;min-height:100dvh;padding:clamp(2rem,5vw,5rem);transition:background-color .18s;display:flex;position:relative}.ssh-login-theme-toggle{top:max(1.25rem, env(safe-area-inset-top));right:max(1.25rem, env(safe-area-inset-right));z-index:2;border:1px solid var(--ssh-login-border);background:var(--ssh-login-surface);width:2.75rem;height:2.75rem;color:var(--ssh-login-muted);cursor:pointer;border-radius:.9rem;place-items:center;transition:border-color .15s,background-color .15s,color .15s,transform .15s;display:grid;position:absolute;box-shadow:0 5px 16px #0f172a0f}.ssh-login-theme-toggle svg{width:1.15rem;height:1.15rem}.ssh-login-theme-toggle:hover{border-color:var(--ssh-login-border-strong);color:var(--ssh-login-accent);transform:translateY(-1px)}.ssh-login-panel-inner{flex-direction:column;justify-content:center;width:min(100%,480px);min-height:min(40rem,100dvh - 4rem);display:flex}.ssh-login-card{border:1px solid var(--ssh-login-border);background:var(--ssh-login-surface);width:100%;box-shadow:var(--ssh-login-card-shadow);border-radius:1.5rem;padding:clamp(1.6rem,4vw,2.6rem);transition:background-color .18s,border-color .18s}.ssh-login-card-heading{align-items:flex-start;gap:1rem;display:flex}.ssh-login-card-heading>div{min-width:0}.ssh-login-mobile-mark{color:#fff;background:#0f766e;border-radius:1.1rem;flex:none;width:3.5rem;height:3.5rem;display:none;box-shadow:0 12px 28px #0f766e38}.ssh-login-mobile-mark svg{width:1.8rem;height:1.8rem}.ssh-login-kicker{color:var(--ssh-login-accent);letter-spacing:.18em}.ssh-login-card h2{color:var(--ssh-login-text);letter-spacing:-.035em;margin:.65rem 0 0;font-size:clamp(1.8rem,4vw,2.25rem);font-weight:900;line-height:1.15}.ssh-login-subtitle{color:var(--ssh-login-muted);margin:.7rem 0 0;font-size:.94rem;line-height:1.6}.ssh-login-alert{color:#9f1239;background:#fff1f2;border:1px solid #fecdd3;border-radius:.85rem;margin-top:1.3rem;padding:.85rem 1rem;font-size:.82rem;font-weight:700;line-height:1.55}.ssh-login-page[data-theme=dark] .ssh-login-alert{color:#fecdd3;background:#8813372e;border-color:#f43f5e59}.ssh-login-form{gap:1.1rem;margin-top:1.6rem;display:grid}.ssh-login-field{gap:.5rem;display:grid}.ssh-login-label{color:var(--ssh-login-text);font-size:.82rem;font-weight:800}.ssh-login-input-wrap{border:2px solid var(--ssh-login-border-strong);background:var(--ssh-login-surface-soft);min-height:3.3rem;color:var(--ssh-login-muted);border-radius:.9rem;align-items:center;transition:border-color .15s,background-color .15s,box-shadow .15s;display:flex;overflow:hidden;box-shadow:inset 0 1px 2px #0f172a09}.ssh-login-input-wrap:focus-within{border-color:var(--ssh-login-accent);background:var(--ssh-login-surface);box-shadow:0 0 0 4px var(--ssh-login-accent-ring)}.ssh-login-field-icon{background:var(--ssh-login-accent-soft);width:2.15rem;height:2.15rem;color:var(--ssh-login-accent);border-radius:.65rem;flex:none;place-items:center;margin-left:.55rem;transition:background-color .15s,color .15s,box-shadow .15s;display:grid}.ssh-login-field-icon svg{width:1.05rem;height:1.05rem}.ssh-login-input-wrap:focus-within .ssh-login-field-icon{box-shadow:inset 0 0 0 1px var(--ssh-login-accent-ring)}.ssh-login-input-wrap input{width:100%;min-width:0;height:3.15rem;color:var(--ssh-login-text);font:inherit;background:0 0;border:0;outline:0;padding:0 .8rem;font-size:.94rem;font-weight:700}.ssh-login-input-wrap input::placeholder{color:var(--ssh-login-muted-soft);opacity:.82;font-weight:600}.ssh-login-input-wrap input:-webkit-autofill{-webkit-text-fill-color:var(--ssh-login-text);caret-color:var(--ssh-login-text);box-shadow:0 0 0 1000px var(--ssh-login-surface-soft) inset}.ssh-login-input-wrap input:-webkit-autofill:hover{-webkit-text-fill-color:var(--ssh-login-text);caret-color:var(--ssh-login-text);box-shadow:0 0 0 1000px var(--ssh-login-surface-soft) inset}.ssh-login-input-wrap input:-webkit-autofill:focus{-webkit-text-fill-color:var(--ssh-login-text);caret-color:var(--ssh-login-text);box-shadow:0 0 0 1000px var(--ssh-login-surface-soft) inset}.ssh-login-input-wrap:focus-within input:-webkit-autofill{box-shadow:0 0 0 1000px var(--ssh-login-surface) inset}.ssh-login-input-wrap input:disabled{cursor:wait;opacity:.68}.ssh-login-password-toggle{width:2.35rem;height:2.35rem;color:var(--ssh-login-muted);cursor:pointer;background:0 0;border:1px solid #0000;border-radius:.7rem;flex:none;place-items:center;margin-right:.45rem;transition:border-color .15s,background-color .15s,color .15s;display:grid}.ssh-login-password-toggle svg{width:1.05rem;height:1.05rem}.ssh-login-password-toggle:hover:not(:disabled){border-color:var(--ssh-login-border);background:var(--ssh-login-accent-soft);color:var(--ssh-login-accent)}.ssh-login-password-toggle:disabled{cursor:wait;opacity:.6}.ssh-login-theme-toggle:focus-visible{outline:3px solid var(--ssh-login-accent-ring);outline-offset:2px}.ssh-login-password-toggle:focus-visible{outline:3px solid var(--ssh-login-accent-ring);outline-offset:2px}.ssh-login-submit:focus-visible{outline:3px solid var(--ssh-login-accent-ring);outline-offset:2px}.ssh-login-fallback:focus-visible{outline:3px solid var(--ssh-login-accent-ring);outline-offset:2px}.ssh-login-password-hint{color:var(--ssh-login-muted);margin:-.55rem 0 -.15rem;font-size:.76rem;line-height:1.5}.ssh-password-requirements{gap:.4rem;margin:-.25rem 0 0;padding:0;list-style:none;display:grid}.ssh-password-requirement{color:var(--ssh-login-muted);align-items:center;gap:.5rem;font-size:.78rem;font-weight:700;display:flex}.ssh-password-requirement svg{width:1rem;height:1rem;color:var(--ssh-login-muted-soft);flex:none}.ssh-password-requirement.is-met,.ssh-password-requirement.is-met svg,.ssh-password-match.is-met{color:var(--ssh-login-accent)}.ssh-password-match{color:#be123c;margin:-.5rem 0 -.1rem;font-size:.78rem;font-weight:800}.ssh-password-setup-summary{background:#14b8a617;border:1px solid #5eead42e;border-radius:1rem;align-items:flex-start;gap:.85rem;max-width:40rem;margin-top:2.4rem;padding:1rem 1.1rem;display:flex}.ssh-password-setup-summary>svg{color:#5eead4;flex:none;width:1.3rem;height:1.3rem;margin-top:.1rem}.ssh-password-setup-summary>div{gap:.25rem;display:grid}.ssh-password-setup-summary strong{color:#f8fafc;font-size:.9rem}.ssh-password-setup-summary span{color:#94a3b8;font-size:.8rem;line-height:1.5}.ssh-login-caps{color:#b45309;margin:-.45rem 0 -.15rem;font-size:.78rem;font-weight:800}.ssh-login-page[data-theme=dark] .ssh-login-caps{color:#fbbf24}.ssh-login-submit{color:#fff;cursor:pointer;min-height:3.3rem;font:inherit;background:#0f766e;border:0;border-radius:.9rem;justify-content:center;align-items:center;gap:.65rem;margin-top:.25rem;font-size:.94rem;font-weight:900;transition:background-color .15s,transform .15s,box-shadow .15s;display:inline-flex;box-shadow:0 12px 26px #0f766e33}.ssh-login-submit:hover:not(:disabled){background:#115e59;transform:translateY(-1px);box-shadow:0 16px 30px #0f766e40}.ssh-login-submit:disabled{cursor:wait;opacity:.72}.ssh-login-submit-spinner,.ssh-login-probe-spinner{flex:none;animation:.75s linear infinite ssh-login-spin;display:inline-block}.ssh-login-submit-spinner{color:#fff;width:1rem;height:1rem}.ssh-login-security-note{color:var(--ssh-login-muted);align-items:flex-start;gap:.55rem;margin-top:1.2rem;font-size:.76rem;line-height:1.5;display:flex}.ssh-login-security-note svg{width:1rem;height:1rem;color:var(--ssh-login-accent);flex:none;margin-top:.06rem}.ssh-login-fallback{border:1px solid var(--ssh-login-border-strong);background:var(--ssh-login-surface);min-height:2.7rem;color:var(--ssh-login-muted);border-radius:.8rem;justify-content:center;align-items:center;margin-top:.9rem;font-size:.8rem;font-weight:800;text-decoration:none;display:flex}.ssh-login-fallback:hover{border-color:var(--ssh-login-muted-soft);background:var(--ssh-login-surface-soft);color:var(--ssh-login-text)}.ssh-login-probe{color:var(--ssh-login-muted);text-align:center;justify-items:center;gap:.6rem;padding:2.5rem;display:grid}.ssh-login-probe-spinner{width:2rem;height:2rem;color:var(--ssh-login-accent);margin-bottom:.4rem}.ssh-login-probe strong{color:var(--ssh-login-text);font-size:1rem}.ssh-login-probe>span:last-child{font-size:.82rem}.ssh-login-footer{color:var(--ssh-login-muted-soft);flex-wrap:wrap;justify-content:center;gap:.4rem;margin-top:1.5rem;font-size:.72rem;display:flex}@keyframes ssh-login-spin{to{transform:rotate(360deg)}}@media (max-width:980px){.ssh-login-page{grid-template-columns:1fr}.ssh-login-brand{display:none}.ssh-login-panel{padding:max(1.25rem, env(safe-area-inset-top)) max(1.25rem, env(safe-area-inset-right)) max(1.25rem, env(safe-area-inset-bottom)) max(1.25rem, env(safe-area-inset-left))}.ssh-login-panel-inner{min-height:calc(100dvh - 2.5rem)}.ssh-login-mobile-mark{display:grid}}@media (max-width:520px){.ssh-login-panel{background:var(--ssh-login-surface);align-items:stretch;padding:0}.ssh-login-theme-toggle{top:max(1rem, env(safe-area-inset-top));right:max(1rem, env(safe-area-inset-right));box-shadow:none}.ssh-login-panel-inner{width:100%;min-height:100dvh}.ssh-login-card{min-height:100dvh;padding:max(4.75rem, calc(env(safe-area-inset-top) + 4.25rem)) max(1.25rem, env(safe-area-inset-right)) max(2rem, env(safe-area-inset-bottom)) max(1.25rem, env(safe-area-inset-left));box-shadow:none;border:0;border-radius:0;flex-direction:column;justify-content:center;display:flex}.ssh-login-card-heading{display:block}.ssh-login-mobile-mark{margin-bottom:1.25rem}.ssh-login-input-wrap input{font-size:16px}.ssh-login-footer{display:none}.ssh-login-probe{align-content:center;min-height:100dvh}}@media (prefers-reduced-motion:reduce){.ssh-login-panel,.ssh-login-card,.ssh-login-theme-toggle,.ssh-login-submit,.ssh-login-input-wrap{transition:none}.ssh-login-submit-spinner,.ssh-login-probe-spinner{animation-duration:1.4s}}.ssh-safeshield-plan-badge{isolation:isolate;letter-spacing:.085em;white-space:nowrap;border:1px solid #0000;border-radius:999px;align-items:center;gap:.5rem;min-height:2rem;padding:.3rem .82rem .3rem .36rem;font-size:.75rem;font-weight:950;line-height:1;display:inline-flex;position:relative;overflow:hidden}.ssh-safeshield-plan-badge[data-compact=true]{min-height:1.72rem;padding:.22rem .66rem .22rem .3rem;font-size:.69rem}.ssh-safeshield-plan-badge-mark{z-index:2;border-radius:999px;flex:none;place-items:center;width:1.2rem;height:1.2rem;font-size:.76rem;line-height:1;display:grid;position:relative}.ssh-safeshield-plan-badge[data-compact=true] .ssh-safeshield-plan-badge-mark{width:1.02rem;height:1.02rem;font-size:.66rem}.ssh-safeshield-plan-badge[data-tier=free]{color:#475569;background:linear-gradient(135deg,#fff 0%,#f1f5f9 100%);border-color:#cbd5e1;box-shadow:inset 0 1px #ffffffe6}.ssh-safeshield-plan-badge[data-tier=free] .ssh-safeshield-plan-badge-mark{color:#475569;background:#e2e8f0}.ssh-safeshield-plan-badge[data-tier=pro]{color:#f0fdfa;background:linear-gradient(135deg,#115e59 0%,#0f766e 62%,#0d9488 100%);border-color:#0f766e;box-shadow:0 1px 2px #0f766e29,inset 0 1px #ffffff24}.ssh-safeshield-plan-badge[data-tier=pro] .ssh-safeshield-plan-badge-mark{color:#ccfbf1;box-shadow:none;background:#f0fdfa1f;border:1px solid #99f6e475}.ssh-safeshield-plan-badge[data-tier=ultimate]{color:#fffbeb;background:linear-gradient(135deg,#78350f 0%,#92400e 58%,#b45309 100%);border-color:#b45309;box-shadow:0 1px 2px #78350f2e,inset 0 1px #ffffff1f}.ssh-safeshield-plan-badge[data-tier=ultimate] .ssh-safeshield-plan-badge-mark{color:#fde68a;box-shadow:none;background:#fffbeb1f;border:1px solid #fde68a75}.ssh-safeshield-plan-badge[data-tier=paid]{color:#f0f9ff;background:linear-gradient(135deg,#0c4a6e 0%,#075985 58%,#0369a1 100%);border-color:#0369a1;box-shadow:0 1px 2px #0369a129,inset 0 1px #ffffff1f}.ssh-safeshield-plan-badge[data-tier=paid] .ssh-safeshield-plan-badge-mark{color:#bae6fd;box-shadow:none;background:#f0f9ff1f;border:1px solid #bae6fd75}.ssh-safeshield-plan-caption{color:#64748b;white-space:nowrap;align-items:center;gap:.34rem;font-size:.68rem;font-weight:850;display:inline-flex}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=free]{color:#cbd5e1;background:#1e293b;border-color:#475569}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=free] .ssh-safeshield-plan-badge-mark{color:#e2e8f0;background:#475569}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=pro]{color:#99f6e4;background:linear-gradient(135deg,#042f2e 0%,#134e4a 100%);border-color:#0f766e;box-shadow:inset 0 1px #99f6e414}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=pro] .ssh-safeshield-plan-badge-mark{color:#99f6e4;background:#0d94881f;border-color:#2dd4bf61}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=ultimate]{color:#fde68a;background:linear-gradient(135deg,#451a03 0%,#78350f 100%);border-color:#b45309;box-shadow:inset 0 1px #fde68a14}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=ultimate] .ssh-safeshield-plan-badge-mark{color:#fde68a;background:#92400e29;border-color:#d9770675}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=paid]{color:#bae6fd;background:linear-gradient(135deg,#082f49 0%,#0c4a6e 100%);border-color:#0369a1;box-shadow:inset 0 1px #bae6fd14}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=paid] .ssh-safeshield-plan-badge-mark{color:#bae6fd;background:#0369a124;border-color:#0ea5e95c}.ssh-app[data-theme=dark] .ssh-safeshield-plan-caption{color:#94a3b8}.ssh-safeshield-upgrade-card{background:linear-gradient(120deg,#f0fdfa 0%,#ecfeff 58%,#fff7ed 100%);border:1px solid #99f6e4;border-radius:1rem;align-items:center;gap:.85rem;margin-top:.75rem;padding:.9rem 1rem;display:flex;box-shadow:0 10px 24px #0f766e14}.ssh-safeshield-upgrade-mark{color:#fff;background:linear-gradient(135deg,#0f766e,#0891b2);border-radius:.8rem;flex:none;place-items:center;width:2.5rem;height:2.5rem;font-size:1.05rem;display:grid;box-shadow:0 8px 18px #0d948838}.ssh-safeshield-upgrade-copy{flex:1;gap:.2rem;min-width:0;display:grid}.ssh-safeshield-upgrade-meta{color:#0f766e;letter-spacing:.12em;text-transform:uppercase;flex-wrap:wrap;align-items:center;gap:.45rem;font-size:.62rem;font-weight:900;display:flex}.ssh-safeshield-upgrade-soon{letter-spacing:.06em;background:#0f766e1a;border-radius:999px;padding:.18rem .45rem}.ssh-safeshield-upgrade-copy strong{color:#0f172a;font-size:.86rem;line-height:1.35rem}.ssh-safeshield-upgrade-copy>span{color:#64748b;font-size:.74rem;font-weight:600;line-height:1.15rem}.ssh-safeshield-upgrade-action{color:#fff;background:#0f766e;border:1px solid #0f766e;border-radius:.8rem;flex:none;justify-content:center;align-items:center;gap:.4rem;min-height:2.45rem;padding:.55rem .8rem;font-size:.74rem;font-weight:900;text-decoration:none;transition:background-color .15s,border-color .15s,transform .15s;display:inline-flex}.ssh-safeshield-upgrade-action:hover{background:#115e59;border-color:#115e59;transform:translateY(-1px)}.ssh-safeshield-upgrade-action:focus-visible{outline-offset:2px;outline:3px solid #14b8a63d}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-card{background:linear-gradient(120deg,#0d948821 0%,#0f172afa 58%,#92400e1f 100%);border-color:#134e4a;box-shadow:0 12px 28px #0000002e}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-mark{background:linear-gradient(135deg,#0d9488,#0891b2);box-shadow:0 8px 20px #2dd4bf21}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-meta{color:#5eead4}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-soon{background:#2dd4bf1f}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-copy strong{color:#f8fafc}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-copy>span{color:#94a3b8}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-action{color:#ccfbf1;background:#0d948838;border-color:#2dd4bf}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-action:hover{background:#0d948852;border-color:#5eead4}@media (max-width:640px){.ssh-safeshield-upgrade-card{flex-wrap:wrap;align-items:flex-start}.ssh-safeshield-upgrade-action{justify-content:center;width:100%}}.ssh-safeshield-refresh{background:#f0fdfab3;border:1px solid #ccfbf1;border-radius:1rem;align-items:center;gap:.875rem;width:27rem;max-width:100%;margin-top:1rem;padding:.75rem .875rem;display:flex}.ssh-safeshield-refresh[data-state=error]{background:#fef2f2c7;border-color:#fecaca}.ssh-safeshield-refresh-copy{min-width:0}.ssh-safeshield-refresh-kicker,.ssh-safeshield-refresh-title,.ssh-safeshield-refresh-description,.ssh-safeshield-refresh-error-code{margin:0}.ssh-safeshield-refresh-kicker{color:#0f766e;letter-spacing:.14em;text-transform:uppercase;font-size:.66rem;font-weight:900}.ssh-safeshield-refresh[data-state=error] .ssh-safeshield-refresh-kicker{color:#b91c1c}.ssh-safeshield-refresh-title{color:#0f172a;margin-top:.25rem;font-size:.875rem;font-weight:800}.ssh-safeshield-refresh-description{color:#475569;margin-top:.25rem;font-size:.75rem;font-weight:600;line-height:1.25rem}.ssh-safeshield-refresh-error-code{overflow-wrap:anywhere;color:#64748b;margin-top:.375rem;font-size:.68rem;font-weight:600}.ssh-safeshield-refresh-error-code code{font:inherit}.ssh-safeshield-refresh-donut{color:#0d9488;place-items:center;width:3.35rem;min-width:3.35rem;height:3.35rem;display:grid;position:relative}.ssh-safeshield-refresh-donut[data-state=error]{color:#dc2626}.ssh-safeshield-refresh-loader-svg{transform-origin:50%;width:100%;height:100%;animation:.82s linear infinite ssh-safeshield-refresh-loader-rotate;position:absolute;top:0;bottom:0;left:0;right:0;transform:rotate(-90deg)}.ssh-safeshield-refresh-loader-svg-error{animation:none}.ssh-safeshield-refresh-loader-track{color:#cbd5e1;stroke:#cbd5e1}.ssh-safeshield-refresh-loader-arc{color:#0f172a;stroke:#0f172a;filter:drop-shadow(0 0 .06rem #0f172a47)}.ssh-safeshield-refresh-donut[data-state=error] .ssh-safeshield-refresh-loader-track{color:#dc2626;stroke:#dc2626}.ssh-safeshield-refresh-donut-label{z-index:1;color:#0f766e;font-variant-numeric:tabular-nums;font-size:.68rem;font-weight:900;position:relative}.ssh-app[data-theme=dark] .ssh-safeshield-refresh{background:#0f766e1f;border-color:#134e4a}.ssh-app[data-theme=dark] .ssh-safeshield-refresh[data-state=error]{background:#7f1d1d2e;border-color:#7f1d1d}.ssh-app[data-theme=dark] .ssh-safeshield-refresh-title{color:#f8fafc}.ssh-app[data-theme=dark] .ssh-safeshield-refresh-description,.ssh-app[data-theme=dark] .ssh-safeshield-refresh-error-code{color:#94a3b8}.ssh-app[data-theme=dark] .ssh-safeshield-refresh-loader-track{color:#334155;stroke:#334155}.ssh-app[data-theme=dark] .ssh-safeshield-refresh-loader-arc{color:#fff;stroke:#fff;filter:drop-shadow(0 0 .08rem #ffffff8c)}.ssh-app[data-theme=dark] .ssh-safeshield-refresh-donut-label{color:#5eead4}.ssh-app[data-theme=dark] .ssh-safeshield-refresh-donut[data-state=error] .ssh-safeshield-refresh-loader-track{color:#ef4444;stroke:#ef4444}@keyframes ssh-safeshield-refresh-loader-rotate{to{transform:rotate(270deg)}}@media (prefers-reduced-motion:reduce){.ssh-safeshield-refresh-loader-svg{animation:none}}.ssh-dashboard-account-banner{background:#f0fdfa;border-color:#99e8df}.ssh-dashboard-account-banner-icon{color:#0f766e;background:#ccfbf1}.ssh-dashboard-account-banner-title{color:#0f172a}.ssh-dashboard-account-banner-description{color:#475569}.ssh-dashboard-account-banner-badge{color:#92400e;background:#fef3c7}.ssh-dashboard-account-banner-action{color:#fff;background:#0f766e}.ssh-dashboard-account-banner-action:hover{color:#fff;background:#115e59}.ssh-dashboard-account-banner-action:focus-visible{outline-color:#0d9488}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner{background:#102927;border-color:#115e59}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner-icon{color:#99f6e4;background:#134e4a}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner-title{color:#f1f5f9}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner-description{color:#cbd5e1}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner-badge{color:#fde68a;background:#78350f}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner-action{color:#fff;background:#0f766e}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner-action:hover{background:#0d9488}.ssh-account-nav-dot{width:10px;height:10px;box-shadow:0 0 0 2px var(--ssh-account-dot-ring,#fff);background-color:#d97706;border-radius:50%;flex:none;margin-left:auto;display:block}.ssh-account-nav-dot-collapsed{margin-left:0;position:absolute;top:5px;right:7px}.ssh-app[data-theme=dark] .ssh-account-nav-dot{--ssh-account-dot-ring:#0f172a;background-color:#fbbf24}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}} +@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}} /*$vite$:1*/ \ No newline at end of file diff --git a/luci-app-smartsafehub/frontend/demo-dist/demo.js b/luci-app-smartsafehub/frontend/demo-dist/demo.js index 6445716ae..08b69c6d2 100644 --- a/luci-app-smartsafehub/frontend/demo-dist/demo.js +++ b/luci-app-smartsafehub/frontend/demo-dist/demo.js @@ -1,4 +1,3 @@ -var e,t,n,r,i,a,o,s,c,l,u={},d=[],f=/^m(i|n|o|s|text|space)$/,p=Array.isArray,m=d.slice,h=Object.assign;function g(e){e&&e.parentNode&&e.remove()}function _(n,r,i,a,o){var s={type:n,props:r,key:i,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o||++t,__i:-1,__u:0};return!o&&e.vnode&&e.vnode(s),s}function v(e){return e.children}function y(e,t){this.props=e,this.context=t,this.__g=0}function b(e,t){if(t==null)return e.__?b(e.__,e.__i+1):null;for(var n;tr&&n.sort(a),t=n.shift(),r=n.length,8&t.__g&&(s=void 0,c=void 0,l=(c=(o=t).__v).__e,u=[],d=[],(f=o.__P)&&((s=h({constructor:void 0},c)).__v=c.__v+1,e.vnode&&e.vnode(s),M(f,s,c,o.__n,f.namespaceURI,32&c.__u?[l]:null,u,l||b(c),32&c.__u,d),s.__v=c.__v,s.__.__k[s.__i]=s,te(u,s,d),c.__=c.__e=null,s.__e!=l&&x(s)))}finally{n.length=i=0}}function w(e,t,n,r,i,a,o,s,c,l,f){var p,m,h,g,_,v,y=r.__k||d,b=t.length;for(c=T(n,t,y,c,b),p=0;pc?x--:x++,S=!0)):(~l||(i>g?x--:i>1]+!!u){for(i=n-1,a=n+1;i>=0||a=0?i--:a++])&&!(2&l.__u)&&s==l.key&&c==l.type)return o}return-1}function k(e,t,n){n??(n=``),t[0]==`-`?e.setProperty(t,n):e[t]=n}function A(e,t,n,r,i){var a,o;n:if(t==`style`){if(typeof n==`string`)e.style.cssText=n;else{if(typeof r==`string`&&(e.style.cssText=r=``),r)for(t in r)n&&t in n||k(e.style,t,``);if(n)for(t in n)r&&n[t]==r[t]||k(e.style,t,n[t])}}else if(t[0]==`o`&&t[1]==`n`)(e.__e||(e.__e={}))[t]=n,n&&r||(o=l[t]||(l[t]=j(t)),(e.__a||(e.__a={}))[t]=c,a=t!=(t=t.replace(s,`$1`)),(t=t.slice(2))[0]<`a`&&(t=t.toLowerCase()),n?e.addEventListener(t,o,a):e.removeEventListener(t,o,a));else{if(i==`http://www.w3.org/2000/svg`)t=t.replace(/xlink(H|:h)/,`h`).replace(/sName$/,`s`);else if(t!=`width`&&t!=`height`&&t!=`href`&&t!=`list`&&t!=`form`&&t!=`tabIndex`&&t!=`download`&&t!=`rowSpan`&&t!=`colSpan`&&t!=`role`&&t!=`popover`&&t in e)try{e[t]=n??``;break n}catch{}typeof n==`function`||(n==null||!1===n&&t[4]!=`-`?e.removeAttribute(t):e.setAttribute(t,t==`popover`&&n==1?``:n))}}function j(t){return function(n){if(this.__e){var r=this.__e[t];if(n[o]==null)n[o]=c++;else if(n[o]=n.__.length&&n.__.push({}),n.__[e]}function F(e){return pe=1,Ce(Fe,e)}function Ce(e,t,n){var r=Se(le++,2);if(r.t=e,!r.__c&&(r.__=[n?n(t):Fe(void 0,t),function(e){var t=r.__N?r.__N[0]:r.__[0],n=r.t(t,e);fe(t,n)||(r.__N=[n,r.__[1]],r.__c.setState({}))}],r.__c=N,!N.__f)){N.__f=!0;var i=N.shouldComponentUpdate;N.shouldComponentUpdate=function(e,t,n){var r=this.__H;if(!r)return!0;var a=!1,o=this.props!=e;if(r.__.some(function(e){e.__N&&(a=!0,fe(e.__[0],e.__N[0])||(o=!0))}),i){var s=i.call(this,e,t,n);return a?s||o:s}return!a||o}}return r.__}function I(e,t){var n=Se(le++,3);!P.__s&&Pe(n.__H,t)&&(n.__P=!0,n.__=e,n.u=t,N.__H.__h.push(n))}function we(e){return pe=5,Te(function(){return{current:e}},[])}function Te(e,t){var n=Se(le++,7);return Pe(n.__H,t)&&(n.__=e(),n.__H=t),n.__}function Ee(e,t){return pe=8,Te(function(){return e},t)}function De(){var e=Se(le++,11);if(!e.__){for(var t=N.__v;!t.__m&&t.__;)t=t.__;var n=t.__m||(t.__m=[0,0]);e.__=`P`+n[0]+`-`+n[1]++}return e.__}function Oe(){var e;do{for(;e=he.shift();)try{Me(e)}catch(t){P.__e(t,{__:(e=e.__P)&&e.__v})}for(;e=me.shift();){var t=e.__H;if(e.__P&&t)try{t.__h.some(Me),t.__h.some(Ne),t.__h=[]}catch(n){t.__h=[],P.__e(n,e.__v)}}}while(he.length)}P.__b=function(e){N=null,ge&&ge(e)},P.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),xe&&xe(e,t)},P.__r=function(e){_e&&_e(e),le=0;var t=(N=e.__c).__H;t&&(ue==N?N.__h=[]:(t.__h.some(Me),t.__h.some(Ne),le=0),t.__h=[],t.__.some(function(e){e.__N&&(e.__=e.__N),e.u=e.__N=void 0})),ue=N},P.diffed=function(e){ve&&ve(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&je(me.push(t)),t.__H.__.some(function(e){e.u&&(e.__H=e.u)})),ue=N=null},P.__c=function(e,t){t.some(function(e){try{e.__h.some(Me),e.__h=e.__h.filter(function(e){return!e.__||Ne(e)})}catch(n){t.some(function(e){e.__h&&(e.__h=[])}),t=[],P.__e(n,e.__v)}}),ye&&ye(e,t)},P.unmount=function(e){be&&be(e);var t,n,r=e.__c;r&&r.__H&&(r.__H.__.some(function(r){try{if(r.__P&&r.__c){if(n===void 0){for(n=e.__;n&&(!n.__c||!n.__c.__P);)n=n.__;n=n&&n.__c}r.__P=n,je(he.push(r))}else Me(r)}catch(e){t=e}}),r.__H=void 0,t&&P.__e(t,r.__v))};var ke=typeof requestAnimationFrame==`function`;function Ae(e){var t,n=function(){clearTimeout(r),ke&&cancelAnimationFrame(t),setTimeout(e)},r=setTimeout(n,35);ke&&(t=requestAnimationFrame(n))}function je(e){e!=1&&de==P.requestAnimationFrame||((de=P.requestAnimationFrame)||Ae)(Oe)}function Me(e){var t=N,n=e.__c;typeof n==`function`&&(e.__c=void 0,n()),N=t}function Ne(e){var t=N;e.__c=e.__(),N=t}function Pe(e,t){return!e||e.length!=t.length||t.some(function(t,n){return!fe(t,e[n])})}function Fe(e,t){return typeof t==`function`?t(e):t}var Ie=Object.fromEntries([{route:`home`,hash:`#home`,label:`대시보드`,title:`대시보드`,description:`네트워크, 장치와 시스템 상태를 한눈에 확인합니다.`},{route:`activity`,hash:`#activity`,label:`최근 활동`,title:`최근 활동`,description:`현재 부팅 이후 인터넷, 보호, 업데이트와 진단 상태 변화를 확인합니다.`},{route:`network`,hash:`#network`,label:`네트워크`,title:`네트워크`,description:`인터넷 연결과 내부 네트워크 주소, DHCP 및 네트워크 충돌을 한 곳에서 관리합니다.`},{route:`wifi`,hash:`#wifi`,label:`Wi-Fi`,title:`Wi-Fi`,description:`기본 무선 네트워크의 이름, 보안과 사용 상태를 관리합니다.`},{route:`iptv`,hash:`#iptv`,label:`IPTV`,title:`IPTV`,description:`SK Broadband와 LG U+ 멀티캐스트 IPTV를 설정합니다.`},{route:`devices`,hash:`#devices`,label:`연결된 기기`,title:`연결된 기기`,description:`네트워크에서 확인된 기기와 연결 방식을 살펴봅니다.`},{route:`safeshield`,hash:`#safeshield`,label:`SafeShield`,title:`SafeShield`,description:`DNS 보호 상태를 확인하고 차단 목록을 관리합니다.`},{route:`rules`,hash:`#rules`,label:`사용자 규칙`,title:`사용자 규칙`,description:`직접 허용하거나 차단할 도메인을 관리합니다.`},{route:`account`,hash:`#account`,label:`SmartSafeHub 계정`,title:`SmartSafeHub 계정`,description:`이 기기의 SmartSafeHub 계정 연결 상태와 구독 플랜을 확인합니다.`},{route:`system`,hash:`#system`,label:`업데이트`,title:`업데이트`,description:`기기의 펌웨어와 관리 소프트웨어 업데이트를 관리합니다.`},{route:`settings`,hash:`#settings`,label:`설정`,title:`설정`,description:`시스템 상태와 시간대를 확인하고 장치 관리 및 진단 기능을 설정합니다.`}].map(e=>[e.route,e])),Le=[`home`,`activity`,`network`,`wifi`,`devices`,`safeshield`,`rules`,`system`],Re=`smartsafehub.theme`;function ze(){if(typeof window>`u`)return`light`;try{let e=window.localStorage.getItem(Re);if(e===`dark`||e===`light`)return e}catch{}return window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}function Be(e){try{window.localStorage.setItem(Re,e)}catch{}}function Ve(e){let t=document.querySelector(`meta[name="theme-color"]`);t&&(t.content=e===`dark`?`#0f172a`:`#f8fafc`),document.documentElement.style.colorScheme=e,document.body.style.backgroundColor=e===`dark`?`#020617`:`#f8fafc`}var He=0;Array.isArray;function L(t,n,r,i,a,o){n||(n={});var s,c,l=n;if(`ref`in l&&typeof t!=`function`)for(c in l={},n)c==`ref`?s=n[c]:l[c]=n[c];var u={type:t,props:l,key:r,ref:s,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--He,__i:-1,__u:0};return(a||o)&&(u.__source=a,u.__self=o),e.vnode&&e.vnode(u),u}function R({children:e,title:t,...n}){return L(`svg`,{"aria-hidden":!t||void 0,fill:`none`,role:t?`img`:void 0,stroke:`currentColor`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`1.8`,viewBox:`0 0 24 24`,...n,children:[t?L(`title`,{children:t}):null,e]})}function Ue(e){return L(R,{...e,children:[L(`path`,{d:`M12 3 5 6v5c0 4.6 2.8 8.1 7 10 4.2-1.9 7-5.4 7-10V6l-7-3Z`}),L(`path`,{d:`m9 12 2 2 4-4`})]})}function We(e){return L(R,{...e,children:[L(`rect`,{height:`7`,rx:`2`,width:`18`,x:`3`,y:`13`}),L(`path`,{d:`M7 16.5h.01M11 16.5h.01M7 10a7 7 0 0 1 10 0M9.5 7.5a3.5 3.5 0 0 1 5 0`})]})}function Ge(e){return L(R,{...e,children:[L(`rect`,{height:`11`,rx:`2`,width:`16`,x:`4`,y:`3`}),L(`path`,{d:`M8 21h8M12 14v7`})]})}function Ke(e){return L(R,{...e,children:[L(`path`,{d:`m3 11 9-8 9 8`}),L(`path`,{d:`M5 10v10h14V10M9 20v-6h6v6`})]})}function qe(e){return L(R,{...e,children:[L(`circle`,{cx:`12`,cy:`8`,r:`4`}),L(`path`,{d:`M4.5 21a7.5 7.5 0 0 1 15 0`})]})}function Je(e){return L(R,{...e,children:[L(`path`,{d:`M12 16V4`}),L(`path`,{d:`m7 9 5-5 5 5`}),L(`path`,{d:`M5 14v5h14v-5`})]})}function Ye(e){return L(R,{...e,children:[L(`circle`,{cx:`12`,cy:`12`,r:`3`}),L(`path`,{d:`M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2.8 2.8-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.6v.2h-4V21a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1L4.2 17l.1-.1a1.7 1.7 0 0 0 .3-1.9A1.7 1.7 0 0 0 3 14H2.8v-4H3a1.7 1.7 0 0 0 1.6-1 1.7 1.7 0 0 0-.3-1.9L4.2 7 7 4.2l.1.1a1.7 1.7 0 0 0 1.9.3A1.7 1.7 0 0 0 10 3V2.8h4V3a1.7 1.7 0 0 0 1 1.6 1.7 1.7 0 0 0 1.9-.3l.1-.1L19.8 7l-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.6 1h.2v4H21a1.7 1.7 0 0 0-1.6 1Z`})]})}function Xe(e){return L(R,{...e,children:[L(`path`,{d:`M5 9.5a10 10 0 0 1 14 0M8 13a6 6 0 0 1 8 0M11 16.5a2 2 0 0 1 2 0`}),L(`path`,{d:`M12 20h.01`})]})}function Ze(e){return L(R,{...e,children:[L(`path`,{d:`M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4z`}),L(`path`,{d:`M6.5 6.5h1M16.5 6.5h1M6.5 16.5h1`}),L(`path`,{d:`M14 14h2v2h-2zM18 14h2v2h-2zM16 16h2v2h-2zM18 18h2v2h-2zM14 18h1`})]})}function Qe(e){return L(R,{...e,children:[L(`path`,{d:`M7 3v4M17 3v4M5 7h14v4a7 7 0 0 1-14 0V7Z`}),L(`path`,{d:`M12 18v3`})]})}function $e(e){return L(R,{...e,children:[L(`circle`,{cx:`12`,cy:`12`,r:`9`}),L(`path`,{d:`M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18`})]})}function et(e){return L(R,{...e,children:[L(`rect`,{height:`13`,rx:`2`,width:`18`,x:`3`,y:`6`}),L(`path`,{d:`m8 2 4 4 4-4M8 22h8`})]})}function tt(e){return L(R,{...e,children:[L(`circle`,{cx:`12`,cy:`12`,r:`9`}),L(`path`,{d:`M12 7v5l3 2`})]})}function nt(e){return L(R,{...e,children:[L(`rect`,{height:`14`,rx:`2`,width:`14`,x:`5`,y:`5`}),L(`path`,{d:`M9 9h6v6H9zM9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M2 15h3M19 9h3M19 15h3`})]})}function z(e){return L(R,{...e,children:[L(`path`,{d:`M20 11a8 8 0 1 0-2.3 5.7`}),L(`path`,{d:`M20 4v7h-7`})]})}function rt(e){return L(R,{...e,children:[L(`path`,{d:`M12 2v10`}),L(`path`,{d:`M18.4 6.6a8 8 0 1 1-12.8 0`})]})}function it(e){return L(R,{...e,children:[L(`path`,{d:`M12 3v12`}),L(`path`,{d:`m7 10 5 5 5-5`}),L(`path`,{d:`M5 21h14`})]})}function at(e){return L(R,{...e,children:[L(`ellipse`,{cx:`12`,cy:`5`,rx:`8`,ry:`3`}),L(`path`,{d:`M4 5v6c0 1.7 3.6 3 8 3s8-1.3 8-3V5M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6`})]})}function ot(e){return L(R,{...e,children:[L(`circle`,{cx:`8`,cy:`15`,r:`4`}),L(`path`,{d:`m11 12 8-8M15 8l2 2M17 6l2 2`})]})}function st(e){return L(R,{...e,children:[L(`path`,{d:`M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z`}),L(`circle`,{cx:`12`,cy:`12`,r:`2.5`})]})}function ct(e){return L(R,{...e,children:[L(`path`,{d:`m3 3 18 18`}),L(`path`,{d:`M10.6 6.2A8.8 8.8 0 0 1 12 6c6 0 9.5 6 9.5 6a15.5 15.5 0 0 1-2.6 3.2`}),L(`path`,{d:`M6.2 6.3C3.8 8 2.5 12 2.5 12s3.5 6 9.5 6a9.8 9.8 0 0 0 3.1-.5`}),L(`path`,{d:`M10.3 10.3a2.5 2.5 0 0 0 3.4 3.4`})]})}function lt(e){return L(R,{...e,children:[L(`rect`,{height:`17`,rx:`2`,width:`18`,x:`3`,y:`4`}),L(`path`,{d:`M8 2v4M16 2v4M3 9h18`})]})}function ut(e){return L(R,{...e,children:[L(`circle`,{cx:`12`,cy:`12`,r:`9`}),L(`path`,{d:`m8 12 2.5 2.5L16 9`})]})}function B(e){return L(R,{...e,children:[L(`path`,{d:`M10.3 4.2 2.7 17.5A2 2 0 0 0 4.4 20h15.2a2 2 0 0 0 1.7-2.5L13.7 4.2a2 2 0 0 0-3.4 0Z`}),L(`path`,{d:`M12 9v4M12 17h.01`})]})}function dt(e){return L(R,{...e,children:L(`path`,{d:`M12 5v14M5 12h14`})})}function ft(e){return L(R,{...e,children:L(`path`,{d:`M4 7h16M9 7V4h6v3M7 7l1 13h8l1-13M10 11v5M14 11v5`})})}function pt(e){return L(R,{...e,children:[L(`circle`,{cx:`11`,cy:`11`,r:`7`}),L(`path`,{d:`m20 20-4-4`})]})}function mt(e){return L(R,{...e,children:[L(`path`,{d:`M8 6h12M8 12h12M8 18h12`}),L(`path`,{d:`M4 6h.01M4 12h.01M4 18h.01`})]})}function ht(e){return L(R,{...e,children:[L(`path`,{d:`M10 5H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h4`}),L(`path`,{d:`M14 8l4 4-4 4M8 12h10`})]})}function gt(e){return L(R,{...e,children:L(`path`,{d:`M4 7h16M4 12h16M4 17h16`})})}function _t(e){return L(R,{...e,children:L(`path`,{d:`m6 9 6 6 6-6`})})}function vt(e){return L(R,{...e,children:L(`path`,{d:`m6 6 12 12M18 6 6 18`})})}function yt(e){return L(R,{...e,children:L(`path`,{d:`m15 18-6-6 6-6`})})}function bt(e){return L(R,{...e,children:L(`path`,{d:`m9 18 6-6-6-6`})})}function xt(e){return L(R,{...e,children:L(`path`,{d:`M20.7 13.2A8.5 8.5 0 1 1 10.8 3.3 6.7 6.7 0 0 0 20.7 13.2Z`})})}function St(e){return L(R,{...e,children:[L(`circle`,{cx:`12`,cy:`12`,r:`4`}),L(`path`,{d:`M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41`})]})}function Ct({description:e,loading:t,onRefresh:n,onToggleTheme:r,refreshing:i,theme:a,title:o}){return L(`header`,{class:`ssh-product-hero border-b border-slate-200 bg-white px-4 py-4 sm:px-6 md:h-[4.5rem] md:py-0 lg:px-8 xl:px-10`,children:L(`div`,{class:`flex w-full items-center justify-between gap-4 md:h-full`,children:[L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-[0.68rem] font-extrabold uppercase tracking-[0.18em] text-teal-700`,children:`SmartSafeHub`}),L(`div`,{class:`mt-1 flex min-w-0 items-baseline gap-3`,children:[L(`h1`,{class:`m-0 truncate text-xl font-black tracking-tight text-slate-950 sm:text-2xl`,children:o}),L(`p`,{class:`m-0 hidden truncate text-sm text-slate-500 lg:block`,children:e})]})]}),L(`div`,{class:`flex shrink-0 items-center gap-2`,children:[L(`button`,{"aria-label":a===`dark`?`라이트 모드로 전환`:`다크 모드로 전환`,class:`ssh-product-header-action hidden h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-600 shadow-sm transition hover:border-teal-300 hover:text-teal-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 md:inline-flex`,onClick:r,title:a===`dark`?`라이트 모드`:`다크 모드`,type:`button`,children:L(a===`dark`?St:xt,{class:`size-5`})}),L(`button`,{"aria-busy":i,"aria-label":i?`새로고침 중`:`현재 화면 새로고침`,class:`ssh-product-header-action hidden h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-white shadow-sm transition hover:border-teal-300 hover:text-teal-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-wait disabled:opacity-60 md:inline-flex ${i?`text-teal-700`:`text-slate-600`}`,disabled:t||i,onClick:n,title:i?`새로고침 중`:`새로고침`,type:`button`,children:i?L(z,{class:`size-5 animate-spin`}):L(z,{class:`size-5`})})]})]})})}var wt=`/cgi-bin/luci`;function Tt(){let e=window.location.pathname,t=e.indexOf(`/cgi-bin/luci`);return t>=0?e.slice(0,t+13):wt}function Et(e){let t=e.startsWith(`/`)?e:`/${e}`;return`${Tt()}${t}`}function Dt(e){return Et(e)}var Ot=[{label:`Overview`,routes:[`home`,`activity`]},{label:`Network`,routes:[`network`,`wifi`,`iptv`,`devices`]},{label:`Security`,routes:[`safeshield`,`rules`]},{label:`System`,routes:[`account`,`system`,`settings`]}];function kt({route:e}){switch(e){case`home`:return L(Ke,{class:`size-5`});case`activity`:return L(tt,{class:`size-5`});case`network`:return L($e,{class:`size-5`});case`wifi`:return L(Xe,{class:`size-5`});case`iptv`:return L(et,{class:`size-5`});case`devices`:return L(Ge,{class:`size-5`});case`safeshield`:return L(Ue,{class:`size-5`});case`rules`:return L(qe,{class:`size-5`});case`account`:return L(ot,{class:`size-5`});case`system`:return L(Je,{class:`size-5`});case`settings`:return L(Ye,{class:`size-5`})}}function At({theme:e}){return L(e===`dark`?St:xt,{class:`size-5`})}function jt(e){return e===`dark`?`라이트 모드`:`다크 모드`}function Mt(e,t=!1){return`relative flex min-h-11 min-w-0 items-center rounded-xl py-2.5 text-sm font-extrabold no-underline transition ${t?`justify-center px-2`:`gap-3 px-3`} ${e?`bg-teal-50 text-teal-800 ring-1 ring-inset ring-teal-200`:`text-slate-600 hover:bg-slate-50 hover:text-slate-950`}`}function Nt({accountRegistered:e,collapsed:t=!1,route:n,updateCount:r,demo:i=!1}){return L(`div`,{class:t?`space-y-3`:`space-y-5`,children:Ot.filter(e=>!i||e.routes.some(e=>Le.includes(e))).map((a,o)=>L(`section`,{"aria-label":a.label,class:t&&o>0?`border-t border-slate-100 pt-3`:void 0,children:[t?null:L(`p`,{class:`mb-2 px-3 text-[0.65rem] font-black uppercase tracking-[0.18em] text-slate-400`,children:a.label}),L(`div`,{class:`space-y-1`,children:a.routes.filter(e=>!i||Le.includes(e)).map(i=>{let a=Ie[i],o=n===i;return L(`a`,{"aria-current":o?`page`:void 0,"aria-label":i===`account`&&e===!1?`${a.label} (계정 연결 필요)`:t?a.label:void 0,class:Mt(o,t),href:a.hash,title:i===`account`&&e===!1?`SmartSafeHub 계정 연결 필요`:t?a.label:void 0,children:[L(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg ${o?`bg-teal-700 text-white`:`bg-slate-100 text-slate-500`}`,children:L(kt,{route:i})}),t?null:L(`span`,{class:`flex min-w-0 flex-1 items-center gap-2`,children:[L(`span`,{class:`truncate`,children:a.label}),i===`iptv`?L(`span`,{class:`rounded-full border border-amber-300 bg-amber-100 px-1.5 py-0.5 text-[9px] font-black uppercase tracking-[0.08em] text-amber-800`,children:`Beta`}):null]}),t&&i===`iptv`?L(`span`,{"aria-label":`Beta 기능`,class:`absolute right-0.5 top-0.5 inline-flex size-5 items-center justify-center rounded-full border border-amber-300 bg-amber-100 text-[13px] font-extrabold leading-none text-amber-900 shadow-sm ring-2 ring-white`,children:`β`}):null,i===`account`&&e===!1?L(`span`,{"aria-label":`계정 연결 필요`,class:`ssh-account-nav-dot ${t?`ssh-account-nav-dot-collapsed`:``}`,"aria-hidden":`true`,title:`계정 연결 필요`}):null,i===`system`&&r>0?t?L(`span`,{"aria-label":`${r}개의 업데이트 유형`,class:`ssh-update-nav-badge`,"data-collapsed":`true`,children:r}):L(`span`,{class:`ssh-update-nav-badge`,"data-collapsed":`false`,children:r}):null]},i)})})]},a.label))})}function Pt({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=jt(c);return I(()=>{p(!1)},[s]),I(()=>{if(!f)return;let e=e=>{e.key===`Escape`&&p(!1)};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[f]),L(v,{children:[L(`nav`,{"aria-label":`SmartSafeHub 모바일 메뉴`,class:`ssh-mobile-navigation sticky top-0 z-40 border-b border-slate-200 bg-white/95 px-4 shadow-sm shadow-slate-900/5 backdrop-blur md:hidden`,children:[L(`div`,{class:`flex min-h-16 items-center justify-between gap-3`,children:[L(`a`,{class:`flex min-w-0 items-center gap-3 no-underline`,href:`#home`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-950 text-teal-300`,children:L(Ue,{class:`size-5`})}),L(`span`,{class:`min-w-0`,children:[L(`strong`,{class:`block truncate text-sm font-black text-slate-950`,children:`SmartSafeHub`}),L(`span`,{class:`block truncate text-xs font-semibold text-slate-500`,children:l})]})]}),L(`div`,{class:`flex shrink-0 items-center gap-2`,children:[L(`button`,{"aria-label":`${m}로 전환`,class:`inline-flex size-11 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-600 transition hover:border-slate-300 hover:bg-slate-50 hover:text-teal-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100`,onClick:a,title:m,type:`button`,children:L(At,{theme:c})}),L(`button`,{"aria-busy":o,"aria-label":o?`새로고침 중`:`현재 화면 새로고침`,class:`inline-flex size-11 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-white transition hover:border-slate-300 hover:bg-slate-50 hover:text-teal-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-wait disabled:opacity-60 ${o?`text-teal-700`:`text-slate-600`}`,disabled:n||o,onClick:r,title:o?`새로고침 중`:`새로고침`,type:`button`,children:o?L(z,{class:`size-5 animate-spin`}):L(z,{class:`size-5`})}),L(`button`,{"aria-controls":`smartsafehub-mobile-menu`,"aria-expanded":f,"aria-label":f?`모바일 메뉴 닫기`:`모바일 메뉴 열기`,class:`inline-flex size-11 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-700 transition hover:bg-slate-50 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100`,onClick:()=>p(e=>!e),type:`button`,children:L(f?vt:gt,{class:`size-5`})})]})]}),f?L(`div`,{class:`ssh-mobile-menu border-t border-slate-100 py-4`,id:`smartsafehub-mobile-menu`,children:[L(Nt,{accountRegistered:e,demo:d,route:s,updateCount:u}),d?null:L(`div`,{class:`mt-5 space-y-1 border-t border-slate-100 pt-4`,children:L(`a`,{class:`${Mt(!1)} text-rose-700 hover:bg-rose-50 hover:text-rose-800`,href:Dt(`/admin/logout`),children:[L(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg bg-rose-50 text-rose-600`,children:L(ht,{class:`size-5`})}),`로그아웃`]})})]}):null]}),L(`aside`,{class:`hidden min-h-screen border-r border-slate-200 bg-white md:flex md:flex-col`,"data-collapsed":t?`true`:`false`,children:L(`div`,{class:`sticky top-0 flex h-screen flex-col`,children:[L(`div`,{class:`flex h-[4.5rem] min-h-[4.5rem] shrink-0 items-center border-b border-slate-100 ${t?`justify-center px-2`:`gap-3 px-4`}`,children:[L(`a`,{"aria-label":`SmartSafeHub Dashboard`,class:`flex items-center no-underline ${t?`justify-center`:`min-w-0 flex-1 gap-3`}`,href:`#home`,title:t?`SmartSafeHub`:void 0,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-950 text-teal-300 shadow-sm shadow-slate-950/10`,children:L(Ue,{class:`size-5`})}),t?null:L(`div`,{class:`min-w-0`,children:[L(`strong`,{class:`block truncate text-sm font-black tracking-tight text-slate-950`,children:`SmartSafeHub`}),L(`span`,{class:`block text-[0.68rem] font-bold uppercase tracking-[0.14em] text-slate-400`,children:`Home Gateway`})]})]}),L(`button`,{"aria-controls":`smartsafehub-desktop-navigation`,"aria-expanded":!t,"aria-label":t?`사이드바 펼치기`:`사이드바 접기`,class:`ssh-sidebar-toggle absolute right-0 top-[4.5rem] z-20 inline-flex size-7 translate-x-1/2 -translate-y-1/2 shrink-0 items-center justify-center rounded-full transition focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100`,onClick:i,title:t?`사이드바 펼치기`:`사이드바 접기`,type:`button`,children:L(t?bt:yt,{class:`size-4`})})]}),L(`nav`,{"aria-label":`SmartSafeHub 메뉴`,class:`flex-1 overflow-y-auto py-5 ${t?`px-2`:`px-3`}`,id:`smartsafehub-desktop-navigation`,children:L(Nt,{accountRegistered:e,collapsed:t,demo:d,route:s,updateCount:u})}),d?null:L(`div`,{class:`border-t border-slate-100 ${t?`p-2`:`p-3`}`,children:L(`a`,{"aria-label":`SmartSafeHub에서 로그아웃`,class:`${Mt(!1,t)} mt-1 text-rose-700 hover:bg-rose-50 hover:text-rose-800`,href:Dt(`/admin/logout`),title:t?`로그아웃`:void 0,children:[L(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg bg-rose-50 text-rose-600`,children:L(ht,{class:`size-5`})}),t?null:`로그아웃`]})})]})})]})}var Ft=`smartsafehub.sidebar.collapsed`;function It(){if(typeof window>`u`)return!1;try{return window.localStorage.getItem(Ft)===`1`}catch{return!1}}function Lt({accountRegistered:e,children:t,loading:n,onRefresh:r,refreshing:i,route:a,updateCount:o,demo:s=!1}){let c=Ie[a],[l,u]=F(It),[d,f]=F(ze);return I(()=>{try{window.localStorage.setItem(Ft,l?`1`:`0`)}catch{}},[l]),I(()=>{Be(d),Ve(d)},[d]),L(`div`,{class:`ssh-app min-h-screen bg-slate-50 text-slate-950 md:grid md:transition-[grid-template-columns] md:duration-200 md:ease-out ${l?`md:grid-cols-[5rem_minmax(0,1fr)]`:`md:grid-cols-[16rem_minmax(0,1fr)]`}`,"data-theme":d,children:[L(Pt,{accountRegistered:e,demo:s,collapsed:l,loading:n,onRefresh:r,onToggleCollapsed:()=>u(e=>!e),onToggleTheme:()=>f(e=>e===`dark`?`light`:`dark`),refreshing:i,route:a,theme:d,title:c.title,updateCount:o}),L(`div`,{class:`min-w-0 md:min-h-screen`,children:[L(Ct,{description:c.description,loading:n,onRefresh:r,onToggleTheme:()=>f(e=>e===`dark`?`light`:`dark`),refreshing:i,theme:d,title:c.title}),L(`main`,{class:`ssh-product-main w-full max-w-[1600px] bg-slate-50 px-4 pb-8 sm:px-6 sm:pb-10 lg:px-8 lg:pb-12 xl:px-10 ${n?`pt-2 sm:pt-2 lg:pt-3`:`pt-5 sm:pt-6 lg:pt-8`}`,children:t})]})]})}function V({action:e,description:t,eyebrow:n,id:r,title:i}){return L(`div`,{class:`ssh-section-heading mb-4 min-w-0`,children:[L(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.18em] text-teal-700`,children:n}),L(`div`,{class:`mt-2 flex min-w-0 flex-wrap items-center justify-between gap-3`,children:[L(`h2`,{class:`m-0 min-w-0 text-xl font-black tracking-tight text-slate-950`,id:r,children:i}),e?L(`div`,{class:`shrink-0`,children:e}):null]}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500`,children:t})]})}var Rt=new Intl.NumberFormat(`ko-KR`),zt=new Intl.DateTimeFormat(`ko-KR`,{dateStyle:`medium`,timeStyle:`short`});function Bt(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 Vt(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?Wt(e):Gt(e,t)}function qt(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 Jt(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 Yt(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 Xt(e,t){let n=e.metadata[t];return typeof n==`string`&&n.trim().length>0?n.trim():null}function Zt(e,t){let n=e.metadata[t];return typeof n==`number`&&Number.isFinite(n)?n:null}function Qt(e,t){let n=e.metadata[t];return typeof n==`boolean`?n:null}function $t(e,t){let n=Xt(e,`error_code`);return n?`${t} (오류 코드: ${n})`:t}function en(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 tn(e){let t=Xt(e,`from_version`),n=Xt(e,`to_version`);return t&&n?`${t} → ${n}`:n?`${n} 버전이 적용되었습니다.`:null}function nn(e){return e?e.toUpperCase():null}function rn(e){switch(e){case`dhcp`:return`자동 IP(DHCP)`;case`pppoe`:return`PPPoE`;case`static`:return`고정 IPv4`;default:return e.toUpperCase()}}function an(e){switch(e.eventType){case`system.booted`:return{title:`공유기 부팅 감지`,description:`공유기가 켜지거나 재시작된 것을 확인했습니다.`};case`network.internet.disconnected`:return{title:`인터넷 연결 끊김 감지`,description:`공유기에서 인터넷 연결을 확인할 수 없었습니다.`};case`network.internet.recovered`:{let t=Zt(e,`downtime_seconds`);return{title:`인터넷 연결 복구`,description:t!==null&&t>0?`약 ${en(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=Zt(e,`domain_count`),n=Xt(e,`artifact_version`),r=[];return t!==null&&t>0&&r.push(`${H(t)}개 도메인 적용`),n&&r.push(`목록 ${n}`),{title:`SafeShield 차단 목록 업데이트 완료`,description:r.length>0?r.join(` · `):`최신 차단 목록을 적용했습니다.`}}case`safeshield.blocklist.update_failed`:return{title:`SafeShield 차단 목록 업데이트 실패`,description:$t(e,`차단 목록을 업데이트하지 못했습니다.`)};case`safeshield.statistics.enabled`:return{title:`SafeShield 차단 통계 수집 활성화`,description:`기기별 차단 통계 수집을 활성화했습니다.`};case`safeshield.statistics.disabled`:return{title:`SafeShield 차단 통계 수집 비활성화`,description:`기기별 차단 통계 수집을 비활성화했습니다.`};case`safeshield.rule.added`:return{title:Xt(e,`rule_action`)===`allow`?`사용자 허용 규칙 추가`:`사용자 차단 규칙 추가`,description:`SafeShield 사용자 규칙이 변경되었습니다.`};case`safeshield.rule.removed`:return{title:Xt(e,`rule_action`)===`allow`?`사용자 허용 규칙 삭제`:`사용자 차단 규칙 삭제`,description:`SafeShield 사용자 규칙이 변경되었습니다.`};case`settings.scheduled_reboot.updated`:{let t=Qt(e,`enabled`);return{title:`예약 재부팅 설정 변경`,description:t===!0?`예약 재부팅 설정을 저장하고 새 일정에 적용했습니다.`:t===!1?`예약 재부팅을 비활성화했습니다.`:`예약 재부팅 설정을 변경했습니다.`}}case`settings.software_updates.updated`:return{title:`관리 소프트웨어 업데이트 설정 변경`,description:Qt(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=Xt(e,`zonename`);return{title:`시간대 설정 변경`,description:t?`공유기 시간대를 ${t}(으)로 변경했습니다.`:`공유기 시간대를 변경했습니다.`}}case`settings.wifi.updated`:return{title:`Wi-Fi 설정 변경`,description:`Wi-Fi 설정을 저장하고 무선 네트워크에 적용했습니다.`};case`settings.wan.updated`:{let t=Xt(e,`protocol`);return{title:`인터넷 설정 변경`,description:t?`${rn(t)} 방식으로 WAN 설정을 변경했습니다.`:`WAN 인터넷 연결 설정을 변경했습니다.`}}case`settings.lan.updated`:return{title:`LAN 설정 변경`,description:`내부 네트워크 및 DHCP 설정을 변경했습니다.`};case`settings.iptv.updated`:{let t=Qt(e,`enabled`),n=Xt(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:tn(e)};case`software.update.failed`:return{title:`관리 소프트웨어 업데이트 실패`,description:$t(e,`관리 소프트웨어를 업데이트하지 못했습니다.`)};case`firmware.update.started`:return{title:`펌웨어 업데이트 시작`,description:`펌웨어 설치를 시작했습니다. 완료 과정에서 공유기가 재부팅됩니다.`};case`firmware.update.failed`:return{title:`펌웨어 업데이트 실패`,description:$t(e,`펌웨어 설치 준비 또는 검증에 실패했습니다.`)};case`license.activated`:{let t=nn(Xt(e,`to_plan`));return{title:`요금제 활성화`,description:t?`${t} 요금제가 활성화되었습니다.`:`요금제가 활성화되었습니다.`}}case`license.changed`:{let t=nn(Xt(e,`from_plan`)),n=nn(Xt(e,`to_plan`));return{title:`요금제 변경`,description:t&&n?`${t} → ${n}`:n?`${n} 요금제로 변경되었습니다.`:`요금제 상태가 변경되었습니다.`}}case`license.cleared`:return{title:`계정 권한 해제`,description:Xt(e,`reason`)===`manual`?`이 공유기에서 계정 권한 연결을 해제했습니다.`:`서버의 계정 권한 상태가 반영되어 이 공유기의 권한 연결이 해제되었습니다.`};case`health.issue.started`:{let t=Zt(e,`warning_count`)??0,n=Zt(e,`critical_count`)??0,r=[];return n>0&&r.push(`이상 ${H(n)}건`),t>0&&r.push(`주의 ${H(t)}건`),{title:`확인이 필요한 장치 상태 발견`,description:r.length>0?r.join(` · `):`장치 상태에서 확인이 필요한 항목이 발견되었습니다.`}}case`health.issue.changed`:{let t=Zt(e,`warning_count`)??0,n=Zt(e,`critical_count`)??0,r=[];return n>0&&r.push(`이상 ${H(n)}건`),t>0&&r.push(`주의 ${H(t)}건`),{title:`확인이 필요한 장치 상태 변경`,description:r.length>0?r.join(` · `):`확인이 필요한 장치 상태 항목이 변경되었습니다.`}}case`health.issue.resolved`:return{title:`장치 상태 정상으로 복구`,description:`이전에 확인이 필요했던 장치 상태가 정상으로 돌아왔습니다.`};default:return{title:`SmartSafeHub 활동`,description:`장치에서 새로운 활동이 기록되었습니다.`}}}function on(e){return e===`error`?{icon:L(B,{class:`size-4`}),surface:`bg-rose-50 text-rose-700 ring-rose-200`}:e===`warning`?{icon:L(B,{class:`size-4`}),surface:`bg-amber-50 text-amber-800 ring-amber-200`}:e===`success`?{icon:L(ut,{class:`size-4`}),surface:`bg-emerald-50 text-emerald-700 ring-emerald-200`}:{icon:L(tt,{class:`size-4`}),surface:`bg-slate-100 text-slate-600 ring-slate-200`}}function sn(e){let t=new Date(e*1e3);return new Intl.DateTimeFormat(`ko-KR`,{hour:`2-digit`,minute:`2-digit`,hour12:!1}).format(t)}function cn(e){let t=new Date(e*1e3);return`${t.getFullYear()}-${t.getMonth()+1}-${t.getDate()}`}function ln(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 un({compact:e=!1,events:t}){if(t.length===0)return L(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 px-4 py-5 text-center`,children:[L(tt,{class:`mx-auto size-5 text-slate-400`}),L(`p`,{class:`mt-2 mb-0 text-sm font-extrabold text-slate-700`,children:`아직 기록된 활동이 없습니다.`}),L(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`상태가 실제로 변경되면 이곳에 시간순으로 기록됩니다.`})]});let n=``;return L(`div`,{class:e?`space-y-2`:`space-y-1`,children:t.map(t=>{let r=an(t),i=on(t.severity),a=cn(t.occurredAt),o=!e&&a!==n;return n=a,L(`div`,{children:[o?L(`p`,{class:`mt-5 mb-2 first:mt-0 text-xs font-black text-slate-500`,children:ln(t.occurredAt)}):null,L(`article`,{class:`relative flex min-w-0 gap-3 ${e?`border-b border-slate-100 py-3 first:pt-0 last:border-b-0 last:pb-0`:`rounded-xl px-2 py-3 hover:bg-slate-50`}`,children:[L(`div`,{class:`w-12 shrink-0 pt-1 text-xs font-bold tabular-nums text-slate-400`,children:L(`time`,{dateTime:new Date(t.occurredAt*1e3).toISOString(),title:Wt(t.occurredAt),children:sn(t.occurredAt)})}),L(`span`,{class:`mt-0.5 grid size-8 shrink-0 place-items-center rounded-full ring-1 ring-inset ${i.surface}`,children:i.icon}),L(`div`,{class:`min-w-0 flex-1`,children:[L(`p`,{class:`m-0 text-sm font-black leading-5 text-slate-950`,children:r.title}),r.description?L(`p`,{class:`mt-1 mb-0 break-words text-xs font-medium leading-5 text-slate-500`,children:r.description}):null]})]})]},t.eventId)})})}function dn({error:e,loading:t,onRetry:n}){return t?L(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 px-4 py-5 text-center`,children:[L(z,{class:`mx-auto size-5 animate-spin text-teal-700`}),L(`p`,{class:`mt-2 mb-0 text-sm font-bold text-slate-500`,children:`최근 활동을 불러오는 중입니다.`})]}):e?L(`div`,{class:`rounded-xl border border-amber-200 bg-amber-50/70 px-4 py-4`,children:L(`div`,{class:`flex items-start gap-3`,children:[L(B,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),L(`div`,{class:`min-w-0 flex-1`,children:[L(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:`최근 활동을 불러오지 못했습니다.`}),L(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:e}),n?L(`button`,{class:`mt-3 inline-flex min-h-9 items-center justify-center rounded-lg border border-amber-300 bg-white px-3 text-xs font-extrabold text-amber-800 transition hover:bg-amber-50`,onClick:n,type:`button`,children:`다시 시도`}):null]})]})}):null}function fn(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 pn(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 mn({actionError:e,actionMessage:t,cloud:n,onDismissFeedback:r,onSetEnabled:i,saving:a}){let o=fn(n),s=n.plan?.toUpperCase()??null,c=n.eligible===!0,l=n.enabled||c;return L(`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:[L(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[L(`div`,{class:`min-w-0 flex-1`,children:[L(`div`,{class:`flex flex-wrap items-center gap-2`,children:L(`span`,{class:`rounded-full px-2.5 py-1 text-xs font-extrabold ring-1 ring-inset ${o.tone}`,children:a?`설정 중`:o.label})}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:o.description})]}),L(`div`,{class:`flex shrink-0 items-center justify-end gap-3 sm:justify-start`,children:[L(`span`,{class:`text-xs font-bold text-slate-600`,children:[`Cloud 전송 `,n.enabled?`켜짐`:`꺼짐`]}),L(`button`,{"aria-checked":n.enabled,"aria-label":`Cloud 활동 기록 전송 사용`,class:`ssh-switch-control relative inline-flex shrink-0 rounded-full border transition focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-not-allowed disabled:opacity-50 ${n.enabled?`border-teal-600 bg-teal-600`:`border-slate-300 bg-slate-200`}`,disabled:a||!l,onClick:()=>i(!n.enabled),role:`switch`,type:`button`,children:L(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${n.enabled?`left-6`:`left-1`}`})})]})]}),(e||t)&&L(`div`,{class:`mt-4 flex min-w-0 items-start justify-between gap-3 rounded-xl border px-4 py-3 text-sm font-bold ${e?`border-rose-200 bg-rose-50 text-rose-800`:`border-emerald-200 bg-emerald-50 text-emerald-800`}`,role:e?`alert`:`status`,children:[L(`span`,{class:`min-w-0`,children:e||t}),L(`button`,{class:`shrink-0 rounded-lg px-2 py-1 text-xs font-extrabold hover:bg-black/5`,onClick:r,type:`button`,children:`닫기`})]}),n.enabled?L(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`Cloud 전송을 끄면 아직 전송하지 않은 대기 기록이 삭제됩니다. 공유기의 로컬 기록은 유지됩니다.`}):null,L(`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:[L(`span`,{children:[`마지막 동기화: `,n.lastSuccessAt>0?Gt(n.lastSuccessAt):`아직 없음`]}),L(`span`,{children:[`Cloud 보관: `,pn(n)]}),n.pendingEvents>0?L(`span`,{children:[`전송 대기: `,H(n.pendingEvents),`건`]}):null]}),n.enabled&&n.phase===`error`&&n.lastErrorCode?L(`p`,{class:`mt-3 mb-0 text-xs font-semibold text-amber-700`,children:[`오류 코드: `,L(`code`,{children:n.lastErrorCode})]}):null,L(`details`,{class:`mt-3 border-t border-slate-200 pt-3`,children:[L(`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 동기화 상세 정보`}),L(`dl`,{class:`mt-3 grid gap-3 sm:grid-cols-4`,children:[L(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[L(`dt`,{class:`text-xs font-bold text-slate-500`,children:`멤버십`}),L(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:s??(n.eligible===!1?`Free`:`확인 중`)})]}),L(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[L(`dt`,{class:`text-xs font-bold text-slate-500`,children:`전송 대기`}),L(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:[H(n.pendingEvents),`건`]})]}),L(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[L(`dt`,{class:`text-xs font-bold text-slate-500`,children:`마지막 동기화`}),L(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:n.lastSuccessAt>0?Gt(n.lastSuccessAt):`아직 없음`})]}),L(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[L(`dt`,{class:`text-xs font-bold text-slate-500`,children:`Cloud 보관`}),L(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:pn(n)})]})]}),L(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`SmartSafeHub 계정에 연결된 Free는 최근 7일, Pro와 Ultimate는 최근 90일 동안 Cloud 활동 기록을 보관합니다. 끄면 전송 대기 데이터와 임시 인증 정보를 삭제하며, 다시 켠 뒤 새로 발생한 활동부터 Cloud에 전송합니다. 로컬 최근 활동은 항상 유지됩니다.`})]})]})}function hn({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 L(`div`,{class:`min-w-0 space-y-6`,children:[L(`section`,{"aria-labelledby":`activity-cloud-title`,class:`min-w-0`,children:[L(V,{id:`activity-cloud-title`,eyebrow:`Cloud history`,title:`Cloud 활동 기록`,description:`활동 기록을 Cloud에 자동으로 전송할지 설정합니다. 공유기 내 최근 활동은 항상 기록됩니다.`}),L(mn,{actionError:e,actionMessage:t,cloud:n?.cloud??{enabled:!1,phase:`preparing`,eligible:null,plan:null,retentionDays:0,pendingEvents:0,lastAttemptAt:0,lastSuccessAt:0,lastUploadedCount:0,lastErrorCode:null,nextSyncAt:0},onDismissFeedback:a,onSetEnabled:s,saving:c})]}),L(`section`,{"aria-labelledby":`activity-history-title`,class:`min-w-0`,children:[L(V,{id:`activity-history-title`,eyebrow:`Local history`,title:`현재 부팅 이후의 최근 활동`,description:`인터넷, SafeShield, 업데이트, 계정 권한과 주요 설정에서 실제 상태가 변경된 시점만 기록합니다.`,action:L(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5 text-xs font-extrabold text-slate-600 ring-1 ring-inset ring-slate-200`,children:[`최대 `,n?.maxEvents??128,`건`]})}),L(`div`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:i||r&&!n?L(dn,{error:r,loading:i,onRetry:o}):L(v,{children:[L(un,{events:f}),d.length>20?L(`div`,{class:`mt-5 flex flex-wrap items-center justify-between gap-3 border-t border-slate-200 pt-4`,children:[L(`p`,{class:`m-0 text-xs font-semibold text-slate-500`,children:[H(f.length),` / `,H(d.length),`건 표시 중`]}),L(`div`,{class:`flex flex-wrap items-center gap-2`,children:[l>20?L(`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?L(`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:[`이전 활동 더 보기 (`,H(Math.min(20,p)),`건)`]}):null]})]}):null]})})]}),L(`aside`,{class:`rounded-2xl border border-slate-200 bg-slate-50 px-5 py-4`,children:[L(`p`,{class:`m-0 text-sm font-extrabold text-slate-800`,children:`로컬 활동 기록 안내`}),L(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`이 기록은 공유기의 메모리 영역에만 보관되어 재부팅하면 초기화됩니다. 직접 설정을 변경한 작업은 성공 시점에 즉시 기록하고, 인터넷 및 장치 진단처럼 외부 상태를 확인해야 하는 항목은 실제 상태 변화가 관찰된 경우에만 추가합니다.`})]})]})}var gn=Object.assign,_n=/^(-|f[lo].*[^se]$|g.{5,}[^ps]$|z|o[pr]|(W.{5})?[lL]i.*(t|mp)$|an|(bo|s).{4}Im|sca|m.{6}[ds]|ta|c.*[st]$|wido|ini)/;function vn(e,t){var n=ce(e,t);return n.containerInfo=t,n}var yn=Symbol.for(`react.element`),bn=/^(?: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]/,xn=/[A-Z0-9]/g,Sn=typeof document<`u`,Cn=function(e){return/fil|che|rad/.test(e)};y.prototype.isReactComponent=!0,[`componentWillMount`,`componentWillReceiveProps`,`componentWillUpdate`].forEach(function(e){Object.defineProperty(y.prototype,e,{configurable:!0,get:function(){return this[`UNSAFE_`+e]},set:function(t){Object.defineProperty(this,e,{configurable:!0,writable:!0,value:t})}})});var wn=e.event;e.event=function(e){return wn&&(e=wn(e)),e.persist=function(){},e.isPropagationStopped=function(){return this.cancelBubble},e.isDefaultPrevented=function(){return this.defaultPrevented},e.nativeEvent=e};var Tn={configurable:!0,get:function(){return this.class}},En=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||Sn&&a==`children`&&n==`noscript`||a==`class`||a==`className`)){if(a==`style`&&typeof o==`object`){var s=void 0;for(var c in o)typeof o[c]!=`number`||_n.test(c)||(s||(s=o=gn({},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`||Cn(t.type)?l==`onfocus`?a=`onfocusin`:l==`onblur`&&(a=`onfocusout`):l=a=`oninput`,l==`oninput`&&r[a=l]&&(a=`oninputCapture`)}else i&&bn.test(a)?a=a.replace(xn,`-$&`).toLowerCase():o===null&&(o=void 0);r[a]=o}}n==`select`&&(r.multiple&&Array.isArray(r.value)&&(r.value=D(t.children).forEach(function(e){e.props.selected=r.value.indexOf(e.props.value)!=-1})),r.defaultValue!=null&&(r.value=D(t.children).forEach(function(e){e.props.selected=r.multiple?r.defaultValue.indexOf(e.props.value)!=-1:r.defaultValue==e.props.value}))),t.class&&!t.className?(r.class=t.class,Object.defineProperty(r,"className",Tn)):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=gn({},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=yn,En&&En(e)};var Dn=8,On=256,kn=8;function An(e){return e.findIndex(e=>!e.disabled)}function jn(e){for(let t=e.length-1;t>=0;--t)if(!e[t]?.disabled)return t;return-1}function Mn(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:An(e)}function Pn({ariaLabel:e,className:t=``,disabled:n=!1,id:r,onChange:i,options:a,value:o,variant:s=`default`}){let c=De(),l=r??`ssh-custom-select-${c}`,u=`${l}-menu`,d=we(null),f=we(null),p=we(null),m=we([]),[h,g]=F(!1),[_,v]=F(()=>Nn(a,o)),[y,b]=F(`bottom`),[x,S]=F(On),[C,w]=F({}),[T,E]=F(null),D=Te(()=>a.find(e=>e.value===o)??a[0]??null,[a,o]),O=()=>{let e=f.current;if(!e)return;let t=e.getBoundingClientRect(),n=Math.min(On,Math.max(44,a.length*42+12)),r=window.innerHeight-t.bottom-kn,i=t.top-kn,o=rr?`top`:`bottom`,s=Math.min(t.width,Math.max(0,window.innerWidth-16)),c=Math.max(kn,Math.min(t.left,window.innerWidth-s-kn)),l=o===`bottom`?r:i;b(o),S(Math.max(44,Math.min(On,Math.floor(l)))),w({left:`${c}px`,width:`${s}px`,...o===`bottom`?{top:`${t.bottom+Dn}px`}:{bottom:`${window.innerHeight-t.top+Dn}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??Nn(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(Nn(a,o)),m.current=m.current.slice(0,a.length)},[a,o]),I(()=>{if(!h)return;O();let e=e=>{let t=e.composedPath();d.current&&t.includes(d.current)||p.current&&t.includes(p.current)||k()},t=e=>{e.key===`Escape`&&(e.preventDefault(),k(!0))},n=()=>O();return document.addEventListener(`pointerdown`,e),document.addEventListener(`keydown`,t),window.addEventListener(`resize`,n),window.addEventListener(`scroll`,n,!0),()=>{document.removeEventListener(`pointerdown`,e),document.removeEventListener(`keydown`,t),window.removeEventListener(`resize`,n),window.removeEventListener(`scroll`,n,!0)}},[h,a.length]),I(()=>{n&&h&&k()},[n,h]);let ee=s===`emphasized`?`min-h-11 border-2 border-slate-300 bg-slate-50 px-4 py-2.5 text-sm font-semibold text-slate-950 shadow-inner focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100`:`min-h-11 border border-slate-300 bg-slate-50 px-3 py-2.5 text-sm font-bold text-slate-900 focus:border-teal-500 focus:ring-2 focus:ring-teal-100`,te=h?L(`div`,{"aria-label":`${e} 목록`,class:`ssh-custom-select-menu fixed z-[1000] overflow-hidden rounded-xl border border-slate-200 bg-white p-1.5 shadow-xl shadow-slate-950/10`,id:u,ref:p,role:`listbox`,style:C,children:L(`div`,{class:`overflow-y-auto overscroll-contain`,style:{maxHeight:`${x}px`},children:a.map((e,t)=>{let n=e.value===o;return L(`button`,{"aria-selected":n,class:`flex min-h-10 w-full items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-left text-sm font-bold transition ${n?`bg-teal-50 text-teal-800`:`text-slate-700 hover:bg-slate-50`} ${e.disabled?`cursor-not-allowed opacity-50`:``}`,disabled:e.disabled,onClick:()=>j(t),onFocus:()=>v(t),onKeyDown:e=>{switch(e.key){case`ArrowDown`:e.preventDefault(),A(Mn(a,t,1));break;case`ArrowUp`:e.preventDefault(),A(Mn(a,t,-1));break;case`Home`:e.preventDefault(),A(An(a));break;case`End`:e.preventDefault(),A(jn(a));break;case`Enter`:case` `:e.preventDefault(),j(t);break;case`Escape`:e.preventDefault(),k(!0);break;case`Tab`:g(!1)}},ref:e=>{m.current[t]=e},role:`option`,tabIndex:_===t?0:-1,type:`button`,children:[L(`span`,{class:`min-w-0 truncate`,children:e.label}),n?L(`span`,{"aria-hidden":`true`,class:`size-2 shrink-0 rounded-full bg-teal-500`}):null]},e.value)})})}):null;return L(`div`,{class:`relative ${t}`,ref:d,children:[L(`button`,{"aria-controls":u,"aria-expanded":h,"aria-haspopup":`listbox`,"aria-label":e,class:`ssh-custom-select-trigger flex w-full cursor-pointer items-center justify-between gap-3 rounded-xl text-left outline-none transition disabled:cursor-not-allowed disabled:bg-slate-100 disabled:text-slate-400 disabled:opacity-60 ${ee}`,disabled:n,id:l,onClick:()=>{h?k():(O(),g(!0))},onKeyDown:e=>{switch(e.key){case`ArrowDown`:e.preventDefault(),M(h?Mn(a,_,1):Nn(a,o));break;case`ArrowUp`:e.preventDefault(),M(h?Mn(a,_,-1):Nn(a,o));break;case`Home`:e.preventDefault(),M(An(a));break;case`End`:e.preventDefault(),M(jn(a));break;case`Enter`:case` `:e.preventDefault(),h?k():M();break;case`Escape`:h&&(e.preventDefault(),k())}},ref:f,type:`button`,children:[L(`span`,{class:`min-w-0 truncate`,children:D?.label??`선택하세요`}),L(_t,{"aria-hidden":`true`,class:`size-4 shrink-0 text-slate-400 transition-transform ${h?`rotate-180`:``}`})]}),te&&T?vn(te,T):null,L(`span`,{class:`sr-only`,"aria-live":`polite`,children:y===`top`?`선택 목록이 위쪽에 열렸습니다.`:``})]})}function Fn(e){let t=e.split(`.`);return[t[0]??``,t[1]??``,t[2]??``,t[3]??``]}function In(e){return e.replace(/\D/g,``).slice(0,3)}function Ln(e){return e.every(e=>e===``)?``:e.join(`.`)}function Rn({disabled:e=!1,label:t,value:n,onChange:r}){let i=Fn(n),a=(e,t)=>{let n=[...i];n[e]=In(t),r(Ln(n))};return L(`div`,{class:`ssh-ipv4-segments mt-2 min-h-11`,"aria-label":`${t} 입력`,children:i.map((n,r)=>L(`div`,{class:`ssh-ipv4-segment`,children:[L(`input`,{"aria-label":`${t} ${r+1}번째 옥텟`,autocomplete:`off`,class:`min-h-11 w-full min-w-0 max-w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-1.5 py-2.5 text-center text-sm font-semibold text-slate-950 shadow-inner outline-none transition focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:cursor-not-allowed disabled:bg-slate-100 sm:px-2`,disabled:e,inputMode:`numeric`,maxLength:3,onInput:e=>a(r,e.currentTarget.value),pattern:`[0-9]*`,spellcheck:!1,value:n}),rt);function Wn(e){return Vn.includes(e)}function Gn(e){switch(e){case`dhcp`:return`자동 IP (DHCP)`;case`pppoe`:return`PPPoE`;case`static`:return`고정 IPv4`;default:return e?e.toUpperCase():`확인 전`}}function Kn(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 qn(e){return e.trim()}function Jn(e,t){let n=Kn(t);return e.protocol===n.protocol?e.protocol===`dhcp`||!Wn(e.protocol)?!1:e.protocol===`pppoe`?qn(e.pppoeUsername)!==n.pppoeUsername||e.pppoePassword.length>0:qn(e.staticAddress)!==n.staticAddress||e.staticPrefixLength!==n.staticPrefixLength||qn(e.staticGateway)!==n.staticGateway||qn(e.dnsPrimary)!==n.dnsPrimary||qn(e.dnsSecondary)!==n.dnsSecondary:!0}function Yn(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 Xn(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 Zn(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 Qn({feedback:e,onDismiss:t}){return L(`div`,{"aria-live":`polite`,class:`mb-5 rounded-2xl border px-4 py-4 text-sm ${e.kind===`success`?`border-emerald-200 bg-emerald-50 text-emerald-900`:e.kind===`warning`?`border-amber-200 bg-amber-50 text-amber-950`:`border-red-200 bg-red-50 text-red-900`}`,children:L(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[L(`p`,{class:`m-0 font-bold leading-6`,children:e.message}),L(`button`,{class:`min-h-10 shrink-0 rounded-lg border border-current bg-transparent px-3 py-1.5 text-xs font-extrabold`,onClick:t,type:`button`,children:`닫기`})]})})}function $n({data:e,busy:t,onReconnect:n}){let r=e.status,i=r.connected;return L(`section`,{class:`grid gap-4 lg:grid-cols-3`,"aria-label":`인터넷 연결 상태`,children:[L(`article`,{class:`rounded-2xl border p-5 shadow-sm shadow-slate-900/5 ${i?`border-emerald-200 bg-emerald-50 text-emerald-950`:`border-amber-200 bg-amber-50 text-amber-950`}`,children:[L(`div`,{class:`flex items-start justify-between gap-3`,children:[L(`span`,{class:`grid size-10 place-items-center rounded-xl ${i?`bg-emerald-100 text-emerald-700`:`bg-amber-100 text-amber-700`}`,children:L(i?ut:B,{class:`size-5`})}),L(`button`,{class:`inline-flex min-h-10 items-center justify-center gap-2 rounded-xl border border-current bg-transparent px-3 py-2 text-xs font-extrabold transition disabled:cursor-wait disabled:opacity-60`,disabled:t,onClick:n,type:`button`,children:[L(z,{class:`size-4 ${t?`animate-spin`:``}`}),`재연결`]})]}),L(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] opacity-70`,children:`인터넷 상태`}),L(`p`,{class:`mt-2 mb-0 text-xl font-black`,children:r.pending?`연결 중`:i?`연결됨`:`연결 안 됨`}),L(`p`,{class:`mt-1 mb-0 text-sm font-semibold leading-6 opacity-80`,children:[Gn(r.protocol??e.configuration.protocol),` · `,Zn(r.uptimeSeconds)]})]}),L(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[L(`span`,{class:`grid size-10 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:L($e,{class:`size-5`})}),L(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`WAN IPv4`}),L(`p`,{class:`mt-2 mb-0 break-all text-xl font-black text-slate-950`,children:r.address?`${r.address}${r.prefixLength===null?``:`/${r.prefixLength}`}`:`주소 없음`}),L(`p`,{class:`mt-1 mb-0 text-sm font-semibold text-slate-600`,children:r.device?`인터페이스 ${r.device}`:`인터페이스 확인 전`})]}),L(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[L(`span`,{class:`grid size-10 place-items-center rounded-xl bg-slate-100 text-slate-700`,children:L(We,{class:`size-5`})}),L(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`Gateway · DNS`}),L(`p`,{class:`mt-2 mb-0 break-all text-base font-black text-slate-950`,children:r.gateway??`Gateway 확인 전`}),L(`p`,{class:`mt-1 mb-0 break-all text-sm font-semibold leading-6 text-slate-600`,children:r.dns.length>0?r.dns.join(` · `):`DNS 확인 전`})]})]})}function er({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(Kn(t)),g(null))},[t,d]);let _=Te(()=>{let e=[{value:`dhcp`,label:`자동 IP (DHCP)`},{value:`pppoe`,label:`PPPoE`},{value:`static`,label:`고정 IPv4`}];return t&&!t.configuration.supported&&e.unshift({value:t.configuration.protocol,label:`현재 설정 (${t.configuration.protocol})`}),e},[t]);if(i)return L(zn,{});if(n)return L(Bn,{message:n,onRetry:s});if(!t)return null;let y=e!==null,b=e=>{u(n=>{let r={...n,...e};return f(Jn(r,t)),r}),g(null)},x=async e=>{if(e.preventDefault(),!Wn(l.protocol))g(`DHCP, PPPoE 또는 고정 IPv4 중 하나를 선택해 주세요.`);else{if(l.protocol===`pppoe`){if(!qn(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(!Yn(l.staticAddress)){g(`고정 IPv4 주소를 올바르게 입력해 주세요.`);return}if(!Yn(l.staticGateway)){g(`기본 게이트웨이를 올바르게 입력해 주세요.`);return}if(!Yn(l.dnsPrimary)){g(`기본 DNS 서버를 올바르게 입력해 주세요.`);return}if(qn(l.dnsSecondary)&&!Yn(l.dnsSecondary)){g(`보조 DNS 서버를 올바르게 입력해 주세요.`);return}}window.confirm(`인터넷 연결 설정을 적용하면 WAN 연결이 다시 시작되어 인터넷이 잠시 끊길 수 있습니다. 계속하시겠습니까?`)&&await c({protocol:l.protocol,pppoeUsername:qn(l.pppoeUsername),pppoePassword:l.pppoePassword,pppoePasswordChanged:l.pppoePassword.length>0,staticAddress:qn(l.staticAddress),staticPrefixLength:l.staticPrefixLength,staticGateway:qn(l.staticGateway),dnsPrimary:qn(l.dnsPrimary),dnsSecondary:qn(l.dnsSecondary)})&&(u(e=>({...e,pppoePassword:``})),f(!1))}};return L(v,{children:[r?L(Qn,{feedback:r,onDismiss:a}):null,L(V,{eyebrow:`Overview`,title:`인터넷 연결 상태`,description:`WAN 연결 상태와 네트워크 정보를 확인합니다.`}),L($n,{data:t,busy:y,onReconnect:()=>{window.confirm(`WAN 연결을 다시 시작합니다. 인터넷이 잠시 끊길 수 있습니다. 계속하시겠습니까?`)&&o()}}),t.configuration.supported?null:L(`section`,{class:`mt-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-amber-950 sm:p-6`,children:L(`div`,{class:`flex items-start gap-3`,children:[L(B,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),L(`div`,{children:[L(`h2`,{class:`m-0 text-base font-black`,children:`현재 WAN 방식은 직접 관리하지 않습니다`}),L(`p`,{class:`mt-2 mb-0 text-sm font-semibold leading-6 text-amber-900/80`,children:[`현재 설정은 `,t.configuration.protocol,`입니다. 아래에서 DHCP, PPPoE 또는 고정 IPv4를 선택해 저장하면 해당 방식으로 전환합니다.`]})]})]})}),L(`div`,{class:`mt-7`,children:L(V,{eyebrow:`Internet`,title:`인터넷 연결 설정`,description:`통신사 또는 상위 네트워크 환경에 맞는 WAN 연결 방식을 선택합니다.`})}),L(`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?L(`div`,{class:`mb-5 flex justify-end`,children:L(`span`,{"aria-live":`polite`,class:`inline-flex shrink-0 self-start items-center gap-1.5 rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs font-extrabold text-amber-800`,role:`status`,children:[L(B,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]})}):null,L(`div`,{children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`연결 방식`}),L(Pn,{ariaLabel:`WAN 연결 방식`,className:`mt-2 max-w-xl`,disabled:y,id:`wan-protocol`,onChange:e=>b(e===`static`&&!qn(l.dnsSecondary)?{protocol:e,dnsSecondary:Hn}:{protocol:e}),options:_,value:l.protocol,variant:`emphasized`}),L(`span`,{class:`mt-2 block text-xs font-semibold leading-5 text-slate-500`,children:`대부분의 가정용 인터넷과 상위 공유기 연결은 자동 IP(DHCP)를 사용합니다.`})]}),l.protocol===`pppoe`?L(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[L(`label`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`PPPoE 사용자명`}),L(`input`,{autocomplete:`username`,class:`mt-2 min-h-11 w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-4 py-2.5 text-sm font-semibold text-slate-950 shadow-inner outline-none transition focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:cursor-not-allowed disabled:bg-slate-100`,disabled:y,onInput:e=>b({pppoeUsername:e.currentTarget.value}),spellcheck:!1,value:l.pppoeUsername})]}),L(`label`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`PPPoE 비밀번호`}),L(`span`,{class:`relative mt-2 block`,children:[L(`input`,{autocomplete:`new-password`,class:`min-h-11 w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-4 py-2.5 pr-12 text-sm font-semibold text-slate-950 shadow-inner outline-none transition focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:cursor-not-allowed disabled:bg-slate-100`,disabled:y,onInput:e=>b({pppoePassword:e.currentTarget.value}),placeholder:t.configuration.protocol===`pppoe`&&t.configuration.pppoe.passwordConfigured?`변경할 때만 입력`:`PPPoE 비밀번호`,...p?{type:`text`}:{type:`password`},value:l.pppoePassword}),L(`button`,{"aria-label":p?`비밀번호 숨기기`:`비밀번호 표시`,class:`absolute inset-y-0 right-1 grid w-10 place-items-center rounded-lg border-0 bg-transparent text-slate-500`,disabled:y,onClick:()=>m(e=>!e),type:`button`,children:L(p?ct:st,{class:`size-5`})})]}),L(`span`,{class:`mt-2 block text-xs text-slate-500`,children:t.configuration.protocol===`pppoe`&&t.configuration.pppoe.passwordConfigured?`비워 두면 현재 저장된 비밀번호를 그대로 사용합니다.`:`통신사에서 제공한 PPPoE 비밀번호를 입력해 주세요.`})]})]}):null,l.protocol===`static`?L(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[L(`label`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`IPv4 주소`}),L(Rn,{disabled:y,label:`IPv4 주소`,onChange:e=>b({staticAddress:e}),value:l.staticAddress})]}),L(`div`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`서브넷 마스크`}),L(Pn,{ariaLabel:`WAN 서브넷 마스크`,className:`mt-2`,disabled:y,id:`wan-static-prefix-length`,onChange:e=>b({staticPrefixLength:Number(e)}),options:Un.map(e=>({value:String(e),label:`/${e} · ${Xn(e)}`})),value:String(l.staticPrefixLength),variant:`emphasized`})]}),L(`label`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`기본 게이트웨이`}),L(Rn,{disabled:y,label:`기본 게이트웨이`,onChange:e=>b({staticGateway:e}),value:l.staticGateway})]}),L(`label`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`기본 DNS`}),L(Rn,{disabled:y,label:`기본 DNS`,onChange:e=>b({dnsPrimary:e}),value:l.dnsPrimary})]}),L(`label`,{class:`block lg:col-start-2`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`보조 DNS (선택)`}),L(Rn,{disabled:y,label:`보조 DNS`,onChange:e=>b({dnsSecondary:e}),value:l.dnsSecondary})]})]}):null,l.protocol===`dhcp`?L(`div`,{class:`mt-6 rounded-xl border border-slate-200 bg-slate-50 px-4 py-4 text-sm font-semibold leading-6 text-slate-600`,children:`상위 공유기 또는 통신사 장비에서 IP 주소, 기본 게이트웨이와 DNS 정보를 자동으로 받습니다. 별도의 계정이나 고정 주소가 없다면 이 방식을 사용하세요.`}):null,h?L(`p`,{class:`mt-5 mb-0 rounded-xl bg-red-50 px-4 py-3 text-sm font-bold text-red-800`,children:h}):null,L(`div`,{class:`mt-6 flex flex-col gap-3 border-t border-slate-200 pt-5 sm:flex-row sm:items-center sm:justify-between`,children:[L(`p`,{class:`m-0 text-xs font-semibold leading-5 text-slate-500`,children:`설정 저장 후 WAN만 다시 연결하며 LAN 관리 페이지와 내부 네트워크 설정은 유지됩니다.`}),L(`button`,{class:`inline-flex min-h-11 w-full shrink-0 items-center justify-center gap-2 rounded-xl border-0 bg-teal-700 px-5 py-2.5 text-sm font-extrabold text-white shadow-sm transition hover:bg-teal-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-200 disabled:cursor-wait disabled:opacity-60 sm:w-auto`,disabled:y||!d,type:`submit`,children:[L(ut,{class:`size-5 ${e===`saving`?`animate-pulse`:``}`}),e===`saving`?`저장 중`:`설정 저장`]})]})]})]})}var tr=[16,20,22,23,24,25,26,27,28],nr=[[`30m`,`30분`],[`1h`,`1시간`],[`6h`,`6시간`],[`12h`,`12시간`],[`24h`,`24시간`],[`3d`,`3일`],[`7d`,`7일`]];function rr(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 ir(e){let t=Fn(e);return t.slice(0,3).some(e=>e===``||Number(e)>255)?null:t.slice(0,3).join(`.`)}function ar(e){return Fn(e)[3]}function or(e){let t=ir(e.lan.address),n=ar(e.dhcp.start??``),r=ar(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 sr({disabled:e,label:t,lanAddress:n,value:r,onChange:i}){let a=ir(n),o=a?.split(`.`)??[``,``,``],s=ar(r),c=e=>{let t=In(e);i(a&&t!==``?`${a}.${t}`:t)};return L(`label`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:t}),L(`div`,{class:`ssh-ipv4-segments mt-2 min-h-11`,"aria-label":`${t} 입력`,children:[o.map((e,t)=>L(`div`,{class:`ssh-ipv4-segment`,children:[L(`span`,{class:`grid min-h-11 w-full min-w-0 max-w-full place-items-center rounded-xl border border-slate-200 bg-slate-100 px-1.5 py-2.5 text-center text-sm font-bold text-slate-500 sm:px-2`,children:e||`—`}),L(`span`,{"aria-hidden":`true`,class:`ssh-ipv4-separator text-sm font-black text-slate-400`,children:`.`})]},t)),L(`div`,{class:`ssh-ipv4-segment`,children:L(`input`,{"aria-label":`${t} 마지막 옥텟`,autocomplete:`off`,class:`min-h-11 w-full min-w-0 max-w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-1.5 py-2.5 text-center text-sm font-semibold text-slate-950 shadow-inner outline-none transition focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:cursor-not-allowed disabled:bg-slate-100 sm:px-2`,disabled:e||!a,inputMode:`numeric`,maxLength:3,onInput:e=>c(e.currentTarget.value),pattern:`[0-9]*`,spellcheck:!1,value:s})})]}),L(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`앞 3개 주소는 공유기 IP 주소와 동일하게 유지됩니다.`})]})}function cr(e){return`${window.location.protocol}//${e}${window.location.pathname}${window.location.search}${window.location.hash}`}function lr({feedback:e,onDismiss:t}){return L(`div`,{"aria-live":`polite`,class:`mb-5 rounded-2xl border px-4 py-4 text-sm ${e.kind===`success`?`border-emerald-200 bg-emerald-50 text-emerald-900`:e.kind===`warning`?`border-amber-200 bg-amber-50 text-amber-950`:`border-red-200 bg-red-50 text-red-900`}`,children:[L(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[L(`p`,{class:`m-0 font-bold leading-6`,children:e.message}),L(`button`,{class:`min-h-10 shrink-0 rounded-lg border border-current bg-transparent px-3 py-1.5 text-xs font-extrabold`,onClick:t,type:`button`,children:`닫기`})]}),e.newAddress?L(`a`,{class:`mt-3 inline-flex min-h-10 items-center rounded-lg border border-current px-3 py-2 font-extrabold underline underline-offset-2`,href:cr(e.newAddress),children:[`새 공유기 주소 `,e.newAddress,`로 연결`]}):null]})}function ur({data:e}){let t=e.conflict.detected;return L(`section`,{class:`grid gap-4 lg:grid-cols-3`,"aria-label":`내부 네트워크 상태`,children:[L(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[L(`span`,{class:`grid size-10 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:L(We,{class:`size-5`})}),L(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`SmartSafeHub LAN`}),L(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:e.lan.subnet}),L(`p`,{class:`mt-1 mb-0 text-sm font-semibold text-slate-600`,children:[`공유기 주소 `,e.lan.address]})]}),L(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[L(`span`,{class:`grid size-10 place-items-center rounded-xl bg-slate-100 text-slate-700`,children:L($e,{class:`size-5`})}),L(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`상위 네트워크`}),L(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:e.wan.subnet??`확인 전`}),L(`p`,{class:`mt-1 mb-0 text-sm font-semibold text-slate-600`,children:e.wan.connected?`WAN ${e.wan.address??`주소 확인 중`}`:`WAN 연결 후 자동으로 확인합니다.`})]}),L(`article`,{class:`rounded-2xl border p-5 shadow-sm shadow-slate-900/5 ${t?`border-red-200 bg-red-50 text-red-950`:`border-emerald-200 bg-emerald-50 text-emerald-950`}`,children:[L(`span`,{class:`grid size-10 place-items-center rounded-xl ${t?`bg-red-100 text-red-700`:`bg-emerald-100 text-emerald-700`}`,children:L(t?B:ut,{class:`size-5`})}),L(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] opacity-70`,children:`대역 충돌`}),L(`p`,{class:`mt-2 mb-0 text-lg font-black`,children:t?`주소 대역이 겹칩니다`:`충돌이 감지되지 않았습니다`}),L(`p`,{class:`mt-1 mb-0 text-sm font-semibold leading-6 opacity-80`,children:t?`상위 네트워크 ${e.conflict.wanSubnet??``}와 다른 LAN 대역이 필요합니다.`:e.wan.connected?`현재 WAN과 LAN이 서로 다른 IPv4 대역을 사용합니다.`:`WAN이 연결되면 상위 네트워크와의 충돌 여부를 확인합니다.`})]})]})}function dr({action:e,data:t,error:n,feedback:r,loading:i,onApplyRecommendation:a,onDismissFeedback:o,onRetry:s,onSave:c}){let[l,u]=F(``),[d,f]=F(24),[p,m]=F(!0),[h,g]=F(``),[_,y]=F(``),[b,x]=F(`12h`),[S,C]=F(!1),[w,T]=F(null),E=(e={})=>{if(!t)return!1;let n=or(t);return(e.ipAddress??l).trim()!==n.ipAddress||(e.prefixLength??d)!==n.prefixLength||(e.dhcpEnabled??p)!==n.dhcpEnabled||(e.dhcpStart??h).trim()!==n.dhcpStart||(e.dhcpEnd??_).trim()!==n.dhcpEnd||(e.leaseTime??b)!==n.leaseTime},D=e=>{let t=ir(e);if(u(e),!t){C(E({ipAddress:e}));return}let n=ar(h),r=ar(_),i=n?`${t}.${n}`:``,a=r?`${t}.${r}`:``;g(i),y(a),C(E({ipAddress:e,dhcpStart:i,dhcpEnd:a}))},O=e=>{g(e),C(E({dhcpStart:e}))},k=e=>{y(e),C(E({dhcpEnd:e}))},A=e=>{f(e),C(E({prefixLength:e}))},j=e=>{x(e),C(E({leaseTime:e}))},M=e=>{m(e),C(E({dhcpEnabled:e}))};I(()=>{if(!t||S)return;let e=or(t);u(e.ipAddress),f(e.prefixLength),m(e.dhcpEnabled),g(e.dhcpStart),y(e.dhcpEnd),x(e.leaseTime),T(null)},[t,S]);let ee=Te(()=>{let e=[...tr];return e.includes(d)||(e.push(d),e.sort((e,t)=>e-t)),e},[d]),te=Te(()=>{let e=[...nr];return e.some(([e])=>e===b)||e.push([b,`현재 설정 (${b})`]),e},[b]);if(i)return L(zn,{});if(n)return L(Bn,{message:n,onRetry:s});if(!t)return null;let ne=e!==null,re=async e=>{e.preventDefault(),rr(l)?!rr(h)||!rr(_)?T(`DHCP 시작/종료 주소를 올바른 IPv4 형식으로 입력해 주세요.`):window.confirm(`내부 네트워크 설정을 적용하면 연결된 기기가 새 DHCP 정보를 받는 동안 잠시 연결이 끊길 수 있습니다. 계속하시겠습니까?`)&&(T(null),await c({ipAddress:l.trim(),prefixLength:d,dhcpEnabled:p,dhcpStart:h.trim(),dhcpEnd:_.trim(),leaseTime:b})&&C(!1)):T(`공유기 주소를 올바른 IPv4 형식으로 입력해 주세요.`)},ie=async()=>{t.recommendation&&window.confirm(`상위 네트워크와 겹치지 않는 ${t.recommendation.subnet} 대역으로 자동 변경합니다. 현재 연결이 잠시 끊길 수 있습니다. 계속하시겠습니까?`)&&(T(null),await a()&&C(!1))};return L(v,{children:[r?L(lr,{feedback:r,onDismiss:o}):null,L(V,{eyebrow:`Overview`,title:`내부 네트워크 상태`,description:`LAN 주소와 DHCP 할당 상태를 확인합니다.`}),L(ur,{data:t}),t.conflict.detected?L(`section`,{class:`mt-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-amber-950 sm:p-6`,children:L(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between`,children:[L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-amber-700`,children:`자동 충돌 해결`}),L(`h2`,{class:`mt-2 mb-0 text-lg font-black`,children:`안전한 LAN 대역으로 변경할 수 있습니다`}),L(`p`,{class:`mt-2 mb-0 text-sm font-semibold leading-6 text-amber-900/80`,children:t.recommendation?`${t.recommendation.subnet} · DHCP ${t.recommendation.dhcpStart} ~ ${t.recommendation.dhcpEnd}`:`현재 활성 네트워크와 겹치지 않는 추천 대역을 찾지 못했습니다. 아래에서 직접 설정해 주세요.`})]}),t.recommendation?L(`button`,{class:`inline-flex min-h-11 shrink-0 items-center justify-center gap-2 rounded-xl border-0 bg-amber-700 px-5 py-2.5 text-sm font-extrabold text-white transition hover:bg-amber-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-amber-200 disabled:cursor-wait disabled:opacity-60`,disabled:ne,onClick:()=>void ie(),type:`button`,children:[L(ut,{class:`size-5 ${e===`auto`?`animate-pulse`:``}`}),e===`auto`?`변경 중`:`추천 대역으로 자동 변경`]}):null]})}):null,L(`div`,{class:`mt-7`,children:L(V,{eyebrow:`Local network`,title:`LAN 및 DHCP 설정`,description:`상위 공유기와 다른 주소 대역을 사용해야 안정적으로 라우팅할 수 있습니다.`})}),L(`form`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,onSubmit:e=>void re(e),children:[S?L(`div`,{class:`mb-5 flex justify-end`,children:L(`span`,{"aria-live":`polite`,class:`inline-flex shrink-0 self-start items-center gap-1.5 rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs font-extrabold text-amber-800`,role:`status`,children:[L(B,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]})}):null,L(`div`,{class:`grid gap-5 lg:grid-cols-2`,children:[L(`div`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`공유기 IP 주소`}),L(Rn,{disabled:ne,label:`공유기 IP 주소`,onChange:D,value:l})]}),L(`div`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`현재 LAN 대역`}),L(`div`,{class:`mt-2 min-h-11 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm font-bold text-slate-700`,children:t.lan.subnet}),L(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`서브넷 마스크는 아래 고급 설정에서 변경할 수 있습니다.`})]}),L(sr,{disabled:ne,label:`DHCP 시작 주소`,lanAddress:l,onChange:O,value:h}),L(sr,{disabled:ne,label:`DHCP 종료 주소`,lanAddress:l,onChange:k,value:_})]}),L(`details`,{class:`mt-6 rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[L(`summary`,{class:`cursor-pointer text-sm font-black text-slate-900`,children:`고급 DHCP 설정`}),L(`div`,{class:`mt-5 grid gap-5 lg:grid-cols-2`,children:[L(`div`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`서브넷 마스크`}),L(Pn,{ariaLabel:`서브넷 마스크`,className:`mt-2`,disabled:ne,id:`lan-prefix-length`,onChange:e=>A(Number(e)),options:ee.map(e=>({value:String(e),label:`/${e} · ${e===t.lan.prefixLength?t.lan.netmask:`CIDR /${e}`}`})),value:String(d),variant:`emphasized`}),L(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`일반 가정용 네트워크는 /24 사용을 권장합니다.`})]}),L(`div`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`DHCP 임대 시간`}),L(Pn,{ariaLabel:`DHCP 임대 시간`,className:`mt-2`,disabled:ne,id:`lan-dhcp-lease-time`,onChange:j,options:te.map(([e,t])=>({value:e,label:t})),value:b,variant:`emphasized`})]}),L(`label`,{class:`inline-flex min-h-11 items-center gap-3 text-sm font-extrabold text-slate-800 lg:col-span-2`,children:[L(`input`,{checked:p,class:`size-5 accent-teal-700`,disabled:ne,onChange:e=>M(e.currentTarget.checked),type:`checkbox`}),`SmartSafeHub DHCP 서버 사용`]}),L(`p`,{class:`m-0 text-xs font-semibold leading-5 text-slate-500 lg:col-span-2`,children:`DHCP를 끄면 SmartSafeHub에 연결되는 장치에 IP 주소를 자동으로 할당하지 않습니다. 별도의 DHCP 서버를 사용하는 고급 구성에서만 비활성화하세요.`})]})]}),w?L(`p`,{class:`mt-5 mb-0 rounded-xl bg-red-50 px-4 py-3 text-sm font-bold text-red-800`,children:w}):null,L(`div`,{class:`mt-6 flex flex-col gap-3 border-t border-slate-200 pt-5 sm:flex-row sm:items-center sm:justify-between`,children:[L(`p`,{class:`m-0 text-xs font-semibold leading-5 text-slate-500`,children:`주소가 바뀌면 현재 관리 페이지 연결이 종료되며 새 IP를 받아 다시 접속해야 합니다.`}),L(`button`,{class:`inline-flex min-h-11 w-full shrink-0 items-center justify-center gap-2 rounded-xl border-0 bg-teal-700 px-5 py-2.5 text-sm font-extrabold text-white shadow-sm transition hover:bg-teal-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-200 disabled:cursor-wait disabled:opacity-60 sm:w-auto`,disabled:ne||!S,type:`submit`,children:[L(ut,{class:`size-5 ${e===`saving`?`animate-pulse`:``}`}),e===`saving`?`저장 중`:`설정 저장`]})]})]})]})}var fr={};fr.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},fr.QRErrorCorrectLevel=(e,t)=>{e.exports={L:1,M:0,Q:3,H:2}},fr.QRMaskPattern=(e,t)=>{e.exports={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7}},fr.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}},fr.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(mr(`index`))(0,mr(`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 gr(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 _r(e){return`${(e.replace(/[\\/:*?"<>|\x00-\x1f\x7f]/g,`_`).replace(/[. ]+$/g,``).trim()||`Wi-Fi`).slice(0,64)}-QR.png`}function vr(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 yr({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:hr(gr(e.ssid,e.security,e.password))})}catch{a(`QR 코드를 생성할 수 없습니다.`)}}).catch(()=>{t&&a(`Wi-Fi 정보를 불러오지 못했습니다.`)}),()=>{t=!1,r(null)}},[e]);let c=n?.matrix.length??0;return L(`div`,{class:`fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70 p-4`,role:`presentation`,onClick:e=>{e.target===e.currentTarget&&t()},children:L(`section`,{role:`dialog`,"aria-modal":`true`,"aria-label":`Wi-Fi QR 코드`,class:`w-full max-w-sm rounded-2xl bg-white p-5 text-center text-slate-950 shadow-2xl sm:p-7`,children:[L(`h2`,{class:`m-0 text-xl font-extrabold`,children:`Wi-Fi QR 코드`}),L(`p`,{class:`mt-2 break-all text-sm font-semibold text-slate-600`,children:n?.ssid??`Wi-Fi 정보를 확인하는 중`}),i?L(`p`,{role:`alert`,class:`my-6 text-sm font-bold text-red-700`,children:i}):null,!n&&!i?L(`p`,{class:`my-6 text-sm`,children:`QR 코드를 만드는 중...`}):null,n?L(`div`,{class:`mx-auto my-5 w-full max-w-[288px] rounded-xl border border-slate-200 bg-white p-3`,children:L(`svg`,{class:`block h-auto w-full`,viewBox:`0 0 ${c+8} ${c+8}`,role:`img`,"aria-label":`${n.ssid} Wi-Fi 연결 QR 코드`,"shape-rendering":`crispEdges`,children:[L(`rect`,{width:c+8,height:c+8,fill:`white`}),n.matrix.map((e,t)=>e.map((e,n)=>e?L(`rect`,{x:n+4,y:t+4,width:`1`,height:`1`,fill:`black`},`${t}-${n}`):null))]})}):null,L(`p`,{class:`text-xs leading-5 text-slate-500`,children:[L(`span`,{class:`block`,children:`카메라로 스캔해 Wi-Fi에 연결하세요.`}),L(`span`,{class:`block`,children:`QR 코드에는 비밀번호가 포함됩니다.`})]}),o?L(`p`,{role:`alert`,class:`mt-3 text-sm font-bold text-red-700`,children:o}):null,n?L(`button`,{class:`mt-5 inline-flex min-h-11 w-full items-center justify-center gap-2 rounded-xl bg-teal-700 px-4 font-extrabold text-white transition hover:bg-teal-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-200`,onClick:()=>{if(n){s(null);try{let e=(n.matrix.length+8)*8,t=document.createElement(`canvas`);t.width=e,t.height=e;let r=t.getContext(`2d`);if(!r)throw Error(`Canvas is not supported`);vr(n.matrix,r,8);let i=document.createElement(`a`);i.href=t.toDataURL(`image/png`),i.download=_r(n.ssid),i.click()}catch{s(`QR 코드 이미지를 저장하지 못했습니다. 다시 시도해 주세요.`)}}},type:`button`,children:[L(it,{class:`size-5`}),`QR 코드 다운로드`]}):null,L(`button`,{class:`mt-2 min-h-11 w-full rounded-xl border border-slate-300 bg-white px-4 font-extrabold text-slate-700 transition hover:bg-slate-50`,onClick:t,type:`button`,children:`닫기`})]})})}function br({guest:e,busy:t,saving:n,onUpdate:r,onLoadQr:i}){let[a,o]=F(!1),[s,c]=F(!1),[l,u]=F(e.ssid),[d,f]=F(``),[p,m]=F(e.enabled),[h,g]=F(null);if(I(()=>{u(e.ssid),m(e.enabled),f(``),g(null)},[e]),!e.supported)return L(`section`,{class:`mt-5 rounded-2xl border border-slate-200 bg-white p-5 sm:p-6`,children:[L(`h2`,{class:`m-0 text-lg font-extrabold text-slate-900`,children:`게스트 Wi-Fi`}),L(`p`,{class:`mt-2 mb-0 text-sm text-slate-600`,children:`2.4GHz 기본 Wi-Fi 장치를 찾지 못해 게스트 Wi-Fi를 사용할 수 없습니다.`})]});let _=l.trim()!==e.ssid||p!==e.enabled||d.length>0,y=t||_||!e.enabled||!e.passwordConfigured,b=async t=>{t.preventDefault();let n=l.trim(),i=new TextEncoder().encode(n).length;i<1||i>32?g(`Wi-Fi 이름은 UTF-8 기준 1~32바이트로 입력해 주세요.`):d&&!(d.length>=8&&d.length<=63||/^[0-9a-fA-F]{64}$/.test(d))?g(`비밀번호는 8~63자 또는 64자리 16진수여야 합니다.`):p&&!e.passwordConfigured&&!d?g(`게스트 전용 비밀번호를 입력해 주세요.`):window.confirm(`게스트 Wi-Fi 설정을 적용하면 무선 연결이 잠시 끊어질 수 있습니다. 계속하시겠습니까?`)&&(g(null),await r({ssid:n,enabled:p,password:d})&&f(``))};return L(v,{children:[L(`form`,{"aria-label":`게스트 Wi-Fi 설정`,class:`mt-5 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-6`,onSubmit:e=>void b(e),children:[L(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[L(`button`,{"aria-controls":`ssh-guest-settings`,"aria-expanded":a,"aria-label":`게스트 Wi-Fi 설정 ${a?`접기`:`펼치기`}`,class:`flex min-w-0 flex-1 items-start gap-3 rounded-xl text-left focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100`,onClick:()=>o(e=>!e),type:`button`,children:[L(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:L(We,{class:`size-6`})}),L(`span`,{class:`min-w-0 flex-1`,children:[L(`span`,{class:`block text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`2.4 GHz · 분리 네트워크`}),L(`span`,{class:`mt-2 block text-xl font-extrabold tracking-tight text-slate-950`,children:`게스트 Wi-Fi`}),L(`span`,{class:`mt-2 block break-all text-sm text-slate-600`,children:e.ssid})]}),L(_t,{class:`mt-3 size-5 shrink-0 text-slate-500 transition-transform ${a?`rotate-180`:``}`})]}),L(`div`,{class:`flex flex-wrap items-center gap-2 sm:justify-end`,children:[_&&L(`span`,{"aria-live":`polite`,class:`inline-flex items-center gap-1 rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs font-extrabold text-amber-800`,children:[L(B,{class:`size-3.5`}),`저장되지 않음`]}),L(`button`,{"aria-label":`게스트 Wi-Fi QR 코드 보기`,class:`inline-flex min-h-10 items-center gap-2 rounded-xl border border-slate-300 bg-white px-3.5 py-2 text-sm font-extrabold text-slate-800 transition hover:border-teal-300 hover:text-teal-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-not-allowed disabled:opacity-50`,disabled:y,onClick:()=>c(!0),title:e.enabled?void 0:`게스트 Wi-Fi를 켜고 저장하면 QR 코드를 사용할 수 있습니다.`,type:`button`,children:[L(Ze,{class:`size-4 shrink-0`}),`QR 코드 보기`]}),L(`span`,{class:`rounded-full px-3 py-1.5 text-xs font-extrabold ${e.enabled?`bg-emerald-50 text-emerald-700`:`bg-slate-100 text-slate-600`}`,children:e.enabled?`사용 중`:`꺼짐`})]})]}),L(`div`,{class:a?``:`hidden`,id:`ssh-guest-settings`,children:[L(`p`,{class:`mt-5 mb-0 text-sm leading-6 text-slate-600`,children:`방문자는 인터넷만 이용하며 내부 네트워크와 공유기 관리 화면에는 접근할 수 없습니다.`}),L(`div`,{class:`mt-5 grid gap-5 lg:grid-cols-2`,children:[L(`label`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`게스트 Wi-Fi 이름 (SSID)`}),L(`input`,{autocomplete:`off`,class:`mt-2 min-h-11 w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-4 py-2.5 text-sm font-semibold text-slate-950 outline-none focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:opacity-60`,disabled:t,maxLength:32,onInput:e=>u(e.currentTarget.value),value:l})]}),L(`label`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`게스트 Wi-Fi 비밀번호`}),L(`input`,{autocomplete:`new-password`,class:`mt-2 min-h-11 w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-4 py-2.5 text-sm font-semibold text-slate-950 outline-none focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:opacity-60`,disabled:t,onInput:e=>f(e.currentTarget.value),placeholder:e.passwordConfigured?`비워 두면 기존 비밀번호 유지`:`8자 이상의 별도 비밀번호 입력`,type:`password`,value:d}),L(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`WPA2/WPA3 혼합 · 기본 Wi-Fi와 다른 비밀번호를 권장합니다.`})]})]}),L(`div`,{class:`mt-5 rounded-xl bg-slate-50 px-4 py-3 text-sm leading-6 text-slate-700`,children:[`내부망 접근 차단 · 게스트 기기 간 통신 차단 · SafeShield DNS 사용`,e.subnet&&L(`span`,{class:`block text-xs text-slate-500`,children:[`게스트 게이트웨이: `,e.subnet]})]}),L(`div`,{class:`mt-5 flex flex-col gap-4 border-t border-slate-200 pt-5 sm:flex-row sm:items-center sm:justify-between`,children:[L(`label`,{class:`inline-flex min-h-11 items-center gap-3 text-sm font-extrabold text-slate-800`,children:[L(`input`,{checked:p,class:`size-5 accent-teal-700`,disabled:t,onChange:e=>m(e.currentTarget.checked),type:`checkbox`}),`게스트 Wi-Fi 사용`]}),L(`button`,{class:`inline-flex min-h-11 w-full items-center justify-center gap-2 rounded-xl bg-teal-700 px-5 py-2.5 text-sm font-extrabold text-white transition hover:bg-teal-800 disabled:opacity-60 sm:w-auto`,disabled:t||!_,type:`submit`,children:[L(ut,{class:`size-5 ${n?`animate-pulse`:``}`}),n?`적용 중`:`설정 저장`]})]}),h&&L(`p`,{class:`mt-4 mb-0 rounded-xl bg-red-50 px-4 py-3 text-sm font-bold text-red-800`,children:h})]})]}),s&&L(yr,{load:i,onClose:()=>c(!1)})]})}var xr=[{value:`sae-mixed`,label:`WPA2/WPA3 혼합 (권장)`},{value:`sae`,label:`WPA3-Personal`},{value:`psk2`,label:`WPA2-Personal`},{value:`none`,label:`암호화 없음`}];function Sr(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 Cr(e){return e.length>=8&&e.length<=63||/^[0-9a-fA-F]{64}$/.test(e)}function wr({network:e,busy:t,saving:n,onUpdate:r,onLoadQr:i}){let[a,o]=F(e.ssid),[s,c]=F(e.enabled),[l,u]=F(e.security===`custom`?`keep`:e.security),[d,f]=F(``),[p,m]=F(!1),h=Ee(()=>i(e.section),[i,e.section]),[g,_]=F(null);I(()=>{o(e.ssid),c(e.enabled),u(e.security===`custom`?`keep`:e.security),f(``),_(null)},[e]);let v=e.security===`custom`&&l===`keep`,y=l===`psk2`||l===`sae-mixed`||l===`sae`,b=e.security===`custom`?`keep`:e.security,x=a.trim()!==e.ssid||s!==e.enabled||l!==b||d.length>0,S=t||x||e.security===`custom`||!e.enabled||e.security!==`none`&&!e.passwordConfigured,C=[...e.security===`custom`?[{value:`keep`,label:`현재 고급 설정 유지 (${e.securityRaw})`}]:[],...xr],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&&!Cr(d)?_(`비밀번호는 8~63자 또는 64자리 16진수여야 합니다.`):y&&!e.passwordConfigured&&d.length===0?_(`보안 Wi-Fi를 사용하려면 비밀번호를 입력해 주세요.`):window.confirm(`Wi-Fi 설정을 적용하면 현재 무선 연결이 잠시 끊어질 수 있습니다. 계속하시겠습니까?`)&&(_(null),await r({section:e.section,ssid:n,security:l,password:d,enabled:s})&&f(``))};return L(`form`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-6`,onSubmit:e=>void w(e),children:[L(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3`,children:[L(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:L(We,{class:`size-6`})}),L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:e.bandLabel}),L(`h2`,{class:`mt-2 mb-0 break-words text-xl font-extrabold tracking-tight text-slate-950`,children:e.ssid||`이름 없는 Wi-Fi`}),L(`p`,{class:`mt-2 mb-0 text-sm text-slate-600`,children:[`채널 `,e.channel??`자동`,` · 연결 기기 `,e.clientCount,`대`]})]})]}),L(`div`,{class:`flex flex-wrap items-center gap-2 sm:justify-end`,children:[x?L(`span`,{"aria-live":`polite`,class:`inline-flex shrink-0 items-center gap-1.5 rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs font-extrabold text-amber-800`,role:`status`,children:[L(B,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}):null,L(`button`,{"aria-label":`${e.bandLabel} Wi-Fi QR 코드 보기`,class:`inline-flex min-h-10 items-center gap-2 rounded-xl border border-slate-300 bg-white px-3.5 py-2 text-sm font-extrabold text-slate-800 shadow-sm transition hover:border-teal-300 hover:text-teal-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-not-allowed disabled:opacity-50`,disabled:S,onClick:()=>m(!0),type:`button`,children:[L(Ze,{class:`size-4 shrink-0`}),`QR 코드 보기`]}),L(`span`,{class:`inline-flex w-fit items-center gap-2 rounded-full px-3 py-1.5 text-xs font-extrabold ring-1 ring-inset ${e.runtimeUp?`bg-emerald-50 text-emerald-700 ring-emerald-200`:`bg-slate-100 text-slate-600 ring-slate-200`}`,children:[L(`span`,{class:`size-2 rounded-full ${e.runtimeUp?`bg-emerald-500`:`bg-slate-400`}`}),e.runtimeUp?`동작 중`:e.enabled?`시작 대기`:`꺼짐`]})]})]}),L(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[L(`label`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`Wi-Fi 이름 (SSID)`}),L(`input`,{autocomplete:`off`,class:`mt-2 min-h-11 w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-4 py-2.5 text-sm font-semibold text-slate-950 shadow-inner outline-none transition placeholder:text-slate-400 focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:cursor-not-allowed disabled:bg-slate-100`,disabled:t,maxLength:32,onInput:e=>o(e.currentTarget.value),value:a})]}),L(`div`,{class:`block`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`보안 방식`}),L(Pn,{ariaLabel:`${e.bandLabel} Wi-Fi 보안 방식`,className:`mt-2`,disabled:t,id:`wifi-security-${e.section}`,onChange:e=>{let t=e;u(t),(t===`none`||t===`keep`)&&f(``)},options:C,value:l,variant:`emphasized`}),L(`span`,{class:`mt-2 block text-xs text-slate-500`,children:[`현재: `,Sr(e)]})]}),L(`label`,{class:`block lg:col-span-2`,children:[L(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`새 비밀번호`}),L(`input`,{autocomplete:`new-password`,class:`mt-2 min-h-11 w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-4 py-2.5 text-sm font-semibold text-slate-950 shadow-inner outline-none transition placeholder:text-slate-400 focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:cursor-not-allowed disabled:bg-slate-100`,disabled:t||l===`none`||v,onInput:e=>f(e.currentTarget.value),placeholder:v?`고급 보안 설정은 기존 LuCI에서 변경합니다`:e.passwordConfigured?`비워 두면 기존 비밀번호를 유지합니다`:`8자 이상의 비밀번호를 입력하세요`,type:`password`,value:d}),L(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`저장된 비밀번호는 일반 설정 조회에 노출되지 않습니다. QR 코드에는 비밀번호가 포함됩니다.`})]})]}),L(`div`,{class:`mt-6 flex flex-col gap-4 border-t border-slate-200 pt-5 sm:flex-row sm:items-center sm:justify-between`,children:[L(`label`,{class:`inline-flex min-h-11 items-center gap-3 text-sm font-extrabold text-slate-800`,children:[L(`input`,{checked:s,class:`size-5 accent-teal-700`,disabled:t,onChange:e=>c(e.currentTarget.checked),type:`checkbox`}),`이 Wi-Fi 사용`]}),L(`button`,{class:`inline-flex min-h-11 w-full items-center justify-center gap-2 rounded-xl border-0 bg-teal-700 px-5 py-2.5 text-sm font-extrabold text-white shadow-sm transition hover:bg-teal-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-200 disabled:cursor-wait disabled:opacity-60 sm:w-auto`,disabled:t||!x,type:`submit`,children:[L(ut,{class:`size-5 ${n?`animate-pulse`:``}`}),n?`적용 중`:`설정 저장`]})]}),p?L(yr,{load:h,onClose:()=>m(!1)}):null,g?L(`p`,{class:`mt-4 mb-0 rounded-xl bg-red-50 px-4 py-3 text-sm font-bold text-red-800`,children:g}):null]})}function Tr({feedback:e,onDismiss:t}){return L(`div`,{"aria-live":`polite`,class:`mb-5 flex flex-col gap-3 rounded-2xl border px-4 py-3 text-sm sm:flex-row sm:items-center sm:justify-between ${e.kind===`success`?`border-emerald-200 bg-emerald-50 text-emerald-900`:e.kind===`warning`?`border-amber-200 bg-amber-50 text-amber-900`:`border-red-200 bg-red-50 text-red-900`}`,children:[L(`p`,{class:`m-0 font-bold leading-6`,children:e.message}),L(`button`,{class:`min-h-11 w-full shrink-0 rounded-lg border border-current bg-transparent sm:min-h-9 sm:w-auto px-3 py-1.5 text-xs font-extrabold`,onClick:t,type:`button`,children:`닫기`})]})}function Er({data:e,error:t,feedback:n,loading:r,updatingSection:i,onDismissFeedback:a,onRetry:o,onUpdate:s,onUpdateGuest:c,onLoadQr:l,onLoadGuestQr:u}){return r?L(zn,{}):t?L(Bn,{message:t,onRetry:o}):e?L(v,{children:[n?L(Tr,{feedback:n,onDismiss:a}):null,L(`section`,{class:`mb-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-amber-950 sm:p-6`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-amber-700`,children:`연결 주의`}),L(`h2`,{class:`mt-2 mb-0 text-lg font-extrabold`,children:`Wi-Fi 이름이나 비밀번호를 바꾸면 현재 연결이 끊어집니다`}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-amber-900/80`,children:`변경한 Wi-Fi 이름과 비밀번호로 다시 연결하면 SmartSafeHub를 계속 사용할 수 있습니다. VLAN 등 고급 무선 옵션은 기존 LuCI에서 관리합니다.`})]}),L(V,{eyebrow:`Wireless`,title:`기본 Wi-Fi`,description:`무선 네트워크의 이름, 비밀번호, 보안 방식과 QR 코드를 관리합니다.`}),e.networks.length?L(`section`,{"aria-label":`기본 Wi-Fi 설정`,class:`grid gap-5 xl:grid-cols-2`,children:e.networks.map(e=>L(wr,{busy:i!==null,network:e,onUpdate:s,onLoadQr:l,saving:i===e.section},e.section))}):L(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 text-center shadow-sm shadow-slate-900/5 sm:p-8`,children:[L(We,{class:`mx-auto size-10 text-slate-400`}),L(`h2`,{class:`mt-4 mb-0 text-lg font-extrabold text-slate-950`,children:`관리할 기본 Wi-Fi를 찾지 못했습니다`}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:`기존 LuCI에서 AP 모드의 무선 네트워크를 먼저 구성해 주세요.`})]}),e.guest&&L(v,{children:[L(`div`,{role:`separator`,"aria-label":`기본 Wi-Fi와 게스트 Wi-Fi 구분`,class:`mt-5 w-full border-t border-slate-200`}),L(`div`,{class:`mt-5`,children:L(V,{eyebrow:`Guest network`,title:`방문자용 네트워크`,description:`기본 네트워크와 분리된 게스트 Wi-Fi를 관리합니다.`})}),L(br,{guest:e.guest,busy:i!==null,saving:i===`ssh_guest`,onUpdate:c,onLoadQr:u})]}),e.guestError&&L(`section`,{class:`mt-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-sm text-amber-900`,role:`status`,children:[`게스트 Wi-Fi 정보를 불러오지 못했습니다. 패키지를 설치한 직후라면 로그아웃 후 다시 로그인해 주세요.`,L(`button`,{class:`ml-3 underline font-bold`,onClick:o,type:`button`,children:`다시 시도`})]})]}):null}function Dr({timestamp:e,fallback:t=`기록 없음`}){let[n,r]=F(()=>Date.now()/1e3),i=typeof e==`number`&&Number.isFinite(e)&&e>0&&e<=864e10;if(I(()=>{if(!i)return;r(Date.now()/1e3);let e=window.setInterval(()=>r(Date.now()/1e3),6e4);return()=>window.clearInterval(e)},[e,i]),!i||e==null)return L(`span`,{children:t});let a=Wt(e);return L(`span`,{class:`inline-flex max-w-full flex-wrap items-center gap-1`,children:[L(`time`,{class:`min-w-0`,dateTime:new Date(e*1e3).toISOString(),title:a,children:Kt(e,n)}),L(`details`,{class:`group inline-flex max-w-full flex-wrap items-center gap-1 align-middle`,children:[L(`summary`,{"aria-label":`정확한 날짜 및 시간 확인`,class:`inline-flex size-6 cursor-pointer list-none items-center justify-center rounded-full text-slate-400 transition hover:text-teal-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-500 [&::-webkit-details-marker]:hidden`,title:`정확한 날짜 및 시간 확인`,children:L(`svg`,{"aria-hidden":`true`,class:`size-3.5`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.8`,viewBox:`0 0 24 24`,children:[L(`circle`,{cx:`12`,cy:`12`,r:`9`}),L(`path`,{d:`M12 11v5m0-8h.01`,strokeLinecap:`round`})]})}),L(`span`,{class:`hidden max-w-full break-words rounded-md bg-slate-100 px-2 py-1 text-xs font-semibold text-slate-700 group-open:inline-block`,children:a})]})]})}function Or(e){return e.trim().toLowerCase().replace(/\.+$/,``)}function kr(e){return e.length===0||e.length>253||!e.includes(`.`)||!/^[a-z0-9.-]+$/.test(e)?!1:e.split(`.`).every(e=>e.length>0&&e.length<=63&&/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(e))}function Ar({action:e,kind:t,onAddRule:n,onDeleteRule:r,rules:i,recommendedDomains: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=Te(()=>{let e=f.trim().toLowerCase();return e?i.filter(t=>t.includes(e)):i},[i,f]),C=f.trim().length>0,w=b&&o?S.filter(e=>a.includes(e)):[],T=b&&o?S.filter(e=>!a.includes(e)):S,E=b?s.filter(e=>!C||e.title.toLowerCase().includes(f.trim().toLowerCase())||e.domains.some(e=>e.includes(f.trim().toLowerCase()))):[],D=C||(v??(s.length||w.length)<=3),O=C||g;async function k(e){e.preventDefault();let r=Or(u);kr(r)?(h(null),await n(t,r,()=>{d(e=>Or(e)===r?``:e)})):h(`example.com 형식의 도메인을 입력해 주세요. URL, IP 주소, 와일드카드는 사용할 수 없습니다.`)}let A=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 j(n){let i=b&&o&&a.includes(n),s=!b||o?i?`추천 규칙`:`직접 추가`:null,c=e?.operation===`delete`&&e.action===t&&e.domain===n;return L(`li`,{class:`flex min-w-0 items-center justify-between gap-3 px-3.5 py-3`,children:[L(`div`,{class:`flex min-w-0 flex-1 flex-wrap items-center gap-2`,children:L(`code`,{class:`min-w-0 break-all font-mono text-sm font-semibold text-slate-800`,children:n})}),i?L(`span`,{class:`shrink-0 rounded-full bg-teal-50 px-2.5 py-1 text-[11px] font-bold text-teal-800`,title:`추천 규칙의 적용과 해제는 웹사이트에서 관리합니다.`,children:`웹사이트 관리`}):L(`button`,{"aria-label":`${n} ${A.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:L(ft,{class:`size-4 ${c?`animate-pulse`:``}`})})]},n)}return L(`article`,{class:`min-w-0 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm shadow-slate-900/5`,children:[L(`div`,{class:`border-b border-slate-200 p-4 sm:p-6`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3`,children:[L(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl ${A.icon}`,children:L(b?ut:Ue,{class:`size-6`})}),L(`div`,{class:`min-w-0`,children:[L(`div`,{class:`flex flex-wrap items-center gap-2`,children:[L(`h2`,{class:`m-0 text-xl font-extrabold tracking-tight text-slate-950`,children:A.label}),L(`span`,{class:`rounded-full px-2.5 py-1 text-xs font-extrabold ${A.badge}`,children:i.length})]}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:A.description})]})]}),L(`form`,{class:`mt-5`,onSubmit:k,children:[L(`label`,{class:`block text-xs font-extrabold text-slate-600`,for:`${t}-domain`,children:`새 도메인`}),L(`div`,{class:`mt-2 flex flex-col gap-2 sm:flex-row`,children:[L(`input`,{"aria-describedby":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}),L(`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 ${A.button}`,disabled:x||u.trim().length===0,type:`submit`,children:[L(dt,{class:`size-4`}),e?.operation===`add`&&e.action===t?`추가 중…`:`추가`]})]}),m?L(`p`,{class:`mt-2 mb-0 text-xs font-semibold leading-5 text-red-700`,id:`${t}-domain-error`,children:m}):null]})]}),L(`div`,{class:`p-4 sm:p-6`,children:[L(`label`,{class:`relative block`,for:`${t}-search`,children:[L(`span`,{class:`sr-only`,children:[A.label,` 검색`]}),L(`span`,{class:`pointer-events-none absolute inset-y-0 left-0 flex w-11 items-center justify-center text-slate-400`,children:L(pt,{class:`size-4`})}),L(`input`,{class:`min-h-11 w-full rounded-xl border-2 border-slate-300 bg-slate-50 py-2.5 pr-4 pl-11 text-sm font-semibold text-slate-950 shadow-inner outline-none transition placeholder:text-slate-400 focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100`,id:`${t}-search`,onInput:e=>p(e.currentTarget.value),placeholder:`${A.label} 검색`,type:`search`,value:f})]}),b&&o?L(`div`,{class:`mt-4 overflow-hidden rounded-xl border border-slate-200`,children:[L(`section`,{"aria-label":`직접 추가한 허용 규칙`,children:[L(`button`,{"aria-controls":`allow-manual-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:()=>_(!O),type:`button`,children:[L(`span`,{class:`min-w-0 text-sm font-extrabold text-slate-800`,children:[`직접 추가한 규칙 `,L(`span`,{class:`ml-1 rounded-full bg-white px-2 py-0.5 text-xs font-bold text-slate-600`,children:[T.length,C?`개 검색됨`:`개`]})]}),L(`span`,{"aria-hidden":`true`,class:`shrink-0 text-sm font-bold text-slate-500`,children:O?`접기 −`:`펼치기 +`})]}),L(`div`,{hidden:!O,id:`allow-manual-domains`,children:T.length?L(`ul`,{class:`m-0 max-h-[20rem] list-none divide-y divide-slate-200 overflow-y-auto p-0`,children:T.map(j)}):L(`p`,{class:`m-0 px-4 py-5 text-sm text-slate-500`,children:C?`검색 결과가 없습니다.`:`직접 추가한 허용 도메인이 없습니다.`})})]}),L(`section`,{"aria-label":`웹사이트 추천 규칙`,class:`border-t border-slate-200`,children:[L(`button`,{"aria-controls":`allow-recommended-domains`,"aria-expanded":D,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(!D),type:`button`,children:[L(`span`,{class:`min-w-0 text-sm font-extrabold text-slate-800`,children:[`추천 규칙 `,L(`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:w.length,C?`개 검색됨`:`개`]})]}),L(`span`,{"aria-hidden":`true`,class:`shrink-0 text-sm font-bold text-slate-500`,children:D?`접기 −`:`펼치기 +`})]}),L(`div`,{hidden:!D,id:`allow-recommended-domains`,children:[E.length?L(`ul`,{class:`m-0 max-h-[22rem] list-none divide-y divide-slate-200 overflow-y-auto p-0`,children:E.map(e=>L(`li`,{class:`flex min-w-0 flex-wrap items-center justify-between gap-3 px-3.5 py-3`,children:[L(`div`,{class:`min-w-0 flex-1`,children:[L(`strong`,{class:`block break-words text-sm text-slate-900`,children:e.title}),L(`div`,{class:`mt-1 flex flex-wrap gap-1.5`,children:e.domains.map(e=>L(`code`,{class:`break-all text-xs text-slate-600`,children:e},e))})]}),c?L(`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:`적용 해제`}):L(`span`,{class:`text-xs text-slate-500`,children:`웹사이트 관리`})]},e.id))}):w.length?L(`ul`,{class:`m-0 max-h-[20rem] list-none divide-y divide-slate-200 overflow-y-auto p-0`,children:w.map(j)}):L(`p`,{class:`m-0 px-4 py-5 text-sm text-slate-500`,children:C?`검색 결과가 없습니다.`:`추천으로 추가된 허용 도메인이 없습니다.`}),C?null:L(`p`,{class:`m-0 border-t border-slate-100 px-4 py-3 text-xs leading-5 text-slate-500`,children:`추천 규칙은 이 공유기에서 적용 해제할 수 있습니다. 새 규칙의 적용은 SmartSafeHub 웹사이트에서 관리해 주세요.`})]})]})]}):L(`div`,{class:`mt-4 max-h-[28rem] overflow-y-auto rounded-xl border border-slate-200`,children:S.length?L(`ul`,{class:`m-0 list-none divide-y divide-slate-200 p-0`,children:S.map(j)}):L(`div`,{class:`px-5 py-10 text-center`,children:[L(mt,{class:`mx-auto size-8 text-slate-300`}),L(`p`,{class:`mt-3 mb-0 text-sm font-bold text-slate-600`,children:C?`검색 결과가 없습니다.`:A.empty})]})})]})]})}function jr({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),[y,b]=F(null),[x,S]=F(!1),[C,w]=F(!1),[T,E]=F(!1),D=we(null),O=we(null),k=we(c);k.current=c;let[A,j]=F(null),[M,ee]=F(null),te=f?.domains??[],ne=f?.rules??[],re=Ee(e=>{p(e),h(!0);let t=JSON.stringify([e.revision,e.domains,e.rules.map(e=>e.id),e.syncStatus]);O.current!==null&&O.current!==t&&k.current(),O.current=t,e.syncStatus===`confirmed`&&D.current===null&&ee(null),D.current!==null&&e.syncStatus===`confirmed`&&!e.rules.some(e=>e.id===D.current)&&(D.current=null,w(!1),E(!1),ee(null),j(`추천 규칙 적용 해제를 확인했습니다.`))},[]);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)}},[t?.allow.join(` -`),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),ee(null),j(null);try{let e=await u();re(e),j(e.syncStatus===`confirmed`?`서버에서 규칙을 가져와 SafeShield 허용 목록을 확인했습니다.`:`추천 규칙 상태를 확인 중입니다. 진행 결과는 자동으로 갱신됩니다.`),c()}catch{ee(`추천 규칙을 동기화하지 못했습니다. 서버 연결 및 기기 등록 상태를 확인해 주세요.`)}finally{_(!1)}}}async function ae(){if(!y||x||!d)return;let e=y.id;D.current=e,S(!0),w(!0),E(!1),ee(null),j(null);try{let t=await d(e);re(t),b(null),D.current!==null&&j(`적용 해제 요청을 처리했습니다. 공유기의 허용 목록을 확인하고 있습니다.`),k.current()}catch{b(null),ee(`적용 해제 결과를 아직 확인하지 못했습니다. 공유기의 상태를 자동으로 다시 확인합니다.`);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`,N=oe===`server_sync`?`서버 동기화 중`:oe===`safeshield_refreshing`?`SafeShield 갱신 중`:oe===`applying`?`추천 규칙 적용 중`:oe===`verifying`?`허용 목록 확인 중`:ce?`재확인 지연`:le?`동기화 실패`:se?`동기화 대기`:`허용 목록 확인`,ue=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,de=C&&D.current!==null?{tone:`amber`,label:T?`적용 해제 처리 지연`:`적용 해제 확인 중`,description:T?`적용 해제 확인이 지연되고 있습니다. SafeShield 갱신 중이라면 완료 후 자동으로 재확인합니다.`:ue??`적용 해제 상태를 확인하고 있습니다. 확인되면 목록과 상태가 자동 갱신됩니다.`}:se||ce||le?{tone:le?`red`:`amber`,label:N,description:ue??``}:M?{tone:`red`,label:`상태 확인 필요`,description:M}:f?.syncWarning?{tone:`amber`,label:`서버 재확인 지연`,description:`이미 확인한 허용 규칙은 유지되며 자동으로 다시 시도합니다.`}:A?{tone:`green`,label:`허용 목록 확인`,description:A}:null;return i&&!t?L(zn,{}):n&&!t?L(Bn,{message:n,onRetry:c}):t?L(v,{children:[y?L(`div`,{class:`fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70 p-4`,role:`presentation`,onClick:()=>{x||b(null)},children:L(`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:[L(`h2`,{class:`m-0 text-lg font-extrabold text-slate-950`,id:`recommended-revoke-title`,children:`추천 규칙 적용 해제`}),L(`p`,{class:`mt-3 text-sm text-slate-700`,children:[L(`strong`,{children:y.title}),` 규칙을 이 공유기에서 해제할까요?`]}),L(`p`,{class:`mt-2 text-xs leading-5 text-slate-600`,children:[`포함된 도메인 `,y.domains.length,`개를 다시 확인합니다. 직접 추가한 허용 규칙과 다른 추천 규칙에 필요한 도메인은 유지됩니다.`]}),L(`div`,{class:`mt-5 flex justify-end gap-2`,children:[L(`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:()=>b(null),type:`button`,children:`취소`}),L(`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?L(`svg`,{class:`size-4 animate-spin`,"aria-hidden":`true`,viewBox:`0 0 24 24`,fill:`none`,children:[L(`circle`,{cx:`12`,cy:`12`,r:`9`,stroke:`currentColor`,"stroke-width":`3`,opacity:`0.25`}),L(`path`,{d:`M21 12a9 9 0 0 0-9-9`,stroke:`currentColor`,"stroke-width":`3`,"stroke-linecap":`round`})]}):null,L(`span`,{role:`status`,children:x?`해제 중…`:`적용 해제`})]})]})]})}):null,L(V,{eyebrow:`Local overrides`,title:`사용자 도메인 규칙`,description:`허용 목록은 오탐을 해제하고, 차단 목록은 원하는 도메인을 SafeShield 보호 목록에 추가합니다.`}),L(`section`,{"aria-label":`규칙 개수 요약`,class:`min-w-0`,children:L(`dl`,{class:`m-0 grid grid-cols-1 gap-3 sm:grid-cols-3`,children:[L(`div`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white px-5 py-4 shadow-sm shadow-slate-900/5`,children:[L(`dt`,{class:`text-sm font-extrabold text-emerald-700`,children:`허용 도메인`}),L(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight text-slate-950`,children:[t.counts.allow,L(`span`,{class:`ml-1 text-sm font-bold text-slate-500`,children:`개`})]}),L(`p`,{class:`mt-1 mb-0 text-xs text-slate-500`,children:`차단하지 않도록 등록한 도메인`})]}),L(`div`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white px-5 py-4 shadow-sm shadow-slate-900/5`,children:[L(`dt`,{class:`text-sm font-extrabold text-red-700`,children:`차단 도메인`}),L(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight text-slate-950`,children:[t.counts.block,L(`span`,{class:`ml-1 text-sm font-bold text-slate-500`,children:`개`})]}),L(`p`,{class:`mt-1 mb-0 text-xs text-slate-500`,children:`추가로 차단하도록 등록한 도메인`})]}),L(`div`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white px-5 py-4 shadow-sm shadow-slate-900/5`,children:[L(`dt`,{class:`text-sm font-extrabold text-slate-600`,children:`전체 등록 규칙`}),L(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight text-slate-950`,children:[t.counts.total,L(`span`,{class:`ml-1 text-sm font-bold text-slate-500`,children:`개`})]}),L(`p`,{class:`mt-1 mb-0 text-xs text-slate-500`,children:`허용 및 차단 규칙의 합계`})]})]})}),L(`div`,{class:`mt-7`,children:L(V,{eyebrow:`Recommendations`,title:`추천 사용자 규칙`,description:`웹사이트에서 선택한 권장 허용 규칙의 동기화 상태를 확인하고 관리합니다.`})}),L(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,"aria-label":`추천 사용자 규칙`,children:[L(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between`,children:[L(`div`,{class:`min-w-0 flex-1`,children:[L(`div`,{class:`flex flex-wrap items-center gap-2`,children:[L(`span`,{class:`grid size-8 place-items-center rounded-lg bg-teal-50 text-teal-700`,children:L(ut,{class:`size-4`})}),L(`h3`,{class:`m-0 text-base font-extrabold text-slate-950`,children:`동기화 상태`}),l?L(`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`?`허용 목록 확인`:N}):null]}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:`웹사이트에서 선택한 허용 규칙을 약 30초마다 확인합니다. 적용 해제는 이 공유기에서도 할 수 있습니다.`}),L(`div`,{class:`mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs font-semibold text-slate-600`,children:[L(`span`,{children:[`추천으로 추가 `,L(`strong`,{class:`text-teal-800`,children:[m?te.length:`—`,`개 도메인`]})]}),f?.syncStatus===`confirmed`?L(`span`,{children:[`허용 확인 `,L(`strong`,{class:`text-emerald-800`,children:[f.confirmedDomains.length,`개`]})]}):null,f?.lastCheckedAt?L(`span`,{children:[f.syncWarning?`최근 재확인 시도`:`최근 확인`,`: `,L(Dr,{timestamp:f.lastCheckedAt})]}):null]})]}),L(`div`,{class:`flex w-full flex-wrap gap-2 sm:w-auto lg:shrink-0`,children:[u?L(`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,L(`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:`추천 규칙 보기 ↗`})]})]}),de?L(`div`,{"aria-live":`polite`,class:`mt-3 flex items-start gap-2 rounded-xl px-3 py-2.5 text-sm ${de.tone===`red`?`bg-red-50 text-red-800`:de.tone===`green`?`bg-emerald-50 text-emerald-800`:`bg-amber-50 text-amber-800`}`,role:`status`,children:[L(`span`,{"aria-hidden":`true`,class:`mt-1 size-2 shrink-0 rounded-full ${de.tone===`red`?`bg-red-500`:de.tone===`green`?`bg-emerald-500`:`bg-amber-500`}`}),L(`div`,{class:`min-w-0`,children:[L(`strong`,{children:de.label}),L(`p`,{class:`mt-1 mb-0 leading-5`,children:de.description})]})]}):null,L(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`추천 규칙 동기화는 SafeShield 차단 목록 갱신과 별개입니다. 허용 목록 확인은 DNS 처리나 서비스 재생 성공을 보장하지 않습니다.`})]}),t.applyLocalOverrides?null:L(`section`,{class:`mt-4 flex items-start gap-3 rounded-2xl border border-amber-200 bg-amber-50 p-4`,role:`alert`,children:[L(B,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),L(`div`,{children:[L(`p`,{class:`m-0 text-sm font-extrabold text-amber-950`,children:`로컬 규칙 적용이 꺼져 있습니다`}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:`규칙은 저장되지만 SafeShield의 apply_local_overrides 설정을 켜기 전까지 DNS에 반영되지 않습니다.`})]})]}),t.safeshieldEnabled?null:L(`section`,{class:`mt-4 flex items-start gap-3 rounded-2xl border border-slate-200 bg-slate-100 p-4`,role:`status`,children:[L(Ue,{class:`mt-0.5 size-5 shrink-0 text-slate-600`}),L(`div`,{children:[L(`p`,{class:`m-0 text-sm font-extrabold text-slate-950`,children:`SafeShield가 비활성화되어 있습니다`}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`규칙을 미리 저장할 수 있으며, SafeShield를 켜고 차단 목록을 갱신하면 적용됩니다.`})]})]}),r?L(`section`,{"aria-live":`polite`,class:`mt-4 flex flex-col items-start gap-3 rounded-2xl border border-teal-200 bg-teal-50 sm:flex-row sm:justify-between p-4`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3`,children:[L(ut,{class:`mt-0.5 size-5 shrink-0 text-teal-700`}),L(`p`,{class:`m-0 text-sm font-semibold leading-6 text-teal-950`,children:r})]}),L(`button`,{class:`shrink-0 rounded-lg border-0 bg-transparent px-2 py-1 text-xs font-extrabold text-teal-800 hover:bg-teal-100`,onClick:s,type:`button`,children:`닫기`})]}):null,n&&t?L(`section`,{"aria-live":`assertive`,class:`mt-4 flex items-start gap-3 rounded-2xl border border-red-200 bg-red-50 p-4`,children:[L(B,{class:`mt-0.5 size-5 shrink-0 text-red-700`}),L(`p`,{class:`m-0 text-sm font-semibold leading-6 text-red-900`,children:n})]}):null,L(`section`,{class:`mt-5 grid gap-5 xl:grid-cols-2`,children:[L(Ar,{action:e,kind:`allow`,onAddRule:a,onDeleteRule:o,rules:t.allow,recommendedDomains:te,recommendedKnown:m,recommendedRules:ne,...d?{onRequestRevoke:b}:{},revokeBusy:x}),L(Ar,{action:e,kind:`block`,onAddRule:a,onDeleteRule:o,rules:t.block,recommendedDomains:te,recommendedKnown:m,recommendedRules:ne,revokeBusy:x})]})]}):L(Bn,{message:`사용자 규칙 응답이 비어 있습니다.`,onRetry:c})}function Mr(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 Pr(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 Fr(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 Ir(e){return e.phase===`checking`||e.phase===`downloading`||e.phase===`verifying`||e.phase===`flashing`?L(z,{class:`size-3.5 animate-spin`}):e.phase===`error`?L(B,{class:`size-3.5`}):e.phase===`ready`||e.updateAvailable?L(it,{class:`size-3.5`}):e.lastCheckAt?L(ut,{class:`size-3.5`}):L(tt,{class:`size-3.5`})}function Lr({checked:e,disabled:t,onChange:n}){return L(`button`,{"aria-checked":e,"aria-label":`현재 설정 유지`,class:`ssh-switch-control relative inline-flex shrink-0 rounded-full border transition focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-not-allowed disabled:opacity-50 ${e?`border-teal-600 bg-teal-600`:`border-slate-300 bg-slate-200`}`,disabled:t,onClick:()=>n(!e),role:`switch`,type:`button`,children:L(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${e?`left-6`:`left-1`}`})})}function Rr({action:e,actionError:t,data:n,software:r,error:i,loading:a,message:o,reconnecting:s,uploadProgress:c,onCheck:l,onDiscard:u,onDismissFeedback:d,onInstall:f,onPrepare:p,onRetry:m,onUpload:h}){let g=we(null),[_,y]=F(null),[b,x]=F(!0),[S,C]=F(!1);I(()=>{n?.prepared&&x(n.prepared.allowBackup)},[n?.prepared]);let w=e!==null||n?.phase===`checking`||n?.phase===`downloading`||n?.phase===`verifying`||n?.phase===`flashing`,T=n?.phase===`ready`?n.prepared:null,E=e===`check`||n?.phase===`checking`,D=e===`prepare`||n?.phase===`downloading`||n?.phase===`verifying`,O=e===`upload`||e===`validate-upload`,k=n?.release?.bundledSoftware?.smartsafehub??null,A=r?.packages.find(e=>e.name===`luci-app-smartsafehub`)?.installedVersion??null,j=Mr(k,A),M=async()=>{_&&await h(_)&&(y(null),g.current&&(g.current.value=``))};return L(`article`,{class:`min-w-0 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm shadow-slate-900/5`,"data-component":`firmware-update-card`,children:[L(`div`,{class:`p-5 sm:p-6`,children:[L(`div`,{class:`flex flex-col gap-5 lg:flex-row lg:items-start lg:justify-between`,children:[L(`div`,{class:`flex min-w-0 gap-4`,children:[L(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-sky-50 text-sky-700`,children:L(We,{class:`size-6`})}),L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-sky-700`,children:`Firmware update`}),L(`h2`,{class:`mt-2 mb-0 text-xl font-black text-slate-950 sm:text-2xl`,children:`펌웨어 업데이트`}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500 lg:whitespace-nowrap`,children:`시스템 펌웨어를 확인하고 설치합니다. 온라인·수동 설치는 안전성 검증 후 진행됩니다.`})]})]}),n?L(`div`,{class:`flex shrink-0 flex-col gap-2 sm:flex-row lg:justify-end`,children:[L(`span`,{class:`inline-flex min-h-11 items-center justify-center gap-1.5 rounded-xl px-3 py-2 text-xs font-extrabold ring-1 ring-inset ${Fr(n)}`,children:[Ir(n),Pr(n)]}),L(`button`,{class:`inline-flex min-h-11 items-center justify-center gap-2 rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-extrabold text-slate-800 transition hover:bg-slate-50 disabled:cursor-wait disabled:opacity-60`,disabled:w||T!==null,onClick:l,type:`button`,children:[L(z,{class:`size-4 ${E?`animate-spin`:``}`}),E?`확인 중...`:`펌웨어 확인`]}),n.updateAvailable&&!T?L(`button`,{class:`inline-flex min-h-11 items-center justify-center gap-2 rounded-xl bg-sky-700 px-4 py-2.5 text-sm font-extrabold text-white transition hover:bg-sky-800 disabled:cursor-wait disabled:opacity-60`,disabled:w,onClick:p,type:`button`,children:[D?L(z,{class:`size-4 animate-spin`}):L(it,{class:`size-4`}),D?`준비 중...`:`다운로드 및 검증`]}):null]}):null]}),t||o?L(`div`,{class:`mt-5 flex flex-col gap-3 rounded-xl border px-4 py-3 text-sm font-bold sm:flex-row sm:items-center sm:justify-between ${t?`border-rose-200 bg-rose-50 text-rose-800`:`border-emerald-200 bg-emerald-50 text-emerald-800`}`,children:[L(`span`,{children:t||o}),L(`button`,{class:`shrink-0 self-start rounded-lg px-3 py-1.5 text-xs font-extrabold hover:bg-black/5 sm:self-auto`,onClick:d,type:`button`,children:`닫기`})]}):null,s||n?.phase===`flashing`?L(`div`,{class:`mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4 sm:p-5`,role:`status`,children:L(`div`,{class:`flex gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-amber-700 ring-1 ring-inset ring-amber-200`,children:L(z,{class:`size-5 animate-spin`})}),L(`div`,{children:[L(`strong`,{class:`block text-sm font-black text-amber-950`,children:`펌웨어를 설치하고 재부팅하고 있습니다.`}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:`전원을 끄지 마세요. 현재 주소에서 공유기가 다시 응답하면 SmartSafeHub가 자동으로 새로고침됩니다.`})]})]})}):null,i&&!n?L(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-800`,children:[L(`p`,{class:`m-0 font-bold`,children:i}),L(`button`,{class:`mt-3 rounded-lg border border-rose-300 bg-white px-3 py-2 text-xs font-extrabold text-rose-700`,onClick:m,type:`button`,children:`다시 시도`})]}):a&&!n?L(`div`,{class:`mt-5 flex items-center gap-3 rounded-xl bg-slate-50 p-4 text-sm font-bold text-slate-500`,children:[L(z,{class:`size-4 animate-spin`}),`펌웨어 상태를 확인하고 있습니다.`]}):n?L(v,{children:[n.lastError?L(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 sm:p-5`,children:L(`div`,{class:`flex gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-rose-700 ring-1 ring-inset ring-rose-200`,children:L(B,{class:`size-5`})}),L(`div`,{class:`min-w-0 flex-1`,children:[L(`strong`,{class:`block text-sm font-black text-rose-950`,children:`펌웨어 작업을 완료하지 못했습니다.`}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-rose-800`,children:n.lastError.message}),L(`details`,{class:`mt-3 rounded-lg border border-rose-200 bg-white p-3`,children:[L(`summary`,{class:`cursor-pointer text-xs font-extrabold text-rose-800`,children:`상세 정보 보기`}),L(`p`,{class:`mt-2 mb-0 break-all font-mono text-[11px] text-slate-600`,children:n.lastError.code})]})]})]})}):null,n.current.metadataAvailable?null:L(`div`,{class:`mt-5 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-xs leading-5 text-amber-900`,children:[L(`strong`,{class:`block font-extrabold`,children:`현재 펌웨어의 버전 정보를 완전히 확인할 수 없습니다.`}),L(`span`,{class:`mt-1 block text-amber-800`,children:`새 펌웨어를 설치하면 이후부터 정확한 설치 버전을 자동으로 확인할 수 있습니다.`}),L(`details`,{class:`mt-2`,children:[L(`summary`,{class:`cursor-pointer font-extrabold text-amber-800`,children:`기술 상세 보기`}),L(`p`,{class:`mt-2 mb-0 break-all font-mono text-[11px] leading-5 text-amber-800`,children:`SmartSafeHub build ID metadata is missing. Future images should include /usr/share/smartsafehub/firmware.json.`})]})]}),L(`dl`,{class:`mt-5 grid overflow-hidden rounded-xl border border-slate-200 bg-slate-200 sm:grid-cols-2 xl:grid-cols-4`,children:[L(`div`,{class:`bg-white p-4`,children:[L(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Device`}),L(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black text-slate-950`,children:n.current.deviceCode||n.current.boardName||`미확인`})]}),L(`div`,{class:`bg-white p-4`,children:[L(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Current version`}),L(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black text-slate-950`,children:n.current.releaseVersion||`미확인`}),n.current.buildId?L(`span`,{class:`mt-1 block break-all text-[11px] leading-5 text-slate-400`,children:[`빌드 ID `,n.current.buildId]}):n.current.openwrtVersion?L(`span`,{class:`mt-1 block text-[11px] leading-5 text-slate-400`,children:[`OpenWrt `,n.current.openwrtVersion]}):null]}),L(`div`,{class:`bg-white p-4`,children:[L(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Available version`}),L(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black ${n.updateAvailable?`text-amber-700`:`text-slate-950`}`,children:n.lastCheckAt?n.release?.version?`${n.release.version}${n.updateAvailable?``:` · 최신`}`:n.updateAvailable?`새 펌웨어`:`최신 버전`:`미확인`}),n.release?.buildId?L(`span`,{class:`mt-1 block break-all text-[11px] leading-5 text-slate-400`,children:[`빌드 ID `,n.release.buildId]}):null]}),L(`div`,{class:`bg-white p-4`,children:[L(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Last check`}),L(`dd`,{class:`mt-2 mb-0 ml-0 text-sm font-black text-slate-950`,children:L(Dr,{fallback:`아직 없음`,timestamp:n.lastCheckAt})})]})]})]}):null,n?.updateAvailable&&n.release?L(`section`,{class:`mt-5 rounded-xl border border-sky-200 bg-sky-50/60 p-4 sm:p-5`,"data-section":`available-firmware`,children:[L(`div`,{class:`flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-sky-700`,children:`Available firmware`}),L(`h3`,{class:`mt-2 mb-0 text-lg font-black text-slate-950`,children:`새 펌웨어가 있습니다.`}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:[`펌웨어 `,n.release.version||`미확인`,` · `,Nr(n.release.sysupgrade.sizeBytes),` · `,(n.release.channel||n.settings.channel)===`beta`?`Beta`:`Stable`]})]}),L(`div`,{class:`flex shrink-0 flex-wrap items-center justify-start gap-2 sm:justify-end`,children:[L(`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?L(`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:[L(`span`,{class:`text-[11px] uppercase tracking-[0.12em] text-slate-500`,children:`관리 SW`}),L(`span`,{class:`text-slate-950`,children:k})]}):null]})]}),k?L(`div`,{class:`mt-4 rounded-xl border border-sky-100 bg-white p-4`,"data-section":`firmware-bundled-software`,children:[L(`div`,{class:`flex flex-wrap items-center justify-between gap-2`,children:[L(`span`,{class:`text-xs font-extrabold text-slate-800`,children:`포함된 관리 소프트웨어`}),L(`div`,{class:`flex flex-wrap items-center gap-2`,children:[L(`span`,{class:`text-sm font-black text-slate-950`,children:k}),j===`same`?L(`span`,{class:`rounded-full bg-emerald-50 px-2.5 py-1 text-[11px] font-extrabold text-emerald-700 ring-1 ring-inset ring-emerald-200`,children:`현재 버전과 동일`}):null]})]}),L(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-600`,children:j===`same`?`관리 소프트웨어는 이미 같은 버전입니다. 펌웨어 업데이트는 시스템 구성 요소를 갱신합니다.`:j===`older`?`현재 설치된 관리 소프트웨어(${A})보다 이전 버전이 포함되어 있습니다. 펌웨어 설치 전 버전을 확인해 주세요.`:j===`newer`?`현재 설치된 관리 소프트웨어(${A})보다 새 버전이 포함되어 있습니다.`:A?`현재 설치 버전 ${A} · 펌웨어 포함 버전 ${k}`:`이 펌웨어에 포함된 관리 소프트웨어 버전입니다.`})]}):null,n.release.releaseNotes.length?L(`ul`,{class:`mt-4 mb-0 space-y-2 rounded-xl bg-white p-4 pl-9 text-sm leading-6 text-slate-700 ring-1 ring-inset ring-sky-100`,children:n.release.releaseNotes.map((e,t)=>L(`li`,{children:e},`${t}-${e}`))}):null]}):null,T?L(`section`,{class:`mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4 sm:p-5`,"data-section":`prepared-firmware`,children:L(`div`,{class:`flex gap-4`,children:[L(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-white text-amber-700 ring-1 ring-inset ring-amber-200`,children:L(ut,{class:`size-5`})}),L(`div`,{class:`min-w-0 flex-1`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-amber-700`,children:`Verified image`}),L(`h3`,{class:`mt-2 mb-0 text-lg font-black text-amber-950`,children:`설치할 펌웨어가 준비되었습니다.`}),L(`p`,{class:`mt-2 mb-0 break-all text-sm leading-6 text-amber-900`,children:[T.filename||`firmware.bin`,` · `,Nr(T.sizeBytes)]}),T.sha256?L(`p`,{class:`mt-2 mb-0 break-all font-mono text-[11px] leading-5 text-amber-800`,children:[`SHA-256 `,T.sha256]}):null,L(`div`,{class:`mt-5 flex items-start justify-between gap-4 rounded-xl border border-amber-200 bg-white p-4`,children:[L(`div`,{children:[L(`strong`,{class:`block text-sm font-black text-slate-950`,children:`현재 설정 유지`}),L(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`Wi-Fi, SafeShield, SmartSafeHub 설정을 유지합니다.`}),T.allowBackup?null:L(`span`,{class:`mt-2 block text-xs font-bold text-rose-700`,children:`이 이미지는 설정 유지 업그레이드를 지원하지 않습니다.`})]}),L(Lr,{checked:b,disabled:!T.allowBackup,onChange:x})]}),S?L(`div`,{class:`mt-4 rounded-xl border border-rose-200 bg-rose-50 p-4`,children:[L(`strong`,{class:`block text-sm font-black text-rose-950`,children:`펌웨어 설치를 시작할까요?`}),L(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-rose-800`,children:`설치 중 전원을 끄면 장치가 복구 불가능한 상태가 될 수 있습니다. 이미지 검증은 설치 직전에 다시 수행됩니다.`}),L(`div`,{class:`mt-4 flex flex-wrap justify-end gap-2`,children:[L(`button`,{class:`min-h-10 rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-extrabold text-slate-700`,onClick:()=>C(!1),type:`button`,children:`취소`}),L(`button`,{class:`min-h-10 rounded-lg bg-rose-700 px-3 py-2 text-xs font-extrabold text-white`,onClick:()=>{C(!1),f(b)},type:`button`,children:`설치 시작`})]})]}):L(`div`,{class:`mt-4 flex flex-wrap justify-end gap-2`,children:[L(`button`,{class:`min-h-10 rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-extrabold text-slate-700`,disabled:e===`discard`,onClick:()=>void u(),type:`button`,children:`파일 삭제`}),L(`button`,{class:`inline-flex min-h-10 items-center gap-2 rounded-lg bg-rose-700 px-4 py-2 text-xs font-extrabold text-white`,onClick:()=>C(!0),type:`button`,children:[L(Je,{class:`size-4`}),`펌웨어 설치`]})]})]})]})}):null]}),L(`details`,{class:`group mx-5 mb-5 rounded-xl border border-slate-200 bg-slate-50/70 sm:mx-6 sm:mb-6`,"data-layout":`firmware-card-subsection`,"data-section":`manual-firmware`,children:[L(`summary`,{class:`flex cursor-pointer list-none items-center justify-between gap-4 px-4 py-4 sm:px-5 [&::-webkit-details-marker]:hidden`,children:[L(`div`,{class:`flex min-w-0 items-center gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-slate-600 ring-1 ring-inset ring-slate-200`,children:L(it,{class:`size-5 rotate-180`})}),L(`div`,{class:`min-w-0`,children:[L(`strong`,{class:`block text-sm font-black text-slate-950`,children:`수동 펌웨어 설치`}),L(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`펌웨어 파일(.bin)을 직접 업로드하여 검증한 뒤 설치합니다.`})]})]}),L(`span`,{class:`shrink-0 rounded-full bg-white px-3 py-1.5 text-[11px] font-extrabold text-slate-600 ring-1 ring-inset ring-slate-200 group-open:hidden`,children:`열기`}),L(`span`,{class:`hidden shrink-0 rounded-full bg-white px-3 py-1.5 text-[11px] font-extrabold text-slate-600 ring-1 ring-inset ring-slate-200 group-open:inline-flex`,children:`닫기`})]}),L(`div`,{class:`border-t border-slate-200 px-5 pb-5 pt-3 sm:px-6`,children:[L(`p`,{class:`mt-0 mb-4 text-xs leading-5 text-slate-500 lg:whitespace-nowrap`,children:"이 장치에 맞는 Sysupgrade 이미지(.bin)만 사용하세요. 업로드한 파일은 이미지 검증과 `sysupgrade --test`를 모두 통과해야 설치할 수 있으며 강제 설치는 제공하지 않습니다."}),L(`div`,{class:`rounded-xl border border-slate-200 bg-white p-4 sm:p-5`,"data-layout":`firmware-file-picker`,children:[L(`input`,{accept:`.bin,application/octet-stream`,"aria-label":`펌웨어 파일 선택`,class:`sr-only`,disabled:w||T!==null,onChange:e=>y(e.currentTarget.files?.[0]??null),ref:g,type:`file`}),L(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between`,children:[L(`div`,{class:`flex min-w-0 items-center gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-50 text-slate-500 ring-1 ring-inset ring-slate-200`,children:L(it,{class:`size-5 rotate-180`})}),L(`div`,{class:`min-w-0`,children:[L(`strong`,{class:`block truncate text-sm font-black text-slate-900`,children:_?_.name:`펌웨어 파일을 선택하세요`}),L(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:_?`${Nr(_.size)} · Sysupgrade 이미지 (.bin)`:`이 장치에 맞는 Sysupgrade 이미지 (.bin)를 선택합니다.`})]})]}),L(`div`,{class:`flex shrink-0 flex-wrap gap-2 sm:justify-end`,children:[L(`button`,{class:`inline-flex min-h-10 items-center justify-center rounded-lg border border-slate-300 bg-white px-3.5 py-2 text-xs font-extrabold text-slate-700 transition hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50`,disabled:w||T!==null,onClick:()=>g.current?.click(),type:`button`,children:_?`다른 파일 선택`:`파일 선택`}),L(`button`,{class:`inline-flex min-h-10 items-center justify-center gap-2 rounded-lg bg-slate-900 px-3.5 py-2 text-xs font-extrabold text-white transition hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50`,disabled:!_||w||T!==null,onClick:()=>void M(),type:`button`,children:[O?L(z,{class:`size-4 animate-spin`}):L(it,{class:`size-4 rotate-180`}),O?`처리 중...`:`업로드 및 검증`]})]})]}),c===null?null:L(`div`,{class:`mt-4 border-t border-slate-100 pt-4`,children:[L(`div`,{class:`mb-2 flex items-center justify-between gap-3 text-xs font-bold text-slate-600`,children:[L(`span`,{children:`업로드 중`}),L(`span`,{children:[c,`%`]})]}),L(`div`,{class:`h-2 overflow-hidden rounded-full bg-slate-200`,children:L(`div`,{class:`h-full rounded-full bg-sky-600 transition-[width]`,style:{width:`${Math.max(0,Math.min(100,c))}%`}})})]})]})]})]})]})}var zr=Array.from({length:24},(e,t)=>{let n=String(t).padStart(2,`0`);return{value:n,label:`${n}시`}}),Br=Array.from({length:60},(e,t)=>{let n=String(t).padStart(2,`0`);return{value:n,label:`${n}분`}});function Vr(e){let t=/^([01]\d|2[0-3]):([0-5]\d)$/.exec(e);return{hour:t?.[1]??`03`,minute:t?.[2]??`00`}}function Hr({ariaLabel:e,className:t=``,disabled:n=!1,id:r,onChange:i,value:a,variant:o=`default`}){let{hour:s,minute:c}=Vr(a),l=(e,t)=>{i(`${e}:${t}`)};return L(`div`,{"aria-label":e,class:`ssh-time-select ${t}`.trim(),"data-time-select":r,role:`group`,children:[L(Pn,{ariaLabel:`${e} 시`,disabled:n,id:`${r}-hour`,onChange:e=>l(e,c),options:zr,value:s,variant:o}),L(Pn,{ariaLabel:`${e} 분`,disabled:n,id:`${r}-minute`,onChange:e=>l(s,e),options:Br,value:c,variant:o})]})}var Ur=7200;function Wr(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,Ur)}var Gr=[{value:3600,label:`1시간마다`},{value:21600,label:`6시간마다`},{value:43200,label:`12시간마다`},{value:86400,label:`24시간마다`}],Kr=`luci-app-smartsafehub`;function qr(e){return e?new Date(e*1e3).toLocaleString(`ko-KR`,{dateStyle:`medium`,timeStyle:`short`}):`아직 없음`}function Jr(e){return e===`stable`?`Stable`:e===`beta`?`Beta`:`미확인`}function Yr(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 Xr(e){return e.code===`UPDATES_INDEX_REFRESH_FAILED`?{title:`패키지 저장소를 확인하지 못했습니다.`,description:`OpenWrt 패키지 저장소 동기화 또는 네트워크 상태가 일시적으로 불안정할 수 있습니다. 잠시 후 업데이트 확인을 다시 실행해 주세요.`}:e.code===`UPDATES_INSTALL_FAILED`?{title:`관리 소프트웨어 업데이트 설치에 실패했습니다.`,description:`패키지 설치가 완료되지 않았습니다. 네트워크 연결과 저장 공간을 확인한 뒤 다시 시도해 주세요.`}:{title:`최근 업데이트 작업을 완료하지 못했습니다.`,description:`잠시 후 다시 시도해 주세요. 문제가 반복되면 상세 정보를 확인해 주세요.`}}function Zr(e,t){return e.phase===`checking`?`업데이트 확인 중`:e.phase===`installing`?`업데이트 설치 중`:e.phase===`error`?`확인 필요`:e.updateCount>0?`업데이트 가능`:t?`업데이트 확인 지연`:e.lastCheckAt?`최신 상태`:`확인 전`}function Qr(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 $r(e,t){return e.phase===`checking`||e.phase===`installing`?L(z,{class:`size-3.5 shrink-0 animate-spin`}):e.phase===`error`?L(B,{class:`size-3.5 shrink-0`}):e.updateCount>0?L(it,{class:`size-3.5 shrink-0`}):t?L(B,{class:`size-3.5 shrink-0`}):e.lastCheckAt?L(ut,{class:`size-3.5 shrink-0`}):L(tt,{class:`size-3.5 shrink-0`})}function ei({checked:e,disabled:t=!1,label:n,onChange:r}){return L(`button`,{"aria-checked":e,"aria-label":n,class:`ssh-switch-control relative inline-flex shrink-0 rounded-full border transition focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-not-allowed disabled:opacity-50 ${e?`border-teal-600 bg-teal-600`:`border-slate-300 bg-slate-200`}`,disabled:t,onClick:()=>r(!e),role:`switch`,type:`button`,children:L(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${e?`left-6`:`left-1`}`})})}function ti({action:e,actionError:t,data:n,firmware:r,error:i,loading:a,message:o,onCheck:s,onDismissFeedback:c,onInstall:l,onRetry:u,onSaveSettings:d}){let[f,p]=F(!0),[m,h]=F(21600),[g,_]=F(!1),[y,b]=F(`03:00`),[x,S]=F(!1),[C,w]=F(!1);I(()=>{n&&!x&&(p(n.settings.checkEnabled),h(n.settings.checkIntervalSeconds),_(n.settings.autoInstall),b(n.settings.autoInstallTime))},[n,x]);let T=e===`check`||n?.phase===`checking`,E=e===`install`||n?.phase===`installing`,D=T||E,O=Wr(n),k=n?.phase===`error`&&n.lastError?.code===`UPDATES_INDEX_REFRESH_FAILED`,A=n?.lastError?Xr(n.lastError):null,j=n?.packages.find(e=>e.name===Kr)??n?.packages[0]??null,M=r?.updateAvailable?r.release?.bundledSoftware?.smartsafehub:null,ee=!(!n?.lastCheckAt||n.updateCount!==0||O||j?.name!==Kr)&&Mr(M,j?.installedVersion)===`same`,te=(e={})=>n?(e.checkEnabled??f)!==n.settings.checkEnabled||(e.checkIntervalSeconds??m)!==n.settings.checkIntervalSeconds||(e.autoInstall??g)!==n.settings.autoInstall||(e.autoInstallTime??y)!==n.settings.autoInstallTime:!1,ne=async()=>{await d({checkEnabled:f,checkIntervalSeconds:m,autoInstall:g,autoInstallTime:y})&&S(!1)};return L(`article`,{class:`min-w-0 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm shadow-slate-900/5 ${n?`grid xl:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]`:``}`,"data-component":`management-software-update-card`,"data-layout":`management-software-sections`,children:[L(`section`,{class:`min-w-0 xl:col-start-1 xl:row-start-1`,"data-section":`software-update-status`,children:[L(`div`,{class:`p-5 sm:p-6`,children:[L(`div`,{class:`flex flex-col gap-5 lg:flex-row lg:items-start lg:justify-between`,children:[L(`div`,{class:`flex min-w-0 gap-4`,children:[L(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-teal-50 text-teal-700`,children:L(Je,{class:`size-6`})}),L(`div`,{class:`min-w-0`,children:[L(`div`,{class:`flex flex-wrap items-center gap-2`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700`,children:`Management software`}),n?L(`span`,{class:`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-extrabold ring-1 ring-inset ${Qr(n,O)}`,children:[L(`span`,{"aria-hidden":`true`,class:`flex items-center justify-center`,children:$r(n,O)}),Zr(n,O)]}):null]}),L(`h2`,{class:`mt-2 mb-0 text-xl font-black text-slate-950 sm:text-2xl`,children:`관리 소프트웨어 업데이트`}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500 xl:whitespace-nowrap`,children:`새 버전을 확인하고 안전하게 설치합니다.`})]})]}),n?L(`div`,{class:`flex shrink-0 flex-col gap-2 sm:flex-row lg:justify-end`,children:[L(`button`,{class:`inline-flex min-h-11 items-center justify-center gap-2 rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-extrabold text-slate-800 transition hover:bg-slate-50 disabled:cursor-wait disabled:opacity-60`,disabled:D,onClick:s,type:`button`,children:[L(z,{class:`size-4 ${T?`animate-spin`:``}`}),T?`확인 중...`:`업데이트 확인`]}),n.updateCount>0&&!C?L(`button`,{class:`inline-flex min-h-11 items-center justify-center gap-2 rounded-xl bg-teal-700 px-4 py-2.5 text-sm font-extrabold text-white transition hover:bg-teal-800 disabled:cursor-wait disabled:opacity-60`,disabled:D,onClick:()=>w(!0),type:`button`,children:[E?L(z,{class:`size-4 animate-spin`}):L(it,{class:`size-4`}),E?`설치 중...`:`업데이트 설치`]}):null]}):null]}),(t||o)&&L(`div`,{class:`mt-5 flex flex-col gap-3 rounded-xl border px-4 py-3 text-sm font-bold sm:flex-row sm:items-center sm:justify-between ${t?`border-rose-200 bg-rose-50 text-rose-800`:`border-emerald-200 bg-emerald-50 text-emerald-800`}`,children:[L(`span`,{children:t||o}),L(`button`,{class:`shrink-0 self-start rounded-lg px-3 py-1.5 text-xs font-extrabold hover:bg-black/5 sm:self-auto`,onClick:c,type:`button`,children:`닫기`})]}),E?L(`div`,{"aria-busy":`true`,"aria-live":`polite`,class:`mt-5 rounded-xl border border-sky-200 bg-sky-50 p-4 sm:p-5`,children:[L(`div`,{class:`flex items-start gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-sky-700 ring-1 ring-inset ring-sky-200`,children:L(z,{"aria-hidden":`true`,class:`size-5 animate-spin`})}),L(`div`,{class:`min-w-0`,children:[L(`h3`,{class:`m-0 text-sm font-black text-sky-950`,children:`관리 소프트웨어를 업데이트하고 있습니다.`}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-sky-800`,children:`설치가 완료되면 이 화면이 자동으로 갱신됩니다. 작업 중에는 웹 연결이 잠시 끊길 수 있습니다.`})]})]}),L(`div`,{"aria-hidden":`true`,class:`ssh-update-progress-track mt-4`,children:L(`span`,{class:`ssh-update-progress-bar`})})]}):null,i&&!n?L(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-800`,children:[L(`p`,{class:`m-0 font-bold`,children:i}),L(`button`,{class:`mt-3 rounded-lg border border-rose-300 bg-white px-3 py-2 text-xs font-extrabold text-rose-700`,onClick:u,type:`button`,children:`다시 시도`})]}):a&&!n?L(`div`,{class:`mt-5 flex items-center gap-3 rounded-xl bg-slate-50 p-4 text-sm font-bold text-slate-500`,children:[L(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-xl bg-white text-teal-700 ring-1 ring-inset ring-slate-200`,children:L(z,{"aria-hidden":`true`,class:`size-4 animate-spin`})}),L(`span`,{children:`관리 소프트웨어 업데이트 상태를 확인하고 있습니다.`})]}):n?L(v,{children:[n.lastError&&A?L(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 sm:p-5`,children:L(`div`,{class:`flex items-start gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-rose-700 ring-1 ring-inset ring-rose-200`,children:L(B,{class:`size-5`})}),L(`div`,{class:`min-w-0 flex-1`,children:[L(`p`,{class:`m-0 text-sm font-extrabold text-rose-950`,children:A.title}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-rose-800`,children:A.description}),n.lastError.at?L(`p`,{class:`mt-2 mb-0 text-xs font-bold text-rose-700`,children:[`발생 시각 `,qr(n.lastError.at)]}):null,L(`details`,{class:`mt-3 rounded-lg border border-rose-200 bg-white p-3`,children:[L(`summary`,{class:`cursor-pointer text-xs font-extrabold text-rose-800`,children:`상세 정보 보기`}),L(`div`,{class:`mt-3 grid gap-2 text-xs leading-5 text-slate-600`,children:[L(`p`,{class:`m-0`,children:[`오류 코드 `,L(`strong`,{class:`font-black text-slate-900`,children:n.lastError.code})]}),L(`pre`,{class:`m-0 max-h-40 overflow-auto whitespace-pre-wrap break-words rounded-lg bg-slate-50 p-3 font-mono text-[11px] leading-5 text-slate-700`,children:n.lastError.message})]})]})]})]})}):null,C?L(`div`,{class:`mt-5 flex flex-col gap-3 rounded-xl border border-amber-200 bg-amber-50 p-4 sm:flex-row sm:items-center sm:justify-between`,children:[L(`div`,{class:`min-w-0`,children:[L(`strong`,{class:`block text-sm font-black text-amber-900`,children:`관리 소프트웨어를 지금 업데이트할까요?`}),L(`span`,{class:`mt-1 block text-xs leading-5 text-amber-800`,children:`설치 중 SmartSafeHub 웹 화면 연결이 잠시 끊길 수 있습니다.`})]}),L(`div`,{class:`flex shrink-0 gap-2`,children:[L(`button`,{class:`min-h-10 rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-extrabold text-slate-700`,disabled:D,onClick:()=>w(!1),type:`button`,children:`취소`}),L(`button`,{class:`min-h-10 rounded-lg bg-amber-700 px-3 py-2 text-xs font-extrabold text-white disabled:opacity-60`,disabled:D,onClick:()=>{w(!1),l()},type:`button`,children:`설치 시작`})]})]}):null]}):null]}),n?L(`div`,{class:`px-5 pb-5 sm:px-6 sm:pb-6`,children:[L(`h3`,{class:`mb-3 text-sm font-black text-slate-950`,children:`현재 상태`}),L(`dl`,{class:`grid overflow-hidden rounded-xl border border-slate-200 bg-slate-200 sm:grid-cols-2`,children:[L(`div`,{class:`bg-white p-4`,children:[L(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Installed`}),L(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black text-slate-950`,children:j?.installedVersion||`미확인`})]}),L(`div`,{class:`bg-white p-4`,children:[L(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Available`}),L(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black ${j?.updateAvailable||O?`text-amber-700`:`text-slate-950`}`,children:!n.lastCheckAt||k&&!j?.updateAvailable?`미확인`:j?.updateAvailable&&j.availableVersion?j.availableVersion:O?`확인 필요`:`최신 버전`})]}),L(`div`,{class:`bg-white p-4`,children:[L(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Last check`}),L(`dd`,{class:`mt-2 mb-0 ml-0 text-sm font-black text-slate-950`,children:L(Dr,{fallback:`아직 없음`,timestamp:n.lastCheckAt})})]}),L(`div`,{class:`bg-white p-4`,children:[L(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Auto install`}),L(`dd`,{class:`mt-2 mb-0 ml-0 text-sm font-black text-slate-950`,children:n.settings.autoInstall?`켜짐 · ${n.settings.autoInstallTime}`:`꺼짐`})]})]})]}):null,n&&n.updateCount===0&&n.lastCheckAt?O?L(`section`,{class:`mx-5 mb-5 rounded-xl border border-amber-200 bg-amber-50 p-4 sm:mx-6 sm:mb-6 sm:p-5`,"data-section":`software-update-result`,children:L(`div`,{class:`flex gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-amber-700 ring-1 ring-inset ring-amber-200`,children:L(B,{class:`size-5`})}),L(`div`,{children:[L(`h3`,{class:`m-0 text-base font-black text-amber-950`,children:`업데이트 확인이 지연되고 있습니다.`}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:[`마지막 업데이트 확인: `,L(Dr,{timestamp:n.lastCheckAt}),`. 자동 확인 상태를 점검하거나 업데이트 확인을 실행해 주세요.`]})]})]})}):L(`section`,{class:`mx-5 mb-5 rounded-xl border border-emerald-200 bg-emerald-50 p-4 sm:mx-6 sm:mb-6 sm:p-5`,"data-section":`software-update-result`,children:L(`div`,{class:`flex gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-emerald-700`,children:L(ut,{class:`size-5`})}),L(`div`,{children:[L(`h3`,{class:`m-0 text-base font-black text-emerald-950`,children:`최신 버전을 사용 중입니다.`}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-emerald-800`,children:[`마지막 업데이트 확인: `,L(Dr,{timestamp:n.lastCheckAt}),`.`]}),ee?L(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-emerald-800`,"data-section":`software-firmware-match`,children:[`새 펌웨어에도 동일한 관리 소프트웨어(`,M,`)가 포함되어 있어 별도로 업데이트할 필요가 없습니다.`]}):null]})]})}):null,n&&n.updateCount===0&&!n.lastCheckAt?L(`section`,{class:`mx-5 mb-5 rounded-xl border border-slate-200 bg-slate-50 p-4 sm:mx-6 sm:mb-6 sm:p-5`,"data-section":`software-update-result`,children:L(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between`,children:[L(`div`,{class:`flex min-w-0 gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-slate-500 ring-1 ring-inset ring-slate-200`,children:L(z,{class:`size-5`})}),L(`div`,{class:`min-w-0`,children:[L(`h3`,{class:`m-0 text-base font-black text-slate-900`,children:`업데이트 상태를 아직 확인하지 않았습니다.`}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`업데이트 확인을 실행하면 현재 설치 버전과 사용 가능한 최신 버전을 확인합니다.`})]})]}),L(`button`,{class:`inline-flex min-h-10 shrink-0 items-center justify-center gap-2 rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-extrabold text-slate-800 transition hover:border-teal-300 hover:text-teal-700 disabled:cursor-wait disabled:opacity-60`,disabled:D,onClick:s,type:`button`,children:[L(z,{class:`size-4`}),`지금 확인`]})]})}):null]}),n?L(v,{children:[L(`section`,{class:`min-w-0 border-t border-slate-200 p-5 sm:p-6 xl:col-start-2 xl:row-start-1 xl:row-span-2 xl:self-stretch xl:border-t-0 xl:border-l`,"data-section":`software-update-settings`,children:[L(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[L(`div`,{class:`flex min-w-0 gap-4`,children:[L(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-600`,children:L(Ye,{class:`size-5`})}),L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Settings`}),L(`h3`,{class:`mt-2 mb-0 text-lg font-black text-slate-950`,children:`자동 업데이트`}),L(`p`,{class:`mt-2 mb-0 max-w-3xl text-sm leading-6 text-slate-500`,children:`관리 소프트웨어의 자동 확인과 자동 설치를 설정합니다.`})]})]}),x?L(`span`,{"aria-live":`polite`,class:`inline-flex shrink-0 self-start items-center gap-1.5 rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs font-extrabold text-amber-800`,role:`status`,children:[L(B,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}):null]}),L(`div`,{class:`ssh-software-settings-grid mt-4`,children:[L(`div`,{class:`ssh-software-settings-channel rounded-xl border border-slate-200 p-4 sm:p-5`,children:L(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[L(`div`,{class:`min-w-0`,children:[L(`strong`,{class:`block text-sm font-black text-slate-950`,children:`업데이트 채널`}),L(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`현재 관리 소프트웨어 업데이트를 확인하는 배포 채널입니다.`})]}),L(`span`,{class:`inline-flex min-h-9 shrink-0 items-center justify-center rounded-full px-3 py-1.5 text-xs font-extrabold ring-1 ring-inset ${Yr(n.settings.channel)}`,title:`현재 업데이트 확인 채널`,children:Jr(n.settings.channel)})]})}),L(`div`,{class:`min-w-0 overflow-hidden rounded-xl border border-slate-200 p-4 sm:p-5`,children:[L(`div`,{class:`flex min-w-0 items-start justify-between gap-4`,children:[L(`div`,{class:`min-w-0`,children:L(`strong`,{class:`block text-sm font-black text-slate-950`,children:`자동으로 업데이트 확인`})}),L(ei,{checked:f,label:`자동 업데이트 확인`,onChange:e=>{p(e),S(te({checkEnabled:e}))}})]}),L(`div`,{class:`mt-4 block`,children:[L(`span`,{class:`text-xs font-extrabold text-slate-700`,children:`확인 주기`}),L(Pn,{ariaLabel:`업데이트 확인 주기`,className:`mt-2`,disabled:!f,id:`software-update-check-interval`,onChange:e=>{let t=Number(e);h(t),S(te({checkIntervalSeconds:t}))},options:Gr.map(e=>({value:String(e.value),label:e.label})),value:String(m),variant:`emphasized`})]})]}),L(`div`,{class:`min-w-0 overflow-hidden rounded-xl border border-slate-200 p-4 sm:p-5`,children:[L(`div`,{class:`flex min-w-0 items-start justify-between gap-4`,children:[L(`div`,{class:`min-w-0`,children:L(`strong`,{class:`block text-sm font-black text-slate-950`,children:`자동으로 업데이트 설치`})}),L(ei,{checked:g,label:`자동 업데이트 설치`,onChange:e=>{_(e),S(te({autoInstall:e}))}})]}),L(`div`,{class:`mt-4 min-w-0`,children:[L(`span`,{class:`text-xs font-extrabold text-slate-700`,children:`설치 시각`}),L(Hr,{ariaLabel:`자동 업데이트 설치 시각`,className:`mt-2`,disabled:!g,id:`software-update-auto-install-time`,onChange:e=>{b(e),S(te({autoInstallTime:e}))},value:y,variant:`emphasized`})]})]})]}),g?L(`p`,{class:`mt-4 mb-0 rounded-xl bg-amber-50 px-4 py-3 text-xs font-bold leading-5 text-amber-800`,children:`자동 설치 중에는 SmartSafeHub 웹 화면 연결이 잠시 끊길 수 있습니다.`}):null,L(`div`,{class:`mt-5 flex flex-col gap-3 border-t border-slate-100 pt-5 sm:flex-row sm:items-center sm:justify-between`,children:[L(`p`,{class:`m-0 text-xs leading-5 text-slate-500`,children:[`마지막 설치: `,qr(n.lastInstallAt)]}),L(`button`,{class:`inline-flex min-h-11 items-center justify-center rounded-xl bg-slate-900 px-4 py-2.5 text-sm font-extrabold text-white transition hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50`,disabled:!x||e===`settings`||D,onClick:()=>void ne(),type:`button`,children:e===`settings`?`저장 중`:`자동 업데이트 설정 저장`})]})]}),n.updateCount>0?L(`section`,{class:`border-t border-slate-200 p-5 sm:p-6 xl:col-start-1 xl:row-start-2`,"data-section":`software-update-release-notes`,children:[L(`div`,{class:`flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700`,children:`Release notes`}),L(`h3`,{class:`mt-2 mb-0 text-lg font-black text-slate-950`,children:`업데이트 내용`}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500`,children:`현재 설치된 버전 이후 포함되는 주요 변경 사항입니다.`})]}),n.releaseNotes.length?L(`span`,{class:`shrink-0 rounded-full bg-slate-100 px-3 py-1.5 text-xs font-extrabold text-slate-600`,children:n.releaseNotes.length===1?`1개 릴리즈`:`${n.releaseNotes.length}개 릴리즈`}):null]}),n.releaseNotes.length?L(`div`,{class:`mt-5 grid gap-3`,children:[n.releaseNotes.map(e=>L(`article`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4 sm:p-5`,children:[L(`div`,{class:`flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-4`,children:[L(`strong`,{class:`break-all text-sm font-black text-slate-950`,children:e.version}),e.date?L(`span`,{class:`text-xs font-bold text-slate-500`,children:e.date}):null]}),e.summary?L(`p`,{class:`mt-3 mb-0 text-sm leading-6 text-slate-700`,children:e.summary}):null,e.sections.length?L(`details`,{class:`mt-4 rounded-lg border border-slate-200 bg-white p-3`,open:n.releaseNotes.length===1,children:[L(`summary`,{class:`cursor-pointer text-xs font-extrabold text-slate-800`,children:`자세한 변경 사항`}),L(`div`,{class:`mt-3 grid gap-4`,children:e.sections.map(t=>L(`section`,{children:[L(`h4`,{class:`m-0 text-xs font-extrabold text-slate-700`,children:t.title}),L(`ul`,{class:`mt-2 mb-0 space-y-1.5 pl-5 text-xs leading-5 text-slate-600`,children:t.items.map((n,r)=>L(`li`,{children:n},`${e.version}-${t.title}-${r}`))})]},`${e.version}-${t.title}`))})]}):null]},e.version)),n.releaseNotesComplete?null:L(`p`,{class:`m-0 rounded-xl bg-amber-50 px-4 py-3 text-xs font-bold leading-5 text-amber-800`,children:`일부 릴리즈 노트를 불러오지 못했습니다. 업데이트 확인과 설치는 계속 사용할 수 있습니다.`})]}):L(`p`,{class:`mt-4 mb-0 rounded-xl bg-slate-50 px-4 py-3 text-sm text-slate-500`,children:`릴리즈 노트를 불러오지 못했습니다. 업데이트 확인과 설치는 계속 사용할 수 있습니다.`})]}):null]}):null]})}function ni({action:e,actionError:t,data:n,error:r,firmware:i,loading:a,message:o,onCheck:s,onDismissFeedback:c,onInstall:l,onRetry:u,onSaveSettings:d}){return L(`section`,{class:`min-w-0 space-y-5`,children:[L(`div`,{children:L(V,{eyebrow:`Firmware`,title:`시스템 펌웨어`,description:`공유기 운영체제의 새 버전을 확인하고 설치합니다.`})}),L(Rr,{action:i.action,actionError:i.actionError,data:i.data,software:n,error:i.error,loading:i.loading,message:i.message,reconnecting:i.reconnecting,uploadProgress:i.uploadProgress,onCheck:()=>void i.check(),onDiscard:i.discard,onDismissFeedback:i.dismissFeedback,onInstall:i.install,onPrepare:()=>void i.prepare(),onRetry:()=>void i.refresh(),onUpload:i.upload}),L(`div`,{class:`pt-2`,children:L(V,{eyebrow:`Software`,title:`관리 소프트웨어`,description:`SmartSafeHub 관리 화면 및 서비스의 업데이트를 확인합니다.`})}),L(ti,{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 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`,"#system":`system`,"#settings":`settings`,"#settings/diagnostics":`settings`,"#settings/time":`settings`,"#settings/management":`settings`};function ii(){return ri[window.location.hash]??`home`}function ai(){let[e,t]=F(ii);return I(()=>{let e=()=>t(ii());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),e}function oi(e){return e===`#lan`||e===`#network/lan`?`lan`:`internet`}function si(e){return e===`#settings/time`?`time`:e===`#settings/management`?`management`:`diagnostics`}function ci(){let e=window.location.hash;return{network:oi(e),settings:si(e)}}function li(){let[e,t]=F(ci);return I(()=>{let e=()=>t(ci());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),e}function ui({active:e,label:t,tabs:n}){return L(`nav`,{"aria-label":t,class:`mb-5 min-w-0`,role:`tablist`,children:L(`div`,{class:`grid w-full min-w-0 gap-2 ${n.length===3?`grid-cols-3 sm:max-w-[592px]`:`grid-cols-2 sm:max-w-[392px]`}`,children:n.map(t=>{let r=t.id===e;return L(`a`,{"aria-controls":`${t.id}-panel`,"aria-selected":r,class:`flex min-h-11 min-w-0 items-center justify-center rounded-xl border px-2 py-2.5 text-center text-[11px] font-extrabold leading-5 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-500 sm:px-4 sm:text-sm ${r?`border-teal-200 bg-teal-50 text-teal-800`:`border-slate-200 bg-white text-slate-600 hover:border-teal-200 hover:text-teal-700`}`,href:t.href,id:`${t.id}-tab`,role:`tab`,tabIndex:r?0:-1,onKeyDown:e=>{if(![`ArrowLeft`,`ArrowRight`,`Home`,`End`].includes(e.key))return;e.preventDefault();let r=n.findIndex(e=>e.id===t.id),i=e.key===`Home`?0:e.key===`End`?n.length-1:(r+(e.key===`ArrowRight`?1:-1)+n.length)%n.length,a=e.currentTarget.parentElement?.children[i];a?.focus(),a?.click()},children:t.label},t.id)})})})}var di=[{value:`all`,label:`전체`},{value:`online`,label:`현재 연결`},{value:`wifi`,label:`Wi-Fi`},{value:`ethernet`,label:`유선/기타`},{value:`offline`,label:`최근 기기`}],fi=new Intl.Collator(`ko`),pi=new Intl.DateTimeFormat(`ko-KR`,{month:`numeric`,day:`numeric`,hour:`2-digit`,minute:`2-digit`});function mi(e){switch(e){case`wifi`:return`Wi-Fi`;case`ethernet`:return`유선/기타`;default:return`연결 방식 미확인`}}function hi(e){return e.hostname??e.ipv4Address??`이름 없는 기기`}function gi(e){return e===null?null:e>=-50?`매우 좋음`:e>=-60?`좋음`:e>=-70?`보통`:`약함`}function _i(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 vi(e){return e.leaseActive?e.leaseExpiresAt===null?`DHCP 임대 활성`:`DHCP ${pi.format(new Date(e.leaseExpiresAt*1e3))}까지`:null}function yi(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 bi({device:e}){return L(`span`,{class:`inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-extrabold ring-1 ring-inset ${e.online?`bg-emerald-50 text-emerald-700 ring-emerald-200`:`bg-slate-100 text-slate-600 ring-slate-200`}`,children:[L(`span`,{class:`size-2 rounded-full ${e.online?`bg-emerald-500`:`bg-slate-400`}`}),e.online?`현재 연결`:`최근 임대`]})}function xi({device:e}){let t=gi(e.signalDbm),n=_i(e.connectedSeconds),r=vi(e);return L(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:[L(`div`,{class:`flex min-w-0 flex-col items-start gap-4 sm:flex-row sm:justify-between`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3`,children:[L(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl ${e.connection===`wifi`?`bg-teal-50 text-teal-700`:e.connection===`ethernet`?`bg-blue-50 text-blue-700`:`bg-slate-100 text-slate-600`}`,children:e.connection===`wifi`?L(Xe,{class:`size-6`}):e.connection===`ethernet`?L(Qe,{class:`size-6`}):L(Ge,{class:`size-6`})}),L(`div`,{class:`min-w-0`,children:[L(`h3`,{class:`m-0 break-words text-base font-extrabold text-slate-950`,children:hi(e)}),L(`p`,{class:`mt-1 mb-0 break-all font-mono text-xs leading-5 text-slate-500`,children:e.mac})]})]}),L(`span`,{class:`shrink-0 self-start`,children:L(bi,{device:e})})]}),L(`dl`,{class:`mt-5 grid gap-3 text-sm sm:grid-cols-2`,children:[L(`div`,{class:`rounded-xl bg-slate-50 p-3.5`,children:[L(`dt`,{class:`text-xs font-bold text-slate-500`,children:`IP 주소`}),L(`dd`,{class:`mt-1.5 mb-0 ml-0 font-extrabold text-slate-900`,children:e.ipv4Address??`확인되지 않음`})]}),L(`div`,{class:`rounded-xl bg-slate-50 p-3.5`,children:[L(`dt`,{class:`text-xs font-bold text-slate-500`,children:`연결 방식`}),L(`dd`,{class:`mt-1.5 mb-0 ml-0 font-extrabold text-slate-900`,children:mi(e.connection)})]})]}),L(`div`,{class:`mt-4 flex flex-wrap gap-2 text-xs font-bold text-slate-600`,children:[e.ssid?L(`span`,{class:`rounded-full bg-teal-50 px-3 py-1.5 text-teal-800`,children:[e.ssid,e.bandLabel?` · ${e.bandLabel}`:``]}):null,t?L(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:[`신호 `,t,` · `,e.signalDbm,` dBm`]}):null,n?L(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:[`연결 `,n]}):null,r?L(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:r}):null,e.interface?L(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:e.interface}):null]})]})}function Si({data:e,error:t,loading:n,onRetry:r}){let[i,a]=F(``),[o,s]=F(`all`),c=Te(()=>{if(!e)return[];let t=i.trim().toLocaleLowerCase();return[...e.devices].filter(e=>yi(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?fi.compare(hi(e),hi(t)):e.connection===`wifi`?-1:1:e.online?-1:1)},[e,o,i]);return n?L(zn,{}):t?L(Bn,{message:t,onRetry:r}):e?L(v,{children:[L(V,{eyebrow:`Overview`,title:`연결 기기 현황`,description:`네트워크에 연결되었거나 최근 확인된 기기를 요약합니다.`}),L(`section`,{"aria-label":`연결 기기 요약`,class:`grid gap-4 sm:grid-cols-2 xl:grid-cols-4`,children:[L(Ci,{description:`${e.totals.offline}대는 최근 DHCP 임대에서 확인`,icon:L(Ge,{class:`size-6`}),label:`확인된 기기`,value:e.totals.known}),L(Ci,{description:`Wi-Fi 또는 ARP에서 현재 확인됨`,icon:L(tt,{class:`size-6`}),label:`현재 연결`,value:e.totals.online}),L(Ci,{description:`무선 AP에 연결된 기기`,icon:L(Xe,{class:`size-6`}),label:`Wi-Fi`,value:e.totals.wireless}),L(Ci,{description:`LAN ARP에서 확인된 비무선 기기`,icon:L(Qe,{class:`size-6`}),label:`유선/기타`,value:e.totals.ethernet})]}),L(`section`,{class:`mt-7`,"aria-label":`기기 목록 및 필터`,children:[L(V,{eyebrow:`Device inventory`,title:`기기 목록`,description:`연결된 기기를 검색하고 무선·유선 상태별로 확인합니다.`}),L(`div`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:L(`div`,{class:`flex min-w-0 flex-col gap-3 lg:flex-row lg:items-center lg:justify-between`,children:[L(`div`,{class:`ssh-touch-scroll flex min-w-0 gap-2 overflow-x-auto pb-1 lg:flex-1`,role:`group`,"aria-label":`기기 필터`,children:di.map(e=>L(`button`,{"aria-pressed":o===e.value,class:`min-h-10 shrink-0 rounded-xl border px-4 py-2 text-sm font-extrabold transition ${o===e.value?`border-teal-700 bg-teal-700 text-white`:`border-slate-200 bg-white text-slate-600 hover:bg-slate-50 hover:text-slate-950`}`,onClick:()=>s(e.value),type:`button`,children:e.label},e.value))}),L(`label`,{class:`relative block w-full lg:w-72 lg:shrink-0 xl:w-80`,children:[L(`span`,{class:`sr-only`,children:`기기 검색`}),L(`span`,{class:`pointer-events-none absolute inset-y-0 left-0 flex w-11 items-center justify-center text-slate-400`,children:L(pt,{class:`size-5`})}),L(`input`,{class:`min-h-11 w-full rounded-xl border-2 border-slate-300 bg-slate-50 py-2.5 pr-4 pl-11 text-sm font-semibold text-slate-950 shadow-inner outline-none transition placeholder:text-slate-400 focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100`,onInput:e=>a(e.currentTarget.value),placeholder:`이름, IP, MAC 또는 SSID 검색`,type:`search`,value:i})]})]})})]}),c.length?L(`section`,{"aria-live":`polite`,class:`mt-5 grid gap-4 lg:grid-cols-2`,children:c.map(e=>L(xi,{device:e},e.id))}):L(`section`,{class:`mt-5 rounded-2xl border border-dashed border-slate-300 bg-white p-5 text-center sm:p-8`,children:[L(Ge,{class:`mx-auto size-10 text-slate-400`}),L(`h2`,{class:`mt-4 mb-0 text-lg font-extrabold text-slate-950`,children:`조건에 맞는 기기가 없습니다`}),L(`p`,{class:`mt-2 mb-0 text-sm text-slate-600`,children:`검색어 또는 필터를 변경해 주세요.`})]}),L(`p`,{class:`mt-5 mb-0 rounded-xl bg-slate-100 px-4 py-3 text-xs leading-5 text-slate-600`,children:`유선/기타 상태는 ARP 테이블을 기준으로 하며, 최근 DHCP 임대는 기기가 연결 해제된 뒤에도 일정 시간 남을 수 있습니다.`})]}):null}function Ci({description:e,icon:t,label:n,value:r}){return L(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:[L(`div`,{class:`flex items-start justify-between gap-4`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-xs font-bold uppercase tracking-[0.14em] text-slate-500`,children:n}),L(`p`,{class:`mt-3 mb-0 text-3xl font-black tracking-tight text-slate-950`,children:r})]}),L(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:t})]}),L(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:e})]})}function wi(e){return e+.5|0}var Ti=(e,t,n)=>Math.max(Math.min(e,n),t);function Ei(e){return Ti(wi(e*2.55),0,255)}function Di(e){return Ti(wi(e*255),0,255)}function Oi(e){return Ti(wi(e/2.55)/100,0,1)}function ki(e){return Ti(wi(e*100),0,100)}var Ai={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},ji=[...`0123456789ABCDEF`],Mi=e=>ji[e&15],Ni=e=>ji[(e&240)>>4]+ji[e&15],Pi=e=>(e&240)>>4==(e&15),Fi=e=>Pi(e.r)&&Pi(e.g)&&Pi(e.b)&&Pi(e.a);function Ii(e){var t=e.length,n;return e[0]===`#`&&(t===4||t===5?n={r:255&Ai[e[1]]*17,g:255&Ai[e[2]]*17,b:255&Ai[e[3]]*17,a:t===5?Ai[e[4]]*17:255}:(t===7||t===9)&&(n={r:Ai[e[1]]<<4|Ai[e[2]],g:Ai[e[3]]<<4|Ai[e[4]],b:Ai[e[5]]<<4|Ai[e[6]],a:t===9?Ai[e[7]]<<4|Ai[e[8]]:255})),n}var Li=(e,t)=>e<255?t(e):``;function Ri(e){var t=Fi(e)?Mi:Ni;return e?`#`+t(e.r)+t(e.g)+t(e.b)+Li(e.a,t):void 0}var zi=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Bi(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 Vi(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 Hi(e,t,n){let r=Bi(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 Ui(e,t,n,r,i){return e===i?(t-n)/r+(t.5?l/(2-i-a):l/(i+a),s=Ui(t,n,r,l,i),s=s*60+.5),[s|0,c||0,o]}function Gi(e,t,n,r){return(Array.isArray(t)?e(t[0],t[1],t[2]):e(t,n,r)).map(Di)}function Ki(e,t,n){return Gi(Bi,e,t,n)}function qi(e,t,n){return Gi(Hi,e,t,n)}function Ji(e,t,n){return Gi(Vi,e,t,n)}function Yi(e){return(e%360+360)%360}function Xi(e){let t=zi.exec(e),n=255,r;if(!t)return;t[5]!==r&&(n=t[6]?Ei(+t[5]):Di(+t[5]));let i=Yi(+t[2]),a=t[3]/100,o=t[4]/100;return r=t[1]===`hwb`?qi(i,a,o):t[1]===`hsv`?Ji(i,a,o):Ki(i,a,o),{r:r[0],g:r[1],b:r[2],a:n}}function Zi(e,t){var n=Wi(e);n[0]=Yi(n[0]+t),n=Ki(n),e.r=n[0],e.g=n[1],e.b=n[2]}function Qi(e){if(!e)return;let t=Wi(e),n=t[0],r=ki(t[1]),i=ki(t[2]);return e.a<255?`hsla(${n}, ${r}%, ${i}%, ${Oi(e.a)})`:`hsl(${n}, ${r}%, ${i}%)`}var $i={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`},ea={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 ta(){let e={},t=Object.keys(ea),n=Object.keys($i),r,i,a,o,s;for(r=0;r>16&255,a>>8&255,a&255]}return e}var na;function ra(e){na||(na=ta(),na.transparent=[0,0,0,0]);let t=na[e.toLowerCase()];return t&&{r:t[0],g:t[1],b:t[2],a:t.length===4?t[3]:255}}var ia=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;function aa(e){let t=ia.exec(e),n=255,r,i,a;if(t){if(t[7]!==r){let e=+t[7];n=t[8]?Ei(e):Ti(e*255,0,255)}return r=+t[1],i=+t[3],a=+t[5],r=255&(t[2]?Ei(r):Ti(r,0,255)),i=255&(t[4]?Ei(i):Ti(i,0,255)),a=255&(t[6]?Ei(a):Ti(a,0,255)),{r,g:i,b:a,a:n}}}function oa(e){return e&&(e.a<255?`rgba(${e.r}, ${e.g}, ${e.b}, ${Oi(e.a)})`:`rgb(${e.r}, ${e.g}, ${e.b})`)}var sa=e=>e<=.0031308?e*12.92:e**(1/2.4)*1.055-.055,ca=e=>e<=.04045?e/12.92:((e+.055)/1.055)**2.4;function la(e,t,n){let r=ca(Oi(e.r)),i=ca(Oi(e.g)),a=ca(Oi(e.b));return{r:Di(sa(r+n*(ca(Oi(t.r))-r))),g:Di(sa(i+n*(ca(Oi(t.g))-i))),b:Di(sa(a+n*(ca(Oi(t.b))-a))),a:e.a+n*(t.a-e.a)}}function ua(e,t,n){if(e){let r=Wi(e);r[t]=Math.max(0,Math.min(r[t]+r[t]*n,t===0?360:1)),r=Ki(r),e.r=r[0],e.g=r[1],e.b=r[2]}}function da(e,t){return e&&Object.assign(t||{},e)}function fa(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=Di(e[3]))):(t=da(e,{r:0,g:0,b:0,a:1}),t.a=Di(t.a)),t}function pa(e){return e.charAt(0)===`r`?aa(e):Xi(e)}var ma=class e{constructor(t){if(t instanceof e)return t;let n=typeof t,r;n===`object`?r=fa(t):n===`string`&&(r=Ii(t)||ra(t)||pa(t)),this._rgb=r,this._valid=!!r}get valid(){return this._valid}get rgb(){var e=da(this._rgb);return e&&(e.a=Oi(e.a)),e}set rgb(e){this._rgb=fa(e)}rgbString(){return this._valid?oa(this._rgb):void 0}hexString(){return this._valid?Ri(this._rgb):void 0}hslString(){return this._valid?Qi(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=la(this._rgb,e._rgb,t)),this}clone(){return new e(this.rgb)}alpha(e){return this._rgb.a=Di(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=wi(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 ua(this._rgb,2,e),this}darken(e){return ua(this._rgb,2,-e),this}saturate(e){return ua(this._rgb,1,e),this}desaturate(e){return ua(this._rgb,1,-e),this}rotate(e){return Zi(this._rgb,e),this}};function ha(){}var ga=(()=>{let e=0;return()=>e++})();function U(e){return e==null}function W(e){if(Array.isArray&&Array.isArray(e))return!0;let t=Object.prototype.toString.call(e);return t.slice(0,7)===`[object`&&t.slice(-6)===`Array]`}function G(e){return e!==null&&Object.prototype.toString.call(e)===`[object Object]`}function _a(e){return(typeof e==`number`||e instanceof Number)&&isFinite(+e)}function va(e,t){return _a(e)?e:t}function K(e,t){return e===void 0?t:e}var ya=(e,t)=>typeof e==`string`&&e.endsWith(`%`)?parseFloat(e)/100:+e/t,ba=(e,t)=>typeof e==`string`&&e.endsWith(`%`)?parseFloat(e)/100*t:+e;function q(e,t,n){if(e&&typeof e.call==`function`)return e.apply(n,t)}function J(e,t,n,r){let i,a,o;if(W(e)){if(a=e.length,r)for(i=a-1;i>=0;i--)t.call(n,e[i],i);else for(i=0;ie,x:e=>e.x,y:e=>e.y};function ka(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 Aa(e){let t=ka(e);return e=>{for(let n of t){if(n===``)break;e=e&&e[n]}return e}}function ja(e,t){return(Oa[t]||(Oa[t]=Aa(t)))(e)}function Ma(e){return e.charAt(0).toUpperCase()+e.slice(1)}var Na=e=>e!==void 0,Pa=e=>typeof e==`function`,Fa=(e,t)=>{if(e.size!==t.size)return!1;for(let n of e)if(!t.has(n))return!1;return!0};function Ia(e){return e.type===`mouseup`||e.type===`click`||e.type===`contextmenu`}var Y=Math.PI,X=2*Y,La=X+Y,Ra=1/0,za=Y/180,Z=Y/2,Ba=Y/4,Va=Y*2/3,Ha=Math.log10,Ua=Math.sign;function Wa(e,t,n){return Math.abs(e-t)e-t).pop(),t}function qa(e){return typeof e==`symbol`||typeof e==`object`&&!!e&&!(Symbol.toPrimitive in e||`toString`in e||`valueOf`in e)}function Ja(e){return!qa(e)&&!isNaN(parseFloat(e))&&isFinite(e)}function Ya(e,t){let n=Math.round(e);return n-t<=e&&n+t>=e}function Xa(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 co(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 lo=(e,t,n,r)=>co(e,n,r?r=>{let i=e[r][t];return ie[r][t]co(e,n,r=>e[r][t]>=n);function fo(e,t,n){let r=0,i=e.length;for(;rr&&e[i-1]>n;)i--;return r>0||i{let n=`_onData`+Ma(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 ho(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)&&(po.forEach(t=>{delete e[t]}),delete e._chartjs)}function go(e){let t=new Set(e);return t.size===e.length?e:Array.from(t)}var _o=function(){return typeof window>`u`?function(e){return e()}:window.requestAnimationFrame}();function vo(e,t){let n=[],r=!1;return function(...i){n=i,r||(r=!0,_o.call(window,()=>{r=!1,e.apply(t,n)}))}}function yo(e,t){let n;return function(...r){return t?(clearTimeout(n),n=setTimeout(e,t,r)):e.apply(this,r),t}}var bo=e=>e===`start`?`left`:e===`end`?`right`:`center`,xo=(e,t,n)=>e===`start`?t:e===`end`?n:(t+n)/2;function So(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(lo(c,u,d).lo,n?r:lo(t,u,o.getPixelForValue(d)).lo),l){let e=c.slice(0,i+1).reverse().findIndex(e=>!U(e[s.axis]));i-=Math.max(0,e)}i=ao(i,0,r-1)}if(m){let e=Math.max(lo(c,o.axis,f,!0).hi+1,n?0:lo(t,u,o.getPixelForValue(f),!0).hi+1);if(l){let t=c.slice(e-1).findIndex(e=>!U(e[s.axis]));e+=Math.max(0,t)}a=ao(e,i,r)-i}else a=r-i}return{start:i,count:a}}function Co(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 wo=e=>e===0||e===1,To=(e,t,n)=>-(2**(10*--e)*Math.sin((e-t)*X/n)),Eo=(e,t,n)=>2**(-10*e)*Math.sin((e-t)*X/n)+1,Do={linear:e=>e,easeInQuad:e=>e*e,easeOutQuad:e=>-e*(e-2),easeInOutQuad:e=>(e/=.5)<1?.5*e*e:-.5*(--e*(e-2)-1),easeInCubic:e=>e*e*e,easeOutCubic:e=>--e*e*e+1,easeInOutCubic:e=>(e/=.5)<1?.5*e*e*e:.5*((e-=2)*e*e+2),easeInQuart:e=>e*e*e*e,easeOutQuart:e=>-(--e*e*e*e-1),easeInOutQuart:e=>(e/=.5)<1?.5*e*e*e*e:-.5*((e-=2)*e*e*e-2),easeInQuint:e=>e*e*e*e*e,easeOutQuint:e=>--e*e*e*e*e+1,easeInOutQuint:e=>(e/=.5)<1?.5*e*e*e*e*e:.5*((e-=2)*e*e*e*e+2),easeInSine:e=>-Math.cos(e*Z)+1,easeOutSine:e=>Math.sin(e*Z),easeInOutSine:e=>-.5*(Math.cos(Y*e)-1),easeInExpo:e=>e===0?0:2**(10*(e-1)),easeOutExpo:e=>e===1?1:-(2**(-10*e))+1,easeInOutExpo:e=>wo(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=>wo(e)?e:To(e,.075,.3),easeOutElastic:e=>wo(e)?e:Eo(e,.075,.3),easeInOutElastic(e){let t=.1125,n=.45;return wo(e)?e:e<.5?.5*To(e*2,t,n):.5+.5*Eo(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-Do.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?Do.easeInBounce(e*2)*.5:Do.easeOutBounce(e*2-1)*.5+.5};function Oo(e){if(e&&typeof e==`object`){let t=e.toString();return t===`[object CanvasPattern]`||t===`[object CanvasGradient]`}return!1}function ko(e){return Oo(e)?e:new ma(e)}function Ao(e){return Oo(e)?e:new ma(e).saturate(.5).darken(.1).hexString()}var jo=[`x`,`y`,`borderWidth`,`radius`,`tension`],Mo=[`color`,`borderColor`,`backgroundColor`];function No(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:Mo},numbers:{type:`number`,properties:jo}}),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 Po(e){e.set(`layout`,{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})}var Fo=new Map;function Io(e,t){t=t||{};let n=e+JSON.stringify(t),r=Fo.get(n);return r||(r=new Intl.NumberFormat(e,t),Fo.set(n,r)),r}function Lo(e,t,n){return Io(t,n).format(e)}var Ro={values(e){return W(e)?e:``+e},numeric(e,t,n){if(e===0)return`0`;let r=this.chart.options.locale,i,a=e;if(n.length>1){let t=Math.max(Math.abs(n[0].value),Math.abs(n[n.length-1].value));(t<1e-4||t>0x38d7ea4c68000)&&(i=`scientific`),a=zo(e,n)}let o=Ha(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),Lo(e,r,c)},logarithmic(e,t,n){if(e===0)return`0`;let r=n[t].significand||e/10**Math.floor(Ha(e));return[1,2,3,5,10,15].includes(r)||t>.8*n.length?Ro.numeric.call(this,e,t,n):``}};function zo(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 Bo={formatters:Ro};function Vo(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:Bo.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 Ho=Object.create(null),Uo=Object.create(null);function Wo(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)=>Ao(t.backgroundColor),this.hoverBorderColor=(e,t)=>Ao(t.borderColor),this.hoverColor=(e,t)=>Ao(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 Go(this,e,t)}get(e){return Wo(this,e)}describe(e,t){return Go(Uo,e,t)}override(e,t){return Go(Ho,e,t)}route(e,t,n,r){let i=Wo(this,e),a=Wo(this,n),o=`_`+t;Object.defineProperties(i,{[o]:{value:i[t],writable:!0},[t]:{enumerable:!0,get(){let e=this[o],t=a[r];return G(e)?Object.assign({},t,e):K(e,t)},set(e){this[o]=e}}})}apply(e){e.forEach(e=>e(this))}}({_scriptable:e=>!e.startsWith(`on`),_indexable:e=>e!==`events`,hover:{_fallback:`interaction`},interaction:{_scriptable:!1,_indexable:!1}},[No,Po,Vo]);function Ko(e){return!e||U(e.size)||U(e.family)?null:(e.style?e.style+` `:``)+(e.weight?e.weight+` `:``)+e.size+`px `+e.family}function qo(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 Jo(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 $o(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,is(e,a),c=0;c+e||0;function ps(e,t){let n={},r=G(t),i=r?Object.keys(t):t,a=G(e)?r?n=>K(e[n],e[t[n]]):t=>e[t]:()=>e;for(let e of i)n[e]=fs(a(e));return n}function ms(e){return ps(e,{top:`y`,right:`x`,bottom:`y`,left:`x`})}function hs(e){return ps(e,[`topLeft`,`topRight`,`bottomLeft`,`bottomRight`])}function gs(e){let t=ms(e);return t.width=t.left+t.right,t.height=t.top+t.bottom,t}function _s(e,t){e=e||{},t=t||Q.font;let n=K(e.size,t.size);typeof n==`string`&&(n=parseInt(n,10));let r=K(e.style,t.style);r&&!(``+r).match(us)&&(console.warn(`Invalid font style specified: "`+r+`"`),r=void 0);let i={family:K(e.family,t.family),lineHeight:ds(K(e.lineHeight,t.lineHeight),n),size:n,style:r,weight:K(e.weight,t.weight),string:``};return i.string=Ko(i),i}function vs(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 bs(e,t){return Object.assign(Object.create(e),t)}function xs(e,t=[``],n,r,i=()=>e[0]){let a=n||e;return r===void 0&&(r=Ls(`_fallback`,e)),new Proxy({[Symbol.toStringTag]:`Object`,_cacheable:!0,_scopes:e,_rootScopes:a,_fallback:r,_getTarget:i,override:n=>xs([n,...e],t,a,r)},{deleteProperty(t,n){return delete t[n],delete t._keys,delete e[0][n],!0},get(n,r){return Es(n,r,()=>Is(r,t,e,n))},getOwnPropertyDescriptor(e,t){return Reflect.getOwnPropertyDescriptor(e._scopes[0],t)},getPrototypeOf(){return Reflect.getPrototypeOf(e[0])},has(e,t){return Rs(e).includes(t)},ownKeys(e){return Rs(e)},set(e,t,n){let r=e._storage||(e._storage=i());return e[t]=r[t]=n,delete e._keys,!0}})}function Ss(e,t,n,r){let i={_cacheable:!1,_proxy:e,_context:t,_subProxy:n,_stack:new Set,_descriptors:Cs(e,r),setContext:t=>Ss(e,t,n,r),override:i=>Ss(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 Es(e,t,()=>Ds(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 Cs(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:Pa(n)?n:()=>n,isIndexable:Pa(r)?r:()=>r}}var ws=(e,t)=>e?e+Ma(t):t,Ts=(e,t)=>G(t)&&e!==`adapters`&&(Object.getPrototypeOf(t)===null||t.constructor===Object);function Es(e,t,n){if(Object.prototype.hasOwnProperty.call(e,t)||t===`constructor`)return e[t];let r=n();return e[t]=r,r}function Ds(e,t,n){let{_proxy:r,_context:i,_subProxy:a,_descriptors:o}=e,s=r[t];return Pa(s)&&o.isScriptable(t)&&(s=Os(t,s,e,n)),W(s)&&s.length&&(s=ks(t,s,e,o.isIndexable)),Ts(t,s)&&(s=Ss(s,i,a&&a[t],o)),s}function Os(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),Ts(e,c)&&(c=Ns(i._scopes,i,e,c)),c}function ks(e,t,n,r){let{_proxy:i,_context:a,_subProxy:o,_descriptors:s}=n;if(a.index!==void 0&&r(e))return t[a.index%t.length];if(G(t[0])){let n=t,r=i._scopes.filter(e=>e!==n);t=[];for(let c of n){let n=Ns(r,i,e,c);t.push(Ss(n,a,o&&o[e],s))}}return t}function As(e,t,n){return Pa(e)?e(t,n):e}var js=(e,t)=>e===!0?t:typeof e==`string`?ja(t,e):void 0;function Ms(e,t,n,r,i){for(let a of t){let t=js(n,a);if(t){e.add(t);let a=As(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 Ns(e,t,n,r){let i=t._rootScopes,a=As(t._fallback,n,r),o=[...e,...i],s=new Set;s.add(r);let c=Ps(s,o,n,a||n,r);return c===null||a!==void 0&&a!==n&&(c=Ps(s,o,a,c,r),c===null)?!1:xs(Array.from(s),[``],i,a,()=>Fs(t,n,r))}function Ps(e,t,n,r,i){for(;n;)n=Ms(e,t,n,r,i);return n}function Fs(e,t,n){let r=e._getTarget();t in r||(r[t]={});let i=r[t];return W(i)&&G(n)?n:i||{}}function Is(e,t,n,r){let i;for(let a of t)if(i=Ls(ws(a,e),n),i!==void 0)return Ts(e,i)?Ns(n,r,e,i):i}function Ls(e,t){for(let n of t){if(!n)continue;let t=n[e];if(t!==void 0)return t}}function Rs(e){let t=e._keys;return t||(t=e._keys=zs(e._scopes)),t}function zs(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 Bs(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 Ws(e,t,n,r){let i=e.skip?t:e,a=t,o=n.skip?t:n,s=to(a,i),c=to(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 Gs(e,t,n){let r=e.length,i,a,o,s,c,l=Hs(e,0);for(let u=0;u!e.skip)),t.cubicInterpolationMode===`monotone`)qs(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 tc(e,t){return ec(e).getPropertyValue(t)}var nc=[`top`,`right`,`bottom`,`left`];function rc(e,t,n){let r={};n=n?`-`+n:``;for(let i=0;i<4;i++){let a=nc[i];r[a]=parseFloat(e[t+`-`+a+n])||0}return r.width=r.left+r.right,r.height=r.top+r.bottom,r}var ic=(e,t,n)=>(e>0||t>0)&&(!n||!n.shadowRoot);function ac(e,t){let n=e.touches,r=n&&n.length?n[0]:e,{offsetX:i,offsetY:a}=r,o=!1,s,c;if(ic(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 oc(e,t){if(`native`in e)return e;let{canvas:n,currentDevicePixelRatio:r}=t,i=ec(n),a=i.boxSizing===`border-box`,o=rc(i,`padding`),s=rc(i,`border`,`width`),{x:c,y:l,box:u}=ac(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 sc(e,t,n){let r,i;if(t===void 0||n===void 0){let a=e&&Qs(e);if(!a)t=e.clientWidth,n=e.clientHeight;else{let e=a.getBoundingClientRect(),o=ec(a),s=rc(o,`border`,`width`),c=rc(o,`padding`);t=e.width-c.width-s.width,n=e.height-c.height-s.height,r=$s(o.maxWidth,a,`clientWidth`),i=$s(o.maxHeight,a,`clientHeight`)}}return{width:t,height:n,maxWidth:r||Ra,maxHeight:i||Ra}}var cc=e=>Math.round(e*10)/10;function lc(e,t,n,r){let i=ec(e),a=rc(i,`margin`),o=$s(i.maxWidth,e,`clientWidth`)||Ra,s=$s(i.maxHeight,e,`clientHeight`)||Ra,c=sc(e,t,n),{width:l,height:u}=c;if(i.boxSizing===`content-box`){let e=rc(i,`border`,`width`),t=rc(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=cc(Math.min(l,o,c.maxWidth)),u=cc(Math.min(u,s,c.maxHeight)),l&&!u&&(u=cc(l/2)),(t!==void 0||n!==void 0)&&r&&c.height&&u>c.height&&(u=c.height,l=cc(Math.floor(u*r))),{width:l,height:u}}function uc(e,t,n){let r=t||1,i=cc(e.height*r),a=cc(e.width*r);e.height=cc(e.height),e.width=cc(e.width);let o=e.canvas;return o.style&&(n||!o.style.height&&!o.style.width)&&(o.style.height=`${e.height}px`,o.style.width=`${e.width}px`),e.currentDevicePixelRatio!==r||o.height!==i||o.width!==a?(e.currentDevicePixelRatio=r,o.height=i,o.width=a,e.ctx.setTransform(r,0,0,r,0,0),!0):!1}var dc=function(){let e=!1;try{let t={get passive(){return e=!0,!1}};Zs()&&(window.addEventListener(`test`,null,t),window.removeEventListener(`test`,null,t))}catch{}return e}();function fc(e,t){let n=tc(e,t),r=n&&n.match(/^(\d+)(\.\d+)?px$/);return r?+r[1]:void 0}function pc(e,t,n,r){return{x:e.x+n*(t.x-e.x),y:e.y+n*(t.y-e.y)}}function mc(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 hc(e,t,n,r){let i={x:e.cp2x,y:e.cp2y},a={x:t.cp1x,y:t.cp1y},o=pc(e,i,n),s=pc(i,a,n),c=pc(a,t,n);return pc(pc(o,s,n),pc(s,c,n),n)}var gc=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}}},_c=function(){return{x(e){return e},setWidth(e){},textAlign(e){return e},xPlus(e,t){return e+t},leftForLtr(e,t){return e}}};function vc(e,t,n){return e?gc(t,n):_c()}function yc(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 bc(e,t){t!==void 0&&(delete e.prevTextDirection,e.canvas.style.setProperty(`direction`,t[0],t[1]))}function xc(e){return e===`angle`?{between:io,compare:no,normalize:ro}:{between:so,compare:(e,t)=>e-t,normalize:e=>e}}function Sc({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 Cc(e,t,n){let{property:r,start:i,end:a}=n,{between:o,normalize:s}=xc(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(Sc({start:g,end:e,loop:f,count:o,style:p})),g=null),n=e,y=_));return g!==null&&m.push(Sc({start:g,end:d,loop:f,count:o,style:p})),m}function Tc(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 Dc(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 Oc(e,t){let n=e.points,r=e.options.spanGaps,i=n.length;if(!i)return[];let a=!!e._loop,{start:o,end:s}=Ec(n,i,a,r);return r===!0?kc(e,[{start:o,end:s,loop:a}],n,t):kc(e,Dc(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=_o.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)}},Bc=`transparent`,Vc={boolean(e,t,n){return n>.5?t:e},color(e,t,n){let r=ko(e||Bc),i=r.valid&&ko(t||Bc);return i&&i.valid?i.mix(r,n).hexString():t},number(e,t,n){return e+(t-e)*n}},Hc=class{constructor(e,t,n,r){let i=t[n];r=vs([e.to,r,i,e.from]);let a=vs([e.from,i,r]);this._active=!0,this._fn=e.fn||Vc[e.type||typeof a],this._easing=Do[e.easing]||Do.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=vs([e.to,t,r,e.from]),this._from=vs([e.from,r,t])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(e){let t=e-this._start,n=this._duration,r=this._prop,i=this._from,a=this._loop,o=this._to,s;this._active=i!==o&&(a||t1?2-s:s,s=this._easing(Math.min(1,Math.max(0,s))),this._target[r]=this._fn(i,o,s)):(this._target[r]=o,this._notify(!0))}wait(){let e=this._promises||(this._promises=[]);return new Promise((t,n)=>{e.push({res:t,rej:n})})}_notify(e){let t=e?`res`:`rej`,n=this._promises||[];for(let e=0;e{let i=e[r];if(!G(i))return;let a={};for(let e of t)a[e]=i[e];(W(i.properties)&&i.properties||[r]).forEach(e=>{(e===r||!n.has(e))&&n.set(e,a)})})}_animateOptions(e,t){let n=t.options,r=Gc(e,n);if(!r)return[];let i=this._createAnimations(r,n);return n.$shared&&Wc(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 Hc(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 zc.add(this._chart,n),!0}};function Wc(e,t){let n=[],r=Object.keys(t);for(let t=0;t0||!n&&t<0)return i.index}return null}function rl(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=$c(a,o,r),d=t.length,f;for(let e=0;en[e].axis===t).shift()}function al(e,t){return bs(e,{active:!1,dataset:void 0,datasetIndex:t,index:t,mode:`default`,type:`dataset`})}function ol(e,t,n){return bs(e,{active:!1,dataIndex:t,parsed:void 0,raw:void 0,element:n,index:t,mode:`default`,type:`data`})}function sl(e,t){let n=e.controller.index,r=e.vScale&&e.vScale.axis;if(r){t=t||e._parsed;for(let e of t){let t=e._stacks;if(!t||t[r]===void 0||t[r][n]===void 0)return;delete t[r][n],t[r]._visualValues!==void 0&&t[r]._visualValues[n]!==void 0&&delete t[r]._visualValues[n]}}}var cl=e=>e===`reset`||e===`none`,ll=(e,t)=>t?e:Object.assign({},e),ul=(e,t,n)=>e&&!t.hidden&&t._stacked&&{keys:Yc(n,!0),values:null},dl=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=Qc(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&&sl(this._cachedMeta),this.index=e}linkScales(){let e=this.chart,t=this._cachedMeta,n=this.getDataset(),r=(e,t,n,r)=>e===`x`?t:e===`r`?r:n,i=t.xAxisID=K(n.xAxisID,il(e,`x`)),a=t.yAxisID=K(n.yAxisID,il(e,`y`)),o=t.rAxisID=K(n.rAxisID,il(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&&ho(this._data,this),e._stacked&&sl(e)}_dataCheck(){let e=this.getDataset(),t=e.data||(e.data=[]),n=this._data;if(G(t)){let e=this._cachedMeta;this._data=Zc(t,e)}else if(n!==t){if(n){ho(n,this);let e=this._cachedMeta;sl(e),e._parsed=[]}t&&Object.isExtensible(t)&&mo(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=Qc(t.vScale,t),t.stack!==n.stack&&(r=!0,sl(t),t.stack=n.stack),this._resyncElements(e),(r||i!==t._stacked)&&(rl(this,t._parsed),t._stacked=Qc(t.vScale,t))}configure(){let e=this.chart.config,t=e.datasetScopeKeys(this._type),n=e.getOptionScopes(this.getDataset(),t,!0);this.options=e.createResolver(n,this.getContext()),this._parsing=this.options.parsing,this._cachedDataOpts={}}parse(e,t){let{_cachedMeta:n,_data:r}=this,{iScale:i,_stacked:a}=n,o=i.axis,s=e===0&&t===r.length||n._sorted,c=e>0&&n._parsed[e-1],l,u,d;if(this._parsing===!1)n._parsed=r,n._sorted=!0,d=r;else{d=W(r[e])?this.parseArrayData(n,r,e,t):G(r[e])?this.parseObjectData(n,r,e,t):this.parsePrimitiveData(n,r,e,t);let i=()=>u[o]===null||c&&u[o]t||u=0;--d)if(!p()){this.updateRangeFromParsed(c,e,f,s);break}}return c}getAllParsedValues(e){let t=this._cachedMeta._parsed,n=[],r,i,a;for(r=0,i=t.length;r=0&&ethis.getContext(n,r,t),u);return p.$shared&&(p.$shared=s,i[a]=Object.freeze(ll(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 Uc(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||cl(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){cl(r)?Object.assign(e,n):this._resolveAnimations(t,r).update(e,n)}updateSharedOptions(e,t,n){e&&!cl(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 pl(e){let t=e.iScale,n=fl(t,e.type),r=t._length,i,a,o,s,c=()=>{o!==32767&&o!==-32768&&(Na(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 _l(e,t,n,r){return W(e)?gl(e,t,n,r):t[n.axis]=n.parse(e,r),t}function vl(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):Ua(e)}function xl(e){let t,n,r,i,a;return e.horizontal?(t=e.base>e.x,n=`left`,r=`right`):(t=e.basee.controller.options.grouped),i=n.options.stacked,a=[],o=this._cachedMeta.controller.getParsed(t),s=o&&o[n.axis],c=e=>{let t=e._parsed.find(e=>e[n.axis]===s),r=t&&t[e.vScale.axis];if(U(r)||isNaN(r))return!0};for(let n of r)if(!(t!==void 0&&c(n))&&((i===!1||a.indexOf(n.stack)===-1||i===void 0&&n.stack===void 0)&&a.push(n.stack),n.index===e))break;return a.length||a.push(void 0),a}_getStackCount(e){return this._getStacks(void 0,e).length}_getAxisCount(){return this._getAxis().length}getFirstScaleIdForIndexAxis(){let e=this.chart.scales,t=this.chart.options.indexAxis;return Object.keys(e).filter(n=>e[n].axis===t).shift()}_getAxis(){let e={},t=this.getFirstScaleIdForIndexAxis();for(let n of this.chart.data.datasets)e[K(this.chart.options.indexAxis===`x`?n.xAxisID:n.yAxisID,t)]=!0;return Object.keys(e)}_getStackIndex(e,t,n){let r=this._getStacks(e,n),i=t===void 0?-1:r.indexOf(t);return i===-1?r.length-1:i}_getRuler(){let e=this.options,t=this._cachedMeta,n=t.iScale,r=[],i,a;for(i=0,a=t.data.length;i=0;--n)t=Math.max(t,e[n].size(this.resolveDataElementOptions(n))/2);return t>0&&t}getLabelAndValue(e){let t=this._cachedMeta,n=this.chart.data.labels||[],{xScale:r,yScale:i}=t,a=this.getParsed(e),o=r.getLabelForValue(a.x),s=i.getLabelForValue(a.y),c=a._custom;return{label:n[e]||``,value:`(`+o+`, `+s+(c?`, `+c:``)+`)`}}update(e){let t=this._cachedMeta.data;this.updateElements(t,0,t.length,e)}updateElements(e,t,n,r){let i=r===`reset`,{iScale:a,vScale:o}=this._cachedMeta,{sharedOptions:s,includeOptions:c}=this._getSharedOptions(t,r),l=a.axis,u=o.axis;for(let d=t;dio(e,s,c,!0)?1:Math.max(t,t*n,r,r*n),m=(e,t,r)=>io(e,s,c,!0)?-1:Math.min(t,t*n,r,r*n),h=p(0,l,d),g=p(Z,u,f),_=m(Y,l,d),v=m(Y+Z,u,f);r=(h-_)/2,i=(g-v)/2,a=-(h+_)/2,o=-(g+v)/2}return{ratioX:r,ratioY:i,offsetX:a,offsetY:o}}var Al=class extends dl{constructor(e,t){super(e,t),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(e,t){let n=this.getDataset().data,r=this._cachedMeta;if(this._parsing===!1)r._parsed=n;else{let i=e=>+n[e];if(G(n[e])){let{key:e=`value`}=this._parsing;i=t=>+ja(n[t],e)}let a,o;for(a=e,o=e+t;a0&&!isNaN(e)?Math.abs(e)/t*X:0}getLabelAndValue(e){let t=this._cachedMeta,n=this.chart,r=n.data.labels||[],i=Lo(t._parsed[e],n.options.locale);return{label:r[e]||``,value:i}}getMaxBorderWidth(e){let t=0,n=this.chart,r,i,a,o,s;if(!e){for(r=0,i=n.data.datasets.length;re!==`spacing`,_indexable:e=>e!==`spacing`&&!e.startsWith(`borderDash`)&&!e.startsWith(`hoverBorderDash`)}),$(Al,`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 jl=class extends dl{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}=So(t,r,a);this._drawStart=o,this._drawCount=s,Co(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=Ja(p)?p:1/0,g=this.chart._animationsDisabled||i||r===`none`,_=t+n,v=e.length,y=t>0&&this.getParsed(t-1);for(let n=0;n=_){v.skip=!0;continue}let b=this.getParsed(n),x=U(b[f]),S=v[d]=a.getPixelForValue(b[d],n),C=v[f]=i||x?o.getBasePixel():o.getPixelForValue(s?this.applyStack(o,b,s):b[f],n);v.skip=isNaN(S)||isNaN(C)||x,v.stop=n>0&&Math.abs(b[d]-y[d])>h,m&&(v.parsed=b,v.raw=c.data[n]),u&&(v.options=l||this.resolveDataElementOptions(n,p.active?`active`:r)),g||this.updateElement(p,n,v,r),y=b}}getMaxOverflow(){let e=this._cachedMeta,t=e.dataset,n=t.options&&t.options.borderWidth||0,r=e.data||[];if(!r.length)return n;let i=r[0].size(this.resolveDataElementOptions(0)),a=r[r.length-1].size(this.resolveDataElementOptions(r.length-1));return Math.max(n,i,a)/2}draw(){let e=this._cachedMeta;e.dataset.updateControlPoints(this.chart.chartArea,e.iScale.axis),super.draw()}};$(jl,`id`,`line`),$(jl,`defaults`,{datasetElementType:`line`,dataElementType:`point`,showLine:!0,spanGaps:!1}),$(jl,`overrides`,{scales:{_index_:{type:`category`},_value_:{type:`linear`}}});var Ml=class extends dl{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=Lo(t._parsed[e].r,n.options.locale);return{label:r[e]||``,value:i}}parseObjectData(e,t,n,r){return Bs.bind(this)(e,t,n,r)}update(e){let t=this._cachedMeta.data;this._updateRadius(),this.updateElements(t,0,t.length,e)}getMinMax(){let e=this._cachedMeta,t={min:1/0,max:-1/0};return e.data.forEach((e,n)=>{let r=this.getParsed(n).r;!isNaN(r)&&this.chart.getDataVisibility(n)&&(rt.max&&(t.max=r))}),t}_updateRadius(){let e=this.chart,t=e.chartArea,n=e.options,r=Math.min(t.right-t.left,t.bottom-t.top),i=Math.max(r/2,0),a=(i-Math.max(n.cutoutPercentage?i/100*n.cutoutPercentage:1,0))/e.getVisibleDatasetCount();this.outerRadius=i-a*this.index,this.innerRadius=this.outerRadius-a}updateElements(e,t,n,r){let i=r===`reset`,a=this.chart,o=a.options.animation,s=this._cachedMeta.rScale,c=s.xCenter,l=s.yCenter,u=s.getIndexAngle(0)-.5*Y,d=u,f,p=360/this.countVisibleElements();for(f=0;f{!isNaN(this.getParsed(n).r)&&this.chart.getDataVisibility(n)&&t++}),t}_computeAngle(e,t,n){return this.chart.getDataVisibility(e)?Za(this.resolveDataElementOptions(e,t).angle||n):0}};$(Ml,`id`,`polarArea`),$(Ml,`defaults`,{dataElementType:`arc`,animation:{animateRotate:!0,animateScale:!0},animations:{numbers:{type:`number`,properties:[`x`,`y`,`startAngle`,`endAngle`,`innerRadius`,`outerRadius`]}},indexAxis:`r`,startAngle:0}),$(Ml,`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 Nl=class extends Al{};$(Nl,`id`,`pie`),$(Nl,`defaults`,{cutout:0,rotation:0,circumference:360,radius:`100%`});var Pl=class extends dl{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 Bs.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}};$(Fl,`id`,`scatter`),$(Fl,`defaults`,{datasetElementType:!1,dataElementType:`point`,showLine:!1,fill:!1}),$(Fl,`overrides`,{interaction:{mode:`point`},scales:{x:{type:`linear`},y:{type:`linear`}}});function Il(){throw Error(`This method is not implemented: Check that a complete date adapter is provided.`)}var Ll={_date:class e{static override(t){Object.assign(e.prototype,t)}constructor(e){$(this,`options`,void 0),this.options=e||{}}init(){}formats(){return Il()}parse(){return Il()}format(){return Il()}add(){return Il()}diff(){return Il()}startOf(){return Il()}endOf(){return Il()}}};function Rl(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?uo:lo;if(!r){let r=o(a,t,n);if(c){let{vScale:t}=i._cachedMeta,{_parsed:n}=e,a=n.slice(0,r.lo+1).reverse().findIndex(e=>!U(e[t.axis]));r.lo-=Math.max(0,a);let o=n.slice(r.hi).findIndex(e=>!U(e[t.axis]));r.hi+=Math.max(0,o)}return r}if(i._sharedOptions){let e=a[0],r=typeof e.getRange==`function`&&e.getRange(t);if(r){let e=o(a,t,n-r),i=o(a,t,n+r);return{lo:e.lo,hi:i.hi}}}}return{lo:0,hi:a.length-1}}function zl(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 Kl={evaluateInteractionItems:zl,modes:{index(e,t,n,r){let i=oc(t,e),a=n.axis||`x`,o=n.includeInvisible||!1,s=n.intersect?Vl(e,i,a,r,o):Wl(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=oc(t,e),a=n.axis||`xy`,o=n.includeInvisible||!1,s=n.intersect?Vl(e,i,a,r,o):Wl(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 Yl(e,t){return e.filter(e=>ql.indexOf(e.pos)===-1&&e.box.axis===t)}function Xl(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 Zl(e){let t=[],n,r,i,a,o,s;for(n=0,r=(e||[]).length;ne.box.fullSize),!0),r=Xl(Jl(t,`left`),!0),i=Xl(Jl(t,`right`)),a=Xl(Jl(t,`top`),!0),o=Xl(Jl(t,`bottom`)),s=Yl(t,`x`),c=Yl(t,`y`);return{fullSize:n,leftAndTop:r.concat(a),rightAndBottom:i.concat(c).concat(o).concat(s),chartArea:Jl(t,`chartArea`),vertical:r.concat(i).concat(c),horizontal:a.concat(o).concat(s)}}function tu(e,t,n,r){return Math.max(e[n],t[n])+Math.max(e[r],t[r])}function nu(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 ru(e,t,n,r){let{pos:i,box:a}=n,o=e.maxPadding;if(!G(i)){n.size&&(e[i]-=n.size);let t=r[n.stack]||{size:0,count:1};t.size=Math.max(t.size,n.horizontal?a.height:a.width),n.size=t.size/t.count,e[i]+=n.size}a.getPadding&&nu(o,a.getPadding());let s=Math.max(0,t.outerWidth-tu(o,e,`left`,`right`)),c=Math.max(0,t.outerHeight-tu(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 iu(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 au(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 ou(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);nu(f,gs(r));let p=Object.assign({maxPadding:f,w:a,h:o,x:i.left,y:i.top},i),m=$l(c.concat(l),d);ou(s.fullSize,p,d,m),ou(c,p,d,m),ou(l,p,d,m)&&ou(c,p,d,m),iu(p),cu(s.leftAndTop,p,d,m),p.x+=p.w,p.y+=p.h,cu(s.rightAndBottom,p,d,m),e.chartArea={left:p.left,top:p.top,right:p.left+p.w,bottom:p.top+p.h,height:p.h,width:p.w},J(s.chartArea,t=>{let n=t.box;Object.assign(n,e.chartArea),n.update(p.w,p.h,{left:0,top:0,right:0,bottom:0})})}},uu=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){}},du=class extends uu{acquireContext(e){return e&&e.getContext&&e.getContext(`2d`)||null}updateConfig(e){e.options.animation=!1}},fu=`$chartjs`,pu={touchstart:`mousedown`,touchmove:`mousemove`,touchend:`mouseup`,pointerenter:`mouseenter`,pointerdown:`mousedown`,pointermove:`mousemove`,pointerup:`mouseup`,pointerleave:`mouseout`,pointerout:`mouseout`},mu=e=>e===null||e===``;function hu(e,t){let n=e.style,r=e.getAttribute(`height`),i=e.getAttribute(`width`);if(e[fu]={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`,mu(i)){let t=fc(e,`width`);t!==void 0&&(e.width=t)}if(mu(r)){if(e.style.height===``)e.height=e.width/(t||2);else{let t=fc(e,`height`);t!==void 0&&(e.height=t)}}return e}var gu=dc?{passive:!0}:!1;function _u(e,t,n){e&&e.addEventListener(t,n,gu)}function vu(e,t,n){e&&e.canvas&&e.canvas.removeEventListener(t,n,gu)}function yu(e,t){let n=pu[e.type]||e.type,{x:r,y:i}=oc(e,t);return{type:n,chart:t,native:e,x:r===void 0?null:r,y:i===void 0?null:i}}function bu(e,t){for(let n of e)if(n===t||n.contains(t))return!0}function xu(e,t,n){let r=e.canvas,i=new MutationObserver(e=>{let t=!1;for(let n of e)t=t||bu(n.addedNodes,r),t=t&&!bu(n.removedNodes,r);t&&n()});return i.observe(document,{childList:!0,subtree:!0}),i}function Su(e,t,n){let r=e.canvas,i=new MutationObserver(e=>{let t=!1;for(let n of e)t=t||bu(n.removedNodes,r),t=t&&!bu(n.addedNodes,r);t&&n()});return i.observe(document,{childList:!0,subtree:!0}),i}var Cu=new Map,wu=0;function Tu(){let e=window.devicePixelRatio;e!==wu&&(wu=e,Cu.forEach((t,n)=>{n.currentDevicePixelRatio!==e&&t()}))}function Eu(e,t){Cu.size||window.addEventListener(`resize`,Tu),Cu.set(e,t)}function Du(e){Cu.delete(e),Cu.size||window.removeEventListener(`resize`,Tu)}function Ou(e,t,n){let r=e.canvas,i=r&&Qs(r);if(!i)return;let a=vo((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),Eu(e,a),o}function ku(e,t,n){n&&n.disconnect(),t===`resize`&&Du(e)}function Au(e,t,n){let r=e.canvas,i=vo(t=>{e.ctx!==null&&n(yu(t,e))},e);return _u(r,t,i),i}var ju=class extends uu{acquireContext(e,t){let n=e&&e.getContext&&e.getContext(`2d`);return n&&n.canvas===e?(hu(e,t),n):null}releaseContext(e){let t=e.canvas;if(!t[fu])return!1;let n=t[fu].initial;[`height`,`width`].forEach(e=>{let r=n[e];U(r)?t.removeAttribute(e):t.setAttribute(e,r)});let r=n.style||{};return Object.keys(r).forEach(e=>{t.style[e]=r[e]}),t.width=t.width,delete t[fu],!0}addEventListener(e,t,n){this.removeEventListener(e,t);let r=e.$proxies||(e.$proxies={});r[t]=({attach:xu,detach:Su,resize:Ou}[t]||Au)(e,t,n)}removeEventListener(e,t){let n=e.$proxies||(e.$proxies={}),r=n[t];r&&(({attach:ku,detach:ku,resize:ku}[t]||vu)(e,t,r),n[t]=void 0)}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(e,t,n,r){return lc(e,t,n,r)}isAttached(e){let t=e&&Qs(e);return!!(t&&t.isConnected)}};function Mu(e){return!Zs()||typeof OffscreenCanvas<`u`&&e instanceof OffscreenCanvas?du:ju}var Nu=class{constructor(){$(this,`x`,void 0),$(this,`y`,void 0),$(this,`active`,!1),$(this,`options`,void 0),$(this,`$animations`,void 0)}tooltipPosition(e){let{x:t,y:n}=this.getProps([`x`,`y`],e);return{x:t,y:n}}hasValue(){return Ja(this.x)&&Ja(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}};$(Nu,`defaults`,{}),$(Nu,`defaultRoutes`,void 0);function Pu(e,t){let n=e.options.ticks,r=Fu(e),i=Math.min(n.maxTicksLimit||r,r),a=n.major.enabled?Lu(t):[],o=a.length,s=a[0],c=a[o-1],l=[];if(o>i)return Ru(t,l,a,o/i),l;let u=Iu(a,t,i);if(o>0){let e,n,r=o>1?Math.round((c-s)/(o-1)):null;for(zu(t,l,u,U(r)?0:s-r,s),e=0,n=o-1;ei)return t}return Math.max(i,1)}function Lu(e){let t=[],n,r;for(n=0,r=e.length;ne===`left`?`right`:e===`right`?`left`:e,Hu=(e,t,n)=>t===`top`||t===`left`?e[t]+n:e[t]-n,Uu=(e,t)=>Math.min(t||e,e);function Wu(e,t){let n=[],r=e.length/t,i=e.length,a=0;for(;ao+s)))return c}function Ku(e,t){J(e,e=>{let n=e.gc,r=n.length/2,i;if(r>t){for(i=0;in?n:t,n=r&&t>n?t:n,{min:va(t,va(n,t)),max:va(n,va(t,n))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){let e=this.chart.data;return this.options.labels||(this.isHorizontal()?e.xLabels:e.yLabels)||e.labels||[]}getLabelItems(e=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(e))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){q(this.options.beforeUpdate,[this])}update(e,t,n){let{beginAtZero:r,grace:i,ticks:a}=this.options,o=a.sampleSize;this.beforeUpdate(),this.maxWidth=e,this.maxHeight=t,this._margins=n=Object.assign({left:0,right:0,top:0,bottom:0},n),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+n.left+n.right:this.height+n.top+n.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=ys(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=ao(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-qu(e.grid)-t.padding-Ju(e.title,this.chart.options.font),c=Math.sqrt(u*u+d*d),a=Qa(Math.min(Math.asin(ao((l.highest.height+6)/o,-1,1)),Math.asin(ao(s/c,-1,1))-Math.asin(ao(d/c,-1,1)))),a=Math.max(r,Math.min(i,a))),this.labelRotation=a}afterCalculateLabelRotation(){q(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){q(this.options.beforeFit,[this])}fit(){let e={width:0,height:0},{chart:t,options:{ticks:n,title:r,grid:i}}=this,a=this._isVisible(),o=this.isHorizontal();if(a){let a=Ju(r,t.options.font);if(o?(e.width=this.maxWidth,e.height=qu(i)+a):(e.height=this.maxHeight,e.width=qu(i)+a),n.display&&this.ticks.length){let{first:t,last:r,widest:i,highest:a}=this._getLabelSizes(),s=n.padding*2,c=Za(this.labelRotation),l=Math.cos(c),u=Math.sin(c);if(o){let t=n.mirror?0:u*i.width+l*a.height;e.height=Math.min(this.maxHeight,e.height+t+s)}else{let t=n.mirror?0:l*i.width+u*a.height;e.width=Math.min(this.maxWidth,e.width+t+s)}this._calculatePadding(t,r,u,l)}}this._handleMargins(),o?(this.width=this._length=t.width-this._margins.left-this._margins.right,this.height=e.height):(this.width=e.width,this.height=this._length=t.height-this._margins.top-this._margins.bottom)}_calculatePadding(e,t,n,r){let{ticks:{align:i,padding:a},position:o}=this.options,s=this.labelRotation!==0,c=o!==`top`&&this.axis===`x`;if(this.isHorizontal()){let o=this.getPixelForTick(0)-this.left,l=this.right-this.getPixelForTick(this.ticks.length-1),u=0,d=0;s?c?(u=r*e.width,d=n*t.height):(u=n*e.height,d=r*t.width):i===`start`?d=t.width:i===`end`?u=e.width:i!==`inner`&&(u=e.width/2,d=t.width/2),this.paddingLeft=Math.max((u-o+a)*this.width/(this.width-o),0),this.paddingRight=Math.max((d-l+a)*this.width/(this.width-l),0)}else{let n=t.height/2,r=e.height/2;i===`start`?(n=0,r=e.height):i===`end`&&(n=t.height,r=0),this.paddingTop=n+a,this.paddingBottom=r+a}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){q(this.options.afterFit,[this])}isHorizontal(){let{axis:e,position:t}=this.options;return t===`top`||t===`bottom`||e===`x`}isFullSize(){return this.options.fullSize}_convertTicksToLabels(e){this.beforeTickToLabelConversion(),this.generateTickLabels(e);let t,n;for(t=0,n=e.length;t({width:a[e]||0,height:o[e]||0});return{first:C(0),last:C(t-1),widest:C(x),highest:C(S),widths:a,heights:o}}getLabelForValue(e){return e}getPixelForValue(e,t){return NaN}getValueForPixel(e){}getPixelForTick(e){let t=this.ticks;return e<0||e>t.length-1?null:this.getPixelForValue(t[e].value)}getPixelForDecimal(e){this._reversePixels&&(e=1-e);let t=this._startPixel+e*this._length;return oo(this._alignToPixels?Yo(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=qu(i),d=[],f=o.setContext(this.getContext()),p=f.display?f.width:0,m=p/2,h=function(e){return Yo(n,e,p)},g,_,v,y,b,x,S,C,w,T,E,D;if(a===`top`)g=h(this.bottom),x=this.bottom-u,C=g-m,T=h(e.top)+m,D=e.bottom;else if(a===`bottom`)g=h(this.top),T=e.top,D=h(e.bottom)-m,x=g+m,C=this.top+u;else if(a===`left`)g=h(this.right),b=this.right-u,S=g-m,w=h(e.left)+m,E=e.right;else if(a===`right`)g=h(this.left),w=e.left,E=h(e.right)-m,b=g+m,S=this.left+u;else if(t===`x`){if(a===`center`)g=h((e.top+e.bottom)/2+.5);else if(G(a)){let e=Object.keys(a)[0],t=a[e];g=h(this.chart.scales[e].getPixelForValue(t))}T=e.top,D=e.bottom,x=g+m,C=x+u}else if(t===`y`){if(a===`center`)g=h((e.left+e.right)/2);else if(G(a)){let e=Object.keys(a)[0],t=a[e];g=h(this.chart.scales[e].getPixelForValue(t))}b=g-m,S=b-u,w=e.left,E=e.right}let O=K(r.ticks.maxTicksLimit,l),k=Math.max(1,Math.ceil(l/O));for(_=0;_0&&(a-=r/2)}f={left:a,top:i,width:r+t.width,height:n+t.height,color:e.backdropColor}}h.push({label:y,font:w,textOffset:D,options:{rotation:m,color:n,strokeColor:s,strokeWidth:l,textAlign:d,textBaseline:O,translation:[b,x],backdrop:f}})}return h}_getXAxisLabelAlignment(){let{position:e,ticks:t}=this.options;if(-Za(this.labelRotation))return e===`top`?`left`:`right`;let n=`center`;return t.align===`start`?n=`left`:t.align===`end`?n=`right`:t.align===`inner`&&(n=`inner`),n}_getYAxisLabelAlignment(e){let{position:t,ticks:{crossAlign:n,mirror:r,padding:i}}=this.options,a=this._getLabelSizes(),o=e+i,s=a.widest.width,c,l;return t===`left`?r?(l=this.right+i,n===`near`?c=`left`:n===`center`?(c=`center`,l+=s/2):(c=`right`,l+=s)):(l=this.right-o,n===`near`?c=`right`:n===`center`?(c=`center`,l-=s/2):(c=`left`,l=this.left)):t===`right`?r?(l=this.left+i,n===`near`?c=`right`:n===`center`?(c=`center`,l-=s/2):(c=`left`,l-=s)):(l=this.left+o,n===`near`?c=`left`:n===`center`?(c=`center`,l+=s/2):(c=`right`,l=this.right)):c=`right`,{textAlign:c,x:l}}_computeLabelArea(){if(this.options.ticks.mirror)return;let e=this.chart,t=this.options.position;if(t===`left`||t===`right`)return{top:0,left:this.left,bottom:e.height,right:this.right};if(t===`top`||t===`bottom`)return{top:this.top,left:0,bottom:this.bottom,right:e.width}}drawBackground(){let{ctx:e,options:{backgroundColor:t},left:n,top:r,width:i,height:a}=this;t&&(e.save(),e.fillStyle=t,e.fillRect(n,r,i,a),e.restore())}getLineWidthForValue(e){let t=this.options.grid;if(!this._isVisible()||!t.display)return 0;let n=this.ticks.findIndex(t=>t.value===e);return n>=0?t.setContext(this.getContext(n)).lineWidth:0}drawGrid(e){let t=this.options.grid,n=this.ctx,r=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(e)),i,a,o=(e,t,r)=>{r.width&&r.color&&(n.save(),n.lineWidth=r.width,n.strokeStyle=r.color,n.setLineDash(r.borderDash||[]),n.lineDashOffset=r.borderDashOffset,n.beginPath(),n.moveTo(e.x,e.y),n.lineTo(t.x,t.y),n.stroke(),n.restore())};if(t.display)for(i=0,a=r.length;i{this.draw(e)}}]:[{z:r,draw:e=>{this.drawBackground(),this.drawGrid(e),this.drawTitle()}},{z:i,draw:()=>{this.drawBorder()}},{z:n,draw:e=>{this.drawLabels(e)}}]}getMatchingVisibleMetas(e){let t=this.chart.getSortedVisibleDatasetMetas(),n=this.axis+`AxisID`,r=[],i,a;for(i=0,a=t.length;i{let r=n.split(`.`),i=r.pop(),a=[e].concat(r).join(`.`),o=t[n].split(`.`),s=o.pop(),c=o.join(`.`);Q.route(a,i,c,s)})}function rd(e){return`id`in e&&`defaults`in e}var id=new class{constructor(){this.controllers=new ed(dl,`datasets`,!0),this.elements=new ed(Nu,`elements`),this.plugins=new ed(Object,`plugins`),this.scales=new ed($u,`scales`),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...e){this._each(`register`,e)}remove(...e){this._each(`unregister`,e)}addControllers(...e){this._each(`register`,e,this.controllers)}addElements(...e){this._each(`register`,e,this.elements)}addPlugins(...e){this._each(`register`,e,this.plugins)}addScales(...e){this._each(`register`,e,this.scales)}getController(e){return this._get(e,this.controllers,`controller`)}getElement(e){return this._get(e,this.elements,`element`)}getPlugin(e){return this._get(e,this.plugins,`plugin`)}getScale(e){return this._get(e,this.scales,`scale`)}removeControllers(...e){this._each(`unregister`,e,this.controllers)}removeElements(...e){this._each(`unregister`,e,this.elements)}removePlugins(...e){this._each(`unregister`,e,this.plugins)}removeScales(...e){this._each(`unregister`,e,this.scales)}_each(e,t,n){[...t].forEach(t=>{let r=n||this._getRegistryForType(t);n||r.isForType(t)||r===this.plugins&&t.id?this._exec(e,r,t):J(t,t=>{let r=n||this._getRegistryForType(t);this._exec(e,r,t)})})}_exec(e,t,n){let r=Ma(e);q(n[`before`+r],[],n),t[e](n),q(n[`after`+r],[],n)}_getRegistryForType(e){for(let t=0;te.filter(e=>!t.some(t=>e.plugin.id===t.plugin.id));this._notify(r(t,n),e,`stop`),this._notify(r(n,t),e,`start`)}};function od(e){let t={},n=[],r=Object.keys(id.plugins.items);for(let e=0;e1&&pd(e[0].toLowerCase());if(t)return t}throw Error(`Cannot determine type of '${e}' axis. Please provide 'axis' or 'position' option.`)}function gd(e,t,n){if(n[t+`AxisID`]===e)return{axis:t}}function _d(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 gd(e,`x`,n[0])||gd(e,`y`,n[0])}return{}}function vd(e,t){let n=Ho[e.type]||{scales:{}},r=t.scales||{},i=ud(e.type,t),a=Object.create(null);return Object.keys(r).forEach(t=>{let o=r[t];if(!G(o))return console.error(`Invalid scale configuration for scale: ${t}`);if(o._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${t}`);let s=hd(t,o,_d(t,e),Q.scales[o.type]),c=fd(s,i),l=n.scales||{};a[t]=Ea(Object.create(null),[{axis:s},o,l[s],l[c]])}),e.data.datasets.forEach(n=>{let i=n.type||e.type,o=n.indexAxis||ud(i,t),s=(Ho[i]||{}).scales||{};Object.keys(s).forEach(e=>{let t=dd(e,o),i=n[t+`AxisID`]||t;a[i]=a[i]||Object.create(null),Ea(a[i],[{axis:t},r[i],s[e]])})}),Object.keys(a).forEach(e=>{let t=a[e];Ea(t,[Q.scales[t.type],Q.scale])}),a}function yd(e){let t=e.options||(e.options={});t.plugins=K(t.plugins,{}),t.scales=vd(e,t)}function bd(e){return e=e||{},e.datasets=e.datasets||[],e.labels=e.labels||[],e}function xd(e){return e=e||{},e.data=bd(e.data),yd(e),e}var Sd=new Map,Cd=new Set;function wd(e,t){let n=Sd.get(e);return n||(n=t(),Sd.set(e,n),Cd.add(n)),n}var Td=(e,t,n)=>{let r=ja(t,n);r!==void 0&&e.add(r)},Ed=class{constructor(e){this._config=xd(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=bd(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(),yd(e)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(e){return wd(e,()=>[[`datasets.${e}`,``]])}datasetAnimationScopeKeys(e,t){return wd(`${e}.transition.${t}`,()=>[[`datasets.${e}.transitions.${t}`,`transitions.${t}`],[`datasets.${e}`,``]])}datasetElementScopeKeys(e,t){return wd(`${e}-${t}`,()=>[[`datasets.${e}.elements.${t}`,`datasets.${e}`,`elements.${t}`,``]])}pluginScopeKeys(e){let t=e.id,n=this.type;return wd(`${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=>Td(s,e,t))),t.forEach(e=>Td(s,r,e)),t.forEach(e=>Td(s,Ho[i]||{},e)),t.forEach(e=>Td(s,Q,e)),t.forEach(e=>Td(s,Uo,e))});let c=Array.from(s);return c.length===0&&c.push(Object.create(null)),Cd.has(t)&&a.set(t,c),c}chartOptionScopes(){let{options:e,type:t}=this;return[e,Ho[t]||{},Q.datasets[t]||{},{type:t},Q,Uo]}resolveNamedOptions(e,t,n,r=[``]){let i={$shared:!0},{resolver:a,subPrefixes:o}=Dd(this._resolverCache,e,r),s=a;if(kd(a,t)){i.$shared=!1,n=Pa(n)?n():n;let t=this.createResolver(e,n,o);s=Ss(a,n,t)}for(let e of t)i[e]=s[e];return i}createResolver(e,t,n=[``],r){let{resolver:i}=Dd(this._resolverCache,e,n);return G(t)?Ss(i,t,void 0,r):i}};function Dd(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:xs(t,n),subPrefixes:n.filter(e=>!e.toLowerCase().includes(`hover`))},r.set(i,a)),a}var Od=e=>G(e)&&Object.getOwnPropertyNames(e).some(t=>Pa(e[t]));function kd(e,t){let{isScriptable:n,isIndexable:r}=Cs(e);for(let i of t){let t=n(i),a=r(i),o=(a||t)&&e[i];if(t&&(Pa(o)||Od(o))||a&&W(o))return!0}return!1}var Ad=`4.5.1`,jd=[`top`,`bottom`,`left`,`right`,`chartArea`];function Md(e,t){return e===`top`||e===`bottom`||jd.indexOf(e)===-1&&t===`x`}function Nd(e,t){return function(n,r){return n[e]===r[e]?n[t]-r[t]:n[e]-r[e]}}function Pd(e){let t=e.chart,n=t.options.animation;t.notifyPlugins(`afterRender`),q(n&&n.onComplete,[e],t)}function Fd(e){let t=e.chart,n=t.options.animation;q(n&&n.onProgress,[e],t)}function Id(e){return Zs()&&typeof e==`string`?e=document.getElementById(e):e&&e.length&&(e=e[0]),e&&e.canvas&&(e=e.canvas),e}var Ld={},Rd=e=>{let t=Id(e);return Object.values(Ld).filter(e=>e.canvas===t).pop()};function zd(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 Bd(e,t,n,r){return!n||e.type===`mouseout`?null:r?t:e}var Vd=class{static register(...e){id.add(...e),Hd()}static unregister(...e){id.remove(...e),Hd()}constructor(e,t){let n=this.config=new Ed(t),r=Id(e),i=Rd(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||(Mu(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=ga(),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 ad,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=yo(e=>this.update(e),a.resizeDelay||0),this._dataChanges=[],Ld[this.id]=this,!o||!s?console.error(`Failed to create chart: can't acquire context from the given item`):(zc.listen(this,`complete`,Pd),zc.listen(this,`progress`,Fd),this._initialize(),this.attached&&this.update())}get aspectRatio(){let{options:{aspectRatio:e,maintainAspectRatio:t},width:n,height:r,_aspectRatio:i}=this;return U(e)?t&&i?i:r?n/r:null:e}get data(){return this.config.data}set data(e){this.config.data=e}get options(){return this._options}set options(e){this.config.options=e}get registry(){return id}_initialize(){return this.notifyPlugins(`beforeInit`),this.options.responsive?this.resize():uc(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins(`afterInit`),this}clear(){return Xo(this.canvas,this.ctx),this}stop(){return zc.stop(this),this}resize(e,t){zc.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,uc(this,o,!0)&&(this.notifyPlugins(`resize`,{size:a}),q(n.onResize,[this,a],this),this.attached&&this._doResize(s)&&this.render())}ensureScalesHaveIDs(){J(this.options.scales||{},(e,t)=>{e.id=t})}buildOrUpdateScales(){let e=this.options,t=e.scales,n=this.scales,r=Object.keys(n).reduce((e,t)=>(e[t]=!1,e),{}),i=[];t&&(i=i.concat(Object.keys(t).map(e=>{let n=t[e],r=hd(e,n),i=r===`r`,a=r===`x`;return{options:n,dposition:i?`chartArea`:a?`bottom`:`left`,dtype:i?`radialLinear`:a?`category`:`linear`}}))),J(i,t=>{let i=t.options,a=i.id,o=hd(a,i),s=K(i.type,t.dtype);(i.position===void 0||Md(i.position,o)!==Md(t.dposition))&&(i.position=t.dposition),r[a]=!0;let c=null;a in n&&n[a].type===s?c=n[a]:(c=new(id.getScale(s))({id:a,type:s,ctx:this.ctx,chart:this}),n[c.id]=c),c.init(i,e)}),J(r,(e,t)=>{e||delete n[t]}),J(n,e=>{lu.configure(this,e,e.options),lu.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(Nd(`z`,`_idx`));let{_active:o,_lastEvent:s}=this;s?this._eventHandler(s,!0):o.length&&this._updateHoverStyles(o,o,!0),this.render()}_updateScales(){J(this.scales,e=>{lu.removeBox(this,e)}),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){let e=this.options;(!Fa(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)zd(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;lu.update(this,this.width,this.height,e);let t=this.chartArea,n=t.width<=0||t.height<=0;this._layers=[],J(this.boxes,e=>{n&&e.position===`chartArea`||(e.configure&&e.configure(),this._layers.push(...e._layers()))},this),this._layers.forEach((e,t)=>{e._idx=t}),this.notifyPlugins(`afterLayout`)}_updateDatasets(e){if(this.notifyPlugins(`beforeDatasetsUpdate`,{mode:e,cancelable:!0})!==!1){for(let e=0,t=this.data.datasets.length;e=0;--t)this._drawDataset(e[t]);this.notifyPlugins(`afterDatasetsDraw`)}_drawDataset(e){let t=this.ctx,n={meta:e,index:e.index,cancelable:!0},r=Fc(this,e);this.notifyPlugins(`beforeDatasetDraw`,n)!==!1&&(r&&es(t,r),e.controller.draw(),r&&ts(t),n.cancelable=!1,this.notifyPlugins(`afterDatasetDraw`,n))}isPointInArea(e){return $o(e,this.chartArea,this._minPadding)}getElementsAtEventForMode(e,t,n,r){let i=Kl.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=bs(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);Na(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(),zc.remove(this),e=0,t=this.data.datasets.length;e{t.addEventListener(this,n,r),e[n]=r},r=(e,t,n)=>{e.offsetX=t,e.offsetY=n,this._eventHandler(e)};J(this.options.events,e=>n(e,r))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});let e=this._responsiveListeners,t=this.platform,n=(n,r)=>{t.addEventListener(this,n,r),e[n]=r},r=(n,r)=>{e[n]&&(t.removeEventListener(this,n,r),delete e[n])},i=(e,t)=>{this.canvas&&this.resize(e,t)},a,o=()=>{r(`attach`,o),this.attached=!0,this.resize(),n(`resize`,i),n(`detach`,a)};a=()=>{this.attached=!1,r(`resize`,i),this._stop(),this._resize(0,0),n(`attach`,o)},t.isAttached(this.canvas)?o():a()}unbindEvents(){J(this._listeners,(e,t)=>{this.platform.removeEventListener(this,t,e)}),this._listeners={},J(this._responsiveListeners,(e,t)=>{this.platform.removeEventListener(this,t,e)}),this._responsiveListeners=void 0}updateHoverStyle(e,t,n){let r=n?`set`:`remove`,i,a,o,s;for(t===`dataset`&&(i=this.getDatasetMeta(e[0].datasetIndex),i.controller[`_`+r+`DatasetHoverStyle`]()),o=0,s=e.length;o{let n=this.getDatasetMeta(e);if(!n)throw Error(`No dataset found at index `+e);return{datasetIndex:e,element:n.data[t],index:t}});xa(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=Ia(e),c=Bd(e,this._lastEvent,n,s);n&&(this._lastEvent=null,q(i.onHover,[e,o,this],this),s&&q(i.onClick,[e,o,this],this));let l=!xa(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)}};$(Vd,`defaults`,Q),$(Vd,`instances`,Ld),$(Vd,`overrides`,Ho),$(Vd,`registry`,id),$(Vd,`version`,Ad),$(Vd,`getChart`,Rd);function Hd(){return J(Vd.instances,e=>e._plugins.invalidate())}function Ud(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,ro(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,ro(r-n));e.arc(i,a,s+l/2,n-t/2,r+t/2,!0)}else{let t=Math.min(l/2,o*ro(r-n));if(u===`round`)e.arc(i,a,t,n-Y/2,r+Y/2,!0);else if(u===`bevel`){let o=2*t*t,s=-o*Math.cos(n+Y/2)+i,c=-o*Math.sin(n+Y/2)+a,l=o*Math.cos(r+Y/2)+i,u=o*Math.sin(r+Y/2)+a;e.lineTo(s,c),e.lineTo(l,u)}}e.closePath(),e.moveTo(0,0),e.rect(0,0,e.canvas.width,e.canvas.height),e.clip(`evenodd`)}function Wd(e,t,n){let{startAngle:r,pixelMargin:i,x:a,y:o,outerRadius:s,innerRadius:c}=t,l=i/s;e.beginPath(),e.arc(a,o,s,r-l,n+l),c>i?(l=i/c,e.arc(a,o,c,n+l,r-l,!0)):e.arc(a,o,i,n+Z,r-Z),e.closePath(),e.clip()}function Gd(e){return ps(e,[`outerStart`,`outerEnd`,`innerStart`,`innerEnd`])}function Kd(e,t,n,r){let i=Gd(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 ao(e,0,Math.min(a,t))};return{outerStart:s(i.outerStart),outerEnd:s(i.outerEnd),innerStart:ao(i.innerStart,0,o),innerEnd:ao(i.innerEnd,0,o)}}function qd(e,t,n,r){return{x:n+e*Math.cos(t),y:r+e*Math.sin(t)}}function Jd(e,t,n,r,i,a){let{x:o,y:s,startAngle:c,pixelMargin:l,innerRadius:u}=t,d=Math.max(t.outerRadius+r+n-l,0),f=u>0?u+r+n+l:0,p=0,m=i-c;if(r){let e=((u>0?u-r:0)+(d>0?d-r:0))/2;p=(m-(e===0?m:m*e/(e+r)))/2}let h=(m-Math.max(.001,m*d-n/Y)/d)/2,g=c+h+p,_=i-h-p,{outerStart:v,outerEnd:y,innerStart:b,innerEnd:x}=Kd(t,f,d,_-g),S=d-v,C=d-y,w=g+v/S,T=_-y/C,E=f+b,D=f+x,O=g+b/E,k=_-x/D;if(e.beginPath(),a){let t=(w+T)/2;if(e.arc(o,s,d,w,t),e.arc(o,s,d,t,T),y>0){let t=qd(C,T,o,s);e.arc(t.x,t.y,y,T,_+Z)}let n=qd(D,_,o,s);if(e.lineTo(n.x,n.y),x>0){let t=qd(D,k,o,s);e.arc(t.x,t.y,x,_+Z,k+Math.PI)}let r=(_-x/f+(g+b/f))/2;if(e.arc(o,s,f,_-x/f,r,!0),e.arc(o,s,f,r,g+b/f,!0),b>0){let t=qd(E,O,o,s);e.arc(t.x,t.y,b,O+Math.PI,g-Z)}let i=qd(S,g,o,s);if(e.lineTo(i.x,i.y),v>0){let t=qd(S,w,o,s);e.arc(t.x,t.y,v,g-Z,w)}}else{e.moveTo(o,s);let t=Math.cos(w)*d+o,n=Math.sin(w)*d+s;e.lineTo(t,n);let r=Math.cos(T)*d+o,i=Math.sin(T)*d+s;e.lineTo(r,i)}e.closePath()}function Yd(e,t,n,r,i){let{fullCircles:a,startAngle:o,circumference:s}=t,c=t.endAngle;if(a){Jd(e,t,n,r,c,i);for(let t=0;t=Y&&p===0&&u!==`miter`&&Ud(e,t,h),a||(Jd(e,t,n,r,h,i),e.stroke())}var Zd=class extends Nu{constructor(e){super(),$(this,`circumference`,void 0),$(this,`endAngle`,void 0),$(this,`fullCircles`,void 0),$(this,`innerRadius`,void 0),$(this,`outerRadius`,void 0),$(this,`pixelMargin`,void 0),$(this,`startAngle`,void 0),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,e&&Object.assign(this,e)}inRange(e,t,n){let{angle:r,distance:i}=eo(this.getProps([`x`,`y`],n),{x:e,y:t}),{startAngle:a,endAngle:o,innerRadius:s,outerRadius:c,circumference:l}=this.getProps([`startAngle`,`endAngle`,`innerRadius`,`outerRadius`,`circumference`],n),u=(this.options.spacing+this.options.borderWidth)/2,d=K(l,o-a),f=io(r,a,o)&&a!==o,p=d>=X||f,m=so(i,s+u,c+u);return p&&m}getCenterPoint(e){let{x:t,y:n,startAngle:r,endAngle:i,innerRadius:a,outerRadius:o}=this.getProps([`x`,`y`,`startAngle`,`endAngle`,`innerRadius`,`outerRadius`],e),{offset:s,spacing:c}=this.options,l=(r+i)/2,u=(a+o+c+s)/2;return{x:t+Math.cos(l)*u,y:n+Math.sin(l)*u}}tooltipPosition(e){return this.getCenterPoint(e)}draw(e){let{options:t,circumference:n}=this,r=(t.offset||0)/4,i=(t.spacing||0)/2,a=t.circular;if(this.pixelMargin=t.borderAlign===`inner`?.33:0,this.fullCircles=n>X?Math.floor(n/X):0,n===0||this.innerRadius<0||this.outerRadius<0)return;e.save();let o=(this.startAngle+this.endAngle)/2;e.translate(Math.cos(o)*r,Math.sin(o)*r);let s=r*(1-Math.sin(Math.min(Y,n||0)));e.fillStyle=t.backgroundColor,e.strokeStyle=t.borderColor,Yd(e,this,s,i,a),Xd(e,this,s,i,a),e.restore()}};$(Zd,`id`,`arc`),$(Zd,`defaults`,{borderAlign:`center`,borderColor:`#fff`,borderDash:[],borderDashOffset:0,borderJoinStyle:void 0,borderRadius:0,borderWidth:2,offset:0,spacing:0,angle:void 0,circular:!0,selfJoin:!1}),$(Zd,`defaultRoutes`,{backgroundColor:`backgroundColor`}),$(Zd,`descriptors`,{_scriptable:!0,_indexable:e=>e!==`borderDash`});function Qd(e,t,n=t){e.lineCap=K(n.borderCapStyle,t.borderCapStyle),e.setLineDash(K(n.borderDash,t.borderDash)),e.lineDashOffset=K(n.borderDashOffset,t.borderDashOffset),e.lineJoin=K(n.borderJoinStyle,t.borderJoinStyle),e.lineWidth=K(n.borderWidth,t.borderWidth),e.strokeStyle=K(n.borderColor,t.borderColor)}function $d(e,t,n){e.lineTo(n.x,n.y)}function ef(e){return e.stepped?ns:e.tension||e.cubicInterpolationMode===`monotone`?rs:$d}function tf(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 af(e){let t=e.options,n=t.borderDash&&t.borderDash.length;return!e._decimated&&!e._loop&&!t.tension&&t.cubicInterpolationMode!==`monotone`&&!t.stepped&&!n?rf:nf}function of(e){return e.stepped?mc:e.tension||e.cubicInterpolationMode===`monotone`?hc:pc}function sf(e,t,n,r){let i=t._path;i||(i=t._path=new Path2D,t.path(i,n,r)&&i.closePath()),Qd(e,t.options),e.stroke(i)}function cf(e,t,n,r){let{segments:i,options:a}=t,o=af(t);for(let s of i)Qd(e,a,s.style),e.beginPath(),o(e,t,s,{start:n,end:n+r-1})&&e.closePath(),e.stroke()}var lf=typeof Path2D==`function`;function uf(e,t,n,r){lf&&!t.options.segment?sf(e,t,n,r):cf(e,t,n,r)}var df=class extends Nu{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;Xs(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=Oc(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=Tc(this,{property:t,start:r,end:r});if(!a.length)return;let o=[],s=of(n),c,l;for(c=0,l=a.length;ce!==`borderDash`&&e!==`fill`});function ff(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,te(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=n.__.length&&n.__.push({}),n.__[e]}function R(e){return pe=1,Ce(Fe,e)}function Ce(e,t,n){var r=Se(le++,2);if(r.t=e,!r.__c&&(r.__=[n?n(t):Fe(void 0,t),function(e){var t=r.__N?r.__N[0]:r.__[0],n=r.t(t,e);fe(t,n)||(r.__N=[n,r.__[1]],r.__c.setState({}))}],r.__c=I,!I.__f)){I.__f=!0;var i=I.shouldComponentUpdate;I.shouldComponentUpdate=function(e,t,n){var r=this.__H;if(!r)return!0;var a=!1,o=this.props!=e;if(r.__.some(function(e){e.__N&&(a=!0,fe(e.__[0],e.__N[0])||(o=!0))}),i){var s=i.call(this,e,t,n);return a?s||o:s}return!a||o}}return r.__}function z(e,t){var n=Se(le++,3);!L.__s&&Pe(n.__H,t)&&(n.__P=!0,n.__=e,n.u=t,I.__H.__h.push(n))}function we(e){return pe=5,Te(function(){return{current:e}},[])}function Te(e,t){var n=Se(le++,7);return Pe(n.__H,t)&&(n.__=e(),n.__H=t),n.__}function Ee(e,t){return pe=8,Te(function(){return e},t)}function De(){var e=Se(le++,11);if(!e.__){for(var t=I.__v;!t.__m&&t.__;)t=t.__;var n=t.__m||(t.__m=[0,0]);e.__=`P`+n[0]+`-`+n[1]++}return e.__}function Oe(){var e;do{for(;e=he.shift();)try{Me(e)}catch(t){L.__e(t,{__:(e=e.__P)&&e.__v})}for(;e=me.shift();){var t=e.__H;if(e.__P&&t)try{t.__h.some(Me),t.__h.some(Ne),t.__h=[]}catch(n){t.__h=[],L.__e(n,e.__v)}}}while(he.length)}L.__b=function(e){I=null,ge&&ge(e)},L.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),xe&&xe(e,t)},L.__r=function(e){_e&&_e(e),le=0;var t=(I=e.__c).__H;t&&(ue==I?I.__h=[]:(t.__h.some(Me),t.__h.some(Ne),le=0),t.__h=[],t.__.some(function(e){e.__N&&(e.__=e.__N),e.u=e.__N=void 0})),ue=I},L.diffed=function(e){ve&&ve(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&je(me.push(t)),t.__H.__.some(function(e){e.u&&(e.__H=e.u)})),ue=I=null},L.__c=function(e,t){t.some(function(e){try{e.__h.some(Me),e.__h=e.__h.filter(function(e){return!e.__||Ne(e)})}catch(t){e.__h=[function(){throw t}]}}),ye&&ye(e,t)},L.unmount=function(e){be&&be(e);var t,n,r=e.__c;r&&r.__H&&(r.__H.__.some(function(r){try{if(r.__P&&r.__c){if(n===void 0){for(n=e.__;n&&(!n.__c||!n.__c.__P);)n=n.__;n=n&&n.__c}r.__P=n,je(he.push(r))}else Me(r)}catch(e){t=e}}),r.__H=void 0,t&&L.__e(t,r.__v))};var ke=typeof requestAnimationFrame==`function`;function Ae(e){var t,n=function(){clearTimeout(r),ke&&cancelAnimationFrame(t),setTimeout(e)},r=setTimeout(n,35);ke&&(t=requestAnimationFrame(n))}function je(e){e!=1&&de==L.requestAnimationFrame||((de=L.requestAnimationFrame)||Ae)(Oe)}function Me(e){var t=I,n=e.__c;typeof n==`function`&&(e.__c=void 0,n()),I=t}function Ne(e){var t=I;e.__c=e.__(),I=t}function Pe(e,t){return!e||e.length!=t.length||t.some(function(t,n){return!fe(t,e[n])})}function Fe(e,t){return typeof t==`function`?t(e):t}var Ie=Object.fromEntries([{route:`home`,hash:`#home`,label:`대시보드`,title:`대시보드`,description:`네트워크, 장치와 시스템 상태를 한눈에 확인합니다.`},{route:`activity`,hash:`#activity`,label:`최근 활동`,title:`최근 활동`,description:`현재 부팅 이후 인터넷, 보호, 업데이트와 진단 상태 변화를 확인합니다.`},{route:`network`,hash:`#network`,label:`네트워크`,title:`네트워크`,description:`인터넷 연결과 내부 네트워크 주소, DHCP 및 네트워크 충돌을 한 곳에서 관리합니다.`},{route:`wifi`,hash:`#wifi`,label:`Wi-Fi`,title:`Wi-Fi`,description:`기본 무선 네트워크의 이름, 보안과 사용 상태를 관리합니다.`},{route:`iptv`,hash:`#iptv`,label:`IPTV`,title:`IPTV`,description:`SK Broadband와 LG U+ 멀티캐스트 IPTV를 설정합니다.`},{route:`devices`,hash:`#devices`,label:`연결된 기기`,title:`연결된 기기`,description:`네트워크에서 확인된 기기와 연결 방식을 살펴봅니다.`},{route:`safeshield`,hash:`#safeshield`,label:`SafeShield`,title:`SafeShield`,description:`DNS 보호 상태를 확인하고 차단 목록을 관리합니다.`},{route:`rules`,hash:`#rules`,label:`사용자 규칙`,title:`사용자 규칙`,description:`직접 허용하거나 차단할 도메인을 관리합니다.`},{route:`account`,hash:`#account`,label:`SmartSafeHub 계정`,title:`SmartSafeHub 계정`,description:`이 기기의 SmartSafeHub 계정 연결 상태와 구독 플랜을 확인합니다.`},{route:`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])),Le=[`home`,`activity`,`network`,`wifi`,`devices`,`safeshield`,`rules`,`account`,`apps`,`system`],Re=`smartsafehub.theme`;function ze(){if(typeof window>`u`)return`light`;try{let e=window.localStorage.getItem(Re);if(e===`dark`||e===`light`)return e}catch{}return window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}function Be(e){try{window.localStorage.setItem(Re,e)}catch{}}function Ve(e){let t=document.querySelector(`meta[name="theme-color"]`);t&&(t.content=e===`dark`?`#0f172a`:`#f8fafc`),document.documentElement.style.colorScheme=e,document.body.style.backgroundColor=e===`dark`?`#020617`:`#f8fafc`}var He=Symbol.for(`pv`),Ue=0;Array.isArray;function B(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:He,__v:--Ue,__i:-1,__u:0};return(a||o)&&(u.__source=a,u.__self=o),e.vnode&&e.vnode(u),u}function V({children:e,title:t,...n}){return B(`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?B(`title`,{children:t}):null,e]})}function We(e){return B(V,{...e,children:[B(`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`}),B(`path`,{d:`m9 12 2 2 4-4`})]})}function Ge(e){return B(V,{...e,children:[B(`rect`,{height:`7`,rx:`2`,width:`18`,x:`3`,y:`13`}),B(`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 Ke(e){return B(V,{...e,children:[B(`rect`,{height:`11`,rx:`2`,width:`16`,x:`4`,y:`3`}),B(`path`,{d:`M8 21h8M12 14v7`})]})}function qe(e){return B(V,{...e,children:[B(`path`,{d:`m3 11 9-8 9 8`}),B(`path`,{d:`M5 10v10h14V10M9 20v-6h6v6`})]})}function Je(e){return B(V,{...e,children:[B(`circle`,{cx:`12`,cy:`8`,r:`4`}),B(`path`,{d:`M4.5 21a7.5 7.5 0 0 1 15 0`})]})}function Ye(e){return B(V,{...e,children:[B(`rect`,{x:`3`,y:`3`,width:`7`,height:`7`,rx:`1.5`}),B(`rect`,{x:`14`,y:`3`,width:`7`,height:`7`,rx:`1.5`}),B(`rect`,{x:`3`,y:`14`,width:`7`,height:`7`,rx:`1.5`}),B(`path`,{d:`M17.5 14v7M14 17.5h7`})]})}function Xe(e){return B(V,{...e,children:[B(`rect`,{x:`2.5`,y:`3.5`,width:`7`,height:`6`,rx:`1.5`}),B(`rect`,{x:`14.5`,y:`3.5`,width:`7`,height:`6`,rx:`1.5`}),B(`rect`,{x:`8.5`,y:`15`,width:`7`,height:`6`,rx:`1.5`}),B(`path`,{d:`M6 9.5v2.5h12V9.5M12 12v3`})]})}function Ze(e){return B(V,{...e,children:[B(`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`}),B(`path`,{d:`M6.5 11v4M4.5 13h4M16 11.5h.01M18.5 13.5h.01`})]})}function Qe(e){return B(V,{...e,children:B(`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 $e(e){return B(V,{...e,children:[B(`circle`,{cx:`12`,cy:`12`,r:`10`}),B(`path`,{d:`M8 12h.01M12 12h.01M16 12h.01`})]})}function et(e){return B(V,{...e,children:[B(`path`,{d:`M12 16V4`}),B(`path`,{d:`m7 9 5-5 5 5`}),B(`path`,{d:`M5 14v5h14v-5`})]})}function tt(e){return B(V,{...e,children:[B(`circle`,{cx:`12`,cy:`12`,r:`3`}),B(`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 nt(e){return B(V,{...e,children:[B(`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`}),B(`path`,{d:`M12 20h.01`})]})}function rt(e){return B(V,{...e,children:[B(`path`,{d:`M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4z`}),B(`path`,{d:`M6.5 6.5h1M16.5 6.5h1M6.5 16.5h1`}),B(`path`,{d:`M14 14h2v2h-2zM18 14h2v2h-2zM16 16h2v2h-2zM18 18h2v2h-2zM14 18h1`})]})}function it(e){return B(V,{...e,children:[B(`path`,{d:`M7 3v4M17 3v4M5 7h14v4a7 7 0 0 1-14 0V7Z`}),B(`path`,{d:`M12 18v3`})]})}function at(e){return B(V,{...e,children:[B(`circle`,{cx:`12`,cy:`12`,r:`9`}),B(`path`,{d:`M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18`})]})}function ot(e){return B(V,{...e,children:[B(`rect`,{height:`13`,rx:`2`,width:`18`,x:`3`,y:`6`}),B(`path`,{d:`m8 2 4 4 4-4M8 22h8`})]})}function st(e){return B(V,{...e,children:[B(`circle`,{cx:`12`,cy:`12`,r:`9`}),B(`path`,{d:`M12 7v5l3 2`})]})}function ct(e){return B(V,{...e,children:[B(`rect`,{height:`14`,rx:`2`,width:`14`,x:`5`,y:`5`}),B(`path`,{d:`M9 9h6v6H9zM9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M2 15h3M19 9h3M19 15h3`})]})}function H(e){return B(V,{...e,children:[B(`path`,{d:`M20 11a8 8 0 1 0-2.3 5.7`}),B(`path`,{d:`M20 4v7h-7`})]})}function lt(e){return B(V,{...e,children:[B(`path`,{d:`M12 2v10`}),B(`path`,{d:`M18.4 6.6a8 8 0 1 1-12.8 0`})]})}function ut(e){return B(V,{...e,children:[B(`path`,{d:`M12 3v12`}),B(`path`,{d:`m7 10 5 5 5-5`}),B(`path`,{d:`M5 21h14`})]})}function dt(e){return B(V,{...e,children:[B(`ellipse`,{cx:`12`,cy:`5`,rx:`8`,ry:`3`}),B(`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 ft(e){return B(V,{...e,children:[B(`circle`,{cx:`8`,cy:`15`,r:`4`}),B(`path`,{d:`m11 12 8-8M15 8l2 2M17 6l2 2`})]})}function pt(e){return B(V,{...e,children:[B(`rect`,{height:`13`,rx:`2`,width:`13`,x:`8`,y:`8`}),B(`path`,{d:`M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3`})]})}function mt(e){return B(V,{...e,children:[B(`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`}),B(`circle`,{cx:`12`,cy:`12`,r:`2.5`})]})}function ht(e){return B(V,{...e,children:[B(`path`,{d:`m3 3 18 18`}),B(`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`}),B(`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`}),B(`path`,{d:`M10.3 10.3a2.5 2.5 0 0 0 3.4 3.4`})]})}function gt(e){return B(V,{...e,children:[B(`rect`,{height:`17`,rx:`2`,width:`18`,x:`3`,y:`4`}),B(`path`,{d:`M8 2v4M16 2v4M3 9h18`})]})}function _t(e){return B(V,{...e,children:[B(`circle`,{cx:`12`,cy:`12`,r:`9`}),B(`path`,{d:`m8 12 2.5 2.5L16 9`})]})}function U(e){return B(V,{...e,children:[B(`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`}),B(`path`,{d:`M12 9v4M12 17h.01`})]})}function vt(e){return B(V,{...e,children:B(`path`,{d:`M12 5v14M5 12h14`})})}function yt(e){return B(V,{...e,children:B(`path`,{d:`M4 7h16M9 7V4h6v3M7 7l1 13h8l1-13M10 11v5M14 11v5`})})}function bt(e){return B(V,{...e,children:[B(`circle`,{cx:`11`,cy:`11`,r:`7`}),B(`path`,{d:`m20 20-4-4`})]})}function xt(e){return B(V,{...e,children:[B(`path`,{d:`M8 6h12M8 12h12M8 18h12`}),B(`path`,{d:`M4 6h.01M4 12h.01M4 18h.01`})]})}function St(e){return B(V,{...e,children:[B(`path`,{d:`M10 5H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h4`}),B(`path`,{d:`M14 8l4 4-4 4M8 12h10`})]})}function Ct(e){return B(V,{...e,children:B(`path`,{d:`M4 7h16M4 12h16M4 17h16`})})}function wt(e){return B(V,{...e,children:B(`path`,{d:`m6 9 6 6 6-6`})})}function Tt(e){return B(V,{...e,children:B(`path`,{d:`m6 6 12 12M18 6 6 18`})})}function Et(e){return B(V,{...e,children:B(`path`,{d:`m15 18-6-6 6-6`})})}function Dt(e){return B(V,{...e,children:B(`path`,{d:`m9 18 6-6-6-6`})})}function Ot(e){return B(V,{...e,children:B(`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 kt(e){return B(V,{...e,children:[B(`circle`,{cx:`12`,cy:`12`,r:`4`}),B(`path`,{d:`M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41`})]})}function At({description:e,loading:t,onRefresh:n,onToggleTheme:r,refreshing:i,theme:a,title:o}){return B(`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:B(`div`,{class:`flex w-full items-center justify-between gap-4 md:h-full`,children:[B(`div`,{class:`min-w-0`,children:[B(`p`,{class:`m-0 text-[0.68rem] font-extrabold uppercase tracking-[0.18em] text-teal-700`,children:`SmartSafeHub`}),B(`div`,{class:`mt-1 flex min-w-0 items-baseline gap-3`,children:[B(`h1`,{class:`m-0 truncate text-xl font-black tracking-tight text-slate-950 sm:text-2xl`,children:o}),B(`p`,{class:`m-0 hidden truncate text-sm text-slate-500 lg:block`,children:e})]})]}),B(`div`,{class:`flex shrink-0 items-center gap-2`,children:[B(`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:B(a===`dark`?kt:Ot,{class:`size-5`})}),B(`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?B(H,{class:`size-5 animate-spin`}):B(H,{class:`size-5`})})]})]})})}var jt=`/cgi-bin/luci`;function Mt(){let e=window.location.pathname,t=e.indexOf(`/cgi-bin/luci`);return t>=0?e.slice(0,t+13):jt}function Nt(e){let t=e.startsWith(`/`)?e:`/${e}`;return`${Mt()}${t}`}function Pt(e){return Nt(e)}var Ft=[{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 It({route:e}){switch(e){case`home`:return B(qe,{class:`size-5`});case`activity`:return B(st,{class:`size-5`});case`network`:return B(at,{class:`size-5`});case`wifi`:return B(nt,{class:`size-5`});case`iptv`:return B(ot,{class:`size-5`});case`devices`:return B(Ke,{class:`size-5`});case`safeshield`:return B(We,{class:`size-5`});case`rules`:return B(Je,{class:`size-5`});case`account`:return B(ft,{class:`size-5`});case`apps`:return B(Ye,{class:`size-5`});case`system`:return B(et,{class:`size-5`});case`settings`:return B(tt,{class:`size-5`})}}function Lt({theme:e}){return B(e===`dark`?kt:Ot,{class:`size-5`})}function Rt(e){return e===`dark`?`라이트 모드`:`다크 모드`}function zt(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 Bt(e){return e===`apps`?`앱으로 확장하기`:null}function Vt({accountRegistered:e,collapsed:t=!1,route:n,updateCount:r,demo:i=!1}){return B(`div`,{class:t?`space-y-3`:`space-y-5`,children:Ft.filter(e=>!i||e.routes.some(e=>Le.includes(e))).map((a,o)=>B(`section`,{"aria-label":a.label,class:t&&o>0?`border-t border-slate-100 pt-3`:void 0,children:[t?null:B(`p`,{class:`mb-2 px-3 text-[0.65rem] font-black uppercase tracking-[0.18em] text-slate-400`,children:a.label}),B(`div`,{class:`space-y-1`,children:a.routes.filter(e=>!i||Le.includes(e)).map(i=>{let a=Ie[i],o=n===i;return B(`a`,{"aria-current":o?`page`:void 0,"aria-label":i===`account`&&e===!1?`${a.label} (계정 연결 필요)`:t?a.label:void 0,class:zt(o,t,i),href:a.hash,title:i===`account`&&e===!1?`SmartSafeHub 계정 연결 필요`:t?a.label:void 0,children:[B(`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:B(It,{route:i})}),t?null:B(`span`,{class:`flex min-w-0 flex-1 items-center gap-2`,children:[B(`span`,{class:`min-w-0 flex-1`,children:[B(`span`,{class:`block truncate`,children:a.label}),Bt(i)?B(`span`,{class:`ssh-apps-nav-description mt-0.5 block truncate text-[11px] font-semibold tracking-normal`,children:Bt(i)}):null]}),i===`iptv`?B(`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`?B(`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?B(`span`,{"aria-label":`계정 연결 필요`,class:`ssh-account-nav-dot ${t?`ssh-account-nav-dot-collapsed`:``}`,"aria-hidden":`true`,title:`계정 연결 필요`}):null,i===`system`&&r>0?t?B(`span`,{"aria-label":`${r}개의 업데이트 유형`,class:`ssh-update-nav-badge`,"data-collapsed":`true`,children:r}):B(`span`,{class:`ssh-update-nav-badge`,"data-collapsed":`false`,children:r}):null]},i)})})]},a.label))})}function Ht({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]=R(!1),m=Rt(c);return z(()=>{p(!1)},[s]),z(()=>{if(!f)return;let e=e=>{e.key===`Escape`&&p(!1)};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[f]),B(b,{children:[B(`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:[B(`div`,{class:`flex min-h-16 items-center justify-between gap-3`,children:[B(`a`,{class:`flex min-w-0 items-center gap-3 no-underline`,href:`#home`,children:[B(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-950 text-teal-300`,children:B(We,{class:`size-5`})}),B(`span`,{class:`min-w-0`,children:[B(`strong`,{class:`block truncate text-sm font-black text-slate-950`,children:`SmartSafeHub`}),B(`span`,{class:`block truncate text-xs font-semibold text-slate-500`,children:l})]})]}),B(`div`,{class:`flex shrink-0 items-center gap-2`,children:[B(`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:B(Lt,{theme:c})}),B(`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?B(H,{class:`size-5 animate-spin`}):B(H,{class:`size-5`})}),B(`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:B(f?Tt:Ct,{class:`size-5`})})]})]}),f?B(`div`,{class:`ssh-mobile-menu border-t border-slate-100 py-4`,id:`smartsafehub-mobile-menu`,children:[B(Vt,{accountRegistered:e,demo:d,route:s,updateCount:u}),d?null:B(`div`,{class:`mt-5 space-y-1 border-t border-slate-100 pt-4`,children:B(`a`,{class:`${zt(!1)} text-rose-700 hover:bg-rose-50 hover:text-rose-800`,href:Pt(`/admin/logout`),children:[B(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg bg-rose-50 text-rose-600`,children:B(St,{class:`size-5`})}),`로그아웃`]})})]}):null]}),B(`aside`,{class:`hidden min-h-screen border-r border-slate-200 bg-white md:flex md:flex-col`,"data-collapsed":t?`true`:`false`,children:B(`div`,{class:`sticky top-0 flex h-screen flex-col`,children:[B(`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:[B(`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:[B(`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:B(We,{class:`size-5`})}),t?null:B(`div`,{class:`min-w-0`,children:[B(`strong`,{class:`block truncate text-sm font-black tracking-tight text-slate-950`,children:`SmartSafeHub`}),B(`span`,{class:`block text-[0.68rem] font-bold uppercase tracking-[0.14em] text-slate-400`,children:`Home Gateway`})]})]}),B(`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:B(t?Dt:Et,{class:`size-4`})})]}),B(`nav`,{"aria-label":`SmartSafeHub 메뉴`,class:`flex-1 overflow-y-auto py-5 ${t?`px-2`:`px-3`}`,id:`smartsafehub-desktop-navigation`,children:B(Vt,{accountRegistered:e,collapsed:t,demo:d,route:s,updateCount:u})}),d?null:B(`div`,{class:`border-t border-slate-100 ${t?`p-2`:`p-3`}`,children:B(`a`,{"aria-label":`SmartSafeHub에서 로그아웃`,class:`${zt(!1,t)} mt-1 text-rose-700 hover:bg-rose-50 hover:text-rose-800`,href:Pt(`/admin/logout`),title:t?`로그아웃`:void 0,children:[B(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg bg-rose-50 text-rose-600`,children:B(St,{class:`size-5`})}),t?null:`로그아웃`]})})]})})]})}var Ut=`smartsafehub.sidebar.collapsed`;function Wt(){if(typeof window>`u`)return!1;try{return window.localStorage.getItem(Ut)===`1`}catch{return!1}}function Gt({accountRegistered:e,children:t,loading:n,onRefresh:r,refreshing:i,route:a,updateCount:o,demo:s=!1}){let c=Ie[a],[l,u]=R(Wt),[d,f]=R(ze);return z(()=>{try{window.localStorage.setItem(Ut,l?`1`:`0`)}catch{}},[l]),z(()=>{Be(d),Ve(d)},[d]),B(`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:[B(Ht,{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}),B(`div`,{class:`min-w-0 md:min-h-screen`,children:[B(At,{description:c.description,loading:n,onRefresh:r,onToggleTheme:()=>f(e=>e===`dark`?`light`:`dark`),refreshing:i,theme:d,title:c.title}),B(`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 W({action:e,description:t,eyebrow:n,id:r,title:i}){return B(`div`,{class:`ssh-section-heading mb-4 min-w-0`,children:[B(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.18em] text-teal-700`,children:n}),B(`div`,{class:`mt-2 flex min-w-0 flex-wrap items-center justify-between gap-3`,children:[B(`h2`,{class:`m-0 min-w-0 text-xl font-black tracking-tight text-slate-950`,id:r,children:i}),e?B(`div`,{class:`shrink-0`,children:e}):null]}),B(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500`,children:t})]})}var Kt=new Intl.NumberFormat(`ko-KR`),qt=new Intl.DateTimeFormat(`ko-KR`,{dateStyle:`medium`,timeStyle:`short`});function Jt(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 Yt(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?Qt(e):$t(e,t)}function tn(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 nn(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 rn(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 an(e,t){let n=e.metadata[t];return typeof n==`string`&&n.trim().length>0?n.trim():null}function on(e,t){let n=e.metadata[t];return typeof n==`number`&&Number.isFinite(n)?n:null}function sn(e,t){let n=e.metadata[t];return typeof n==`boolean`?n:null}function cn(e,t){let n=an(e,`error_code`);return n?`${t} (오류 코드: ${n})`:t}function ln(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 un(e){let t=an(e,`from_version`),n=an(e,`to_version`);return t&&n?`${t} → ${n}`:n?`${n} 버전이 적용되었습니다.`:null}function dn(e){return e?e.toUpperCase():null}function fn(e){switch(e){case`dhcp`:return`자동 IP(DHCP)`;case`pppoe`:return`PPPoE`;case`static`:return`고정 IPv4`;default:return e.toUpperCase()}}function pn(e){switch(e.eventType){case`system.booted`:return{title:`공유기 부팅 감지`,description:`공유기가 켜지거나 재시작된 것을 확인했습니다.`};case`network.internet.disconnected`:return{title:`인터넷 연결 끊김 감지`,description:`공유기에서 인터넷 연결을 확인할 수 없었습니다.`};case`network.internet.recovered`:{let t=on(e,`downtime_seconds`);return{title:`인터넷 연결 복구`,description:t!==null&&t>0?`약 ${ln(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=on(e,`domain_count`),n=an(e,`artifact_version`),r=[];return t!==null&&t>0&&r.push(`${G(t)}개 도메인 적용`),n&&r.push(`목록 ${n}`),{title:`SafeShield 차단 목록 업데이트 완료`,description:r.length>0?r.join(` · `):`최신 차단 목록을 적용했습니다.`}}case`safeshield.blocklist.update_failed`:return{title:`SafeShield 차단 목록 업데이트 실패`,description:cn(e,`차단 목록을 업데이트하지 못했습니다.`)};case`safeshield.statistics.enabled`:return{title:`SafeShield 차단 통계 수집 활성화`,description:`기기별 차단 통계 수집을 활성화했습니다.`};case`safeshield.statistics.disabled`:return{title:`SafeShield 차단 통계 수집 비활성화`,description:`기기별 차단 통계 수집을 비활성화했습니다.`};case`safeshield.rule.added`:return{title:an(e,`rule_action`)===`allow`?`사용자 허용 규칙 추가`:`사용자 차단 규칙 추가`,description:`SafeShield 사용자 규칙이 변경되었습니다.`};case`safeshield.rule.removed`:return{title:an(e,`rule_action`)===`allow`?`사용자 허용 규칙 삭제`:`사용자 차단 규칙 삭제`,description:`SafeShield 사용자 규칙이 변경되었습니다.`};case`settings.scheduled_reboot.updated`:{let t=sn(e,`enabled`);return{title:`예약 재부팅 설정 변경`,description:t===!0?`예약 재부팅 설정을 저장하고 새 일정에 적용했습니다.`:t===!1?`예약 재부팅을 비활성화했습니다.`:`예약 재부팅 설정을 변경했습니다.`}}case`settings.software_updates.updated`:return{title:`관리 소프트웨어 업데이트 설정 변경`,description:sn(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=an(e,`zonename`);return{title:`시간대 설정 변경`,description:t?`공유기 시간대를 ${t}(으)로 변경했습니다.`:`공유기 시간대를 변경했습니다.`}}case`settings.wifi.updated`:return{title:`Wi-Fi 설정 변경`,description:`Wi-Fi 설정을 저장하고 무선 네트워크에 적용했습니다.`};case`settings.wan.updated`:{let t=an(e,`protocol`);return{title:`인터넷 설정 변경`,description:t?`${fn(t)} 방식으로 WAN 설정을 변경했습니다.`:`WAN 인터넷 연결 설정을 변경했습니다.`}}case`settings.lan.updated`:return{title:`LAN 설정 변경`,description:`내부 네트워크 및 DHCP 설정을 변경했습니다.`};case`settings.iptv.updated`:{let t=sn(e,`enabled`),n=an(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:un(e)};case`software.update.failed`:return{title:`관리 소프트웨어 업데이트 실패`,description:cn(e,`관리 소프트웨어를 업데이트하지 못했습니다.`)};case`firmware.update.started`:return{title:`펌웨어 업데이트 시작`,description:`펌웨어 설치를 시작했습니다. 완료 과정에서 공유기가 재부팅됩니다.`};case`firmware.update.failed`:return{title:`펌웨어 업데이트 실패`,description:cn(e,`펌웨어 설치 준비 또는 검증에 실패했습니다.`)};case`license.activated`:{let t=dn(an(e,`to_plan`));return{title:`요금제 활성화`,description:t?`${t} 요금제가 활성화되었습니다.`:`요금제가 활성화되었습니다.`}}case`license.changed`:{let t=dn(an(e,`from_plan`)),n=dn(an(e,`to_plan`));return{title:`요금제 변경`,description:t&&n?`${t} → ${n}`:n?`${n} 요금제로 변경되었습니다.`:`요금제 상태가 변경되었습니다.`}}case`license.cleared`:return{title:`계정 권한 해제`,description:an(e,`reason`)===`manual`?`이 공유기에서 계정 권한 연결을 해제했습니다.`:`서버의 계정 권한 상태가 반영되어 이 공유기의 권한 연결이 해제되었습니다.`};case`health.issue.started`:{let t=on(e,`warning_count`)??0,n=on(e,`critical_count`)??0,r=[];return n>0&&r.push(`이상 ${G(n)}건`),t>0&&r.push(`주의 ${G(t)}건`),{title:`확인이 필요한 장치 상태 발견`,description:r.length>0?r.join(` · `):`장치 상태에서 확인이 필요한 항목이 발견되었습니다.`}}case`health.issue.changed`:{let t=on(e,`warning_count`)??0,n=on(e,`critical_count`)??0,r=[];return n>0&&r.push(`이상 ${G(n)}건`),t>0&&r.push(`주의 ${G(t)}건`),{title:`확인이 필요한 장치 상태 변경`,description:r.length>0?r.join(` · `):`확인이 필요한 장치 상태 항목이 변경되었습니다.`}}case`health.issue.resolved`:return{title:`장치 상태 정상으로 복구`,description:`이전에 확인이 필요했던 장치 상태가 정상으로 돌아왔습니다.`};default:return{title:`SmartSafeHub 활동`,description:`장치에서 새로운 활동이 기록되었습니다.`}}}function mn(e){return e===`error`?{icon:B(U,{class:`size-4`}),surface:`bg-rose-50 text-rose-700 ring-rose-200`}:e===`warning`?{icon:B(U,{class:`size-4`}),surface:`bg-amber-50 text-amber-800 ring-amber-200`}:e===`success`?{icon:B(_t,{class:`size-4`}),surface:`bg-emerald-50 text-emerald-700 ring-emerald-200`}:{icon:B(st,{class:`size-4`}),surface:`bg-slate-100 text-slate-600 ring-slate-200`}}function hn(e){let t=new Date(e*1e3);return new Intl.DateTimeFormat(`ko-KR`,{hour:`2-digit`,minute:`2-digit`,hour12:!1}).format(t)}function gn(e){let t=new Date(e*1e3);return`${t.getFullYear()}-${t.getMonth()+1}-${t.getDate()}`}function _n(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 vn({compact:e=!1,events:t}){if(t.length===0)return B(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 px-4 py-5 text-center`,children:[B(st,{class:`mx-auto size-5 text-slate-400`}),B(`p`,{class:`mt-2 mb-0 text-sm font-extrabold text-slate-700`,children:`아직 기록된 활동이 없습니다.`}),B(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`상태가 실제로 변경되면 이곳에 시간순으로 기록됩니다.`})]});let n=``;return B(`div`,{class:e?`space-y-2`:`space-y-1`,children:t.map(t=>{let r=pn(t),i=mn(t.severity),a=gn(t.occurredAt),o=!e&&a!==n;return n=a,B(`div`,{children:[o?B(`p`,{class:`mt-5 mb-2 first:mt-0 text-xs font-black text-slate-500`,children:_n(t.occurredAt)}):null,B(`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:[B(`div`,{class:`w-12 shrink-0 pt-1 text-xs font-bold tabular-nums text-slate-400`,children:B(`time`,{dateTime:new Date(t.occurredAt*1e3).toISOString(),title:Qt(t.occurredAt),children:hn(t.occurredAt)})}),B(`span`,{class:`mt-0.5 grid size-8 shrink-0 place-items-center rounded-full ring-1 ring-inset ${i.surface}`,children:i.icon}),B(`div`,{class:`min-w-0 flex-1`,children:[B(`p`,{class:`m-0 text-sm font-black leading-5 text-slate-950`,children:r.title}),r.description?B(`p`,{class:`mt-1 mb-0 break-words text-xs font-medium leading-5 text-slate-500`,children:r.description}):null]})]})]},t.eventId)})})}function yn({error:e,loading:t,onRetry:n}){return t?B(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 px-4 py-5 text-center`,children:[B(H,{class:`mx-auto size-5 animate-spin text-teal-700`}),B(`p`,{class:`mt-2 mb-0 text-sm font-bold text-slate-500`,children:`최근 활동을 불러오는 중입니다.`})]}):e?B(`div`,{class:`rounded-xl border border-amber-200 bg-amber-50/70 px-4 py-4`,children:B(`div`,{class:`flex items-start gap-3`,children:[B(U,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),B(`div`,{class:`min-w-0 flex-1`,children:[B(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:`최근 활동을 불러오지 못했습니다.`}),B(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:e}),n?B(`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 bn(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 xn(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 Sn({actionError:e,actionMessage:t,cloud:n,onDismissFeedback:r,onSetEnabled:i,saving:a}){let o=bn(n),s=n.plan?.toUpperCase()??null,c=n.eligible===!0,l=n.enabled||c;return B(`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:[B(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[B(`div`,{class:`min-w-0 flex-1`,children:[B(`div`,{class:`flex flex-wrap items-center gap-2`,children:B(`span`,{class:`rounded-full px-2.5 py-1 text-xs font-extrabold ring-1 ring-inset ${o.tone}`,children:a?`설정 중`:o.label})}),B(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:o.description})]}),B(`div`,{class:`flex shrink-0 items-center justify-end gap-3 sm:justify-start`,children:[B(`span`,{class:`text-xs font-bold text-slate-600`,children:[`Cloud 전송 `,n.enabled?`켜짐`:`꺼짐`]}),B(`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:B(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${n.enabled?`left-6`:`left-1`}`})})]})]}),(e||t)&&B(`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:[B(`span`,{class:`min-w-0`,children:e||t}),B(`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?B(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`Cloud 전송을 끄면 아직 전송하지 않은 대기 기록이 삭제됩니다. 공유기의 로컬 기록은 유지됩니다.`}):null,B(`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:[B(`span`,{children:[`마지막 동기화: `,n.lastSuccessAt>0?$t(n.lastSuccessAt):`아직 없음`]}),B(`span`,{children:[`Cloud 보관: `,xn(n)]}),n.pendingEvents>0?B(`span`,{children:[`전송 대기: `,G(n.pendingEvents),`건`]}):null]}),n.enabled&&n.phase===`error`&&n.lastErrorCode?B(`p`,{class:`mt-3 mb-0 text-xs font-semibold text-amber-700`,children:[`오류 코드: `,B(`code`,{children:n.lastErrorCode})]}):null,B(`details`,{class:`mt-3 border-t border-slate-200 pt-3`,children:[B(`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 동기화 상세 정보`}),B(`dl`,{class:`mt-3 grid gap-3 sm:grid-cols-4`,children:[B(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[B(`dt`,{class:`text-xs font-bold text-slate-500`,children:`멤버십`}),B(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:s??(n.eligible===!1?`Free`:`확인 중`)})]}),B(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[B(`dt`,{class:`text-xs font-bold text-slate-500`,children:`전송 대기`}),B(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:[G(n.pendingEvents),`건`]})]}),B(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[B(`dt`,{class:`text-xs font-bold text-slate-500`,children:`마지막 동기화`}),B(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:n.lastSuccessAt>0?$t(n.lastSuccessAt):`아직 없음`})]}),B(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[B(`dt`,{class:`text-xs font-bold text-slate-500`,children:`Cloud 보관`}),B(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:xn(n)})]})]}),B(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`SmartSafeHub 계정에 연결된 Free는 최근 7일, Pro와 Ultimate는 최근 90일 동안 Cloud 활동 기록을 보관합니다. 끄면 전송 대기 데이터와 임시 인증 정보를 삭제하며, 다시 켠 뒤 새로 발생한 활동부터 Cloud에 전송합니다. 로컬 최근 활동은 항상 유지됩니다.`})]})]})}function Cn({actionError:e,actionMessage:t,data:n,error:r,loading:i,onDismissActionFeedback:a,onRetry:o,onSetCloudSync:s,savingCloudSync:c}){let[l,u]=R(20),d=n?.events??[],f=d.slice(0,l),p=d.length-f.length;return B(`div`,{class:`min-w-0 space-y-6`,children:[B(`section`,{"aria-labelledby":`activity-cloud-title`,class:`min-w-0`,children:[B(W,{id:`activity-cloud-title`,eyebrow:`Cloud history`,title:`Cloud 활동 기록`,description:`활동 기록을 Cloud에 자동으로 전송할지 설정합니다. 공유기 내 최근 활동은 항상 기록됩니다.`}),B(Sn,{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})]}),B(`section`,{"aria-labelledby":`activity-history-title`,class:`min-w-0`,children:[B(W,{id:`activity-history-title`,eyebrow:`Local history`,title:`현재 부팅 이후의 최근 활동`,description:`인터넷, SafeShield, 업데이트, 계정 권한과 주요 설정에서 실제 상태가 변경된 시점만 기록합니다.`,action:B(`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,`건`]})}),B(`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?B(yn,{error:r,loading:i,onRetry:o}):B(b,{children:[B(vn,{events:f}),d.length>20?B(`div`,{class:`mt-5 flex flex-wrap items-center justify-between gap-3 border-t border-slate-200 pt-4`,children:[B(`p`,{class:`m-0 text-xs font-semibold text-slate-500`,children:[G(f.length),` / `,G(d.length),`건 표시 중`]}),B(`div`,{class:`flex flex-wrap items-center gap-2`,children:[l>20?B(`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?B(`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:[`이전 활동 더 보기 (`,G(Math.min(20,p)),`건)`]}):null]})]}):null]})})]}),B(`aside`,{class:`rounded-2xl border border-slate-200 bg-slate-50 px-5 py-4`,children:[B(`p`,{class:`m-0 text-sm font-extrabold text-slate-800`,children:`로컬 활동 기록 안내`}),B(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`이 기록은 공유기의 메모리 영역에만 보관되어 재부팅하면 초기화됩니다. 직접 설정을 변경한 작업은 성공 시점에 즉시 기록하고, 인터넷 및 장치 진단처럼 외부 상태를 확인해야 하는 항목은 실제 상태 변화가 관찰된 경우에만 추가합니다.`})]})]})}function wn(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 Tn(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:wn(e.availableBytes),usedText:wn(e.totalBytes-e.availableBytes),totalText:wn(e.totalBytes),level:t<=.1?`critical`:t<=.2?`warning`:`normal`}}var En=2*Math.PI*39;function Dn({storage:e}){let t=Tn(e),n=t?(t.availableRatio*En).toFixed(2):`0`;return B(`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:[B(`div`,{class:`ssh-apps-storage-ring relative grid size-24 shrink-0 place-items-center`,"data-level":t?.level??`unavailable`,children:[B(`svg`,{"aria-hidden":`true`,class:`size-24`,viewBox:`0 0 96 96`,children:[B(`circle`,{class:`ssh-apps-storage-track`,cx:`48`,cy:`48`,r:`39`}),t&&t.availableRatio>0?B(`circle`,{class:`ssh-apps-storage-free`,cx:`48`,cy:`48`,r:`39`,"stroke-dasharray":`${n} ${En.toFixed(2)}`}):null]}),B(`div`,{class:`absolute inset-0 flex flex-col items-center justify-center text-center`,children:[B(`strong`,{class:`text-lg leading-6 font-black text-slate-950`,children:t?`${t.availablePercent}%`:`—`}),B(`span`,{class:`text-[10px] font-bold text-slate-500`,children:`여유`})]})]}),B(`div`,{"aria-live":`polite`,class:`min-w-0 flex-1`,children:[B(`h3`,{class:`m-0 text-sm font-black text-slate-950`,children:`저장 공간`}),t?B(b,{children:[B(`p`,{class:`ssh-apps-storage-available mb-0 mt-1 text-base font-extrabold text-teal-700`,children:[t.availableText,` 남음`]}),B(`p`,{class:`mb-0 mt-1 text-[11px] leading-4 text-slate-500`,title:`앱 설치 가능 용량을 기준으로 계산하며, 파일시스템 예약 공간이 사용량에 포함될 수 있습니다.`,children:[`사용 `,t.usedText,` / 전체 `,t.totalText]})]}):B(`p`,{class:`mb-0 mt-1 text-xs leading-5 text-slate-500`,children:`용량 정보를 확인할 수 없습니다.`})]})]})}function On(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 kn({category:e,class:t}){switch(e){case`all`:return B(Ye,{class:t});case`network`:return B(Xe,{class:t});case`security`:return B(We,{class:t});case`smart_home`:return B(qe,{class:t});case`entertainment`:return B(Ze,{class:t});case`utilities`:return B(Qe,{class:t});case`other`:return B($e,{class:t});default:return B($e,{class:t})}}function An(e,t){return e instanceof Error?e.message:t}function jn(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 Mn=`smartsafehub:apps:dismissed-operation:v1`;function Nn(e){return!e||e.phase===`idle`?``:e.operationId?`operation:${e.operationId}`:`legacy:${e.phase}:${e.action}:${e.appId}:${e.code}`}function Pn(){try{return typeof window>`u`?null:window.localStorage.getItem(Mn)}catch{return null}}function Fn(e){if(e)try{typeof window<`u`&&window.localStorage.setItem(Mn,e)}catch{}}var In={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:`앱 제거에 실패했습니다.`},Ln=[{id:`all`,label:`전체`},{id:`network`,label:`네트워크`},{id:`security`,label:`보안`},{id:`smart_home`,label:`스마트홈`},{id:`entertainment`,label:`엔터테인먼트`},{id:`utilities`,label:`도구`},{id:`other`,label:`기타`}];function Rn({action:e}){let t=e===`remove`;return B(`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:[B(`svg`,{"aria-hidden":`true`,class:`size-4 animate-spin`,fill:`none`,viewBox:`0 0 24 24`,children:[B(`circle`,{cx:`12`,cy:`12`,r:`9`,stroke:`currentColor`,"stroke-width":`3`,opacity:`0.25`}),B(`path`,{d:`M21 12a9 9 0 0 0-9-9`,stroke:`currentColor`,"stroke-width":`3`,"stroke-linecap":`round`})]}),t?`제거 중…`:e===`update`?`업데이트 중…`:`설치 중…`]})}function zn({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]=R(`all`),[g,_]=R(``),[v,y]=R(null),[x,S]=R(null),C=we(null),w=we(null),[T,E]=R(null),[D,O]=R(Pn),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`,ee=Nn(N),te=e=>{O(e),Fn(e)},ne=N&&N.phase!==`idle`&&ee!==D;z(()=>{if(N?.phase!==`success`||!ne)return;let e=window.setTimeout(()=>te(ee),7e3);return()=>window.clearTimeout(e)},[ee,N?.phase,ne]);let re=N?.action===`install`||N?.action===`update`||N?.action===`remove`?N.action:null,ie=Te(()=>A.filter(e=>(m===`all`||On(e.category)===m)&&`${e.name} ${e.summary}`.toLocaleLowerCase().includes(g.trim().toLocaleLowerCase())),[A,m,g]);z(()=>{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 ae=()=>{S(null),w.current?.focus()},oe=(e,t)=>{!a||s||v!==null||P||F||(w.current=t,S(e))},se=async(e,t)=>{if(!(!a||s||v!==null||P||F)){if(f)t===`remove`&&ae(),p?.();else if((t!==`update`||window.confirm(`${e.name} 앱을 최신 승인 버전으로 업데이트하시겠습니까?\n\n업데이트 중 공유기를 재부팅하지 마세요.`))&&(t!==`install`||window.confirm(`${e.name} 앱을 설치하시겠습니까?\n\n앱을 변경하는 동안 공유기를 재부팅하지 마세요.`))){t===`remove`&&ae(),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(An(t,`${e.name} 앱 작업을 시작하지 못했습니다.`))}finally{y(null)}}}};return B(`section`,{class:`min-w-0 space-y-6`,children:[x?B(`div`,{class:`fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70 p-4`,onClick:e=>{e.target===e.currentTarget&&ae()},role:`presentation`,children:B(`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:[B(`h2`,{class:`m-0 text-lg font-extrabold text-slate-950`,id:`apps-remove-title`,children:`앱 제거 확인`}),B(`p`,{class:`mt-3 break-words text-sm text-slate-700`,id:`apps-remove-description`,children:[B(`strong`,{children:x.name}),` 앱을 이 공유기에서 제거하시겠습니까?`]}),B(`p`,{class:`mt-2 text-xs leading-5 text-slate-600`,children:`앱에서 제공하던 기능을 더 이상 사용할 수 없습니다. 제거하는 동안 공유기를 재부팅하지 마세요.`}),B(`div`,{class:`mt-5 flex justify-end gap-2`,children:[B(`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:ae,ref:C,type:`button`,children:`취소`}),B(`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 se(x,`remove`),type:`button`,children:`앱 제거`})]})]})}):null,B(`div`,{class:`min-w-0`,children:[B(W,{eyebrow:`SmartSafeHub Apps`,title:`Apps`,description:`SmartSafeHub에서 직접 검증한 앱으로 공유기의 기능을 확장하세요.`}),B(`div`,{class:`grid min-w-0 gap-3 lg:grid-cols-[minmax(0,1fr)_minmax(0,18.5rem)]`,children:[B(`div`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:[B(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[B(`div`,{class:`flex min-w-0 items-center gap-3`,children:[B(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-teal-50 text-teal-700`,children:B(Ye,{class:`size-6`})}),B(`div`,{class:`min-w-0`,children:[B(`h3`,{class:`m-0 text-base font-black text-slate-950`,children:`검증된 앱`}),B(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`현재 펌웨어에서 확인된 앱만 표시합니다.`})]})]}),B(`div`,{class:`flex flex-wrap items-center gap-2`,children:[B(`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?B(`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,B(`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:[B(H,{class:`size-4 ${r?`animate-spin`:``}`}),r?`확인 중`:`목록 새로고침`]})]})]}),B(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`검증된 APK를 설치·업데이트·제거할 수 있습니다. 작업 중에는 다른 앱 작업을 시작할 수 없습니다.`})]}),B(Dn,{storage:o?.storage})]})]}),M&&(M.phase===`starting`||M.phase===`running`||M.phase===`failed`)?B(`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:[B(`strong`,{children:M.phase===`failed`?`Apps 자동 복원을 완료하지 못했습니다.`:`펌웨어 업데이트 후 Apps를 복원하는 중입니다.`}),M.appId?B(`span`,{children:[` · `,M.appId]}):null,M.phase===`failed`?B(`p`,{class:`mb-0 mt-1 text-xs`,children:[In[M.code]??`승인·연결·설치 상태를 확인해 주세요.`,` 필요한 경우 앱 카드에서 설치 마무리를 진행할 수 있습니다.`]}):B(`p`,{class:`mb-0 mt-1 text-xs`,children:`검증된 패키지만 다시 설치합니다. 완료될 때까지 앱 설치·제거를 시작하지 마세요.`})]}):null,ne&&N?B(`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:B(`div`,{class:`flex items-start justify-between gap-3`,children:[B(`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&&In[N.code]?B(`p`,{class:`mb-0 mt-1 text-xs`,children:In[N.code]}):null,P?B(`p`,{class:`mb-0 mt-1 text-xs`,children:[N.action===`remove`?`앱 제거와 패키지 정리가 진행 중입니다.`:N.action===`update`?`승인된 버전으로 앱을 업데이트하고 있습니다.`:`설치 확인과 패키지 처리가 진행 중입니다.`,` 화면을 닫아도 작업은 계속됩니다.`]}):null]}),P?null:B(`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:()=>te(ee),type:`button`,children:`닫기 ×`})]})}):null,T?B(`p`,{class:`rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-800`,role:`alert`,children:T}):null,s?B(`div`,{class:`rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900`,role:`alert`,children:[`설치 상태를 확인하지 못했습니다. 설치 및 제거 버튼이 비활성화됩니다. `,B(`button`,{class:`underline`,onClick:c,type:`button`,children:`다시 확인`})]}):null,t&&!k?B(`div`,{class:`rounded-2xl border border-rose-200 bg-rose-50 p-5`,role:`alert`,children:[B(`p`,{class:`m-0 text-sm font-bold text-rose-800`,children:`앱 목록을 불러오지 못했습니다.`}),B(`p`,{class:`mt-2 mb-0 text-sm text-rose-700`,children:t}),B(`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?B(`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?B(b,{children:[A.length>0?B(`div`,{class:`space-y-4`,children:[B(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[B(`div`,{class:`flex flex-wrap items-center gap-2`,role:`group`,"aria-label":`앱 카테고리`,children:Ln.map(e=>B(`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:[B(kn,{category:e.id,class:`size-4 shrink-0`}),e.label]},e.id))}),B(`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})]}),B(`div`,{class:`grid min-w-0 gap-3 sm:grid-cols-2 xl:grid-cols-3`,children:ie.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?wn(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?re:null,h=!a||!!s||v!==null||P||F,g=c&&(n||!!t?.partial),_=n&&t?.webUiReady?jn(e.webUi):null;return B(`article`,{class:`flex min-w-0 flex-col rounded-2xl border border-slate-200 bg-white p-5 shadow-sm`,children:[B(`div`,{class:`flex min-w-0 items-start gap-3`,children:[B(`span`,{class:`grid size-14 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-600`,children:B(kn,{category:On(e.category),class:`size-7`})}),B(`div`,{class:`min-w-0 flex-1`,children:[B(`div`,{class:`flex min-w-0 flex-wrap items-start justify-between gap-x-3 gap-y-2`,children:[B(`h3`,{class:`m-0 min-w-0 flex-1 break-words text-sm font-black text-slate-950`,children:e.name}),n&&c?B(`span`,{class:`ssh-apps-installed-badge shrink-0 rounded-full border px-2 py-0.5 text-[11px] font-bold`,children:`✓ 설치됨`}):null]}),B(`div`,{class:`mt-1 flex flex-wrap items-center gap-x-2 gap-y-1`,children:[B(`span`,{class:`text-xs text-slate-500`,title:n?`공유기에 설치된 대표 APK 패키지 버전입니다.`:`현재 제공 중인 승인 버전입니다.`,children:n?u?`설치 v${u}`:`설치 버전 확인 불가`:`v${e.version}`}),l?B(`span`,{class:`ssh-apps-update-badge rounded-full border px-2 py-0.5 text-[11px] font-bold`,children:`업데이트 가능`}):null]}),d||i!==null?B(`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?B(`span`,{title:`현재 서버가 승인한 앱 릴리스 버전입니다. 설치된 APK 패키지의 버전과 다를 수 있습니다.`,children:[`제공 v`,e.version]}):null,d&&i!==null?B(`span`,{"aria-hidden":`true`,children:`·`}):null,i===null?null:B(`span`,{title:`설치된 APK 패키지의 용량 합계입니다. 앱 데이터 및 캐시는 포함되지 않습니다.`,children:[`설치 용량 약 `,i]})]}):null]})]}),B(`p`,{class:`mt-4 mb-4 min-h-10 break-words text-sm leading-5 text-slate-600`,children:e.summary}),B(`div`,{class:`mt-auto flex flex-wrap items-center justify-between gap-2 border-t border-slate-100 pt-3`,children:[B(`span`,{class:`text-xs font-bold text-teal-700`,children:`✓ 검증된 앱`}),m?B(Rn,{action:m}):B(`span`,{class:`flex flex-wrap items-center gap-2`,children:[c&&t?.partial&&!n?B(`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 se(e,`install`),type:`button`,children:`설치 마무리`}):null,l?B(`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 se(e,`update`),type:`button`,children:`업데이트`}):null,_?f?B(`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:`열기 ↗`}):B(`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?B(`span`,{class:`text-xs text-amber-700`,title:`이 앱의 웹 UI 패키지를 설치해야 열 수 있습니다.`,children:`웹 UI 미설치`}):null,n&&!c?B(`span`,{class:`text-xs text-slate-500`,title:`공유기 소프트웨어 메뉴 또는 SSH로 설치한 앱은 Apps에서 제거하지 않습니다.`,children:`기존에 설치됨`}):t?.partial&&!c?B(`span`,{class:`text-xs text-amber-700`,children:`일부 패키지 설치됨`}):B(`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?oe(e,t.currentTarget):void se(e,`install`),type:`button`,children:g?`제거`:c?`재설치`:`설치`})]})]})]},e.id)})}),ie.length===0?B(`p`,{class:`rounded-xl bg-slate-100 p-5 text-center text-sm text-slate-600`,children:`검색 조건에 맞는 앱이 없습니다.`}):null]}):B(`div`,{class:`rounded-2xl border border-dashed border-slate-300 bg-white px-5 py-12 text-center`,children:[B(Ye,{class:`mx-auto size-10 text-slate-400`}),B(`h3`,{class:`mt-4 mb-2 text-base font-black text-slate-900`,children:`아직 등록된 앱이 없습니다.`}),B(`p`,{class:`m-0 text-sm leading-6 text-slate-500`,children:`현재 기기와 펌웨어에 대해 승인된 앱이 준비되면 이곳에서 확인할 수 있습니다.`})]}),B(`p`,{class:`text-xs text-slate-400`,children:[`기기 `,k.deviceCode,` · 펌웨어 `,k.firmwareBuildId,` · `,k.channel,` 채널`]})]}):null,j.length>0?B(`div`,{class:`rounded-2xl border border-amber-200 bg-amber-50 p-5`,children:[B(`h3`,{class:`m-0 text-sm font-black text-amber-900`,children:`제공이 중단된 앱`}),B(`p`,{class:`mb-3 mt-1 text-xs text-amber-800`,children:`현재 승인 목록에서 제외되었지만 기기에 남아 있는 앱입니다. 설치는 불가능하며 제거할 수 있습니다.`}),B(`div`,{class:`space-y-2`,children:j.map(([e])=>{let t=v?.appId===e?v.action:P&&N?.appId===e?re:null;return B(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[B(`span`,{class:`break-all text-sm text-amber-900`,children:e}),t?B(Rn,{action:t}):B(`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=>oe({id:e,name:e,summary:``,description:``,category:``,version:``},t.currentTarget),type:`button`,children:`제거`})]},e)})})]}):null]})}function Bn(){return B(`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:B(`div`,{class:`flex items-center gap-4`,children:[B(H,{class:`size-8 shrink-0 animate-spin text-teal-700`}),B(`div`,{class:`min-w-0 text-left`,children:B(`h2`,{class:`m-0 text-lg font-extrabold text-slate-950`,children:`장치 상태를 확인하고 있습니다`})})]})})}function Vn({message:e,onRetry:t}){return B(`section`,{"aria-live":`assertive`,class:`rounded-2xl border border-red-200 bg-red-50 p-5 sm:p-6`,children:[B(`p`,{class:`m-0 text-xs font-bold uppercase tracking-[0.16em] text-red-700`,children:`연결 오류`}),B(`h2`,{class:`mt-2 mb-0 text-lg font-extrabold text-red-950`,children:`장치 상태를 불러오지 못했습니다`}),B(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-red-800`,children:e}),B(`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:[B(H,{class:`size-4`}),`다시 시도`]})]})}var Hn=`https://www.smartsafehub.com/dashboard/devices/`;async function Un(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 Wn(e){return(e||`free`).toUpperCase()}function Gn(e){return e===`FREE`?`free`:e===`PRO`?`pro`:e===`ULTIMATE`?`ultimate`:`paid`}function Kn({plan:e}){let t=Gn(e),n=t!==`free`;return B(`span`,{class:`ssh-safeshield-plan-badge`,"data-compact":`false`,"data-tier":t,title:n?`${e} 멤버십`:`무료 플랜`,children:[B(`span`,{"aria-hidden":`true`,class:`ssh-safeshield-plan-badge-mark`,children:n?`✦`:`•`}),B(`span`,{children:e})]})}function qn(e){if(!e.pairingCode||!e.pairingExpiresAt)return!1;let t=Date.parse(e.pairingExpiresAt);return Number.isFinite(t)&&t<=Date.now()}function Jn({data:e,error:t,loading:n,pairingBusy:r,refreshing:i,syncError:a,onRequestPairing:o,onRetry:s,demo:c=!1,onDemoAction:l}){let[u,d]=R(null),[f,p]=R(!1);if(z(()=>{if(!f)return;let e=window.setTimeout(()=>p(!1),1800);return()=>window.clearTimeout(e)},[f]),n&&!e)return B(Bn,{});if(t&&!e)return B(Vn,{message:t,onRetry:s});let m=e,h=m?.accountRegistered===!0,g=Wn(m?.plan??null),_=m?qn(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 Un(v),d(null),p(!0)}catch{p(!1),d(`연결 코드를 복사하지 못했습니다. 코드를 직접 선택해 복사해 주세요.`)}}return B(`div`,{class:`min-w-0`,children:[B(W,{eyebrow:`Account connection`,title:`기기 계정 연결`,description:`계정에 연결하면 웹사이트에서 기기를 관리하고 Cloud 기능과 구독 권한을 사용할 수 있습니다.`}),B(`div`,{class:`grid items-start gap-5 xl:items-stretch xl:grid-cols-[minmax(0,1.2fr)_minmax(18rem,0.8fr)]`,children:[B(`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?B(`div`,{children:[B(`div`,{class:`flex items-center gap-3`,children:[B(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-full bg-emerald-100 text-emerald-700`,children:B(_t,{class:`size-5`})}),B(`div`,{children:[B(`p`,{class:`m-0 text-base font-black text-slate-950`,children:`SmartSafeHub 계정에 연결됨`}),B(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`이 기기가 계정과 정상적으로 연결되어 있습니다. 연결 코드는 더 이상 필요하지 않습니다.`})]})]}),c?B(`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:`웹사이트에서 기기 관리`}):B(`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:Hn,rel:`noopener noreferrer`,target:`_blank`,children:`웹사이트에서 기기 관리`}),B(`p`,{class:`mt-4 mb-0 border-t border-slate-100 pt-4 text-xs font-semibold leading-5 text-slate-500`,children:`웹사이트에서 기기 등록을 해제하면 공유기도 다음 계정 상태 동기화에서 자동으로 연결 해제를 반영합니다.`})]}):B(`div`,{children:[B(`div`,{children:[B(`div`,{class:`flex flex-wrap items-center justify-between gap-2`,children:[B(`p`,{class:`m-0 text-sm font-black text-slate-900`,children:`계정 연결 코드`}),B(`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:[B(st,{class:`size-3.5`}),`10분 동안 유효`]})]}),B(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-500`,children:`코드를 발급한 뒤 smartsafehub.com에 로그인하여 기기를 연결하세요.`})]}),v?B(`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:[B(`div`,{class:`min-w-0`,children:[B(`span`,{class:`block text-[0.7rem] font-black text-teal-700`,children:`연결 코드`}),B(`code`,{class:`mt-1 block select-all break-all font-mono text-2xl font-black tracking-[0.14em] text-slate-950`,children:v})]}),B(`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:[B(f?_t:pt,{class:`size-4`}),B(`span`,{"aria-live":`polite`,children:f?`복사됨`:`복사`})]})]}):_?B(`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,B(`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?B(H,{class:`size-4 animate-spin`}):null,y?`발급 중…`:v?`새 코드 발급`:`연결 코드 발급`]}),m?.pairingOperation?.phase===`failed`?B(`p`,{class:`mt-3 mb-0 text-sm font-semibold text-red-700`,children:`연결 코드 발급에 실패했습니다. 다시 시도해 주세요.`}):null,u?B(`p`,{class:`mt-3 mb-0 text-sm font-semibold text-red-700`,children:u}):null]})}),B(`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:[B(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Device status`}),B(`h2`,{class:`mt-2 mb-0 text-lg font-black tracking-tight text-slate-950`,children:`연결 상태`}),B(`dl`,{class:`mt-5 mb-0 grid gap-4`,children:[B(`div`,{class:`flex items-center justify-between gap-4 border-b border-slate-100 pb-4`,children:[B(`dt`,{class:`text-sm font-bold text-slate-500`,children:`계정`}),B(`dd`,{class:`m-0 text-sm font-black ${h?`text-emerald-700`:`text-slate-700`}`,children:h?`연결됨`:`연결 필요`})]}),B(`div`,{class:`flex items-center justify-between gap-4 border-b border-slate-100 pb-4`,children:[B(`dt`,{class:`text-sm font-bold text-slate-500`,children:`요금제`}),B(`dd`,{class:`m-0 flex min-w-0 flex-wrap items-center justify-end gap-2`,children:[B(Kn,{plan:g}),g===`FREE`?B(`span`,{class:`ssh-safeshield-plan-caption`,children:`기본 플랜`}):null]})]}),B(`div`,{class:`flex items-center justify-between gap-4`,children:[B(`dt`,{class:`text-sm font-bold text-slate-500`,children:`마지막 확인`}),B(`dd`,{class:`m-0 text-sm font-black text-slate-700`,children:$t(m?.lastSuccessAt??0)})]})]}),i?B(`p`,{class:`mt-5 mb-0 text-xs font-bold text-teal-700`,children:`최신 상태를 확인하고 있습니다…`}):null,a?h?B(`p`,{class:`mt-5 mb-0 text-xs font-semibold leading-5 text-slate-500`,children:`최신 상태 확인이 지연되고 있습니다. 마지막으로 확인된 연결 상태를 표시합니다.`}):B(`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]})]})]})}var Yn=Object.assign;function Xn(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 Zn=/^(-|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 Qn(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:Xn(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 $n(e,t){var n=ce(e,t);return n.containerInfo=t,n}var er=Symbol.for(`react.element`),tr=/^(?: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]/,nr=/[A-Z0-9]/g,rr=typeof document<`u`,ir=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 ar=e.event;e.event=function(e){return ar&&(e=ar(e)),e.persist=function(){},e.isPropagationStopped=function(){return this.cancelBubble},e.isDefaultPrevented=function(){return this.defaultPrevented},e.nativeEvent=e};var or={configurable:!0,get:function(){return this.class}},sr=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||rr&&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`||Zn.test(c)||(s||(s=o=Yn({},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`||ir(t.type)?l==`onfocus`?a=`onfocusin`:l==`onblur`&&(a=`onfocusout`):l=a=`oninput`,l==`oninput`&&r[a=l]&&(a=`oninputCapture`)}else i&&tr.test(a)?a=a.replace(nr,`-$&`).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",or)):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=Yn({},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=er,sr&&sr(e)};var cr=8,lr=256,ur=8;function dr(e){return e.findIndex(e=>!e.disabled)}function fr(e){for(let t=e.length-1;t>=0;--t)if(!e[t]?.disabled)return t;return-1}function pr(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:dr(e)}function hr({ariaLabel:e,className:t=``,disabled:n=!1,id:r,onChange:i,options:a,value:o,variant:s=`default`}){let c=De(),l=r??`ssh-custom-select-${c}`,u=`${l}-menu`,d=we(null),f=we(null),p=we(null),m=we([]),[h,g]=R(!1),[_,v]=R(()=>mr(a,o)),[y,b]=R(`bottom`),[x,S]=R(lr),[C,w]=R({}),[T,E]=R(null),D=Te(()=>a.find(e=>e.value===o)??a[0]??null,[a,o]),O=()=>{let e=f.current;if(!e)return;let t=e.getBoundingClientRect(),n=Math.min(lr,Math.max(44,a.length*42+12)),r=window.innerHeight-t.bottom-ur,i=t.top-ur,o=rr?`top`:`bottom`,s=Math.min(t.width,Math.max(0,window.innerWidth-16)),c=Math.max(ur,Math.min(t.left,window.innerWidth-s-ur)),l=o===`bottom`?r:i;b(o),S(Math.max(44,Math.min(lr,Math.floor(l)))),w({left:`${c}px`,width:`${s}px`,...o===`bottom`?{top:`${t.bottom+cr}px`}:{bottom:`${window.innerHeight-t.top+cr}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??mr(a,o)))};z(()=>{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)},[]),z(()=>{v(mr(a,o)),m.current=m.current.slice(0,a.length)},[a,o]),z(()=>{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]),z(()=>{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?B(`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:B(`div`,{class:`overflow-y-auto overscroll-contain`,style:{maxHeight:`${x}px`},children:a.map((e,t)=>{let n=e.value===o;return B(`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(pr(a,t,1));break;case`ArrowUp`:e.preventDefault(),A(pr(a,t,-1));break;case`Home`:e.preventDefault(),A(dr(a));break;case`End`:e.preventDefault(),A(fr(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:[B(`span`,{class:`min-w-0 truncate`,children:e.label}),n?B(`span`,{"aria-hidden":`true`,class:`size-2 shrink-0 rounded-full bg-teal-500`}):null]},e.value)})})}):null;return B(`div`,{class:`relative ${t}`,ref:d,children:[B(`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?pr(a,_,1):mr(a,o));break;case`ArrowUp`:e.preventDefault(),M(h?pr(a,_,-1):mr(a,o));break;case`Home`:e.preventDefault(),M(dr(a));break;case`End`:e.preventDefault(),M(fr(a));break;case`Enter`:case` `:e.preventDefault(),h?k():M();break;case`Escape`:h&&(e.preventDefault(),k())}},ref:f,type:`button`,children:[B(`span`,{class:`min-w-0 truncate`,children:D?.label??`선택하세요`}),B(wt,{"aria-hidden":`true`,class:`size-4 shrink-0 text-slate-400 transition-transform ${h?`rotate-180`:``}`})]}),P&&T?$n(P,T):null,B(`span`,{class:`sr-only`,"aria-live":`polite`,children:y===`top`?`선택 목록이 위쪽에 열렸습니다.`:``})]})}function gr(e){let t=e.split(`.`);return[t[0]??``,t[1]??``,t[2]??``,t[3]??``]}function _r(e){return e.replace(/\D/g,``).slice(0,3)}function vr(e){return e.every(e=>e===``)?``:e.join(`.`)}function yr({disabled:e=!1,label:t,value:n,onChange:r}){let i=gr(n),a=(e,t)=>{let n=[...i];n[e]=_r(t),r(vr(n))};return B(`div`,{class:`ssh-ipv4-segments mt-2 min-h-11`,"aria-label":`${t} 입력`,children:i.map((n,r)=>B(`div`,{class:`ssh-ipv4-segment`,children:[B(`input`,{"aria-label":`${t} ${r+1}번째 옥텟`,autocomplete:`off`,class:`min-h-11 w-full min-w-0 max-w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-1.5 py-2.5 text-center text-sm font-semibold text-slate-950 shadow-inner outline-none transition focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:cursor-not-allowed disabled:bg-slate-100 sm:px-2`,disabled:e,inputMode:`numeric`,maxLength:3,onInput:e=>a(r,e.currentTarget.value),pattern:`[0-9]*`,spellcheck:!1,value:n}),rt);function Cr(e){return br.includes(e)}function wr(e){switch(e){case`dhcp`:return`자동 IP (DHCP)`;case`pppoe`:return`PPPoE`;case`static`:return`고정 IPv4`;default:return e?e.toUpperCase():`확인 전`}}function Tr(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 Er(e){return e.trim()}function Dr(e,t){let n=Tr(t);return e.protocol===n.protocol?e.protocol===`dhcp`||!Cr(e.protocol)?!1:e.protocol===`pppoe`?Er(e.pppoeUsername)!==n.pppoeUsername||e.pppoePassword.length>0:Er(e.staticAddress)!==n.staticAddress||e.staticPrefixLength!==n.staticPrefixLength||Er(e.staticGateway)!==n.staticGateway||Er(e.dnsPrimary)!==n.dnsPrimary||Er(e.dnsSecondary)!==n.dnsSecondary:!0}function Or(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 kr(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 Ar(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 jr({feedback:e,onDismiss:t}){return B(`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:B(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[B(`p`,{class:`m-0 font-bold leading-6`,children:e.message}),B(`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 Mr({data:e,busy:t,onReconnect:n}){let r=e.status,i=r.connected;return B(`section`,{class:`grid gap-4 lg:grid-cols-3`,"aria-label":`인터넷 연결 상태`,children:[B(`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:[B(`div`,{class:`flex items-start justify-between gap-3`,children:[B(`span`,{class:`grid size-10 place-items-center rounded-xl ${i?`bg-emerald-100 text-emerald-700`:`bg-amber-100 text-amber-700`}`,children:B(i?_t:U,{class:`size-5`})}),B(`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:[B(H,{class:`size-4 ${t?`animate-spin`:``}`}),`재연결`]})]}),B(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] opacity-70`,children:`인터넷 상태`}),B(`p`,{class:`mt-2 mb-0 text-xl font-black`,children:r.pending?`연결 중`:i?`연결됨`:`연결 안 됨`}),B(`p`,{class:`mt-1 mb-0 text-sm font-semibold leading-6 opacity-80`,children:[wr(r.protocol??e.configuration.protocol),` · `,Ar(r.uptimeSeconds)]})]}),B(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[B(`span`,{class:`grid size-10 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:B(at,{class:`size-5`})}),B(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`WAN IPv4`}),B(`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}`}`:`주소 없음`}),B(`p`,{class:`mt-1 mb-0 text-sm font-semibold text-slate-600`,children:r.device?`인터페이스 ${r.device}`:`인터페이스 확인 전`})]}),B(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[B(`span`,{class:`grid size-10 place-items-center rounded-xl bg-slate-100 text-slate-700`,children:B(Ge,{class:`size-5`})}),B(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`Gateway · DNS`}),B(`p`,{class:`mt-2 mb-0 break-all text-base font-black text-slate-950`,children:r.gateway??`Gateway 확인 전`}),B(`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 Nr({action:e,data:t,error:n,feedback:r,loading:i,onDismissFeedback:a,onReconnect:o,onRetry:s,onSave:c}){let[l,u]=R({protocol:`dhcp`,pppoeUsername:``,pppoePassword:``,staticAddress:``,staticPrefixLength:24,staticGateway:``,dnsPrimary:``,dnsSecondary:``}),[d,f]=R(!1),[p,m]=R(!1),[h,g]=R(null);z(()=>{t&&!d&&(u(Tr(t)),g(null))},[t,d]);let _=Te(()=>{let e=[{value:`dhcp`,label:`자동 IP (DHCP)`},{value:`pppoe`,label:`PPPoE`},{value:`static`,label:`고정 IPv4`}];return t&&!t.configuration.supported&&e.unshift({value:t.configuration.protocol,label:`현재 설정 (${t.configuration.protocol})`}),e},[t]);if(i)return B(Bn,{});if(n)return B(Vn,{message:n,onRetry:s});if(!t)return null;let v=e!==null,y=e=>{u(n=>{let r={...n,...e};return f(Dr(r,t)),r}),g(null)},x=async e=>{if(e.preventDefault(),!Cr(l.protocol))g(`DHCP, PPPoE 또는 고정 IPv4 중 하나를 선택해 주세요.`);else{if(l.protocol===`pppoe`){if(!Er(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(!Or(l.staticAddress)){g(`고정 IPv4 주소를 올바르게 입력해 주세요.`);return}if(!Or(l.staticGateway)){g(`기본 게이트웨이를 올바르게 입력해 주세요.`);return}if(!Or(l.dnsPrimary)){g(`기본 DNS 서버를 올바르게 입력해 주세요.`);return}if(Er(l.dnsSecondary)&&!Or(l.dnsSecondary)){g(`보조 DNS 서버를 올바르게 입력해 주세요.`);return}}window.confirm(`인터넷 연결 설정을 적용하면 WAN 연결이 다시 시작되어 인터넷이 잠시 끊길 수 있습니다. 계속하시겠습니까?`)&&await c({protocol:l.protocol,pppoeUsername:Er(l.pppoeUsername),pppoePassword:l.pppoePassword,pppoePasswordChanged:l.pppoePassword.length>0,staticAddress:Er(l.staticAddress),staticPrefixLength:l.staticPrefixLength,staticGateway:Er(l.staticGateway),dnsPrimary:Er(l.dnsPrimary),dnsSecondary:Er(l.dnsSecondary)})&&(u(e=>({...e,pppoePassword:``})),f(!1))}};return B(b,{children:[r?B(jr,{feedback:r,onDismiss:a}):null,B(W,{eyebrow:`Overview`,title:`인터넷 연결 상태`,description:`WAN 연결 상태와 네트워크 정보를 확인합니다.`}),B(Mr,{data:t,busy:v,onReconnect:()=>{window.confirm(`WAN 연결을 다시 시작합니다. 인터넷이 잠시 끊길 수 있습니다. 계속하시겠습니까?`)&&o()}}),t.configuration.supported?null:B(`section`,{class:`mt-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-amber-950 sm:p-6`,children:B(`div`,{class:`flex items-start gap-3`,children:[B(U,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),B(`div`,{children:[B(`h2`,{class:`m-0 text-base font-black`,children:`현재 WAN 방식은 직접 관리하지 않습니다`}),B(`p`,{class:`mt-2 mb-0 text-sm font-semibold leading-6 text-amber-900/80`,children:[`현재 설정은 `,t.configuration.protocol,`입니다. 아래에서 DHCP, PPPoE 또는 고정 IPv4를 선택해 저장하면 해당 방식으로 전환합니다.`]})]})]})}),B(`div`,{class:`mt-7`,children:B(W,{eyebrow:`Internet`,title:`인터넷 연결 설정`,description:`통신사 또는 상위 네트워크 환경에 맞는 WAN 연결 방식을 선택합니다.`})}),B(`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?B(`div`,{class:`mb-5 flex justify-end`,children:B(`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:[B(U,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]})}):null,B(`div`,{children:[B(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`연결 방식`}),B(hr,{ariaLabel:`WAN 연결 방식`,className:`mt-2 max-w-xl`,disabled:v,id:`wan-protocol`,onChange:e=>y(e===`static`&&!Er(l.dnsSecondary)?{protocol:e,dnsSecondary:xr}:{protocol:e}),options:_,value:l.protocol,variant:`emphasized`}),B(`span`,{class:`mt-2 block text-xs font-semibold leading-5 text-slate-500`,children:`대부분의 가정용 인터넷과 상위 공유기 연결은 자동 IP(DHCP)를 사용합니다.`})]}),l.protocol===`pppoe`?B(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[B(`label`,{class:`block`,children:[B(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`PPPoE 사용자명`}),B(`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})]}),B(`label`,{class:`block`,children:[B(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`PPPoE 비밀번호`}),B(`span`,{class:`relative mt-2 block`,children:[B(`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}),B(`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:B(p?ht:mt,{class:`size-5`})})]}),B(`span`,{class:`mt-2 block text-xs text-slate-500`,children:t.configuration.protocol===`pppoe`&&t.configuration.pppoe.passwordConfigured?`비워 두면 현재 저장된 비밀번호를 그대로 사용합니다.`:`통신사에서 제공한 PPPoE 비밀번호를 입력해 주세요.`})]})]}):null,l.protocol===`static`?B(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[B(`label`,{class:`block`,children:[B(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`IPv4 주소`}),B(yr,{disabled:v,label:`IPv4 주소`,onChange:e=>y({staticAddress:e}),value:l.staticAddress})]}),B(`div`,{class:`block`,children:[B(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`서브넷 마스크`}),B(hr,{ariaLabel:`WAN 서브넷 마스크`,className:`mt-2`,disabled:v,id:`wan-static-prefix-length`,onChange:e=>y({staticPrefixLength:Number(e)}),options:Sr.map(e=>({value:String(e),label:`/${e} · ${kr(e)}`})),value:String(l.staticPrefixLength),variant:`emphasized`})]}),B(`label`,{class:`block`,children:[B(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`기본 게이트웨이`}),B(yr,{disabled:v,label:`기본 게이트웨이`,onChange:e=>y({staticGateway:e}),value:l.staticGateway})]}),B(`label`,{class:`block`,children:[B(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`기본 DNS`}),B(yr,{disabled:v,label:`기본 DNS`,onChange:e=>y({dnsPrimary:e}),value:l.dnsPrimary})]}),B(`label`,{class:`block lg:col-start-2`,children:[B(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`보조 DNS (선택)`}),B(yr,{disabled:v,label:`보조 DNS`,onChange:e=>y({dnsSecondary:e}),value:l.dnsSecondary})]})]}):null,l.protocol===`dhcp`?B(`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?B(`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,B(`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:[B(`p`,{class:`m-0 text-xs font-semibold leading-5 text-slate-500`,children:`설정 저장 후 WAN만 다시 연결하며 LAN 관리 페이지와 내부 네트워크 설정은 유지됩니다.`}),B(`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:[B(_t,{class:`size-5 ${e===`saving`?`animate-pulse`:``}`}),e===`saving`?`저장 중`:`설정 저장`]})]})]})]})}var Pr=[16,20,22,23,24,25,26,27,28],Fr=[[`30m`,`30분`],[`1h`,`1시간`],[`6h`,`6시간`],[`12h`,`12시간`],[`24h`,`24시간`],[`3d`,`3일`],[`7d`,`7일`]];function Ir(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 Lr(e){let t=gr(e);return t.slice(0,3).some(e=>e===``||Number(e)>255)?null:t.slice(0,3).join(`.`)}function Rr(e){return gr(e)[3]}function zr(e){let t=Lr(e.lan.address),n=Rr(e.dhcp.start??``),r=Rr(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 Br({disabled:e,label:t,lanAddress:n,value:r,onChange:i}){let a=Lr(n),o=a?.split(`.`)??[``,``,``],s=Rr(r),c=e=>{let t=_r(e);i(a&&t!==``?`${a}.${t}`:t)};return B(`label`,{class:`block`,children:[B(`span`,{class:`text-sm font-extrabold text-slate-800`,children:t}),B(`div`,{class:`ssh-ipv4-segments mt-2 min-h-11`,"aria-label":`${t} 입력`,children:[o.map((e,t)=>B(`div`,{class:`ssh-ipv4-segment`,children:[B(`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||`—`}),B(`span`,{"aria-hidden":`true`,class:`ssh-ipv4-separator text-sm font-black text-slate-400`,children:`.`})]},t)),B(`div`,{class:`ssh-ipv4-segment`,children:B(`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})})]}),B(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`앞 3개 주소는 공유기 IP 주소와 동일하게 유지됩니다.`})]})}function Vr(e){return`${window.location.protocol}//${e}${window.location.pathname}${window.location.search}${window.location.hash}`}function Hr({feedback:e,onDismiss:t}){return B(`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:[B(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[B(`p`,{class:`m-0 font-bold leading-6`,children:e.message}),B(`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?B(`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:Vr(e.newAddress),children:[`새 공유기 주소 `,e.newAddress,`로 연결`]}):null]})}function Ur({data:e}){let t=e.conflict.detected;return B(`section`,{class:`grid gap-4 lg:grid-cols-3`,"aria-label":`내부 네트워크 상태`,children:[B(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[B(`span`,{class:`grid size-10 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:B(Ge,{class:`size-5`})}),B(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`SmartSafeHub LAN`}),B(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:e.lan.subnet}),B(`p`,{class:`mt-1 mb-0 text-sm font-semibold text-slate-600`,children:[`공유기 주소 `,e.lan.address]})]}),B(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[B(`span`,{class:`grid size-10 place-items-center rounded-xl bg-slate-100 text-slate-700`,children:B(at,{class:`size-5`})}),B(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`상위 네트워크`}),B(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:e.wan.subnet??`확인 전`}),B(`p`,{class:`mt-1 mb-0 text-sm font-semibold text-slate-600`,children:e.wan.connected?`WAN ${e.wan.address??`주소 확인 중`}`:`WAN 연결 후 자동으로 확인합니다.`})]}),B(`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:[B(`span`,{class:`grid size-10 place-items-center rounded-xl ${t?`bg-red-100 text-red-700`:`bg-emerald-100 text-emerald-700`}`,children:B(t?U:_t,{class:`size-5`})}),B(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] opacity-70`,children:`대역 충돌`}),B(`p`,{class:`mt-2 mb-0 text-lg font-black`,children:t?`주소 대역이 겹칩니다`:`충돌이 감지되지 않았습니다`}),B(`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 Wr({action:e,data:t,error:n,feedback:r,loading:i,onApplyRecommendation:a,onDismissFeedback:o,onRetry:s,onSave:c}){let[l,u]=R(``),[d,f]=R(24),[p,m]=R(!0),[h,g]=R(``),[_,v]=R(``),[y,x]=R(`12h`),[S,C]=R(!1),[w,T]=R(null),E=(e={})=>{if(!t)return!1;let n=zr(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=Lr(e);if(u(e),!t){C(E({ipAddress:e}));return}let n=Rr(h),r=Rr(_),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}))};z(()=>{if(!t||S)return;let e=zr(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=Te(()=>{let e=[...Pr];return e.includes(d)||(e.push(d),e.sort((e,t)=>e-t)),e},[d]),P=Te(()=>{let e=[...Fr];return e.some(([e])=>e===y)||e.push([y,`현재 설정 (${y})`]),e},[y]);if(i)return B(Bn,{});if(n)return B(Vn,{message:n,onRetry:s});if(!t)return null;let F=e!==null,ee=async e=>{e.preventDefault(),Ir(l)?!Ir(h)||!Ir(_)?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 형식으로 입력해 주세요.`)},te=async()=>{t.recommendation&&window.confirm(`상위 네트워크와 겹치지 않는 ${t.recommendation.subnet} 대역으로 자동 변경합니다. 현재 연결이 잠시 끊길 수 있습니다. 계속하시겠습니까?`)&&(T(null),await a()&&C(!1))};return B(b,{children:[r?B(Hr,{feedback:r,onDismiss:o}):null,B(W,{eyebrow:`Overview`,title:`내부 네트워크 상태`,description:`LAN 주소와 DHCP 할당 상태를 확인합니다.`}),B(Ur,{data:t}),t.conflict.detected?B(`section`,{class:`mt-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-amber-950 sm:p-6`,children:B(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between`,children:[B(`div`,{class:`min-w-0`,children:[B(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-amber-700`,children:`자동 충돌 해결`}),B(`h2`,{class:`mt-2 mb-0 text-lg font-black`,children:`안전한 LAN 대역으로 변경할 수 있습니다`}),B(`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?B(`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 te(),type:`button`,children:[B(_t,{class:`size-5 ${e===`auto`?`animate-pulse`:``}`}),e===`auto`?`변경 중`:`추천 대역으로 자동 변경`]}):null]})}):null,B(`div`,{class:`mt-7`,children:B(W,{eyebrow:`Local network`,title:`LAN 및 DHCP 설정`,description:`상위 공유기와 다른 주소 대역을 사용해야 안정적으로 라우팅할 수 있습니다.`})}),B(`form`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,onSubmit:e=>void ee(e),children:[S?B(`div`,{class:`mb-5 flex justify-end`,children:B(`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:[B(U,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]})}):null,B(`div`,{class:`grid gap-5 lg:grid-cols-2`,children:[B(`div`,{class:`block`,children:[B(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`공유기 IP 주소`}),B(yr,{disabled:F,label:`공유기 IP 주소`,onChange:D,value:l})]}),B(`div`,{class:`block`,children:[B(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`현재 LAN 대역`}),B(`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}),B(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`서브넷 마스크는 아래 고급 설정에서 변경할 수 있습니다.`})]}),B(Br,{disabled:F,label:`DHCP 시작 주소`,lanAddress:l,onChange:O,value:h}),B(Br,{disabled:F,label:`DHCP 종료 주소`,lanAddress:l,onChange:k,value:_})]}),B(`details`,{class:`mt-6 rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[B(`summary`,{class:`cursor-pointer text-sm font-black text-slate-900`,children:`고급 DHCP 설정`}),B(`div`,{class:`mt-5 grid gap-5 lg:grid-cols-2`,children:[B(`div`,{class:`block`,children:[B(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`서브넷 마스크`}),B(hr,{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`}),B(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`일반 가정용 네트워크는 /24 사용을 권장합니다.`})]}),B(`div`,{class:`block`,children:[B(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`DHCP 임대 시간`}),B(hr,{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`})]}),B(`label`,{class:`inline-flex min-h-11 items-center gap-3 text-sm font-extrabold text-slate-800 lg:col-span-2`,children:[B(`input`,{checked:p,class:`size-5 accent-teal-700`,disabled:F,onChange:e=>M(e.currentTarget.checked),type:`checkbox`}),`SmartSafeHub DHCP 서버 사용`]}),B(`p`,{class:`m-0 text-xs font-semibold leading-5 text-slate-500 lg:col-span-2`,children:`DHCP를 끄면 SmartSafeHub에 연결되는 장치에 IP 주소를 자동으로 할당하지 않습니다. 별도의 DHCP 서버를 사용하는 고급 구성에서만 비활성화하세요.`})]})]}),w?B(`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,B(`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:[B(`p`,{class:`m-0 text-xs font-semibold leading-5 text-slate-500`,children:`주소가 바뀌면 현재 관리 페이지 연결이 종료되며 새 IP를 받아 다시 접속해야 합니다.`}),B(`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:F||!S,type:`submit`,children:[B(_t,{class:`size-5 ${e===`saving`?`animate-pulse`:``}`}),e===`saving`?`저장 중`:`설정 저장`]})]})]})]})}var Gr={};Gr.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},Gr.QRErrorCorrectLevel=(e,t)=>{e.exports={L:1,M:0,Q:3,H:2}},Gr.QRMaskPattern=(e,t)=>{e.exports={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7}},Gr.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}},Gr.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(qr(`index`))(0,qr(`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 Yr(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 Xr(e){return`${(e.replace(/[\\/:*?"<>|\x00-\x1f\x7f]/g,`_`).replace(/[. ]+$/g,``).trim()||`Wi-Fi`).slice(0,64)}-QR.png`}function Zr(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 Qr({load:e,onClose:t}){let[n,r]=R(null),[i,a]=R(null),[o,s]=R(null);z(()=>{let t=!0;return e().then(e=>{if(t)try{r({ssid:e.ssid,matrix:Jr(Yr(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 B(`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:B(`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:[B(`h2`,{class:`m-0 text-xl font-extrabold`,children:`Wi-Fi QR 코드`}),B(`p`,{class:`mt-2 break-all text-sm font-semibold text-slate-600`,children:n?.ssid??`Wi-Fi 정보를 확인하는 중`}),i?B(`p`,{role:`alert`,class:`my-6 text-sm font-bold text-red-700`,children:i}):null,!n&&!i?B(`p`,{class:`my-6 text-sm`,children:`QR 코드를 만드는 중...`}):null,n?B(`div`,{class:`mx-auto my-5 w-full max-w-[288px] rounded-xl border border-slate-200 bg-white p-3`,children:B(`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:[B(`rect`,{width:c+8,height:c+8,fill:`white`}),n.matrix.map((e,t)=>e.map((e,n)=>e?B(`rect`,{x:n+4,y:t+4,width:`1`,height:`1`,fill:`black`},`${t}-${n}`):null))]})}):null,B(`p`,{class:`text-xs leading-5 text-slate-500`,children:[B(`span`,{class:`block`,children:`카메라로 스캔해 Wi-Fi에 연결하세요.`}),B(`span`,{class:`block`,children:`QR 코드에는 비밀번호가 포함됩니다.`})]}),o?B(`p`,{role:`alert`,class:`mt-3 text-sm font-bold text-red-700`,children:o}):null,n?B(`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`);Zr(n.matrix,r,8);let i=document.createElement(`a`);i.href=t.toDataURL(`image/png`),i.download=Xr(n.ssid),i.click()}catch{s(`QR 코드 이미지를 저장하지 못했습니다. 다시 시도해 주세요.`)}}},type:`button`,children:[B(ut,{class:`size-5`}),`QR 코드 다운로드`]}):null,B(`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 $r({guest:e,busy:t,saving:n,onUpdate:r,onLoadQr:i}){let[a,o]=R(!1),[s,c]=R(!1),[l,u]=R(e.ssid),[d,f]=R(``),[p,m]=R(e.enabled),[h,g]=R(null);if(z(()=>{u(e.ssid),m(e.enabled),f(``),g(null)},[e]),!e.supported)return B(`section`,{class:`mt-5 rounded-2xl border border-slate-200 bg-white p-5 sm:p-6`,children:[B(`h2`,{class:`m-0 text-lg font-extrabold text-slate-900`,children:`게스트 Wi-Fi`}),B(`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 B(b,{children:[B(`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:[B(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[B(`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:[B(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:B(Ge,{class:`size-6`})}),B(`span`,{class:`min-w-0 flex-1`,children:[B(`span`,{class:`block text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`2.4 GHz · 분리 네트워크`}),B(`span`,{class:`mt-2 block text-xl font-extrabold tracking-tight text-slate-950`,children:`게스트 Wi-Fi`}),B(`span`,{class:`mt-2 block break-all text-sm text-slate-600`,children:e.ssid})]}),B(wt,{class:`mt-3 size-5 shrink-0 text-slate-500 transition-transform ${a?`rotate-180`:``}`})]}),B(`div`,{class:`flex flex-wrap items-center gap-2 sm:justify-end`,children:[_&&B(`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:[B(U,{class:`size-3.5`}),`저장되지 않음`]}),B(`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:[B(rt,{class:`size-4 shrink-0`}),`QR 코드 보기`]}),B(`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?`사용 중`:`꺼짐`})]})]}),B(`div`,{class:a?``:`hidden`,id:`ssh-guest-settings`,children:[B(`p`,{class:`mt-5 mb-0 text-sm leading-6 text-slate-600`,children:`방문자는 인터넷만 이용하며 내부 네트워크와 공유기 관리 화면에는 접근할 수 없습니다.`}),B(`div`,{class:`mt-5 grid gap-5 lg:grid-cols-2`,children:[B(`label`,{class:`block`,children:[B(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`게스트 Wi-Fi 이름 (SSID)`}),B(`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})]}),B(`label`,{class:`block`,children:[B(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`게스트 Wi-Fi 비밀번호`}),B(`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}),B(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`WPA2/WPA3 혼합 · 기본 Wi-Fi와 다른 비밀번호를 권장합니다.`})]})]}),B(`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&&B(`span`,{class:`block text-xs text-slate-500`,children:[`게스트 게이트웨이: `,e.subnet]})]}),B(`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:[B(`label`,{class:`inline-flex min-h-11 items-center gap-3 text-sm font-extrabold text-slate-800`,children:[B(`input`,{checked:p,class:`size-5 accent-teal-700`,disabled:t,onChange:e=>m(e.currentTarget.checked),type:`checkbox`}),`게스트 Wi-Fi 사용`]}),B(`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:[B(_t,{class:`size-5 ${n?`animate-pulse`:``}`}),n?`적용 중`:`설정 저장`]})]}),h&&B(`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&&B(Qr,{load:i,onClose:()=>c(!1)})]})}var ei=[{value:`sae-mixed`,label:`WPA2/WPA3 혼합 (권장)`},{value:`sae`,label:`WPA3-Personal`},{value:`psk2`,label:`WPA2-Personal`},{value:`none`,label:`암호화 없음`}];function ti(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 ni(e){return e.length>=8&&e.length<=63||/^[0-9a-fA-F]{64}$/.test(e)}function ri({network:e,busy:t,saving:n,onUpdate:r,onLoadQr:i}){let[a,o]=R(e.ssid),[s,c]=R(e.enabled),[l,u]=R(e.security===`custom`?`keep`:e.security),[d,f]=R(``),[p,m]=R(!1),h=Ee(()=>i(e.section),[i,e.section]),[g,_]=R(null);z(()=>{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})`}]:[],...ei],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&&!ni(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 B(`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:[B(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[B(`div`,{class:`flex min-w-0 items-start gap-3`,children:[B(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:B(Ge,{class:`size-6`})}),B(`div`,{class:`min-w-0`,children:[B(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:e.bandLabel}),B(`h2`,{class:`mt-2 mb-0 break-words text-xl font-extrabold tracking-tight text-slate-950`,children:e.ssid||`이름 없는 Wi-Fi`}),B(`p`,{class:`mt-2 mb-0 text-sm text-slate-600`,children:[`채널 `,e.channel??`자동`,` · 연결 기기 `,e.clientCount,`대`]})]})]}),B(`div`,{class:`flex flex-wrap items-center gap-2 sm:justify-end`,children:[x?B(`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:[B(U,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}):null,B(`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:[B(rt,{class:`size-4 shrink-0`}),`QR 코드 보기`]}),B(`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:[B(`span`,{class:`size-2 rounded-full ${e.runtimeUp?`bg-emerald-500`:`bg-slate-400`}`}),e.runtimeUp?`동작 중`:e.enabled?`시작 대기`:`꺼짐`]})]})]}),B(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[B(`label`,{class:`block`,children:[B(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`Wi-Fi 이름 (SSID)`}),B(`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})]}),B(`div`,{class:`block`,children:[B(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`보안 방식`}),B(hr,{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`}),B(`span`,{class:`mt-2 block text-xs text-slate-500`,children:[`현재: `,ti(e)]})]}),B(`label`,{class:`block lg:col-span-2`,children:[B(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`새 비밀번호`}),B(`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}),B(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`저장된 비밀번호는 일반 설정 조회에 노출되지 않습니다. QR 코드에는 비밀번호가 포함됩니다.`})]})]}),B(`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:[B(`label`,{class:`inline-flex min-h-11 items-center gap-3 text-sm font-extrabold text-slate-800`,children:[B(`input`,{checked:s,class:`size-5 accent-teal-700`,disabled:t,onChange:e=>c(e.currentTarget.checked),type:`checkbox`}),`이 Wi-Fi 사용`]}),B(`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:[B(_t,{class:`size-5 ${n?`animate-pulse`:``}`}),n?`적용 중`:`설정 저장`]})]}),p?B(Qr,{load:h,onClose:()=>m(!1)}):null,g?B(`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 ii({feedback:e,onDismiss:t}){return B(`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:[B(`p`,{class:`m-0 font-bold leading-6`,children:e.message}),B(`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 ai({data:e,error:t,feedback:n,loading:r,updatingSection:i,onDismissFeedback:a,onRetry:o,onUpdate:s,onUpdateGuest:c,onLoadQr:l,onLoadGuestQr:u}){return r?B(Bn,{}):t?B(Vn,{message:t,onRetry:o}):e?B(b,{children:[n?B(ii,{feedback:n,onDismiss:a}):null,B(`section`,{class:`mb-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-amber-950 sm:p-6`,children:[B(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-amber-700`,children:`연결 주의`}),B(`h2`,{class:`mt-2 mb-0 text-lg font-extrabold`,children:`Wi-Fi 이름이나 비밀번호를 바꾸면 현재 연결이 끊어집니다`}),B(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-amber-900/80`,children:`변경한 Wi-Fi 이름과 비밀번호로 다시 연결하면 SmartSafeHub를 계속 사용할 수 있습니다. VLAN 등 고급 무선 옵션은 기존 LuCI에서 관리합니다.`})]}),B(W,{eyebrow:`Wireless`,title:`기본 Wi-Fi`,description:`무선 네트워크의 이름, 비밀번호, 보안 방식과 QR 코드를 관리합니다.`}),e.networks.length?B(`section`,{"aria-label":`기본 Wi-Fi 설정`,class:`grid gap-5 xl:grid-cols-2`,children:e.networks.map(e=>B(ri,{busy:i!==null,network:e,onUpdate:s,onLoadQr:l,saving:i===e.section},e.section))}):B(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 text-center shadow-sm shadow-slate-900/5 sm:p-8`,children:[B(Ge,{class:`mx-auto size-10 text-slate-400`}),B(`h2`,{class:`mt-4 mb-0 text-lg font-extrabold text-slate-950`,children:`관리할 기본 Wi-Fi를 찾지 못했습니다`}),B(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:`기존 LuCI에서 AP 모드의 무선 네트워크를 먼저 구성해 주세요.`})]}),e.guest&&B(b,{children:[B(`div`,{role:`separator`,"aria-label":`기본 Wi-Fi와 게스트 Wi-Fi 구분`,class:`mt-5 w-full border-t border-slate-200`}),B(`div`,{class:`mt-5`,children:B(W,{eyebrow:`Guest network`,title:`방문자용 네트워크`,description:`기본 네트워크와 분리된 게스트 Wi-Fi를 관리합니다.`})}),B($r,{guest:e.guest,busy:i!==null,saving:i===`ssh_guest`,onUpdate:c,onLoadQr:u})]}),e.guestError&&B(`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 정보를 불러오지 못했습니다. 패키지를 설치한 직후라면 로그아웃 후 다시 로그인해 주세요.`,B(`button`,{class:`ml-3 underline font-bold`,onClick:o,type:`button`,children:`다시 시도`})]})]}):null}function oi({timestamp:e,fallback:t=`기록 없음`}){let[n,r]=R(()=>Date.now()/1e3),i=typeof e==`number`&&Number.isFinite(e)&&e>0&&e<=864e10;if(z(()=>{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 B(`span`,{children:t});let a=Qt(e);return B(`span`,{class:`inline-flex max-w-full flex-wrap items-center gap-1`,children:[B(`time`,{class:`min-w-0`,dateTime:new Date(e*1e3).toISOString(),title:a,children:en(e,n)}),B(`details`,{class:`group inline-flex max-w-full flex-wrap items-center gap-1 align-middle`,children:[B(`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:B(`svg`,{"aria-hidden":`true`,class:`size-3.5`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.8`,viewBox:`0 0 24 24`,children:[B(`circle`,{cx:`12`,cy:`12`,r:`9`}),B(`path`,{d:`M12 11v5m0-8h.01`,strokeLinecap:`round`})]})}),B(`span`,{class:`hidden max-w-full break-words rounded-md bg-slate-100 px-2 py-1 text-xs font-semibold text-slate-700 group-open:inline-block`,children:a})]})]})}function si(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 ci(e,t){return t?e.filter(e=>e.title.toLowerCase().includes(t)||e.domains.some(e=>e.includes(t))):e}function li(e,t){return e===t||e.length===t.length&&e.every((e,n)=>e===t[n])}function ui(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&&li(e.domains,r.domains)})}function di(e,t){return{domains:li(e.domains,t.domains)?e.domains:t.domains,rules:ui(e.rules,t.rules)?e.rules:t.rules}}var fi=[],pi=[];function mi(e){return e.trim().toLowerCase().replace(/\.+$/,``)}function hi(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 gi=Qn(function({action:e,kind:t,onAddRule:n,onDeleteRule:r,rules:i,recommendedDomainSet:a,recommendedKnown:o,recommendedRules:s,onRequestRevoke:c,revokeBusy:l}){let[u,d]=R(``),[f,p]=R(``),[m,h]=R(null),[g,_]=R(!0),[v,y]=R(null),b=t===`allow`,x=e!==null,S=Te(()=>f.trim().toLowerCase(),[f]),C=S.length>0,{filteredRules:w,manualRules:T,managedRules:E}=Te(()=>si(i,S,b&&o,a),[i,S,b,o,a]),D=Te(()=>b?ci(s,S):[],[b,s,S]),O=C||(v??(s.length||E.length)<=3),k=C||g;async function A(e){e.preventDefault();let r=mi(u);hi(r)?(h(null),await n(t,r,()=>{d(e=>mi(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 B(`li`,{class:`flex min-w-0 items-center justify-between gap-3 px-3.5 py-3`,children:[B(`div`,{class:`flex min-w-0 flex-1 flex-wrap items-center gap-2`,children:B(`code`,{class:`min-w-0 break-all font-mono text-sm font-semibold text-slate-800`,children:n})}),i?B(`span`,{class:`shrink-0 rounded-full bg-teal-50 px-2.5 py-1 text-[11px] font-bold text-teal-800`,title:`추천 규칙의 적용과 해제는 웹사이트에서 관리합니다.`,children:`웹사이트 관리`}):B(`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:B(yt,{class:`size-4 ${c?`animate-pulse`:``}`})})]},n)}return B(`article`,{class:`min-w-0 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm shadow-slate-900/5`,children:[B(`div`,{class:`border-b border-slate-200 p-4 sm:p-6`,children:[B(`div`,{class:`flex min-w-0 items-start gap-3`,children:[B(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl ${j.icon}`,children:B(b?_t:We,{class:`size-6`})}),B(`div`,{class:`min-w-0`,children:[B(`div`,{class:`flex flex-wrap items-center gap-2`,children:[B(`h2`,{class:`m-0 text-xl font-extrabold tracking-tight text-slate-950`,children:j.label}),B(`span`,{class:`rounded-full px-2.5 py-1 text-xs font-extrabold ${j.badge}`,children:i.length})]}),B(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:j.description})]})]}),B(`form`,{class:`mt-5`,onSubmit:A,children:[B(`label`,{class:`block text-xs font-extrabold text-slate-600`,for:`${t}-domain`,children:`새 도메인`}),B(`div`,{class:`mt-2 flex flex-col gap-2 sm:flex-row`,children:[B(`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}),B(`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:[B(vt,{class:`size-4`}),e?.operation===`add`&&e.action===t?`추가 중…`:`추가`]})]}),m?B(`p`,{class:`mt-2 mb-0 text-xs font-semibold leading-5 text-red-700`,id:`${t}-domain-error`,children:m}):null]})]}),B(`div`,{class:`p-4 sm:p-6`,children:[B(`label`,{class:`relative block`,for:`${t}-search`,children:[B(`span`,{class:`sr-only`,children:[j.label,` 검색`]}),B(`span`,{class:`pointer-events-none absolute inset-y-0 left-0 flex w-11 items-center justify-center text-slate-400`,children:B(bt,{class:`size-4`})}),B(`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?B(`div`,{class:`mt-4 overflow-hidden rounded-xl border border-slate-200`,children:[B(`section`,{"aria-label":`직접 추가한 허용 규칙`,children:[B(`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:[B(`span`,{class:`min-w-0 text-sm font-extrabold text-slate-800`,children:[`직접 추가한 규칙 `,B(`span`,{class:`ml-1 rounded-full bg-white px-2 py-0.5 text-xs font-bold text-slate-600`,children:[T.length,C?`개 검색됨`:`개`]})]}),B(`span`,{"aria-hidden":`true`,class:`shrink-0 text-sm font-bold text-slate-500`,children:k?`접기 −`:`펼치기 +`})]}),B(`div`,{hidden:!k,id:`allow-manual-domains`,children:T.length?B(`ul`,{class:`m-0 max-h-[20rem] list-none divide-y divide-slate-200 overflow-y-auto p-0`,children:T.map(M)}):B(`p`,{class:`m-0 px-4 py-5 text-sm text-slate-500`,children:C?`검색 결과가 없습니다.`:`직접 추가한 허용 도메인이 없습니다.`})})]}),B(`section`,{"aria-label":`웹사이트 추천 규칙`,class:`border-t border-slate-200`,children:[B(`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:[B(`span`,{class:`min-w-0 text-sm font-extrabold text-slate-800`,children:[`추천 규칙 `,B(`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?`개 검색됨`:`개`]})]}),B(`span`,{"aria-hidden":`true`,class:`shrink-0 text-sm font-bold text-slate-500`,children:O?`접기 −`:`펼치기 +`})]}),B(`div`,{hidden:!O,id:`allow-recommended-domains`,children:[D.length?B(`ul`,{class:`m-0 max-h-[22rem] list-none divide-y divide-slate-200 overflow-y-auto p-0`,children:D.map(e=>B(`li`,{class:`flex min-w-0 flex-wrap items-center justify-between gap-3 px-3.5 py-3`,children:[B(`div`,{class:`min-w-0 flex-1`,children:[B(`strong`,{class:`block break-words text-sm text-slate-900`,children:e.title}),B(`div`,{class:`mt-1 flex flex-wrap gap-1.5`,children:e.domains.map(e=>B(`code`,{class:`break-all text-xs text-slate-600`,children:e},e))})]}),c?B(`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:`적용 해제`}):B(`span`,{class:`text-xs text-slate-500`,children:`웹사이트 관리`})]},e.id))}):E.length?B(`ul`,{class:`m-0 max-h-[20rem] list-none divide-y divide-slate-200 overflow-y-auto p-0`,children:E.map(M)}):B(`p`,{class:`m-0 px-4 py-5 text-sm text-slate-500`,children:C?`검색 결과가 없습니다.`:`추천으로 추가된 허용 도메인이 없습니다.`}),C?null:B(`p`,{class:`m-0 border-t border-slate-100 px-4 py-3 text-xs leading-5 text-slate-500`,children:`추천 규칙은 이 공유기에서 적용 해제할 수 있습니다. 새 규칙의 적용은 SmartSafeHub 웹사이트에서 관리해 주세요.`})]})]})]}):B(`div`,{class:`mt-4 max-h-[28rem] overflow-y-auto rounded-xl border border-slate-200`,children:w.length?B(`ul`,{class:`m-0 list-none divide-y divide-slate-200 p-0`,children:w.map(M)}):B(`div`,{class:`px-5 py-10 text-center`,children:[B(xt,{class:`mx-auto size-8 text-slate-300`}),B(`p`,{class:`mt-3 mb-0 text-sm font-bold text-slate-600`,children:C?`검색 결과가 없습니다.`:j.empty})]})})]})]})});function _i({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]=R(null),[m,h]=R(!1),[g,_]=R(!1),[v,y]=R(null),[x,S]=R(!1),[C,w]=R(!1),[T,E]=R(!1),D=we(null),O=we(null),k=we(null),A=we(c);A.current=c;let[j,M]=R(null),[N,P]=R(null),F=f?.domains??fi,ee=f?.rules??pi,te=Te(()=>new Set(F),[F]),ne=Ee(e=>{p(t=>{if(!t)return e;let{domains:n,rules:r}=di(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},[]);z(()=>{if(!l)return;let e=l,t=!0,n=!1;function r(){n||g||x||(n=!0,e().then(e=>{t&&ne(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,ne]),z(()=>{if(!C)return;let e=window.setTimeout(()=>E(!0),6e4);return()=>window.clearTimeout(e)},[C]);async function re(){if(!(!u||g||x)){_(!0),P(null),M(null);try{let e=await u(),t=ne(e);M(e.syncStatus===`confirmed`?`서버에서 규칙을 가져와 SafeShield 허용 목록을 확인했습니다.`:`추천 규칙 상태를 확인 중입니다. 진행 결과는 자동으로 갱신됩니다.`),e.syncStatus===`confirmed`&&!t&&c()}catch{P(`추천 규칙을 동기화하지 못했습니다. 서버 연결 및 기기 등록 상태를 확인해 주세요.`)}finally{_(!1)}}}async function ie(){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);ne(t),y(null),D.current!==null&&M(`적용 해제 요청을 처리했습니다. 공유기의 허용 목록을 확인하고 있습니다.`)}catch{y(null),P(`적용 해제 결과를 아직 확인하지 못했습니다. 공유기의 상태를 자동으로 다시 확인합니다.`);try{let e=await l?.();e&&ne(e)}catch{}}finally{S(!1)}}let ae=f?.syncStage,oe=f?.syncStatus===`pending`,se=f?.syncStatus===`delayed`,ce=f?.syncStatus===`failed`,le=ae===`server_sync`?`서버 동기화 중`:ae===`safeshield_refreshing`?`SafeShield 갱신 중`:ae===`applying`?`추천 규칙 적용 중`:ae===`verifying`?`허용 목록 확인 중`:se?`재확인 지연`:ce?`동기화 실패`:oe?`동기화 대기`:`허용 목록 확인`,I=ae===`safeshield_refreshing`&&oe?`SafeShield가 보호 목록을 갱신하고 있습니다. 갱신이 끝나면 추천 규칙을 자동으로 다시 확인합니다.`:ae===`applying`&&oe?`추천 규칙을 SafeShield 허용 목록에 반영하고 있습니다.`:ae===`verifying`&&oe?`SafeShield 허용 목록에서 추천 규칙 반영 결과를 확인하고 있습니다.`:ae===`server_sync`&&oe?`SmartSafeHub 서버에서 최신 추천 규칙을 확인하고 있습니다.`:se?`서버 응답이 지연되고 있습니다. 이미 확인한 허용 규칙은 유지되며 자동으로 다시 시도합니다.`:ce?f?.syncReason===`auth_rejected`?`SmartSafeHub 기기 인증이 거부되었습니다. 공유기 계정 연결 상태를 확인해 주세요.`:f?.syncReason===`api_rejected`?`서버가 추천 규칙 요청을 거부했습니다. Hub API 배포 상태를 확인해 주세요.`:f?.syncReason===`invalid_response`?`서버 응답을 확인할 수 없습니다. 규칙은 유지되며 다음 동기화에서 다시 확인합니다.`:`추천 규칙 반영을 완료하지 못했습니다. 다음 자동 동기화에서 다시 시도합니다.`:oe?`추천 규칙의 적용 결과를 기다리고 있습니다.`:null,ue=C&&D.current!==null?{tone:`amber`,label:T?`적용 해제 처리 지연`:`적용 해제 확인 중`,description:T?`적용 해제 확인이 지연되고 있습니다. SafeShield 갱신 중이라면 완료 후 자동으로 재확인합니다.`:I??`적용 해제 상태를 확인하고 있습니다. 확인되면 목록과 상태가 자동 갱신됩니다.`}:oe||se||ce?{tone:ce?`red`:`amber`,label:le,description:I??``}:N?{tone:`red`,label:`상태 확인 필요`,description:N}:f?.syncWarning?{tone:`amber`,label:`서버 재확인 지연`,description:`이미 확인한 허용 규칙은 유지되며 자동으로 다시 시도합니다.`}:j?{tone:`green`,label:`허용 목록 확인`,description:j}:null;return i&&!t?B(Bn,{}):n&&!t?B(Vn,{message:n,onRetry:c}):t?B(b,{children:[v?B(`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:B(`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:[B(`h2`,{class:`m-0 text-lg font-extrabold text-slate-950`,id:`recommended-revoke-title`,children:`추천 규칙 적용 해제`}),B(`p`,{class:`mt-3 text-sm text-slate-700`,children:[B(`strong`,{children:v.title}),` 규칙을 이 공유기에서 해제할까요?`]}),B(`p`,{class:`mt-2 text-xs leading-5 text-slate-600`,children:[`포함된 도메인 `,v.domains.length,`개를 다시 확인합니다. 직접 추가한 허용 규칙과 다른 추천 규칙에 필요한 도메인은 유지됩니다.`]}),B(`div`,{class:`mt-5 flex justify-end gap-2`,children:[B(`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:`취소`}),B(`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 ie(),type:`button`,children:[x?B(`svg`,{class:`size-4 animate-spin`,"aria-hidden":`true`,viewBox:`0 0 24 24`,fill:`none`,children:[B(`circle`,{cx:`12`,cy:`12`,r:`9`,stroke:`currentColor`,"stroke-width":`3`,opacity:`0.25`}),B(`path`,{d:`M21 12a9 9 0 0 0-9-9`,stroke:`currentColor`,"stroke-width":`3`,"stroke-linecap":`round`})]}):null,B(`span`,{role:`status`,children:x?`해제 중…`:`적용 해제`})]})]})]})}):null,B(W,{eyebrow:`Local overrides`,title:`사용자 도메인 규칙`,description:`허용 목록은 오탐을 해제하고, 차단 목록은 원하는 도메인을 SafeShield 보호 목록에 추가합니다.`}),B(`section`,{"aria-label":`규칙 개수 요약`,class:`min-w-0`,children:B(`dl`,{class:`m-0 grid grid-cols-1 gap-3 sm:grid-cols-3`,children:[B(`div`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white px-5 py-4 shadow-sm shadow-slate-900/5`,children:[B(`dt`,{class:`text-sm font-extrabold text-emerald-700`,children:`허용 도메인`}),B(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight text-slate-950`,children:[t.counts.allow,B(`span`,{class:`ml-1 text-sm font-bold text-slate-500`,children:`개`})]}),B(`p`,{class:`mt-1 mb-0 text-xs text-slate-500`,children:`차단하지 않도록 등록한 도메인`})]}),B(`div`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white px-5 py-4 shadow-sm shadow-slate-900/5`,children:[B(`dt`,{class:`text-sm font-extrabold text-red-700`,children:`차단 도메인`}),B(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight text-slate-950`,children:[t.counts.block,B(`span`,{class:`ml-1 text-sm font-bold text-slate-500`,children:`개`})]}),B(`p`,{class:`mt-1 mb-0 text-xs text-slate-500`,children:`추가로 차단하도록 등록한 도메인`})]}),B(`div`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white px-5 py-4 shadow-sm shadow-slate-900/5`,children:[B(`dt`,{class:`text-sm font-extrabold text-slate-600`,children:`전체 등록 규칙`}),B(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight text-slate-950`,children:[t.counts.total,B(`span`,{class:`ml-1 text-sm font-bold text-slate-500`,children:`개`})]}),B(`p`,{class:`mt-1 mb-0 text-xs text-slate-500`,children:`허용 및 차단 규칙의 합계`})]})]})}),B(`div`,{class:`mt-7`,children:B(W,{eyebrow:`Recommendations`,title:`추천 사용자 규칙`,description:`웹사이트에서 선택한 권장 허용 규칙의 동기화 상태를 확인하고 관리합니다.`})}),B(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,"aria-label":`추천 사용자 규칙`,children:[B(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between`,children:[B(`div`,{class:`min-w-0 flex-1`,children:[B(`div`,{class:`flex flex-wrap items-center gap-2`,children:[B(`span`,{class:`grid size-8 place-items-center rounded-lg bg-teal-50 text-teal-700`,children:B(_t,{class:`size-4`})}),B(`h3`,{class:`m-0 text-base font-extrabold text-slate-950`,children:`동기화 상태`}),l?B(`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`?`허용 목록 확인`:le}):null]}),B(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:`웹사이트에서 선택한 허용 규칙을 약 30초마다 확인합니다. 적용 해제는 이 공유기에서도 할 수 있습니다.`}),B(`div`,{class:`mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs font-semibold text-slate-600`,children:[B(`span`,{children:[`추천으로 추가 `,B(`strong`,{class:`text-teal-800`,children:[m?F.length:`—`,`개 도메인`]})]}),f?.syncStatus===`confirmed`?B(`span`,{children:[`허용 확인 `,B(`strong`,{class:`text-emerald-800`,children:[f.confirmedDomains.length,`개`]})]}):null,f?.lastCheckedAt?B(`span`,{children:[f.syncWarning?`최근 재확인 시도`:`최근 확인`,`: `,B(oi,{timestamp:f.lastCheckedAt})]}):null]})]}),B(`div`,{class:`flex w-full flex-wrap gap-2 sm:w-auto lg:shrink-0`,children:[u?B(`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 re(),type:`button`,children:g?`동기화 중…`:`지금 동기화`}):null,B(`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?B(`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:[B(`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`}`}),B(`div`,{class:`min-w-0`,children:[B(`strong`,{children:ue.label}),B(`p`,{class:`mt-1 mb-0 leading-5`,children:ue.description})]})]}):null,B(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`추천 규칙 동기화는 SafeShield 차단 목록 갱신과 별개입니다. 허용 목록 확인은 DNS 처리나 서비스 재생 성공을 보장하지 않습니다.`})]}),t.applyLocalOverrides?null:B(`section`,{class:`mt-4 flex items-start gap-3 rounded-2xl border border-amber-200 bg-amber-50 p-4`,role:`alert`,children:[B(U,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),B(`div`,{children:[B(`p`,{class:`m-0 text-sm font-extrabold text-amber-950`,children:`로컬 규칙 적용이 꺼져 있습니다`}),B(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:`규칙은 저장되지만 SafeShield의 apply_local_overrides 설정을 켜기 전까지 DNS에 반영되지 않습니다.`})]})]}),t.safeshieldEnabled?null:B(`section`,{class:`mt-4 flex items-start gap-3 rounded-2xl border border-slate-200 bg-slate-100 p-4`,role:`status`,children:[B(We,{class:`mt-0.5 size-5 shrink-0 text-slate-600`}),B(`div`,{children:[B(`p`,{class:`m-0 text-sm font-extrabold text-slate-950`,children:`SafeShield가 비활성화되어 있습니다`}),B(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`규칙을 미리 저장할 수 있으며, SafeShield를 켜고 차단 목록을 갱신하면 적용됩니다.`})]})]}),r?B(`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:[B(`div`,{class:`flex min-w-0 items-start gap-3`,children:[B(_t,{class:`mt-0.5 size-5 shrink-0 text-teal-700`}),B(`p`,{class:`m-0 text-sm font-semibold leading-6 text-teal-950`,children:r})]}),B(`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?B(`section`,{"aria-live":`assertive`,class:`mt-4 flex items-start gap-3 rounded-2xl border border-red-200 bg-red-50 p-4`,children:[B(U,{class:`mt-0.5 size-5 shrink-0 text-red-700`}),B(`p`,{class:`m-0 text-sm font-semibold leading-6 text-red-900`,children:n})]}):null,B(`section`,{class:`mt-5 grid gap-5 xl:grid-cols-2`,children:[B(gi,{action:e,kind:`allow`,onAddRule:a,onDeleteRule:o,rules:t.allow,recommendedDomainSet:te,recommendedKnown:m,recommendedRules:ee,...d?{onRequestRevoke:y}:{},revokeBusy:x}),B(gi,{action:e,kind:`block`,onAddRule:a,onDeleteRule:o,rules:t.block,recommendedDomainSet:te,recommendedKnown:m,recommendedRules:ee,revokeBusy:x})]})]}):B(Vn,{message:`사용자 규칙 응답이 비어 있습니다.`,onRetry:c})}function vi(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 bi(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 xi(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 Si(e){return e.phase===`checking`||e.phase===`downloading`||e.phase===`verifying`||e.phase===`flashing`?B(H,{class:`size-3.5 animate-spin`}):e.phase===`error`?B(U,{class:`size-3.5`}):e.phase===`ready`||e.updateAvailable?B(ut,{class:`size-3.5`}):e.lastCheckAt?B(_t,{class:`size-3.5`}):B(st,{class:`size-3.5`})}function Ci({checked:e,disabled:t,onChange:n}){return B(`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:B(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${e?`left-6`:`left-1`}`})})}function wi({action:e,actionError:t,data:n,software:r,error:i,loading:a,message:o,reconnecting:s,uploadProgress:c,onCheck:l,onDiscard:u,onDismissFeedback:d,onInstall:f,onPrepare:p,onRetry:m,onUpload:h}){let g=we(null),[_,v]=R(null),[y,x]=R(!0),[S,C]=R(!1);z(()=>{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=vi(k,A),M=async()=>{_&&await h(_)&&(v(null),g.current&&(g.current.value=``))};return B(`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:[B(`div`,{class:`p-5 sm:p-6`,children:[B(`div`,{class:`flex flex-col gap-5 lg:flex-row lg:items-start lg:justify-between`,children:[B(`div`,{class:`flex min-w-0 gap-4`,children:[B(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-sky-50 text-sky-700`,children:B(Ge,{class:`size-6`})}),B(`div`,{class:`min-w-0`,children:[B(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-sky-700`,children:`Firmware update`}),B(`h2`,{class:`mt-2 mb-0 text-xl font-black text-slate-950 sm:text-2xl`,children:`펌웨어 업데이트`}),B(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500 lg:whitespace-nowrap`,children:`시스템 펌웨어를 확인하고 설치합니다. 온라인·수동 설치는 안전성 검증 후 진행됩니다.`})]})]}),n?B(`div`,{class:`flex shrink-0 flex-col gap-2 sm:flex-row lg:justify-end`,children:[B(`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 ${xi(n)}`,children:[Si(n),bi(n)]}),B(`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:[B(H,{class:`size-4 ${E?`animate-spin`:``}`}),E?`확인 중...`:`펌웨어 확인`]}),n.updateAvailable&&!T?B(`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?B(H,{class:`size-4 animate-spin`}):B(ut,{class:`size-4`}),D?`준비 중...`:`다운로드 및 검증`]}):null]}):null]}),t||o?B(`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:[B(`span`,{children:t||o}),B(`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`?B(`div`,{class:`mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4 sm:p-5`,role:`status`,children:B(`div`,{class:`flex gap-3`,children:[B(`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:B(H,{class:`size-5 animate-spin`})}),B(`div`,{children:[B(`strong`,{class:`block text-sm font-black text-amber-950`,children:`펌웨어를 설치하고 재부팅하고 있습니다.`}),B(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:`전원을 끄지 마세요. 현재 주소에서 공유기가 다시 응답하면 SmartSafeHub가 자동으로 새로고침됩니다.`})]})]})}):null,i&&!n?B(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-800`,children:[B(`p`,{class:`m-0 font-bold`,children:i}),B(`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?B(`div`,{class:`mt-5 flex items-center gap-3 rounded-xl bg-slate-50 p-4 text-sm font-bold text-slate-500`,children:[B(H,{class:`size-4 animate-spin`}),`펌웨어 상태를 확인하고 있습니다.`]}):n?B(b,{children:[n.lastError?B(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 sm:p-5`,children:B(`div`,{class:`flex gap-3`,children:[B(`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:B(U,{class:`size-5`})}),B(`div`,{class:`min-w-0 flex-1`,children:[B(`strong`,{class:`block text-sm font-black text-rose-950`,children:`펌웨어 작업을 완료하지 못했습니다.`}),B(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-rose-800`,children:n.lastError.message}),B(`details`,{class:`mt-3 rounded-lg border border-rose-200 bg-white p-3`,children:[B(`summary`,{class:`cursor-pointer text-xs font-extrabold text-rose-800`,children:`상세 정보 보기`}),B(`p`,{class:`mt-2 mb-0 break-all font-mono text-[11px] text-slate-600`,children:n.lastError.code})]})]})]})}):null,n.current.metadataAvailable?null:B(`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:[B(`strong`,{class:`block font-extrabold`,children:`현재 펌웨어의 버전 정보를 완전히 확인할 수 없습니다.`}),B(`span`,{class:`mt-1 block text-amber-800`,children:`새 펌웨어를 설치하면 이후부터 정확한 설치 버전을 자동으로 확인할 수 있습니다.`}),B(`details`,{class:`mt-2`,children:[B(`summary`,{class:`cursor-pointer font-extrabold text-amber-800`,children:`기술 상세 보기`}),B(`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.`})]})]}),B(`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:[B(`div`,{class:`bg-white p-4`,children:[B(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Device`}),B(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black text-slate-950`,children:n.current.deviceCode||n.current.boardName||`미확인`})]}),B(`div`,{class:`bg-white p-4`,children:[B(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Current version`}),B(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black text-slate-950`,children:n.current.releaseVersion||`미확인`}),n.current.buildId?B(`span`,{class:`mt-1 block break-all text-[11px] leading-5 text-slate-400`,children:[`빌드 ID `,n.current.buildId]}):n.current.openwrtVersion?B(`span`,{class:`mt-1 block text-[11px] leading-5 text-slate-400`,children:[`OpenWrt `,n.current.openwrtVersion]}):null]}),B(`div`,{class:`bg-white p-4`,children:[B(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Available version`}),B(`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?B(`span`,{class:`mt-1 block break-all text-[11px] leading-5 text-slate-400`,children:[`빌드 ID `,n.release.buildId]}):null]}),B(`div`,{class:`bg-white p-4`,children:[B(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Last check`}),B(`dd`,{class:`mt-2 mb-0 ml-0 text-sm font-black text-slate-950`,children:B(oi,{fallback:`아직 없음`,timestamp:n.lastCheckAt})})]})]})]}):null,n?.updateAvailable&&n.release?B(`section`,{class:`mt-5 rounded-xl border border-sky-200 bg-sky-50/60 p-4 sm:p-5`,"data-section":`available-firmware`,children:[B(`div`,{class:`flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4`,children:[B(`div`,{children:[B(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-sky-700`,children:`Available firmware`}),B(`h3`,{class:`mt-2 mb-0 text-lg font-black text-slate-950`,children:`새 펌웨어가 있습니다.`}),B(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:[`펌웨어 `,n.release.version||`미확인`,` · `,yi(n.release.sysupgrade.sizeBytes),` · `,(n.release.channel||n.settings.channel)===`beta`?`Beta`:`Stable`]})]}),B(`div`,{class:`flex shrink-0 flex-wrap items-center justify-start gap-2 sm:justify-end`,children:[B(`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?B(`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:[B(`span`,{class:`text-[11px] uppercase tracking-[0.12em] text-slate-500`,children:`관리 SW`}),B(`span`,{class:`text-slate-950`,children:k})]}):null]})]}),k?B(`div`,{class:`mt-4 rounded-xl border border-sky-100 bg-white p-4`,"data-section":`firmware-bundled-software`,children:[B(`div`,{class:`flex flex-wrap items-center justify-between gap-2`,children:[B(`span`,{class:`text-xs font-extrabold text-slate-800`,children:`포함된 관리 소프트웨어`}),B(`div`,{class:`flex flex-wrap items-center gap-2`,children:[B(`span`,{class:`text-sm font-black text-slate-950`,children:k}),j===`same`?B(`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]})]}),B(`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?B(`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)=>B(`li`,{children:e},`${t}-${e}`))}):null]}):null,T?B(`section`,{class:`mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4 sm:p-5`,"data-section":`prepared-firmware`,children:B(`div`,{class:`flex gap-4`,children:[B(`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:B(_t,{class:`size-5`})}),B(`div`,{class:`min-w-0 flex-1`,children:[B(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-amber-700`,children:`Verified image`}),B(`h3`,{class:`mt-2 mb-0 text-lg font-black text-amber-950`,children:`설치할 펌웨어가 준비되었습니다.`}),B(`p`,{class:`mt-2 mb-0 break-all text-sm leading-6 text-amber-900`,children:[T.filename||`firmware.bin`,` · `,yi(T.sizeBytes)]}),T.sha256?B(`p`,{class:`mt-2 mb-0 break-all font-mono text-[11px] leading-5 text-amber-800`,children:[`SHA-256 `,T.sha256]}):null,B(`div`,{class:`mt-5 flex items-start justify-between gap-4 rounded-xl border border-amber-200 bg-white p-4`,children:[B(`div`,{children:[B(`strong`,{class:`block text-sm font-black text-slate-950`,children:`현재 설정 유지`}),B(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`Wi-Fi, SafeShield, SmartSafeHub 설정을 유지합니다.`}),T.allowBackup?null:B(`span`,{class:`mt-2 block text-xs font-bold text-rose-700`,children:`이 이미지는 설정 유지 업그레이드를 지원하지 않습니다.`})]}),B(Ci,{checked:y,disabled:!T.allowBackup,onChange:x})]}),S?B(`div`,{class:`mt-4 rounded-xl border border-rose-200 bg-rose-50 p-4`,children:[B(`strong`,{class:`block text-sm font-black text-rose-950`,children:`펌웨어 설치를 시작할까요?`}),B(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-rose-800`,children:`설치 중 전원을 끄면 장치가 복구 불가능한 상태가 될 수 있습니다. 이미지 검증은 설치 직전에 다시 수행됩니다.`}),B(`div`,{class:`mt-4 flex flex-wrap justify-end gap-2`,children:[B(`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:`취소`}),B(`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:`설치 시작`})]})]}):B(`div`,{class:`mt-4 flex flex-wrap justify-end gap-2`,children:[B(`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:`파일 삭제`}),B(`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:[B(et,{class:`size-4`}),`펌웨어 설치`]})]})]})]})}):null]}),B(`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:[B(`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:[B(`div`,{class:`flex min-w-0 items-center gap-3`,children:[B(`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:B(ut,{class:`size-5 rotate-180`})}),B(`div`,{class:`min-w-0`,children:[B(`strong`,{class:`block text-sm font-black text-slate-950`,children:`수동 펌웨어 설치`}),B(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`펌웨어 파일(.bin)을 직접 업로드하여 검증한 뒤 설치합니다.`})]})]}),B(`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:`열기`}),B(`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:`닫기`})]}),B(`div`,{class:`border-t border-slate-200 px-5 pb-5 pt-3 sm:px-6`,children:[B(`p`,{class:`mt-0 mb-4 text-xs leading-5 text-slate-500 lg:whitespace-nowrap`,children:"이 장치에 맞는 Sysupgrade 이미지(.bin)만 사용하세요. 업로드한 파일은 이미지 검증과 `sysupgrade --test`를 모두 통과해야 설치할 수 있으며 강제 설치는 제공하지 않습니다."}),B(`div`,{class:`rounded-xl border border-slate-200 bg-white p-4 sm:p-5`,"data-layout":`firmware-file-picker`,children:[B(`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`}),B(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between`,children:[B(`div`,{class:`flex min-w-0 items-center gap-3`,children:[B(`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:B(ut,{class:`size-5 rotate-180`})}),B(`div`,{class:`min-w-0`,children:[B(`strong`,{class:`block truncate text-sm font-black text-slate-900`,children:_?_.name:`펌웨어 파일을 선택하세요`}),B(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:_?`${yi(_.size)} · Sysupgrade 이미지 (.bin)`:`이 장치에 맞는 Sysupgrade 이미지 (.bin)를 선택합니다.`})]})]}),B(`div`,{class:`flex shrink-0 flex-wrap gap-2 sm:justify-end`,children:[B(`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:_?`다른 파일 선택`:`파일 선택`}),B(`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?B(H,{class:`size-4 animate-spin`}):B(ut,{class:`size-4 rotate-180`}),O?`처리 중...`:`업로드 및 검증`]})]})]}),c===null?null:B(`div`,{class:`mt-4 border-t border-slate-100 pt-4`,children:[B(`div`,{class:`mb-2 flex items-center justify-between gap-3 text-xs font-bold text-slate-600`,children:[B(`span`,{children:`업로드 중`}),B(`span`,{children:[c,`%`]})]}),B(`div`,{class:`h-2 overflow-hidden rounded-full bg-slate-200`,children:B(`div`,{class:`h-full rounded-full bg-sky-600 transition-[width]`,style:{width:`${Math.max(0,Math.min(100,c))}%`}})})]})]})]})]})]})}var Ti=Array.from({length:24},(e,t)=>{let n=String(t).padStart(2,`0`);return{value:n,label:`${n}시`}}),Ei=Array.from({length:60},(e,t)=>{let n=String(t).padStart(2,`0`);return{value:n,label:`${n}분`}});function Di(e){let t=/^([01]\d|2[0-3]):([0-5]\d)$/.exec(e);return{hour:t?.[1]??`03`,minute:t?.[2]??`00`}}function Oi({ariaLabel:e,className:t=``,disabled:n=!1,id:r,onChange:i,value:a,variant:o=`default`}){let{hour:s,minute:c}=Di(a),l=(e,t)=>{i(`${e}:${t}`)};return B(`div`,{"aria-label":e,class:`ssh-time-select ${t}`.trim(),"data-time-select":r,role:`group`,children:[B(hr,{ariaLabel:`${e} 시`,disabled:n,id:`${r}-hour`,onChange:e=>l(e,c),options:Ti,value:s,variant:o}),B(hr,{ariaLabel:`${e} 분`,disabled:n,id:`${r}-minute`,onChange:e=>l(s,e),options:Ei,value:c,variant:o})]})}var ki=7200;function Ai(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,ki)}var ji=[{value:3600,label:`1시간마다`},{value:21600,label:`6시간마다`},{value:43200,label:`12시간마다`},{value:86400,label:`24시간마다`}],Mi=`luci-app-smartsafehub`;function Ni(e){return e?new Date(e*1e3).toLocaleString(`ko-KR`,{dateStyle:`medium`,timeStyle:`short`}):`아직 없음`}function Pi(e){return e===`stable`?`Stable`:e===`beta`?`Beta`:`미확인`}function Fi(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 Ii(e){return e.code===`UPDATES_INDEX_REFRESH_FAILED`?{title:`패키지 저장소를 확인하지 못했습니다.`,description:`OpenWrt 패키지 저장소 동기화 또는 네트워크 상태가 일시적으로 불안정할 수 있습니다. 잠시 후 업데이트 확인을 다시 실행해 주세요.`}:e.code===`UPDATES_INSTALL_FAILED`?{title:`관리 소프트웨어 업데이트 설치에 실패했습니다.`,description:`패키지 설치가 완료되지 않았습니다. 네트워크 연결과 저장 공간을 확인한 뒤 다시 시도해 주세요.`}:{title:`최근 업데이트 작업을 완료하지 못했습니다.`,description:`잠시 후 다시 시도해 주세요. 문제가 반복되면 상세 정보를 확인해 주세요.`}}function Li(e,t){return e.phase===`checking`?`업데이트 확인 중`:e.phase===`installing`?`업데이트 설치 중`:e.phase===`error`?`확인 필요`:e.updateCount>0?`업데이트 가능`:t?`업데이트 확인 지연`:e.lastCheckAt?`최신 상태`:`확인 전`}function Ri(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 zi(e,t){return e.phase===`checking`||e.phase===`installing`?B(H,{class:`size-3.5 shrink-0 animate-spin`}):e.phase===`error`?B(U,{class:`size-3.5 shrink-0`}):e.updateCount>0?B(ut,{class:`size-3.5 shrink-0`}):t?B(U,{class:`size-3.5 shrink-0`}):e.lastCheckAt?B(_t,{class:`size-3.5 shrink-0`}):B(st,{class:`size-3.5 shrink-0`})}function Bi({checked:e,disabled:t=!1,label:n,onChange:r}){return B(`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:B(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${e?`left-6`:`left-1`}`})})}function Vi({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]=R(!0),[m,h]=R(21600),[g,_]=R(!1),[v,y]=R(`03:00`),[x,S]=R(!1),[C,w]=R(!1);z(()=>{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=Ai(n),k=n?.phase===`error`&&n.lastError?.code===`UPDATES_INDEX_REFRESH_FAILED`,A=n?.lastError?Ii(n.lastError):null,j=n?.packages.find(e=>e.name===Mi)??n?.packages[0]??null,M=r?.updateAvailable?r.release?.bundledSoftware?.smartsafehub:null,N=!(!n?.lastCheckAt||n.updateCount!==0||O||j?.name!==Mi)&&vi(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,F=async()=>{await d({checkEnabled:f,checkIntervalSeconds:m,autoInstall:g,autoInstallTime:v})&&S(!1)};return B(`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:[B(`section`,{class:`min-w-0 xl:col-start-1 xl:row-start-1`,"data-section":`software-update-status`,children:[B(`div`,{class:`p-5 sm:p-6`,children:[B(`div`,{class:`flex flex-col gap-5 lg:flex-row lg:items-start lg:justify-between`,children:[B(`div`,{class:`flex min-w-0 gap-4`,children:[B(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-teal-50 text-teal-700`,children:B(et,{class:`size-6`})}),B(`div`,{class:`min-w-0`,children:[B(`div`,{class:`flex flex-wrap items-center gap-2`,children:[B(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700`,children:`Management software`}),n?B(`span`,{class:`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-extrabold ring-1 ring-inset ${Ri(n,O)}`,children:[B(`span`,{"aria-hidden":`true`,class:`flex items-center justify-center`,children:zi(n,O)}),Li(n,O)]}):null]}),B(`h2`,{class:`mt-2 mb-0 text-xl font-black text-slate-950 sm:text-2xl`,children:`관리 소프트웨어 업데이트`}),B(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500 xl:whitespace-nowrap`,children:`새 버전을 확인하고 안전하게 설치합니다.`})]})]}),n?B(`div`,{class:`flex shrink-0 flex-col gap-2 sm:flex-row lg:justify-end`,children:[B(`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:[B(H,{class:`size-4 ${T?`animate-spin`:``}`}),T?`확인 중...`:`업데이트 확인`]}),n.updateCount>0&&!C?B(`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?B(H,{class:`size-4 animate-spin`}):B(ut,{class:`size-4`}),E?`설치 중...`:`업데이트 설치`]}):null]}):null]}),(t||o)&&B(`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:[B(`span`,{children:t||o}),B(`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?B(`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:[B(`div`,{class:`flex items-start gap-3`,children:[B(`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:B(H,{"aria-hidden":`true`,class:`size-5 animate-spin`})}),B(`div`,{class:`min-w-0`,children:[B(`h3`,{class:`m-0 text-sm font-black text-sky-950`,children:`관리 소프트웨어를 업데이트하고 있습니다.`}),B(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-sky-800`,children:`설치가 완료되면 이 화면이 자동으로 갱신됩니다. 작업 중에는 웹 연결이 잠시 끊길 수 있습니다.`})]})]}),B(`div`,{"aria-hidden":`true`,class:`ssh-update-progress-track mt-4`,children:B(`span`,{class:`ssh-update-progress-bar`})})]}):null,i&&!n?B(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-800`,children:[B(`p`,{class:`m-0 font-bold`,children:i}),B(`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?B(`div`,{class:`mt-5 flex items-center gap-3 rounded-xl bg-slate-50 p-4 text-sm font-bold text-slate-500`,children:[B(`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:B(H,{"aria-hidden":`true`,class:`size-4 animate-spin`})}),B(`span`,{children:`관리 소프트웨어 업데이트 상태를 확인하고 있습니다.`})]}):n?B(b,{children:[n.lastError&&A?B(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 sm:p-5`,children:B(`div`,{class:`flex items-start gap-3`,children:[B(`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:B(U,{class:`size-5`})}),B(`div`,{class:`min-w-0 flex-1`,children:[B(`p`,{class:`m-0 text-sm font-extrabold text-rose-950`,children:A.title}),B(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-rose-800`,children:A.description}),n.lastError.at?B(`p`,{class:`mt-2 mb-0 text-xs font-bold text-rose-700`,children:[`발생 시각 `,Ni(n.lastError.at)]}):null,B(`details`,{class:`mt-3 rounded-lg border border-rose-200 bg-white p-3`,children:[B(`summary`,{class:`cursor-pointer text-xs font-extrabold text-rose-800`,children:`상세 정보 보기`}),B(`div`,{class:`mt-3 grid gap-2 text-xs leading-5 text-slate-600`,children:[B(`p`,{class:`m-0`,children:[`오류 코드 `,B(`strong`,{class:`font-black text-slate-900`,children:n.lastError.code})]}),B(`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?B(`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:[B(`div`,{class:`min-w-0`,children:[B(`strong`,{class:`block text-sm font-black text-amber-900`,children:`관리 소프트웨어를 지금 업데이트할까요?`}),B(`span`,{class:`mt-1 block text-xs leading-5 text-amber-800`,children:`설치 중 SmartSafeHub 웹 화면 연결이 잠시 끊길 수 있습니다.`})]}),B(`div`,{class:`flex shrink-0 gap-2`,children:[B(`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:`취소`}),B(`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?B(`div`,{class:`px-5 pb-5 sm:px-6 sm:pb-6`,children:[B(`h3`,{class:`mb-3 text-sm font-black text-slate-950`,children:`현재 상태`}),B(`dl`,{class:`grid overflow-hidden rounded-xl border border-slate-200 bg-slate-200 sm:grid-cols-2`,children:[B(`div`,{class:`bg-white p-4`,children:[B(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Installed`}),B(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black text-slate-950`,children:j?.installedVersion||`미확인`})]}),B(`div`,{class:`bg-white p-4`,children:[B(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Available`}),B(`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?`확인 필요`:`최신 버전`})]}),B(`div`,{class:`bg-white p-4`,children:[B(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Last check`}),B(`dd`,{class:`mt-2 mb-0 ml-0 text-sm font-black text-slate-950`,children:B(oi,{fallback:`아직 없음`,timestamp:n.lastCheckAt})})]}),B(`div`,{class:`bg-white p-4`,children:[B(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Auto install`}),B(`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?B(`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:B(`div`,{class:`flex gap-3`,children:[B(`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:B(U,{class:`size-5`})}),B(`div`,{children:[B(`h3`,{class:`m-0 text-base font-black text-amber-950`,children:`업데이트 확인이 지연되고 있습니다.`}),B(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:[`마지막 업데이트 확인: `,B(oi,{timestamp:n.lastCheckAt}),`. 자동 확인 상태를 점검하거나 업데이트 확인을 실행해 주세요.`]})]})]})}):B(`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:B(`div`,{class:`flex gap-3`,children:[B(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-emerald-700`,children:B(_t,{class:`size-5`})}),B(`div`,{children:[B(`h3`,{class:`m-0 text-base font-black text-emerald-950`,children:`최신 버전을 사용 중입니다.`}),B(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-emerald-800`,children:[`마지막 업데이트 확인: `,B(oi,{timestamp:n.lastCheckAt}),`.`]}),N?B(`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?B(`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:B(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between`,children:[B(`div`,{class:`flex min-w-0 gap-3`,children:[B(`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:B(H,{class:`size-5`})}),B(`div`,{class:`min-w-0`,children:[B(`h3`,{class:`m-0 text-base font-black text-slate-900`,children:`업데이트 상태를 아직 확인하지 않았습니다.`}),B(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`업데이트 확인을 실행하면 현재 설치 버전과 사용 가능한 최신 버전을 확인합니다.`})]})]}),B(`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:[B(H,{class:`size-4`}),`지금 확인`]})]})}):null]}),n?B(b,{children:[B(`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:[B(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[B(`div`,{class:`flex min-w-0 gap-4`,children:[B(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-600`,children:B(tt,{class:`size-5`})}),B(`div`,{class:`min-w-0`,children:[B(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Settings`}),B(`h3`,{class:`mt-2 mb-0 text-lg font-black text-slate-950`,children:`자동 업데이트`}),B(`p`,{class:`mt-2 mb-0 max-w-3xl text-sm leading-6 text-slate-500`,children:`관리 소프트웨어의 자동 확인과 자동 설치를 설정합니다.`})]})]}),x?B(`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:[B(U,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}):null]}),B(`div`,{class:`ssh-software-settings-grid mt-4`,children:[B(`div`,{class:`ssh-software-settings-channel rounded-xl border border-slate-200 p-4 sm:p-5`,children:B(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[B(`div`,{class:`min-w-0`,children:[B(`strong`,{class:`block text-sm font-black text-slate-950`,children:`업데이트 채널`}),B(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`현재 관리 소프트웨어 업데이트를 확인하는 배포 채널입니다.`})]}),B(`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 ${Fi(n.settings.channel)}`,title:`현재 업데이트 확인 채널`,children:Pi(n.settings.channel)})]})}),B(`div`,{class:`min-w-0 overflow-hidden rounded-xl border border-slate-200 p-4 sm:p-5`,children:[B(`div`,{class:`flex min-w-0 items-start justify-between gap-4`,children:[B(`div`,{class:`min-w-0`,children:B(`strong`,{class:`block text-sm font-black text-slate-950`,children:`자동으로 업데이트 확인`})}),B(Bi,{checked:f,label:`자동 업데이트 확인`,onChange:e=>{p(e),S(P({checkEnabled:e}))}})]}),B(`div`,{class:`mt-4 block`,children:[B(`span`,{class:`text-xs font-extrabold text-slate-700`,children:`확인 주기`}),B(hr,{ariaLabel:`업데이트 확인 주기`,className:`mt-2`,disabled:!f,id:`software-update-check-interval`,onChange:e=>{let t=Number(e);h(t),S(P({checkIntervalSeconds:t}))},options:ji.map(e=>({value:String(e.value),label:e.label})),value:String(m),variant:`emphasized`})]})]}),B(`div`,{class:`min-w-0 overflow-hidden rounded-xl border border-slate-200 p-4 sm:p-5`,children:[B(`div`,{class:`flex min-w-0 items-start justify-between gap-4`,children:[B(`div`,{class:`min-w-0`,children:B(`strong`,{class:`block text-sm font-black text-slate-950`,children:`자동으로 업데이트 설치`})}),B(Bi,{checked:g,label:`자동 업데이트 설치`,onChange:e=>{_(e),S(P({autoInstall:e}))}})]}),B(`div`,{class:`mt-4 min-w-0`,children:[B(`span`,{class:`text-xs font-extrabold text-slate-700`,children:`설치 시각`}),B(Oi,{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?B(`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,B(`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:[B(`p`,{class:`m-0 text-xs leading-5 text-slate-500`,children:[`마지막 설치: `,Ni(n.lastInstallAt)]}),B(`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 F(),type:`button`,children:e===`settings`?`저장 중`:`자동 업데이트 설정 저장`})]})]}),n.updateCount>0?B(`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:[B(`div`,{class:`flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4`,children:[B(`div`,{children:[B(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700`,children:`Release notes`}),B(`h3`,{class:`mt-2 mb-0 text-lg font-black text-slate-950`,children:`업데이트 내용`}),B(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500`,children:`현재 설치된 버전 이후 포함되는 주요 변경 사항입니다.`})]}),n.releaseNotes.length?B(`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?B(`div`,{class:`mt-5 grid gap-3`,children:[n.releaseNotes.map(e=>B(`article`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4 sm:p-5`,children:[B(`div`,{class:`flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-4`,children:[B(`strong`,{class:`break-all text-sm font-black text-slate-950`,children:e.version}),e.date?B(`span`,{class:`text-xs font-bold text-slate-500`,children:e.date}):null]}),e.summary?B(`p`,{class:`mt-3 mb-0 text-sm leading-6 text-slate-700`,children:e.summary}):null,e.sections.length?B(`details`,{class:`mt-4 rounded-lg border border-slate-200 bg-white p-3`,open:n.releaseNotes.length===1,children:[B(`summary`,{class:`cursor-pointer text-xs font-extrabold text-slate-800`,children:`자세한 변경 사항`}),B(`div`,{class:`mt-3 grid gap-4`,children:e.sections.map(t=>B(`section`,{children:[B(`h4`,{class:`m-0 text-xs font-extrabold text-slate-700`,children:t.title}),B(`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)=>B(`li`,{children:n},`${e.version}-${t.title}-${r}`))})]},`${e.version}-${t.title}`))})]}):null]},e.version)),n.releaseNotesComplete?null:B(`p`,{class:`m-0 rounded-xl bg-amber-50 px-4 py-3 text-xs font-bold leading-5 text-amber-800`,children:`일부 릴리즈 노트를 불러오지 못했습니다. 업데이트 확인과 설치는 계속 사용할 수 있습니다.`})]}):B(`p`,{class:`mt-4 mb-0 rounded-xl bg-slate-50 px-4 py-3 text-sm text-slate-500`,children:`릴리즈 노트를 불러오지 못했습니다. 업데이트 확인과 설치는 계속 사용할 수 있습니다.`})]}):null]}):null]})}function Hi({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 B(`section`,{class:`min-w-0 space-y-5`,children:[B(`div`,{children:B(W,{eyebrow:`Firmware`,title:`시스템 펌웨어`,description:`공유기 운영체제의 새 버전을 확인하고 설치합니다.`})}),B(wi,{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}),B(`div`,{class:`pt-2`,children:B(W,{eyebrow:`Software`,title:`관리 소프트웨어`,description:`SmartSafeHub 관리 화면 및 서비스의 업데이트를 확인합니다.`})}),B(Vi,{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 Ui={"#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 Wi(){return Ui[window.location.hash]??`home`}function Gi(){let[e,t]=R(Wi);return z(()=>{let e=()=>t(Wi());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),e}function Ki(e){return e===`#lan`||e===`#network/lan`?`lan`:`internet`}function qi(e){return e===`#settings/time`?`time`:e===`#settings/management`?`management`:`diagnostics`}function Ji(){let e=window.location.hash;return{network:Ki(e),settings:qi(e)}}function Yi(){let[e,t]=R(Ji);return z(()=>{let e=()=>t(Ji());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),e}function Xi({active:e,label:t,tabs:n}){return B(`nav`,{"aria-label":t,class:`mb-5 min-w-0`,role:`tablist`,children:B(`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 B(`a`,{"aria-controls":`${t.id}-panel`,"aria-selected":r,class:`flex min-h-11 min-w-0 items-center justify-center rounded-xl border px-2 py-2.5 text-center text-[11px] font-extrabold leading-5 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-500 sm:px-4 sm:text-sm ${r?`border-teal-200 bg-teal-50 text-teal-800`:`border-slate-200 bg-white text-slate-600 hover:border-teal-200 hover:text-teal-700`}`,href:t.href,id:`${t.id}-tab`,role:`tab`,tabIndex:r?0:-1,onKeyDown:e=>{if(![`ArrowLeft`,`ArrowRight`,`Home`,`End`].includes(e.key))return;e.preventDefault();let r=n.findIndex(e=>e.id===t.id),i=e.key===`Home`?0:e.key===`End`?n.length-1:(r+(e.key===`ArrowRight`?1:-1)+n.length)%n.length,a=e.currentTarget.parentElement?.children[i];a?.focus(),a?.click()},children:t.label},t.id)})})})}var Zi=[{value:`all`,label:`전체`},{value:`online`,label:`현재 연결`},{value:`wifi`,label:`Wi-Fi`},{value:`ethernet`,label:`유선/기타`},{value:`offline`,label:`최근 기기`}],Qi=new Intl.Collator(`ko`),$i=new Intl.DateTimeFormat(`ko-KR`,{month:`numeric`,day:`numeric`,hour:`2-digit`,minute:`2-digit`});function ea(e){switch(e){case`wifi`:return`Wi-Fi`;case`ethernet`:return`유선/기타`;default:return`연결 방식 미확인`}}function ta(e){return e.hostname??e.ipv4Address??`이름 없는 기기`}function na(e){return e===null?null:e>=-50?`매우 좋음`:e>=-60?`좋음`:e>=-70?`보통`:`약함`}function ra(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 ia(e){return e.leaseActive?e.leaseExpiresAt===null?`DHCP 임대 활성`:`DHCP ${$i.format(new Date(e.leaseExpiresAt*1e3))}까지`:null}function aa(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 oa({device:e}){return B(`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:[B(`span`,{class:`size-2 rounded-full ${e.online?`bg-emerald-500`:`bg-slate-400`}`}),e.online?`현재 연결`:`최근 임대`]})}function sa({device:e}){let t=na(e.signalDbm),n=ra(e.connectedSeconds),r=ia(e);return B(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:[B(`div`,{class:`flex min-w-0 flex-col items-start gap-4 sm:flex-row sm:justify-between`,children:[B(`div`,{class:`flex min-w-0 items-start gap-3`,children:[B(`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`?B(nt,{class:`size-6`}):e.connection===`ethernet`?B(it,{class:`size-6`}):B(Ke,{class:`size-6`})}),B(`div`,{class:`min-w-0`,children:[B(`h3`,{class:`m-0 break-words text-base font-extrabold text-slate-950`,children:ta(e)}),B(`p`,{class:`mt-1 mb-0 break-all font-mono text-xs leading-5 text-slate-500`,children:e.mac})]})]}),B(`span`,{class:`shrink-0 self-start`,children:B(oa,{device:e})})]}),B(`dl`,{class:`mt-5 grid gap-3 text-sm sm:grid-cols-2`,children:[B(`div`,{class:`rounded-xl bg-slate-50 p-3.5`,children:[B(`dt`,{class:`text-xs font-bold text-slate-500`,children:`IP 주소`}),B(`dd`,{class:`mt-1.5 mb-0 ml-0 font-extrabold text-slate-900`,children:e.ipv4Address??`확인되지 않음`})]}),B(`div`,{class:`rounded-xl bg-slate-50 p-3.5`,children:[B(`dt`,{class:`text-xs font-bold text-slate-500`,children:`연결 방식`}),B(`dd`,{class:`mt-1.5 mb-0 ml-0 font-extrabold text-slate-900`,children:ea(e.connection)})]})]}),B(`div`,{class:`mt-4 flex flex-wrap gap-2 text-xs font-bold text-slate-600`,children:[e.ssid?B(`span`,{class:`rounded-full bg-teal-50 px-3 py-1.5 text-teal-800`,children:[e.ssid,e.bandLabel?` · ${e.bandLabel}`:``]}):null,t?B(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:[`신호 `,t,` · `,e.signalDbm,` dBm`]}):null,n?B(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:[`연결 `,n]}):null,r?B(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:r}):null,e.interface?B(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:e.interface}):null]})]})}function ca({data:e,error:t,loading:n,onRetry:r}){let[i,a]=R(``),[o,s]=R(`all`),c=Te(()=>{if(!e)return[];let t=i.trim().toLocaleLowerCase();return[...e.devices].filter(e=>aa(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?Qi.compare(ta(e),ta(t)):e.connection===`wifi`?-1:1:e.online?-1:1)},[e,o,i]);return n?B(Bn,{}):t?B(Vn,{message:t,onRetry:r}):e?B(b,{children:[B(W,{eyebrow:`Overview`,title:`연결 기기 현황`,description:`네트워크에 연결되었거나 최근 확인된 기기를 요약합니다.`}),B(`section`,{"aria-label":`연결 기기 요약`,class:`grid gap-4 sm:grid-cols-2 xl:grid-cols-4`,children:[B(la,{description:`${e.totals.offline}대는 최근 DHCP 임대에서 확인`,icon:B(Ke,{class:`size-6`}),label:`확인된 기기`,value:e.totals.known}),B(la,{description:`Wi-Fi 또는 ARP에서 현재 확인됨`,icon:B(st,{class:`size-6`}),label:`현재 연결`,value:e.totals.online}),B(la,{description:`무선 AP에 연결된 기기`,icon:B(nt,{class:`size-6`}),label:`Wi-Fi`,value:e.totals.wireless}),B(la,{description:`LAN ARP에서 확인된 비무선 기기`,icon:B(it,{class:`size-6`}),label:`유선/기타`,value:e.totals.ethernet})]}),B(`section`,{class:`mt-7`,"aria-label":`기기 목록 및 필터`,children:[B(W,{eyebrow:`Device inventory`,title:`기기 목록`,description:`연결된 기기를 검색하고 무선·유선 상태별로 확인합니다.`}),B(`div`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:B(`div`,{class:`flex min-w-0 flex-col gap-3 lg:flex-row lg:items-center lg:justify-between`,children:[B(`div`,{class:`ssh-touch-scroll flex min-w-0 gap-2 overflow-x-auto pb-1 lg:flex-1`,role:`group`,"aria-label":`기기 필터`,children:Zi.map(e=>B(`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))}),B(`label`,{class:`relative block w-full lg:w-72 lg:shrink-0 xl:w-80`,children:[B(`span`,{class:`sr-only`,children:`기기 검색`}),B(`span`,{class:`pointer-events-none absolute inset-y-0 left-0 flex w-11 items-center justify-center text-slate-400`,children:B(bt,{class:`size-5`})}),B(`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?B(`section`,{"aria-live":`polite`,class:`mt-5 grid gap-4 lg:grid-cols-2`,children:c.map(e=>B(sa,{device:e},e.id))}):B(`section`,{class:`mt-5 rounded-2xl border border-dashed border-slate-300 bg-white p-5 text-center sm:p-8`,children:[B(Ke,{class:`mx-auto size-10 text-slate-400`}),B(`h2`,{class:`mt-4 mb-0 text-lg font-extrabold text-slate-950`,children:`조건에 맞는 기기가 없습니다`}),B(`p`,{class:`mt-2 mb-0 text-sm text-slate-600`,children:`검색어 또는 필터를 변경해 주세요.`})]}),B(`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 la({description:e,icon:t,label:n,value:r}){return B(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:[B(`div`,{class:`flex items-start justify-between gap-4`,children:[B(`div`,{children:[B(`p`,{class:`m-0 text-xs font-bold uppercase tracking-[0.14em] text-slate-500`,children:n}),B(`p`,{class:`mt-3 mb-0 text-3xl font-black tracking-tight text-slate-950`,children:r})]}),B(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:t})]}),B(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:e})]})}function ua(e){return e+.5|0}var da=(e,t,n)=>Math.max(Math.min(e,n),t);function fa(e){return da(ua(e*2.55),0,255)}function pa(e){return da(ua(e*255),0,255)}function ma(e){return da(ua(e/2.55)/100,0,1)}function ha(e){return da(ua(e*100),0,100)}var ga={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},_a=[...`0123456789ABCDEF`],va=e=>_a[e&15],ya=e=>_a[(e&240)>>4]+_a[e&15],ba=e=>(e&240)>>4==(e&15),xa=e=>ba(e.r)&&ba(e.g)&&ba(e.b)&&ba(e.a);function Sa(e){var t=e.length,n;return e[0]===`#`&&(t===4||t===5?n={r:255&ga[e[1]]*17,g:255&ga[e[2]]*17,b:255&ga[e[3]]*17,a:t===5?ga[e[4]]*17:255}:(t===7||t===9)&&(n={r:ga[e[1]]<<4|ga[e[2]],g:ga[e[3]]<<4|ga[e[4]],b:ga[e[5]]<<4|ga[e[6]],a:t===9?ga[e[7]]<<4|ga[e[8]]:255})),n}var Ca=(e,t)=>e<255?t(e):``;function wa(e){var t=xa(e)?va:ya;return e?`#`+t(e.r)+t(e.g)+t(e.b)+Ca(e.a,t):void 0}var Ta=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Ea(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 Da(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 Oa(e,t,n){let r=Ea(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 ka(e,t,n,r,i){return e===i?(t-n)/r+(t.5?l/(2-i-a):l/(i+a),s=ka(t,n,r,l,i),s=s*60+.5),[s|0,c||0,o]}function ja(e,t,n,r){return(Array.isArray(t)?e(t[0],t[1],t[2]):e(t,n,r)).map(pa)}function Ma(e,t,n){return ja(Ea,e,t,n)}function Na(e,t,n){return ja(Oa,e,t,n)}function Pa(e,t,n){return ja(Da,e,t,n)}function Fa(e){return(e%360+360)%360}function Ia(e){let t=Ta.exec(e),n=255,r;if(!t)return;t[5]!==r&&(n=t[6]?fa(+t[5]):pa(+t[5]));let i=Fa(+t[2]),a=t[3]/100,o=t[4]/100;return r=t[1]===`hwb`?Na(i,a,o):t[1]===`hsv`?Pa(i,a,o):Ma(i,a,o),{r:r[0],g:r[1],b:r[2],a:n}}function La(e,t){var n=Aa(e);n[0]=Fa(n[0]+t),n=Ma(n),e.r=n[0],e.g=n[1],e.b=n[2]}function Ra(e){if(!e)return;let t=Aa(e),n=t[0],r=ha(t[1]),i=ha(t[2]);return e.a<255?`hsla(${n}, ${r}%, ${i}%, ${ma(e.a)})`:`hsl(${n}, ${r}%, ${i}%)`}var za={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`},Ba={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 Va(){let e={},t=Object.keys(Ba),n=Object.keys(za),r,i,a,o,s;for(r=0;r>16&255,a>>8&255,a&255]}return e}var Ha;function Ua(e){Ha||(Ha=Va(),Ha.transparent=[0,0,0,0]);let t=Ha[e.toLowerCase()];return t&&{r:t[0],g:t[1],b:t[2],a:t.length===4?t[3]:255}}var Wa=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;function Ga(e){let t=Wa.exec(e),n=255,r,i,a;if(t){if(t[7]!==r){let e=+t[7];n=t[8]?fa(e):da(e*255,0,255)}return r=+t[1],i=+t[3],a=+t[5],r=255&(t[2]?fa(r):da(r,0,255)),i=255&(t[4]?fa(i):da(i,0,255)),a=255&(t[6]?fa(a):da(a,0,255)),{r,g:i,b:a,a:n}}}function Ka(e){return e&&(e.a<255?`rgba(${e.r}, ${e.g}, ${e.b}, ${ma(e.a)})`:`rgb(${e.r}, ${e.g}, ${e.b})`)}var qa=e=>e<=.0031308?e*12.92:e**(1/2.4)*1.055-.055,Ja=e=>e<=.04045?e/12.92:((e+.055)/1.055)**2.4;function Ya(e,t,n){let r=Ja(ma(e.r)),i=Ja(ma(e.g)),a=Ja(ma(e.b));return{r:pa(qa(r+n*(Ja(ma(t.r))-r))),g:pa(qa(i+n*(Ja(ma(t.g))-i))),b:pa(qa(a+n*(Ja(ma(t.b))-a))),a:e.a+n*(t.a-e.a)}}function Xa(e,t,n){if(e){let r=Aa(e);r[t]=Math.max(0,Math.min(r[t]+r[t]*n,t===0?360:1)),r=Ma(r),e.r=r[0],e.g=r[1],e.b=r[2]}}function Za(e,t){return e&&Object.assign(t||{},e)}function Qa(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=pa(e[3]))):(t=Za(e,{r:0,g:0,b:0,a:1}),t.a=pa(t.a)),t}function $a(e){return e.charAt(0)===`r`?Ga(e):Ia(e)}var eo=class e{constructor(t){if(t instanceof e)return t;let n=typeof t,r;n===`object`?r=Qa(t):n===`string`&&(r=Sa(t)||Ua(t)||$a(t)),this._rgb=r,this._valid=!!r}get valid(){return this._valid}get rgb(){var e=Za(this._rgb);return e&&(e.a=ma(e.a)),e}set rgb(e){this._rgb=Qa(e)}rgbString(){return this._valid?Ka(this._rgb):void 0}hexString(){return this._valid?wa(this._rgb):void 0}hslString(){return this._valid?Ra(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=Ya(this._rgb,e._rgb,t)),this}clone(){return new e(this.rgb)}alpha(e){return this._rgb.a=pa(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=ua(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 Xa(this._rgb,2,e),this}darken(e){return Xa(this._rgb,2,-e),this}saturate(e){return Xa(this._rgb,1,e),this}desaturate(e){return Xa(this._rgb,1,-e),this}rotate(e){return La(this._rgb,e),this}};function to(){}var no=(()=>{let e=0;return()=>e++})();function K(e){return e==null}function q(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 J(e){return e!==null&&Object.prototype.toString.call(e)===`[object Object]`}function ro(e){return(typeof e==`number`||e instanceof Number)&&isFinite(+e)}function io(e,t){return ro(e)?e:t}function Y(e,t){return e===void 0?t:e}var ao=(e,t)=>typeof e==`string`&&e.endsWith(`%`)?parseFloat(e)/100:+e/t,oo=(e,t)=>typeof e==`string`&&e.endsWith(`%`)?parseFloat(e)/100*t:+e;function so(e,t,n){if(e&&typeof e.call==`function`)return e.apply(n,t)}function X(e,t,n,r){let i,a,o;if(q(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 _o(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 vo(e){let t=_o(e);return e=>{for(let n of t){if(n===``)break;e=e&&e[n]}return e}}function yo(e,t){return(go[t]||(go[t]=vo(t)))(e)}function bo(e){return e.charAt(0).toUpperCase()+e.slice(1)}var xo=e=>e!==void 0,So=e=>typeof e==`function`,Co=(e,t)=>{if(e.size!==t.size)return!1;for(let n of e)if(!t.has(n))return!1;return!0};function wo(e){return e.type===`mouseup`||e.type===`click`||e.type===`contextmenu`}var Z=Math.PI,Q=2*Z,To=Q+Z,Eo=1/0,Do=Z/180,Oo=Z/2,ko=Z/4,Ao=Z*2/3,jo=Math.log10,Mo=Math.sign;function No(e,t,n){return Math.abs(e-t)e-t).pop(),t}function Io(e){return typeof e==`symbol`||typeof e==`object`&&!!e&&!(Symbol.toPrimitive in e||`toString`in e||`valueOf`in e)}function Lo(e){return!Io(e)&&!isNaN(parseFloat(e))&&isFinite(e)}function Ro(e,t){let n=Math.round(e);return n-t<=e&&n+t>=e}function zo(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 Zo(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 Qo=(e,t,n,r)=>Zo(e,n,r?r=>{let i=e[r][t];return ie[r][t]Zo(e,n,r=>e[r][t]>=n);function es(e,t,n){let r=0,i=e.length;for(;rr&&e[i-1]>n;)i--;return r>0||i{let n=`_onData`+bo(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 rs(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)&&(ts.forEach(t=>{delete e[t]}),delete e._chartjs)}function is(e){let t=new Set(e);return t.size===e.length?e:Array.from(t)}var as=function(){return typeof window>`u`?function(e){return e()}:window.requestAnimationFrame}();function os(e,t){let n=[],r=!1;return function(...i){n=i,r||(r=!0,as.call(window,()=>{r=!1,e.apply(t,n)}))}}function ss(e,t){let n;return function(...r){return t?(clearTimeout(n),n=setTimeout(e,t,r)):e.apply(this,r),t}}var cs=e=>e===`start`?`left`:e===`end`?`right`:`center`,ls=(e,t,n)=>e===`start`?t:e===`end`?n:(t+n)/2;function us(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(Qo(c,u,d).lo,n?r:Qo(t,u,o.getPixelForValue(d)).lo),l){let e=c.slice(0,i+1).reverse().findIndex(e=>!K(e[s.axis]));i-=Math.max(0,e)}i=Jo(i,0,r-1)}if(m){let e=Math.max(Qo(c,o.axis,f,!0).hi+1,n?0:Qo(t,u,o.getPixelForValue(f),!0).hi+1);if(l){let t=c.slice(e-1).findIndex(e=>!K(e[s.axis]));e+=Math.max(0,t)}a=Jo(e,i,r)-i}else a=r-i}return{start:i,count:a}}function ds(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 fs=e=>e===0||e===1,ps=(e,t,n)=>-(2**(10*--e)*Math.sin((e-t)*Q/n)),ms=(e,t,n)=>2**(-10*e)*Math.sin((e-t)*Q/n)+1,hs={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*Oo)+1,easeOutSine:e=>Math.sin(e*Oo),easeInOutSine:e=>-.5*(Math.cos(Z*e)-1),easeInExpo:e=>e===0?0:2**(10*(e-1)),easeOutExpo:e=>e===1?1:-(2**(-10*e))+1,easeInOutExpo:e=>fs(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=>fs(e)?e:ps(e,.075,.3),easeOutElastic:e=>fs(e)?e:ms(e,.075,.3),easeInOutElastic(e){let t=.1125,n=.45;return fs(e)?e:e<.5?.5*ps(e*2,t,n):.5+.5*ms(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-hs.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?hs.easeInBounce(e*2)*.5:hs.easeOutBounce(e*2-1)*.5+.5};function gs(e){if(e&&typeof e==`object`){let t=e.toString();return t===`[object CanvasPattern]`||t===`[object CanvasGradient]`}return!1}function _s(e){return gs(e)?e:new eo(e)}function vs(e){return gs(e)?e:new eo(e).saturate(.5).darken(.1).hexString()}var ys=[`x`,`y`,`borderWidth`,`radius`,`tension`],bs=[`color`,`borderColor`,`backgroundColor`];function xs(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:bs},numbers:{type:`number`,properties:ys}}),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 Ss(e){e.set(`layout`,{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})}var Cs=new Map;function ws(e,t){t=t||{};let n=e+JSON.stringify(t),r=Cs.get(n);return r||(r=new Intl.NumberFormat(e,t),Cs.set(n,r)),r}function Ts(e,t,n){return ws(t,n).format(e)}var Es={values(e){return q(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=Ds(e,n)}let o=jo(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),Ts(e,r,c)},logarithmic(e,t,n){if(e===0)return`0`;let r=n[t].significand||e/10**Math.floor(jo(e));return[1,2,3,5,10,15].includes(r)||t>.8*n.length?Es.numeric.call(this,e,t,n):``}};function Ds(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 Os={formatters:Es};function ks(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:Os.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 As=Object.create(null),js=Object.create(null);function Ms(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)=>vs(t.backgroundColor),this.hoverBorderColor=(e,t)=>vs(t.borderColor),this.hoverColor=(e,t)=>vs(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 Ns(this,e,t)}get(e){return Ms(this,e)}describe(e,t){return Ns(js,e,t)}override(e,t){return Ns(As,e,t)}route(e,t,n,r){let i=Ms(this,e),a=Ms(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 J(e)?Object.assign({},t,e):Y(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}},[xs,Ss,ks]);function Fs(e){return!e||K(e.size)||K(e.family)?null:(e.style?e.style+` `:``)+(e.weight?e.weight+` `:``)+e.size+`px `+e.family}function Is(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 Ls(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 Hs(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,qs(e,a),c=0;c+e||0;function nc(e,t){let n={},r=J(t),i=r?Object.keys(t):t,a=J(e)?r?n=>Y(e[n],e[t[n]]):t=>e[t]:()=>e;for(let e of i)n[e]=tc(a(e));return n}function rc(e){return nc(e,{top:`y`,right:`x`,bottom:`y`,left:`x`})}function ic(e){return nc(e,[`topLeft`,`topRight`,`bottomLeft`,`bottomRight`])}function ac(e){let t=rc(e);return t.width=t.left+t.right,t.height=t.top+t.bottom,t}function oc(e,t){e=e||{},t=t||Ps.font;let n=Y(e.size,t.size);typeof n==`string`&&(n=parseInt(n,10));let r=Y(e.style,t.style);r&&!(``+r).match($s)&&(console.warn(`Invalid font style specified: "`+r+`"`),r=void 0);let i={family:Y(e.family,t.family),lineHeight:ec(Y(e.lineHeight,t.lineHeight),n),size:n,style:r,weight:Y(e.weight,t.weight),string:``};return i.string=Fs(i),i}function sc(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 lc(e,t){return Object.assign(Object.create(e),t)}function uc(e,t=[``],n,r,i=()=>e[0]){let a=n||e;return r===void 0&&(r=Ec(`_fallback`,e)),new Proxy({[Symbol.toStringTag]:`Object`,_cacheable:!0,_scopes:e,_rootScopes:a,_fallback:r,_getTarget:i,override:n=>uc([n,...e],t,a,r)},{deleteProperty(t,n){return delete t[n],delete t._keys,delete e[0][n],!0},get(n,r){return hc(n,r,()=>Tc(r,t,e,n))},getOwnPropertyDescriptor(e,t){return Reflect.getOwnPropertyDescriptor(e._scopes[0],t)},getPrototypeOf(){return Reflect.getPrototypeOf(e[0])},has(e,t){return Dc(e).includes(t)},ownKeys(e){return Dc(e)},set(e,t,n){let r=e._storage||(e._storage=i());return e[t]=r[t]=n,delete e._keys,!0}})}function dc(e,t,n,r){let i={_cacheable:!1,_proxy:e,_context:t,_subProxy:n,_stack:new Set,_descriptors:fc(e,r),setContext:t=>dc(e,t,n,r),override:i=>dc(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 hc(e,t,()=>gc(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 fc(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:So(n)?n:()=>n,isIndexable:So(r)?r:()=>r}}var pc=(e,t)=>e?e+bo(t):t,mc=(e,t)=>J(t)&&e!==`adapters`&&(Object.getPrototypeOf(t)===null||t.constructor===Object);function hc(e,t,n){if(Object.prototype.hasOwnProperty.call(e,t)||t===`constructor`)return e[t];let r=n();return e[t]=r,r}function gc(e,t,n){let{_proxy:r,_context:i,_subProxy:a,_descriptors:o}=e,s=r[t];return So(s)&&o.isScriptable(t)&&(s=_c(t,s,e,n)),q(s)&&s.length&&(s=vc(t,s,e,o.isIndexable)),mc(t,s)&&(s=dc(s,i,a&&a[t],o)),s}function _c(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),mc(e,c)&&(c=Sc(i._scopes,i,e,c)),c}function vc(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(J(t[0])){let n=t,r=i._scopes.filter(e=>e!==n);t=[];for(let c of n){let n=Sc(r,i,e,c);t.push(dc(n,a,o&&o[e],s))}}return t}function yc(e,t,n){return So(e)?e(t,n):e}var bc=(e,t)=>e===!0?t:typeof e==`string`?yo(t,e):void 0;function xc(e,t,n,r,i){for(let a of t){let t=bc(n,a);if(t){e.add(t);let a=yc(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 Sc(e,t,n,r){let i=t._rootScopes,a=yc(t._fallback,n,r),o=[...e,...i],s=new Set;s.add(r);let c=Cc(s,o,n,a||n,r);return c===null||a!==void 0&&a!==n&&(c=Cc(s,o,a,c,r),c===null)?!1:uc(Array.from(s),[``],i,a,()=>wc(t,n,r))}function Cc(e,t,n,r,i){for(;n;)n=xc(e,t,n,r,i);return n}function wc(e,t,n){let r=e._getTarget();t in r||(r[t]={});let i=r[t];return q(i)&&J(n)?n:i||{}}function Tc(e,t,n,r){let i;for(let a of t)if(i=Ec(pc(a,e),n),i!==void 0)return mc(e,i)?Sc(n,r,e,i):i}function Ec(e,t){for(let n of t){if(!n)continue;let t=n[e];if(t!==void 0)return t}}function Dc(e){let t=e._keys;return t||(t=e._keys=Oc(e._scopes)),t}function Oc(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 kc(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 Nc(e,t,n,r){let i=e.skip?t:e,a=t,o=n.skip?t:n,s=Wo(a,i),c=Wo(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 Pc(e,t,n){let r=e.length,i,a,o,s,c,l=jc(e,0);for(let u=0;u!e.skip)),t.cubicInterpolationMode===`monotone`)Ic(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 Wc(e,t){return Uc(e).getPropertyValue(t)}var Gc=[`top`,`right`,`bottom`,`left`];function Kc(e,t,n){let r={};n=n?`-`+n:``;for(let i=0;i<4;i++){let a=Gc[i];r[a]=parseFloat(e[t+`-`+a+n])||0}return r.width=r.left+r.right,r.height=r.top+r.bottom,r}var qc=(e,t,n)=>(e>0||t>0)&&(!n||!n.shadowRoot);function Jc(e,t){let n=e.touches,r=n&&n.length?n[0]:e,{offsetX:i,offsetY:a}=r,o=!1,s,c;if(qc(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 Yc(e,t){if(`native`in e)return e;let{canvas:n,currentDevicePixelRatio:r}=t,i=Uc(n),a=i.boxSizing===`border-box`,o=Kc(i,`padding`),s=Kc(i,`border`,`width`),{x:c,y:l,box:u}=Jc(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 Xc(e,t,n){let r,i;if(t===void 0||n===void 0){let a=e&&Vc(e);if(!a)t=e.clientWidth,n=e.clientHeight;else{let e=a.getBoundingClientRect(),o=Uc(a),s=Kc(o,`border`,`width`),c=Kc(o,`padding`);t=e.width-c.width-s.width,n=e.height-c.height-s.height,r=Hc(o.maxWidth,a,`clientWidth`),i=Hc(o.maxHeight,a,`clientHeight`)}}return{width:t,height:n,maxWidth:r||Eo,maxHeight:i||Eo}}var Zc=e=>Math.round(e*10)/10;function Qc(e,t,n,r){let i=Uc(e),a=Kc(i,`margin`),o=Hc(i.maxWidth,e,`clientWidth`)||Eo,s=Hc(i.maxHeight,e,`clientHeight`)||Eo,c=Xc(e,t,n),{width:l,height:u}=c;if(i.boxSizing===`content-box`){let e=Kc(i,`border`,`width`),t=Kc(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=Zc(Math.min(l,o,c.maxWidth)),u=Zc(Math.min(u,s,c.maxHeight)),l&&!u&&(u=Zc(l/2)),(t!==void 0||n!==void 0)&&r&&c.height&&u>c.height&&(u=c.height,l=Zc(Math.floor(u*r))),{width:l,height:u}}function $c(e,t,n){let r=t||1,i=Zc(e.height*r),a=Zc(e.width*r);e.height=Zc(e.height),e.width=Zc(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 el=function(){let e=!1;try{let t={get passive(){return e=!0,!1}};Bc()&&(window.addEventListener(`test`,null,t),window.removeEventListener(`test`,null,t))}catch{}return e}();function tl(e,t){let n=Wc(e,t),r=n&&n.match(/^(\d+)(\.\d+)?px$/);return r?+r[1]:void 0}function nl(e,t,n,r){return{x:e.x+n*(t.x-e.x),y:e.y+n*(t.y-e.y)}}function rl(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 il(e,t,n,r){let i={x:e.cp2x,y:e.cp2y},a={x:t.cp1x,y:t.cp1y},o=nl(e,i,n),s=nl(i,a,n),c=nl(a,t,n);return nl(nl(o,s,n),nl(s,c,n),n)}var al=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}}},ol=function(){return{x(e){return e},setWidth(e){},textAlign(e){return e},xPlus(e,t){return e+t},leftForLtr(e,t){return e}}};function sl(e,t,n){return e?al(t,n):ol()}function cl(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 ll(e,t){t!==void 0&&(delete e.prevTextDirection,e.canvas.style.setProperty(`direction`,t[0],t[1]))}function ul(e){return e===`angle`?{between:qo,compare:Go,normalize:Ko}:{between:Xo,compare:(e,t)=>e-t,normalize:e=>e}}function dl({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 fl(e,t,n){let{property:r,start:i,end:a}=n,{between:o,normalize:s}=ul(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(dl({start:g,end:e,loop:f,count:o,style:p})),g=null),n=e,y=_));return g!==null&&m.push(dl({start:g,end:d,loop:f,count:o,style:p})),m}function ml(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 gl(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 _l(e,t){let n=e.points,r=e.options.spanGaps,i=n.length;if(!i)return[];let a=!!e._loop,{start:o,end:s}=hl(n,i,a,r);return r===!0?vl(e,[{start:o,end:s,loop:a}],n,t):vl(e,gl(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=as.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)}},kl=`transparent`,Al={boolean(e,t,n){return n>.5?t:e},color(e,t,n){let r=_s(e||kl),i=r.valid&&_s(t||kl);return i&&i.valid?i.mix(r,n).hexString():t},number(e,t,n){return e+(t-e)*n}},jl=class{constructor(e,t,n,r){let i=t[n];r=sc([e.to,r,i,e.from]);let a=sc([e.from,i,r]);this._active=!0,this._fn=e.fn||Al[e.type||typeof a],this._easing=hs[e.easing]||hs.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=sc([e.to,t,r,e.from]),this._from=sc([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(!J(i))return;let a={};for(let e of t)a[e]=i[e];(q(i.properties)&&i.properties||[r]).forEach(e=>{e!==r&&n.has(e)||n.set(e,a)})})}_animateOptions(e,t){let n=t.options,r=Pl(e,n);if(!r)return[];let i=this._createAnimations(r,n);return n.$shared&&Nl(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 jl(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 Ol.add(this._chart,n),!0}};function Nl(e,t){let n=[],r=Object.keys(t);for(let t=0;t0||!n&&t<0)return i.index}return null}function Kl(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=Hl(a,o,r),d=t.length,f;for(let e=0;en[e].axis===t).shift()}function Jl(e,t){return lc(e,{active:!1,dataset:void 0,datasetIndex:t,index:t,mode:`default`,type:`dataset`})}function Yl(e,t,n){return lc(e,{active:!1,dataIndex:t,parsed:void 0,raw:void 0,element:n,index:t,mode:`default`,type:`data`})}function Xl(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 Zl=e=>e===`reset`||e===`none`,Ql=(e,t)=>t?e:Object.assign({},e),$l=(e,t,n)=>e&&!t.hidden&&t._stacked&&{keys:Rl(n,!0),values:null},eu=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=Vl(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&&Xl(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=Y(n.xAxisID,ql(e,`x`)),a=t.yAxisID=Y(n.yAxisID,ql(e,`y`)),o=t.rAxisID=Y(n.rAxisID,ql(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&&rs(this._data,this),e._stacked&&Xl(e)}_dataCheck(){let e=this.getDataset(),t=e.data||(e.data=[]),n=this._data;if(J(t)){let e=this._cachedMeta;this._data=Bl(t,e)}else if(n!==t){if(n){rs(n,this);let e=this._cachedMeta;Xl(e),e._parsed=[]}t&&Object.isExtensible(t)&&ns(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=Vl(t.vScale,t),t.stack!==n.stack&&(r=!0,Xl(t),t.stack=n.stack),this._resyncElements(e),(r||i!==t._stacked)&&(Kl(this,t._parsed),t._stacked=Vl(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=q(r[e])?this.parseArrayData(n,r,e,t):J(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(Ql(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 Ml(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||Zl(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){Zl(r)?Object.assign(e,n):this._resolveAnimations(t,r).update(e,n)}updateSharedOptions(e,t,n){e&&!Zl(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 nu(e){let t=e.iScale,n=tu(t,e.type),r=t._length,i,a,o,s,c=()=>{o!==32767&&o!==-32768&&(xo(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 ou(e,t,n,r){return q(e)?au(e,t,n,r):t[n.axis]=n.parse(e,r),t}function su(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):Mo(e)}function uu(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(K(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[Y(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;dqo(e,s,c,!0)?1:Math.max(t,t*n,r,r*n),m=(e,t,r)=>qo(e,s,c,!0)?-1:Math.min(t,t*n,r,r*n),h=p(0,l,d),g=p(Oo,u,f),_=m(Z,l,d),v=m(Z+Oo,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 yu=class extends eu{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(J(n[e])){let{key:e=`value`}=this._parsing;i=t=>+yo(n[t],e)}let a,o;for(a=e,o=e+t;a0&&!isNaN(e)?Math.abs(e)/t*Q:0}getLabelAndValue(e){let t=this._cachedMeta,n=this.chart,r=n.data.labels||[],i=Ts(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`)}),$(yu,`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 bu=class extends eu{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}=us(t,r,a);this._drawStart=o,this._drawCount=s,ds(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=Lo(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=K(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()}};$(bu,`id`,`line`),$(bu,`defaults`,{datasetElementType:`line`,dataElementType:`point`,showLine:!0,spanGaps:!1}),$(bu,`overrides`,{scales:{_index_:{type:`category`},_value_:{type:`linear`}}});var xu=class extends eu{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=Ts(t._parsed[e].r,n.options.locale);return{label:r[e]||``,value:i}}parseObjectData(e,t,n,r){return kc.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*Z,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)?Bo(this.resolveDataElementOptions(e,t).angle||n):0}};$(xu,`id`,`polarArea`),$(xu,`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`}),$(xu,`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 Su=class extends yu{};$(Su,`id`,`pie`),$(Su,`defaults`,{cutout:0,rotation:0,circumference:360,radius:`100%`});var Cu=class extends eu{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 kc.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}};$(wu,`id`,`scatter`),$(wu,`defaults`,{datasetElementType:!1,dataElementType:`point`,showLine:!1,fill:!1}),$(wu,`overrides`,{interaction:{mode:`point`},scales:{x:{type:`linear`},y:{type:`linear`}}});function Tu(){throw Error(`This method is not implemented: Check that a complete date adapter is provided.`)}var Eu={_date:class e{static override(t){Object.assign(e.prototype,t)}constructor(e){$(this,`options`,void 0),this.options=e||{}}init(){}formats(){return Tu()}parse(){return Tu()}format(){return Tu()}add(){return Tu()}diff(){return Tu()}startOf(){return Tu()}endOf(){return Tu()}}};function Du(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?$o:Qo;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=>!K(e[t.axis]));r.lo-=Math.max(0,a);let o=n.slice(r.hi).findIndex(e=>!K(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 Ou(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 Fu={evaluateInteractionItems:Ou,modes:{index(e,t,n,r){let i=Yc(t,e),a=n.axis||`x`,o=n.includeInvisible||!1,s=n.intersect?Au(e,i,a,r,o):Nu(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=Yc(t,e),a=n.axis||`xy`,o=n.includeInvisible||!1,s=n.intersect?Au(e,i,a,r,o):Nu(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 Ru(e,t){return e.filter(e=>Iu.indexOf(e.pos)===-1&&e.box.axis===t)}function zu(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 Bu(e){let t=[],n,r,i,a,o,s;for(n=0,r=(e||[]).length;ne.box.fullSize),!0),r=zu(Lu(t,`left`),!0),i=zu(Lu(t,`right`)),a=zu(Lu(t,`top`),!0),o=zu(Lu(t,`bottom`)),s=Ru(t,`x`),c=Ru(t,`y`);return{fullSize:n,leftAndTop:r.concat(a),rightAndBottom:i.concat(c).concat(o).concat(s),chartArea:Lu(t,`chartArea`),vertical:r.concat(i).concat(c),horizontal:a.concat(o).concat(s)}}function Wu(e,t,n,r){return Math.max(e[n],t[n])+Math.max(e[r],t[r])}function Gu(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 Ku(e,t,n,r){let{pos:i,box:a}=n,o=e.maxPadding;if(!J(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&&Gu(o,a.getPadding());let s=Math.max(0,t.outerWidth-Wu(o,e,`left`,`right`)),c=Math.max(0,t.outerHeight-Wu(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 qu(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 Ju(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 Yu(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);Gu(f,ac(r));let p=Object.assign({maxPadding:f,w:a,h:o,x:i.left,y:i.top},i),m=Hu(c.concat(l),d);Yu(s.fullSize,p,d,m),Yu(c,p,d,m),Yu(l,p,d,m)&&Yu(c,p,d,m),qu(p),Zu(s.leftAndTop,p,d,m),p.x+=p.w,p.y+=p.h,Zu(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},X(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})})}},$u=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){}},ed=class extends $u{acquireContext(e){return e&&e.getContext&&e.getContext(`2d`)||null}updateConfig(e){e.options.animation=!1}},td=`$chartjs`,nd={touchstart:`mousedown`,touchmove:`mousemove`,touchend:`mouseup`,pointerenter:`mouseenter`,pointerdown:`mousedown`,pointermove:`mousemove`,pointerup:`mouseup`,pointerleave:`mouseout`,pointerout:`mouseout`},rd=e=>e===null||e===``;function id(e,t){let n=e.style,r=e.getAttribute(`height`),i=e.getAttribute(`width`);if(e[td]={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`,rd(i)){let t=tl(e,`width`);t!==void 0&&(e.width=t)}if(rd(r)){if(e.style.height===``)e.height=e.width/(t||2);else{let t=tl(e,`height`);t!==void 0&&(e.height=t)}}return e}var ad=el?{passive:!0}:!1;function od(e,t,n){e&&e.addEventListener(t,n,ad)}function sd(e,t,n){e&&e.canvas&&e.canvas.removeEventListener(t,n,ad)}function cd(e,t){let n=nd[e.type]||e.type,{x:r,y:i}=Yc(e,t);return{type:n,chart:t,native:e,x:r===void 0?null:r,y:i===void 0?null:i}}function ld(e,t){for(let n of e)if(n===t||n.contains(t))return!0}function ud(e,t,n){let r=e.canvas,i=new MutationObserver(e=>{let t=!1;for(let n of e)t=t||ld(n.addedNodes,r),t=t&&!ld(n.removedNodes,r);t&&n()});return i.observe(document,{childList:!0,subtree:!0}),i}function dd(e,t,n){let r=e.canvas,i=new MutationObserver(e=>{let t=!1;for(let n of e)t=t||ld(n.removedNodes,r),t=t&&!ld(n.addedNodes,r);t&&n()});return i.observe(document,{childList:!0,subtree:!0}),i}var fd=new Map,pd=0;function md(){let e=window.devicePixelRatio;e!==pd&&(pd=e,fd.forEach((t,n)=>{n.currentDevicePixelRatio!==e&&t()}))}function hd(e,t){fd.size||window.addEventListener(`resize`,md),fd.set(e,t)}function gd(e){fd.delete(e),fd.size||window.removeEventListener(`resize`,md)}function _d(e,t,n){let r=e.canvas,i=r&&Vc(r);if(!i)return;let a=os((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),hd(e,a),o}function vd(e,t,n){n&&n.disconnect(),t===`resize`&&gd(e)}function yd(e,t,n){let r=e.canvas,i=os(t=>{e.ctx!==null&&n(cd(t,e))},e);return od(r,t,i),i}var bd=class extends $u{acquireContext(e,t){let n=e&&e.getContext&&e.getContext(`2d`);return n&&n.canvas===e?(id(e,t),n):null}releaseContext(e){let t=e.canvas;if(!t[td])return!1;let n=t[td].initial;[`height`,`width`].forEach(e=>{let r=n[e];K(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[td],!0}addEventListener(e,t,n){this.removeEventListener(e,t);let r=e.$proxies||(e.$proxies={});r[t]=({attach:ud,detach:dd,resize:_d}[t]||yd)(e,t,n)}removeEventListener(e,t){let n=e.$proxies||(e.$proxies={}),r=n[t];r&&(({attach:vd,detach:vd,resize:vd}[t]||sd)(e,t,r),n[t]=void 0)}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(e,t,n,r){return Qc(e,t,n,r)}isAttached(e){let t=e&&Vc(e);return!!(t&&t.isConnected)}};function xd(e){return!Bc()||typeof OffscreenCanvas<`u`&&e instanceof OffscreenCanvas?ed:bd}var Sd=class{constructor(){$(this,`x`,void 0),$(this,`y`,void 0),$(this,`active`,!1),$(this,`options`,void 0),$(this,`$animations`,void 0)}tooltipPosition(e){let{x:t,y:n}=this.getProps([`x`,`y`],e);return{x:t,y:n}}hasValue(){return Lo(this.x)&&Lo(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}};$(Sd,`defaults`,{}),$(Sd,`defaultRoutes`,void 0);function Cd(e,t){let n=e.options.ticks,r=wd(e),i=Math.min(n.maxTicksLimit||r,r),a=n.major.enabled?Ed(t):[],o=a.length,s=a[0],c=a[o-1],l=[];if(o>i)return Dd(t,l,a,o/i),l;let u=Td(a,t,i);if(o>0){let e,n,r=o>1?Math.round((c-s)/(o-1)):null;for(Od(t,l,u,K(r)?0:s-r,s),e=0,n=o-1;ei)return t}return Math.max(i,1)}function Ed(e){let t=[],n,r;for(n=0,r=e.length;ne===`left`?`right`:e===`right`?`left`:e,jd=(e,t,n)=>t===`top`||t===`left`?e[t]+n:e[t]-n,Md=(e,t)=>Math.min(t||e,e);function Nd(e,t){let n=[],r=e.length/t,i=e.length,a=0;for(;ao+s)))return c}function Fd(e,t){X(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:io(t,io(n,t)),max:io(n,io(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(){so(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=cc(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=Jo(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-Id(e.grid)-t.padding-Ld(e.title,this.chart.options.font),c=Math.sqrt(u*u+d*d),a=Vo(Math.min(Math.asin(Jo((l.highest.height+6)/o,-1,1)),Math.asin(Jo(s/c,-1,1))-Math.asin(Jo(d/c,-1,1)))),a=Math.max(r,Math.min(i,a))),this.labelRotation=a}afterCalculateLabelRotation(){so(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){so(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=Ld(r,t.options.font);if(o?(e.width=this.maxWidth,e.height=Id(i)+a):(e.height=this.maxHeight,e.width=Id(i)+a),n.display&&this.ticks.length){let{first:t,last:r,widest:i,highest:a}=this._getLabelSizes(),s=n.padding*2,c=Bo(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(){so(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 Yo(this._alignToPixels?Rs(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=Id(i),d=[],f=o.setContext(this.getContext()),p=f.display?f.width:0,m=p/2,h=function(e){return Rs(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(J(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(J(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=Y(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(-Bo(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(`.`);Ps.route(a,i,c,s)})}function Kd(e){return`id`in e&&`defaults`in e}var qd=new class{constructor(){this.controllers=new Ud(eu,`datasets`,!0),this.elements=new Ud(Sd,`elements`),this.plugins=new Ud(Object,`plugins`),this.scales=new Ud(Hd,`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):X(t,t=>{let r=n||this._getRegistryForType(t);this._exec(e,r,t)})})}_exec(e,t,n){let r=bo(e);so(n[`before`+r],[],n),t[e](n),so(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 Yd(e){let t={},n=[],r=Object.keys(qd.plugins.items);for(let e=0;e1&&nf(e[0].toLowerCase());if(t)return t}throw Error(`Cannot determine type of '${e}' axis. Please provide 'axis' or 'position' option.`)}function of(e,t,n){if(n[t+`AxisID`]===e)return{axis:t}}function sf(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 of(e,`x`,n[0])||of(e,`y`,n[0])}return{}}function cf(e,t){let n=As[e.type]||{scales:{}},r=t.scales||{},i=$d(e.type,t),a=Object.create(null);return Object.keys(r).forEach(t=>{let o=r[t];if(!J(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=af(t,o,sf(t,e),Ps.scales[o.type]),c=tf(s,i),l=n.scales||{};a[t]=mo(Object.create(null),[{axis:s},o,l[s],l[c]])}),e.data.datasets.forEach(n=>{let i=n.type||e.type,o=n.indexAxis||$d(i,t),s=(As[i]||{}).scales||{};Object.keys(s).forEach(e=>{let t=ef(e,o),i=n[t+`AxisID`]||t;a[i]=a[i]||Object.create(null),mo(a[i],[{axis:t},r[i],s[e]])})}),Object.keys(a).forEach(e=>{let t=a[e];mo(t,[Ps.scales[t.type],Ps.scale])}),a}function lf(e){let t=e.options||(e.options={});t.plugins=Y(t.plugins,{}),t.scales=cf(e,t)}function uf(e){return e=e||{},e.datasets=e.datasets||[],e.labels=e.labels||[],e}function df(e){return e=e||{},e.data=uf(e.data),lf(e),e}var ff=new Map,pf=new Set;function mf(e,t){let n=ff.get(e);return n||(n=t(),ff.set(e,n),pf.add(n)),n}var hf=(e,t,n)=>{let r=yo(t,n);r!==void 0&&e.add(r)},gf=class{constructor(e){this._config=df(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=uf(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(),lf(e)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(e){return mf(e,()=>[[`datasets.${e}`,``]])}datasetAnimationScopeKeys(e,t){return mf(`${e}.transition.${t}`,()=>[[`datasets.${e}.transitions.${t}`,`transitions.${t}`],[`datasets.${e}`,``]])}datasetElementScopeKeys(e,t){return mf(`${e}-${t}`,()=>[[`datasets.${e}.elements.${t}`,`datasets.${e}`,`elements.${t}`,``]])}pluginScopeKeys(e){let t=e.id,n=this.type;return mf(`${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=>hf(s,e,t))),t.forEach(e=>hf(s,r,e)),t.forEach(e=>hf(s,As[i]||{},e)),t.forEach(e=>hf(s,Ps,e)),t.forEach(e=>hf(s,js,e))});let c=Array.from(s);return c.length===0&&c.push(Object.create(null)),pf.has(t)&&a.set(t,c),c}chartOptionScopes(){let{options:e,type:t}=this;return[e,As[t]||{},Ps.datasets[t]||{},{type:t},Ps,js]}resolveNamedOptions(e,t,n,r=[``]){let i={$shared:!0},{resolver:a,subPrefixes:o}=_f(this._resolverCache,e,r),s=a;if(yf(a,t)){i.$shared=!1,n=So(n)?n():n;let t=this.createResolver(e,n,o);s=dc(a,n,t)}for(let e of t)i[e]=s[e];return i}createResolver(e,t,n=[``],r){let{resolver:i}=_f(this._resolverCache,e,n);return J(t)?dc(i,t,void 0,r):i}};function _f(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:uc(t,n),subPrefixes:n.filter(e=>!e.toLowerCase().includes(`hover`))},r.set(i,a)),a}var vf=e=>J(e)&&Object.getOwnPropertyNames(e).some(t=>So(e[t]));function yf(e,t){let{isScriptable:n,isIndexable:r}=fc(e);for(let i of t){let t=n(i),a=r(i),o=(a||t)&&e[i];if(t&&(So(o)||vf(o))||a&&q(o))return!0}return!1}var bf=`4.6.1`,xf=[`top`,`bottom`,`left`,`right`,`chartArea`];function Sf(e,t){return e===`top`||e===`bottom`||xf.indexOf(e)===-1&&t===`x`}function Cf(e,t){return function(n,r){return n[e]===r[e]?n[t]-r[t]:n[e]-r[e]}}function wf(e){let t=e.chart,n=t.options.animation;t.notifyPlugins(`afterRender`),so(n&&n.onComplete,[e],t)}function Tf(e){let t=e.chart,n=t.options.animation;so(n&&n.onProgress,[e],t)}function Ef(e){return Bc()&&typeof e==`string`?e=document.getElementById(e):e&&e.length&&(e=e[0]),e&&e.canvas&&(e=e.canvas),e}var Df={},Of=e=>{let t=Ef(e);return Object.values(Df).filter(e=>e.canvas===t).pop()};function kf(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 Af(e,t,n,r){return!n||e.type===`mouseout`?null:r?t:e}var jf=class{static register(...e){qd.add(...e),Mf()}static unregister(...e){qd.remove(...e),Mf()}constructor(e,t){let n=this.config=new gf(t),r=Ef(e),i=Of(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||(xd(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=no(),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 Jd,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=ss(e=>this.update(e),a.resizeDelay||0),this._dataChanges=[],Df[this.id]=this,!o||!s?console.error(`Failed to create chart: can't acquire context from the given item`):(Ol.listen(this,`complete`,wf),Ol.listen(this,`progress`,Tf),this._initialize(),this.attached&&this.update())}get aspectRatio(){let{options:{aspectRatio:e,maintainAspectRatio:t},width:n,height:r,_aspectRatio:i}=this;return K(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 qd}_initialize(){return this.notifyPlugins(`beforeInit`),this.options.responsive?this.resize():$c(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins(`afterInit`),this}clear(){return zs(this.canvas,this.ctx),this}stop(){return Ol.stop(this),this}resize(e,t){Ol.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,$c(this,o,!0)&&(this.notifyPlugins(`resize`,{size:a}),so(n.onResize,[this,a],this),this.attached&&this._doResize(s)&&this.render())}ensureScalesHaveIDs(){X(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=af(e,n),i=r===`r`,a=r===`x`;return{options:n,dposition:i?`chartArea`:a?`bottom`:`left`,dtype:i?`radialLinear`:a?`category`:`linear`}}))),X(i,t=>{let i=t.options,a=i.id,o=af(a,i),s=Y(i.type,t.dtype);(i.position===void 0||Sf(i.position,o)!==Sf(t.dposition))&&(i.position=t.dposition),r[a]=!0;let c=null;a in n&&n[a].type===s?c=n[a]:(c=new(qd.getScale(s))({id:a,type:s,ctx:this.ctx,chart:this}),n[c.id]=c),c.init(i,e)}),X(r,(e,t)=>{e||delete n[t]}),X(n,e=>{Qu.configure(this,e,e.options),Qu.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(Cf(`z`,`_idx`));let{_active:o,_lastEvent:s}=this;s?this._eventHandler(s,!0):o.length&&this._updateHoverStyles(o,o,!0),this.render()}_updateScales(){X(this.scales,e=>{Qu.removeBox(this,e)}),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){let e=this.options;Co(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)kf(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;Qu.update(this,this.width,this.height,e);let t=this.chartArea,n=t.width<=0||t.height<=0;this._layers=[],X(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=wl(this,e);this.notifyPlugins(`beforeDatasetDraw`,n)!==!1&&(r&&Us(t,r),e.controller.draw(),r&&Ws(t),n.cancelable=!1,this.notifyPlugins(`afterDatasetDraw`,n))}isPointInArea(e){return Hs(e,this.chartArea,this._minPadding)}getElementsAtEventForMode(e,t,n,r){let i=Fu.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=lc(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);xo(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(),Ol.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)};X(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(){X(this._listeners,(e,t)=>{this.platform.removeEventListener(this,t,e)}),this._listeners={},X(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}});co(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=wo(e),c=Af(e,this._lastEvent,n,s);n&&(this._lastEvent=null,so(i.onHover,[e,o,this],this),s&&so(i.onClick,[e,o,this],this));let l=!co(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)}};$(jf,`defaults`,Ps),$(jf,`instances`,Df),$(jf,`overrides`,As),$(jf,`registry`,qd),$(jf,`version`,bf),$(jf,`getChart`,Of);function Mf(){return X(jf.instances,e=>e._plugins.invalidate())}function Nf(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,Ko(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,Ko(r-n));e.arc(i,a,s+l/2,n-t/2,r+t/2,!0)}else{let t=Math.min(l/2,o*Ko(r-n));if(u===`round`)e.arc(i,a,t,n-Z/2,r+Z/2,!0);else if(u===`bevel`){let o=2*t*t,s=-o*Math.cos(n+Z/2)+i,c=-o*Math.sin(n+Z/2)+a,l=o*Math.cos(r+Z/2)+i,u=o*Math.sin(r+Z/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 Pf(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+Oo,r-Oo),e.closePath(),e.clip()}function Ff(e){return nc(e,[`outerStart`,`outerEnd`,`innerStart`,`innerEnd`])}function If(e,t,n,r){let i=Ff(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 Jo(e,0,Math.min(a,t))};return{outerStart:s(i.outerStart),outerEnd:s(i.outerEnd),innerStart:Jo(i.innerStart,0,o),innerEnd:Jo(i.innerEnd,0,o)}}function Lf(e,t,n,r){return{x:n+e*Math.cos(t),y:r+e*Math.sin(t)}}function Rf(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+Q),u>0&&(e.moveTo(i+Math.cos(o)*u,a+Math.sin(o)*u),e.arc(i,a,u,o+Q,o,!0)),e.closePath()}function zf(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)>=Q-1e-4){Rf(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/Z)/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}=If(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=Lf(k,j,o,s);e.arc(t.x,t.y,T,j,b+Oo)}let n=Lf(N,S,o,s);if(e.lineTo(n.x,n.y),D>0){let t=Lf(N,F,o,s);e.arc(t.x,t.y,D,S+Oo,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=Lf(M,P,o,s);e.arc(t.x,t.y,E,P+Math.PI,x-Oo)}let i=Lf(O,y,o,s);if(e.lineTo(i.x,i.y),w>0){let t=Lf(O,A,o,s);e.arc(t.x,t.y,w,y-Oo,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 Bf(e,t,n,r,i){let{fullCircles:a,startAngle:o,circumference:s}=t,c=t.endAngle;if(a){zf(e,t,n,r,c,i);for(let t=0;t=Q-1e-4;if(a){zf(e,t,n,r,h,i);for(let t=0;t0)&&c.selfJoin&&h-o>=Z&&p===0&&u!==`miter`&&Nf(e,t,h),a||(zf(e,t,n,r,h,i),e.stroke())}var Hf=class extends Sd{constructor(e){super(),$(this,`circumference`,void 0),$(this,`endAngle`,void 0),$(this,`fullCircles`,void 0),$(this,`innerRadius`,void 0),$(this,`outerRadius`,void 0),$(this,`pixelMargin`,void 0),$(this,`startAngle`,void 0),$(this,`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}=Uo(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=Y(l,o-a),f=qo(r,a,o)&&a!==o,p=d>=Q||f,m=Xo(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>Q?Math.floor(n/Q):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(Z,n||0)));e.fillStyle=t.backgroundColor,e.strokeStyle=t.borderColor,Bf(e,this,s,i,a),Vf(e,this,s,i,a),e.restore()}};$(Hf,`id`,`arc`),$(Hf,`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}),$(Hf,`defaultRoutes`,{backgroundColor:`backgroundColor`}),$(Hf,`descriptors`,{_scriptable:!0,_indexable:e=>e!==`borderDash`});function Uf(e,t,n=t){e.lineCap=Y(n.borderCapStyle,t.borderCapStyle),e.setLineDash(Y(n.borderDash,t.borderDash)),e.lineDashOffset=Y(n.borderDashOffset,t.borderDashOffset),e.lineJoin=Y(n.borderJoinStyle,t.borderJoinStyle),e.lineWidth=Y(n.borderWidth,t.borderWidth),e.strokeStyle=Y(n.borderColor,t.borderColor)}function Wf(e,t,n){e.lineTo(n.x,n.y)}function Gf(e){return e.stepped?Gs:e.tension||e.cubicInterpolationMode===`monotone`?Ks:Wf}function Kf(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 Yf(e){let t=e.options,n=t.borderDash&&t.borderDash.length;return!e._decimated&&!e._loop&&!t.tension&&t.cubicInterpolationMode!==`monotone`&&!t.stepped&&!n?Jf:qf}function Xf(e){return e.stepped?rl:e.tension||e.cubicInterpolationMode===`monotone`?il:nl}function Zf(e,t,n,r){let i=t._path;i||(i=t._path=new Path2D,t.path(i,n,r)&&i.closePath()),Uf(e,t.options),e.stroke(i)}function Qf(e,t,n,r){let{segments:i,options:a}=t,o=Yf(t);for(let s of i)Uf(e,a,s.style),e.beginPath(),o(e,t,s,{start:n,end:n+r-1})&&e.closePath(),e.stroke()}var $f=typeof Path2D==`function`;function ep(e,t,n,r){$f&&!t.options.segment?Zf(e,t,n,r):Qf(e,t,n,r)}var tp=class extends Sd{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;zc(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=_l(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=ml(this,{property:t,start:r,end:r});if(!a.length)return;let o=[],s=Xf(n),c,l;for(c=0,l=a.length;ce!==`borderDash`&&e!==`fill`});function np(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 Df(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 Of(e,t){let n=e.chart.ctx,{body:r,footer:i,title:a}=e,{boxWidth:o,boxHeight:s}=t,c=_s(t.bodyFont),l=_s(t.titleFont),u=_s(t.footerFont),d=a.length,f=i.length,p=r.length,m=gs(t.padding),h=m.height,g=0,_=r.reduce((e,t)=>e+t.before.length+t.lines.length+t.after.length,0);if(_+=e.beforeBody.length+e.afterBody.length,d&&(h+=d*l.lineHeight+(d-1)*t.titleSpacing+t.titleMarginBottom),_){let e=t.displayColors?Math.max(s,c.lineHeight):c.lineHeight;h+=p*e+(_-p)*c.lineHeight+(_-1)*t.bodySpacing}f&&(h+=t.footerMarginTop+f*u.lineHeight+(f-1)*t.footerSpacing);let v=0,y=function(e){g=Math.max(g,n.measureText(e).width+v)};return n.save(),n.font=l.string,J(e.title,y),n.font=c.string,J(e.beforeBody.concat(e.afterBody),y),v=t.displayColors?o+2+t.boxPadding:0,J(r,e=>{J(e.before,y),J(e.lines,y),J(e.after,y)}),v=0,n.font=u.string,J(e.footer,y),n.restore(),g+=m.width,{width:g,height:h}}function kf(e,t){let{y:n,height:r}=t;return ne.height-r/2?`bottom`:`center`}function Af(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 jf(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`),Af(l,e,t,n)&&(l=`center`),l}function Mf(e,t,n){let r=n.yAlign||t.yAlign||kf(e,n);return{xAlign:n.xAlign||t.xAlign||jf(e,t,n,r),yAlign:r}}function Nf(e,t){let{x:n,width:r}=e;return t===`right`?n-=r:t===`center`&&(n-=r/2),n}function Pf(e,t,n){let{y:r,height:i}=e;return t===`top`?r+=n:r-=t===`bottom`?i+n:i/2,r}function Ff(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}=hs(o),m=Nf(t,s),h=Pf(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:ao(m,0,r.width-t.width),y:ao(h,0,r.height-t.height)}}function If(e,t,n){let r=gs(n.padding);return t===`center`?e.x+e.width/2:t===`right`?e.x+e.width-r.right:e.x+r.left}function Lf(e){return Tf([],Ef(e))}function Rf(e,t,n){return bs(e,{tooltip:t,tooltipItems:n,type:`tooltip`})}function zf(e,t){let n=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return n?e.override(n):e}var Bf={beforeTitle:ha,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=zf(n,e);Tf(t.before,Ef(Vf(i,`beforeLabel`,this,e))),Tf(t.lines,Vf(i,`label`,this,e)),Tf(t.after,Ef(Vf(i,`afterLabel`,this,e))),r.push(t)}),r}getAfterBody(e,t){return Lf(Vf(t.callbacks,`afterBody`,this,e))}getFooter(e,t){let{callbacks:n}=t,r=Vf(n,`beforeFooter`,this,e),i=Vf(n,`footer`,this,e),a=Vf(n,`afterFooter`,this,e),o=[];return o=Tf(o,Ef(r)),o=Tf(o,Ef(i)),o=Tf(o,Ef(a)),o}_createItems(e){let t=this._active,n=this.chart.data,r=[],i=[],a=[],o=[],s,c;for(s=0,c=t.length;se.filter(t,r,i,n))),e.itemSort&&(o=o.sort((t,r)=>e.itemSort(t,r,n))),J(o,t=>{let n=zf(e.callbacks,t);r.push(Vf(n,`labelColor`,this,t)),i.push(Vf(n,`labelPointStyle`,this,t)),a.push(Vf(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=wf[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=Of(this,n),o=Object.assign({},e,t),s=Mf(this.chart,n,o),c=Ff(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}=hs(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=vc(n.rtl,this.x,this.width);for(e.x=If(this,n.titleAlign,n),t.textAlign=c.textAlign(n.titleAlign),t.textBaseline=`middle`,a=_s(n.titleFont),o=n.titleSpacing,t.fillStyle=n.titleColor,t.font=a.string,s=0;se!==0)?(e.beginPath(),e.fillStyle=i.multiKeyBackground,cs(e,{x:t,y:p,w:c,h:s,radius:o}),e.fill(),e.stroke(),e.fillStyle=a.backgroundColor,e.beginPath(),cs(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=_s(n.bodyFont),d=u.lineHeight,f=0,p=vc(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=If(this,h,n),t.fillStyle=n.bodyColor,J(this.beforeBody,m),f=o&&h!==`right`?a===`center`?c/2+l:c+2+l:0,y=0,x=r.length;y0&&t.stroke()}_updateAnimationTarget(e){let t=this.chart,n=this.$animations,r=n&&n.x,i=n&&n.y;if(r||i){let n=wf[e.position].call(this,this._active,this._eventPosition);if(!n)return;let a=this._size=Of(this,e),o=Object.assign({},n,this._size),s=Mf(t,e,o),c=Ff(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=gs(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),yc(e,t.textDirection),i.y+=a.top,this.drawTitle(i,e,t),this.drawBody(i,e,t),this.drawFooter(i,e,t),bc(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=!xa(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||!xa(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=wf[i.position].call(this,e,t);return a!==!1&&(n!==a.x||r!==a.y)}};$(Hf,`positioners`,wf);var Uf={id:`tooltip`,_element:Hf,positioners:wf,afterInit(e,t,n){n&&(e.tooltip=new Hf({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:Bf},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`]},Wf=(e,t,n,r)=>(typeof t==`string`?(n=e.push(t)-1,r.unshift({index:n,label:t})):isNaN(t)&&(n=null),n);function Gf(e,t,n,r){let i=e.indexOf(t);return i===-1?Wf(e,t,n,r):i===e.lastIndexOf(t)?i:n}var Kf=(e,t)=>e===null?null:ao(Math.round(e),0,t);function qf(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}};$(Jf,`id`,`category`),$(Jf,`defaults`,{ticks:{callback:qf}});function Yf(e,t){let n=[],{bounds:r,step:i,min:a,max:o,precision:s,count:c,maxTicks:l,maxDigits:u,includeBounds:d}=e,f=i||1,p=l-1,{min:m,max:h}=t,g=!U(a),_=!U(o),v=!U(c),y=(h-m)/(u+1),b=Ga((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=Ga(w*b/p/f)*f),U(s)||(x=10**s,b=Math.ceil(b*x)/x),r===`ticks`?(S=Math.floor(m/b)*b,C=Math.ceil(h/b)*b):(S=m,C=h),g&&_&&i&&Ya((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=Wa(w,Math.round(w),b/1e3)?Math.round(w):Math.ceil(w));let T=Math.max($a(b),$a(S));x=10**(U(s)?T:s),S=Math.round(S*x)/x,C=Math.round(C*x)/x;let E=0;for(g&&(d&&S!==a?(n.push({value:a}),So)break;n.push({value:e})}return _&&d&&C!==o?n.length&&Wa(n[n.length-1].value,o,Xf(o,y,e))?n[n.length-1].value=o:n.push({value:o}):(!_||C===o)&&n.push({value:C}),n}function Xf(e,t,{horizontal:n,minRotation:r}){let i=Za(r),a=(n?Math.sin(i):Math.cos(i))||.001,o=.75*t*(``+e).length;return Math.min(t/a,o)}var Zf=class extends $u{constructor(e){super(e),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(e,t){return U(e)||(typeof e==`number`||e instanceof Number)&&!isFinite(+e)?null:+e}handleTickRangeOptions(){let{beginAtZero:e}=this.options,{minDefined:t,maxDefined:n}=this.getUserBounds(),{min:r,max:i}=this,a=e=>r=t?r:e,o=e=>i=n?i:e;if(e){let e=Ua(r),t=Ua(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=Yf({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`&&Xa(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 Lo(e,this.chart.options.locale,this.options.ticks.format)}},Qf=class extends Zf{determineDataLimits(){let{min:e,max:t}=this.getMinMax(!0);this.min=_a(e)?e:0,this.max=_a(t)?t:1,this.handleTickRangeOptions()}computeTickLimit(){let e=this.isHorizontal(),t=e?this.width:this.height,n=Za(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}};$(Qf,`id`,`linear`),$(Qf,`defaults`,{ticks:{callback:Bo.formatters.numeric}});var $f=e=>Math.floor(Ha(e)),ep=(e,t)=>10**($f(e)+t);function tp(e){return e/10**$f(e)==1}function np(e,t,n){let r=10**n,i=Math.floor(e/r);return Math.ceil(t/r)-i}function rp(e,t){let n=$f(t-e);for(;np(e,t,n)>10;)n++;for(;np(e,t,n)<10;)n--;return Math.min(n,$f(e))}function ip(e,{min:t,max:n}){t=va(e.min,t);let r=[],i=$f(t),a=rp(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=va(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=va(e.max,f);return r.push({value:p,major:tp(p),significand:d}),r}var ap=class extends $u{constructor(e){super(e),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(e,t){let n=Zf.prototype.parse.apply(this,[e,t]);if(n===0)this._zero=!0;else return _a(n)&&n>0?n:null}determineDataLimits(){let{min:e,max:t}=this.getMinMax(!0);this.min=_a(e)?Math.max(0,e):null,this.max=_a(t)?Math.max(0,t):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!_a(this._userMin)&&(this.min=e===ep(this.min,0)?ep(this.min,-1):ep(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(ep(n,-1)),a(ep(r,1)))),n<=0&&i(ep(r,-1)),r<=0&&a(ep(n,1)),this.min=n,this.max=r}buildTicks(){let e=this.options,t=ip({min:this._userMin,max:this._userMax},this);return e.bounds===`ticks`&&Xa(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`:Lo(e,this.chart.options.locale,this.options.ticks.format)}configure(){let e=this.min;super.configure(),this._startValue=Ha(e),this._valueRange=Ha(this.max)-Ha(e)}getPixelForValue(e){return(e===void 0||e===0)&&(e=this.min),e===null||isNaN(e)?NaN:this.getPixelForDecimal(e===this.min?0:(Ha(e)-this._startValue)/this._valueRange)}getValueForPixel(e){let t=this.getDecimalForPixel(e);return 10**(this._startValue+t*this._valueRange)}};$(ap,`id`,`logarithmic`),$(ap,`defaults`,{ticks:{callback:Bo.formatters.logarithmic,major:{enabled:!0}}});function op(e){let t=e.ticks;if(t.display&&e.display){let e=gs(t.backdropPadding);return K(t.font&&t.font.size,Q.font.size)+e.height}return 0}function sp(e,t,n){return n=W(n)?n:[n],{w:Jo(e,t.string,n),h:n.length*t.lineHeight}}function cp(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 lp(e){let t={l:e.left+e._padding.left,r:e.right-e._padding.right,t:e.top+e._padding.top,b:e.bottom-e._padding.bottom},n=Object.assign({},t),r=[],i=[],a=e._pointLabels.length,o=e.options.pointLabels,s=o.centerPointLabels?Y/a:0;for(let c=0;ct.r&&(s=(r.end-t.r)/a,e.r=Math.max(e.r,t.r+s)),i.startt.b&&(c=(i.end-t.b)/o,e.b=Math.max(e.b,t.b+c))}function dp(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(Qa(ro(c.angle+Z))),u=gp(c.y,s.h,l),d=mp(l),f=hp(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 fp(e,t){if(!t)return!0;let{left:n,top:r,right:i,bottom:a}=e;return!($o({x:n,y:r},t)||$o({x:n,y:a},t)||$o({x:i,y:r},t)||$o({x:i,y:a},t))}function pp(e,t,n){let r=[],i=e._pointLabels.length,a=e.options,{centerPointLabels:o,display:s}=a.pointLabels,c={extra:op(a)/2,additionalAngle:o?Y/i:0},l;for(let a=0;a270||n<90)&&(e-=t),e}function _p(e,t,n){let{left:r,top:i,right:a,bottom:o}=n,{backdropColor:s}=t;if(!U(s)){let n=hs(t.borderRadius),c=gs(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(),cs(e,{x:l,y:u,w:d,h:f,radius:n}),e.fill()):e.fillRect(l,u,d,f)}}function vp(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));_p(n,a,t);let o=_s(a.font),{x:s,y:c,textAlign:l}=t;ss(n,e._pointLabels[i],s,c+o.lineHeight/2,o,{color:a.color,textAlign:l,textBaseline:`middle`})}}function yp(e,t,n,r){let{ctx:i}=e;if(n)i.arc(e.xCenter,e.yCenter,t,0,X);else{let n=e.getPointPosition(0,t);i.moveTo(n.x,n.y);for(let a=1;a{let n=q(this.options.pointLabels.callback,[e,t],this);return n||n===0?n:``}).filter((e,t)=>this.chart.getDataVisibility(t))}fit(){let e=this.options;e.display&&e.pointLabels.display?lp(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(e,t,n,r){this.xCenter+=Math.floor((e-t)/2),this.yCenter+=Math.floor((n-r)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(e,t,n,r))}getIndexAngle(e){let t=X/(this._pointLabels.length||1),n=this.options.startAngle||0;return ro(e*t+Za(n))}getDistanceFromCenterForValue(e){if(U(e))return NaN;let t=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-e)*t:(e-this.min)*t}getValueForDistanceFromCenter(e){if(U(e))return NaN;let t=e/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-t:this.min+t}getPointLabelContext(e){let t=this._pointLabels||[];if(e>=0&&e{if(t!==0||t===0&&this.min<0){s=this.getDistanceFromCenterForValue(e.value);let n=this.getContext(t),o=r.setContext(n),c=i.setContext(n);bp(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=_s(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=gs(s.backdropPadding);e.fillRect(-a/2-t.left,-i-c.size/2-t.top,a+t.width,c.size+t.height)}ss(e,r.label,0,-i,c,{color:s.color,strokeColor:s.textStrokeColor,strokeWidth:s.textStrokeWidth})}),e.restore()}drawTitle(){}};$(Sp,`id`,`radialLinear`),$(Sp,`defaults`,{display:!0,animate:!0,position:`chartArea`,angleLines:{display:!0,lineWidth:1,borderDash:[],borderDashOffset:0},grid:{circular:!1},startAngle:0,ticks:{showLabelBackdrop:!0,callback:Bo.formatters.numeric},pointLabels:{backdropColor:void 0,backdropPadding:2,display:!0,font:{size:10},callback(e){return e},padding:5,centerPointLabels:!1}}),$(Sp,`defaultRoutes`,{"angleLines.color":`borderColor`,"pointLabels.color":`color`,"ticks.color":`color`}),$(Sp,`descriptors`,{angleLines:{_fallback:`grid`}});var Cp={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}},wp=Object.keys(Cp);function Tp(e,t){return e-t}function Ep(e,t){if(U(t))return null;let n=e._adapter,{parser:r,round:i,isoWeekday:a}=e._parseOpts,o=t;return typeof r==`function`&&(o=r(o)),_a(o)||(o=typeof r==`string`?n.parse(o,r):n.parse(o)),o===null?null:(i&&(o=i===`week`&&(Ja(a)||a===!0)?n.startOf(o,`isoWeek`,a):n.startOf(o,i)),+o)}function Dp(e,t,n,r){let i=wp.length;for(let a=wp.indexOf(e);a=wp.indexOf(n);a--){let n=wp[a];if(Cp[n].common&&e._adapter.diff(i,r,n)>=t-1)return n}return wp[n?wp.indexOf(n):0]}function kp(e){for(let t=wp.indexOf(e)+1,n=wp.length;t=t?n[r]:n[i];e[a]=!0}}function jp(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 Mp(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=ao(t,0,a),n=ao(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||Dp(i.minUnit,t,n,this._getLabelCapacity(t)),o=K(r.ticks.stepSize,1),s=a===`week`&&i.isoWeekday,c=Ja(s)||s===!0,l={},u=t,d,f;if(c&&(u=+e.startOf(u,`isoWeek`,s)),u=+e.startOf(u,c?`day`:a),e.diff(n,t,a)>1e5*o)throw Error(t+` and `+n+` are too far apart with stepSize of `+o+` `+a);let p=r.ticks.source===`data`&&this.getDataTimestamps();for(d=u,f=0;d+e)}getLabelForValue(e){let t=this._adapter,n=this.options.time;return n.tooltipFormat?t.format(e,n.tooltipFormat):t.format(e,n.displayFormats.datetime)}format(e,t){let n=this.options.time.displayFormats,r=this._unit,i=t||n[r];return this._adapter.format(e,i)}_tickFormatFunction(e,t,n,r){let i=this.options,a=i.ticks.callback;if(a)return q(a,[e,t,n],this);let o=i.time.displayFormats,s=this._unit,c=this._majorUnit,l=s&&o[s],u=c&&o[c],d=n[t],f=c&&u&&d&&d.major;return this._adapter.format(e,r||(f?u:l))}generateTickLabels(e){let t,n,r;for(t=0,n=e.length;t0?o:1}getDataTimestamps(){let e=this._cache.data||[],t,n;if(e.length)return e;let r=this.getMatchingVisibleMetas();if(this._normalized&&r.length)return this._cache.data=r[0].controller.getAllParsedValues(this);for(t=0,n=r.length;t=e[r].pos&&t<=e[i].pos&&({lo:r,hi:i}=lo(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}=lo(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 Fp=class extends Np{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=Pp(t,this.min),this._tableRange=Pp(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(Pp(this._table,e)-this._minPos)/this._tableRange}getValueForPixel(e){let t=this._offsets,n=this.getDecimalForPixel(e)/t.factor-t.end;return Pp(this._table,n*this._tableRange+this._minPos,!0)}};$(Fp,`id`,`timeseries`),$(Fp,`defaults`,Np.defaults),Vd.register(Dl,Cf,Jf,Qf,Uf);var Ip=new Intl.DateTimeFormat(`ko-KR`,{hour:`2-digit`,hour12:!1}),Lp=`#94a3b8`,Rp=`rgba(148, 163, 184, 0.18)`,zp=`#0d9488`,Bp=`#0f766e`;function Vp(e){return Ip.format(new Date(e*1e3))}function Hp(e){return e.map((t,n)=>n%3==0||n===e.length-1?Vp(t.bucketStart):``)}function Up({buckets:e}){let t=we(null),n=we(null),r=we(e);return r.current=e,I(()=>{let e=t.current;if(!e)return;let i=window.matchMedia(`(prefers-reduced-motion: reduce)`).matches,a=window.getComputedStyle(e).fontFamily,o={animation:!i&&{duration:500,easing:`easeOutQuart`},maintainAspectRatio:!1,responsive:!0,interaction:{intersect:!1,mode:`index`},plugins:{legend:{display:!1},tooltip:{displayColors:!1,callbacks:{title:e=>{let t=e[0]?.dataIndex??-1,n=r.current[t];return n?Vp(n.bucketStart):``},label:e=>`차단 ${H(Number(e.raw??0))}`,afterLabel:e=>{let t=r.current[e.dataIndex];return t?`DNS 요청 ${H(t.queries)}`:``}}}},scales:{x:{border:{display:!1},grid:{display:!1},ticks:{autoSkip:!1,color:Lp,font:{family:a,size:11,weight:700},maxRotation:0,minRotation:0}},y:{beginAtZero:!0,border:{display:!1},grid:{color:Rp},ticks:{color:Lp,font:{family:a,size:11,weight:700},maxTicksLimit:4,precision:0}}}};return n.current=new Vd(e,{type:`bar`,data:{labels:Hp(r.current),datasets:[{data:r.current.map(e=>e.blocked),backgroundColor:zp,hoverBackgroundColor:Bp,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=Hp(e),t.data.datasets[0].data=e.map(e=>e.blocked),t.update())},[e]),L(`div`,{class:`mt-5 h-44 sm:h-52`,children:L(`canvas`,{"aria-label":`최근 24시간 시간대별 SafeShield 차단 요청`,ref:t,role:`img`,children:`최근 24시간 시간대별 SafeShield 차단 요청 차트`})})}var Wp=3600,Gp=24;function Kp(e,t){return e<=0?`0.0%`:`${(t/e*100).toFixed(1)}%`}function qp(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/Wp)*Wp}function Jp(e){let t=new Map(e.hourly.map(e=>[e.bucketStart,e])),n=qp(e)-23*Wp;return Array.from({length:Gp},(e,r)=>{let i=n+r*Wp;return t.get(i)??{bucketStart:i,queries:0,blocked:0}})}function Yp({children:e}){return L(`div`,{class:`mt-5 flex min-h-52 items-center justify-center rounded-xl border border-slate-200 bg-slate-50 px-5 text-center`,children:L(`p`,{class:`m-0 max-w-lg text-sm font-semibold leading-6 text-slate-500`,children:e})})}function Xp({data:e,error:t,loading:n,nowTimestamp:r,refreshing:i}){let a;if(n&&!e)a=L(Yp,{children:`SafeShield 보호 활동을 불러오는 중입니다.`});else if(t&&!e)a=L(Yp,{children:`SafeShield 보호 활동을 불러오지 못했습니다. SafeShield 페이지에서 상태를 확인해 주세요.`});else if(!e||!e.available)a=L(Yp,{children:`현재 SafeShield에서 차단 통계를 제공하지 않습니다. 통계 기능을 확인해 주세요.`});else if(!e.enabled)a=L(Yp,{children:`통계 수집이 꺼져 있습니다. SafeShield 페이지에서 통계를 켜면 최근 24시간 활동이 표시됩니다.`});else{let t=Jp(e),n=t.reduce((e,t)=>({queries:e.queries+t.queries,blocked:e.blocked+t.blocked}),{queries:0,blocked:0});a=L(v,{children:[L(`dl`,{class:`mt-5 grid gap-3 sm:grid-cols-3`,children:[L(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3`,children:[L(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:`DNS 요청`}),L(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-slate-950`,children:H(n.queries)})]}),L(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3`,children:[L(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:`차단`}),L(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-teal-700`,children:H(n.blocked)})]}),L(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3`,children:[L(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:`차단율`}),L(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-slate-950`,children:Kp(n.queries,n.blocked)})]})]}),L(Up,{buckets:t})]})}return L(`article`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:[L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`SafeShield activity`}),L(`div`,{class:`mt-2 flex min-w-0 items-center justify-between gap-3`,children:[L(`h3`,{class:`m-0 min-w-0 text-xl font-black text-slate-950`,children:`최근 24시간 보호 활동`}),L(`a`,{class:`shrink-0 whitespace-nowrap text-xs font-extrabold text-teal-700 no-underline hover:text-teal-800`,href:`#safeshield`,children:`상세 통계 →`})]}),e?.updatedAt?L(`p`,{class:`mt-2 mb-0 text-[0.68rem] font-semibold text-slate-400`,title:Wt(e.updatedAt),children:i?`갱신 중…`:`집계: ${Gt(e.updatedAt,r)}`}):null]}),a,t&&e?L(`p`,{class:`mt-3 mb-0 text-xs font-bold text-amber-700`,children:`최근 통계 갱신에 실패해 마지막 정상 데이터를 표시하고 있습니다.`}):null]})}function Zp(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 Qp({detail:e,eyebrow:t,href:n,icon:r,linkLabel:i=`자세히 보기 →`,meta:a,metaState:o,metaWarning:s=!1,state:c=`neutral`,value:l}){let u=L(v,{children:[L(`div`,{class:`flex items-start justify-between gap-4`,children:[L(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.16em] text-slate-500`,children:t}),L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl ring-1 ring-inset ${Zp(c)}`,children:r})]}),L(`p`,{class:`mt-4 mb-0 break-words text-2xl font-black tracking-tight text-slate-950`,children:l}),L(`p`,{class:`mt-2 mb-0 min-h-10 text-sm font-medium leading-5 text-slate-500`,children:e}),a?L(`p`,{class:`mt-3 mb-0 text-xs font-bold ${o===`healthy`?`text-emerald-700`:o===`warning`||s?`text-amber-700`:`text-slate-400`}`,children:a}):null,n?L(`span`,{class:`mt-4 inline-flex text-xs font-extrabold text-teal-700`,children:i}):null]}),d=`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 transition`;return n?L(`a`,{class:`${d} no-underline hover:border-slate-300 hover:shadow-md`,href:n,children:u}):L(`article`,{class:d,children:u})}var $p=6e4;function em(e,t){return!e||!Number.isFinite(e)||e<=0?null:Math.max(0,t-e)}function tm(e,t,n,r){return!t||t<=0?`${e}: ${r}`:L(`span`,{title:Wt(t),children:[e,`: `,Gt(t,n)]})}function nm(e){if(!e)return null;switch(e.toLowerCase()){case`dhcp`:return`DHCP`;case`static`:return`Static`;case`pppoe`:return`PPPoE`;default:return e.toUpperCase()}}function rm(e){if(!e)return!1;let t=e.split(`.`).map(e=>Number(e));if(t.length!==4||t.some(e=>!Number.isInteger(e)||e<0||e>255))return!1;let[n,r]=t;return n===void 0||r===void 0?!1:n===10||n===172&&r>=16&&r<=31||n===192&&r===168}function im({label:e,value:t}){return L(`div`,{class:`flex min-w-0 items-start justify-between gap-4 border-b border-slate-100 py-3 last:border-b-0`,children:[L(`dt`,{class:`text-sm font-semibold text-slate-500`,children:e}),L(`dd`,{class:`m-0 min-w-0 max-w-[68%] break-words text-right text-sm font-extrabold text-slate-950`,children:t})]})}function am(e){return e===`initializing`?{badge:`bg-teal-100 text-teal-800`,icon:`text-teal-700`,label:`준비 중`,panel:`border-teal-200 bg-teal-50`}:e===`critical`?{badge:`bg-rose-100 text-rose-800`,icon:`text-rose-700`,label:`이상`,panel:`border-rose-200 bg-rose-50/70`}:e===`warning`?{badge:`bg-amber-100 text-amber-800`,icon:`text-amber-700`,label:`주의`,panel:`border-amber-200 bg-amber-50/70`}:e===`unknown`?{badge:`bg-slate-200 text-slate-700`,icon:`text-slate-500`,label:`확인 불가`,panel:`border-slate-200 bg-slate-50`}:{badge:`bg-emerald-100 text-emerald-800`,icon:`text-emerald-700`,label:`정상`,panel:`border-emerald-200 bg-emerald-50/70`}}function om({data:e,error:t,loading:n,nowTimestamp:r}){let i=!!(e&&e.generatedAt>0),a=am(i&&e?e.overall:`unknown`),o=i&&e?e.checks.filter(e=>e.status!==`ok`):[],s=o.slice(0,2),c=Math.max(0,o.length-s.length);return L(`div`,{class:`mt-5 border-t border-slate-200 pt-5`,children:[L(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[L(`div`,{class:`flex items-center gap-2`,children:[L(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:`장치 진단`}),i?L(`span`,{class:`rounded-full px-2.5 py-1 text-[11px] font-extrabold ${a.badge}`,children:a.label}):null]}),L(`a`,{class:`text-xs font-extrabold text-teal-700 no-underline hover:text-teal-900`,href:`#settings`,children:`상세 보기 →`})]}),t&&!i?L(`div`,{class:`mt-3 rounded-xl border border-amber-200 bg-amber-50/70 px-4 py-3`,children:L(`div`,{class:`flex items-start gap-2`,children:[L(B,{class:`mt-0.5 size-4 shrink-0 text-amber-700`}),L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-sm font-extrabold text-slate-900`,children:`진단 상태를 확인하지 못했습니다.`}),L(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`설정에서 장치 진단 상태를 다시 확인해 주세요.`})]})]})}):i&&e?L(`div`,{class:`mt-3 rounded-xl border px-4 py-3 ${a.panel}`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3`,children:[e.overall===`ok`?L(ut,{class:`mt-0.5 size-5 shrink-0 ${a.icon}`}):e.overall===`initializing`?L(tt,{class:`mt-0.5 size-5 shrink-0 ${a.icon}`}):L(B,{class:`mt-0.5 size-5 shrink-0 ${a.icon}`}),L(`div`,{class:`min-w-0 flex-1`,children:[L(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:e.summary.message}),L(`p`,{class:`mt-1 mb-0 text-xs font-semibold leading-5 text-slate-500`,children:[`마지막 진단: `,Gt(e.generatedAt,r),` · 검사 항목:`,` `,e.summary.total,`개`]})]})]}),s.length>0?L(`div`,{class:`mt-3 grid gap-2 sm:grid-cols-2`,children:s.map(e=>L(`div`,{class:`min-w-0 rounded-lg border border-white/80 bg-white/70 px-3 py-2`,children:[L(`p`,{class:`m-0 text-xs font-extrabold text-slate-900`,children:e.label}),L(`p`,{class:`mt-1 mb-0 line-clamp-2 text-xs leading-5 text-slate-500`,children:e.message})]},e.id))}):null,c>0?L(`p`,{class:`mt-2 mb-0 text-xs font-extrabold text-slate-600`,children:[`외 `,c,`건의 확인 항목이 있습니다.`]}):null]}):L(`div`,{class:`mt-3 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3`,children:[L(`p`,{class:`m-0 text-sm font-extrabold text-slate-900`,children:n?`진단 상태를 확인하고 있습니다.`:`아직 생성된 진단 결과가 없습니다.`}),L(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`진단 결과가 생성되면 현재 장치 상태를 이곳에 요약해서 표시합니다.`})]})]})}function sm(e,t,n){if(t&&!e)return{detail:`보호 상태를 확인하고 있습니다.`,state:`neutral`,value:`확인 중`};if(n&&!e)return{detail:`SafeShield 상태를 불러오지 못했습니다.`,state:`warning`,value:`확인 필요`};if(!e||!e.available)return{detail:`SafeShield 서비스를 사용할 수 없습니다.`,state:`warning`,value:`사용 불가`};if(!e.enabled||e.status===`disabled`)return{detail:`DNS 보호가 비활성화되어 있습니다.`,state:`neutral`,value:`꺼짐`};if(e.status===`running`)return{detail:`차단 목록을 갱신하고 있습니다.`,state:`neutral`,value:`갱신 중`};if(!(e.active&&e.runtime.dnsmasqRunning&&e.runtime.dnsRuntimeOk&&e.health.overall!==`error`&&e.issueCounts.errors===0))return{detail:e.runtime.lastErrorCode?`최근 오류: ${e.runtime.lastErrorCode}`:`DNS 보호 상태를 확인해 주세요.`,state:`warning`,value:`확인 필요`};let r=e.entitlement.plan?.trim(),i=e.blocklist.validLineCount>0?`${H(e.blocklist.validLineCount)}개 차단 규칙`:`차단 목록 적용됨`;return{detail:r?`${i} · ${r.toUpperCase()}`:i,state:`healthy`,value:`보호 중`}}function cm({accountRegistered:e,activity:t,activityError:n,activityLoading:r,data:i,devices:a,devicesError:o,devicesLoading:s,error:c,firmware:l,firmwareError:u,firmwareLoading:d,health:f,healthError:p,healthLoading:m,lan:h,lanError:g,lanLoading:_,loading:v,onRetry:y,safeshield:b,safeshieldError:x,safeshieldLoading:S,statistics:C,statisticsError:w,statisticsLoading:T,statisticsRefreshing:E,updates:D,updatesError:O,updatesLoading:k}){let A=Te(()=>i?Yt(i.runtime.memory):null,[i]),[j,M]=F(()=>Math.floor(Date.now()/1e3));if(I(()=>{let e=window.setInterval(()=>{M(Math.floor(Date.now()/1e3))},$p);return()=>window.clearInterval(e)},[]),v&&!i)return L(zn,{});if(c&&!i)return L(Bn,{message:c,onRetry:y});if(!i)return null;let ee=A?Math.round(A.percent):0,te=sm(b,S,x),ne=D?.packages.find(e=>e.name===`luci-app-smartsafehub`)??D?.packages[0]??null,re=!!(D&&D.updateCount>0),ie=D?re?`${D.updateCount}개 업데이트`:D.lastCheckAt?`최신 상태`:`확인 전`:O?`확인 필요`:`확인 중`,ae=ne?re&&ne.availableVersion?`${ne.installedVersion} → ${ne.availableVersion}`:`설치 버전 ${ne.installedVersion}`:O?`업데이트 상태를 불러오지 못했습니다.`:k?`업데이트 상태를 확인하고 있습니다.`:`업데이트 상태를 확인할 수 없습니다.`,oe=a?`${H(a.totals.online)}대`:s?`확인 중`:`확인 필요`,se=a?`Wi-Fi ${H(a.totals.wireless)}대 · 유선/기타 ${H(a.totals.ethernet)}대`:o||`연결 기기 요약을 불러오고 있습니다.`,ce=nm(i.network.protocol),le=i.network.available?i.network.ipv4Address?`WAN ${i.network.ipv4Address}${ce?` · ${ce}`:``}`:ce?`${ce} · WAN 주소를 받지 못했습니다.`:`WAN 주소를 받지 못했습니다.`:`WAN 인터페이스를 찾을 수 없습니다.`,N=!!h?.conflict.detected,ue=!i.network.up||N||g!==null&&!h?`warning`:h?`healthy`:`neutral`,de=N?`네트워크 충돌`:i.network.up?`정상 연결`:`연결 확인`,fe=i.network.up?N?`⚠ LAN 대역과 충돌합니다`:_&&!h?`네트워크 대역 확인 중`:g&&!h?`네트워크 대역을 확인하지 못했습니다.`:h?`✓ 네트워크 충돌 없음`:`네트워크 대역 확인 필요`:`WAN 연결 상태를 확인해 주세요.`,pe=!i.network.up||N||g!==null&&!h?`warning`:h?`healthy`:`neutral`,me=i.network.ipv4Address?rm(i.network.ipv4Address)?`${i.network.ipv4Address} · 사설 네트워크`:i.network.ipv4Address:`할당되지 않음`,he=em(b?.timestamps.lastSuccess,j),P=b?Math.max(b.timestamps.refreshIntervalS*2,86400):null,ge=!!(b?.enabled&&b.status!==`running`&&he!==null&&P!==null&&he>P),_e=Wr(D,j),ve=!!l?.current.metadataAvailable,ye=d&&!l?`확인 중`:u&&!l?`확인 필요`:ve?l?.current.releaseVersion?`SmartSafeHub ${l.current.releaseVersion}`:`SmartSafeHub 펌웨어`:`${i.software.distribution} ${i.software.version}`,be=d&&!l?`확인 중`:u&&!l?`확인 필요`:ve&&l?.current.buildId||i.software.revision,xe=ve?`빌드 ID`:`리비전`;return L(`div`,{class:`min-w-0 space-y-6`,children:[L(`section`,{"aria-labelledby":`dashboard-overview-title`,children:[L(V,{description:`인터넷, SafeShield, 연결 기기와 소프트웨어 상태를 한눈에 확인합니다.`,eyebrow:`Overview`,id:`dashboard-overview-title`,title:`시스템 개요`}),L(`div`,{class:`grid min-w-0 gap-4 sm:grid-cols-2 xl:grid-cols-4`,children:[L(Qp,{detail:le,eyebrow:`Internet`,href:`#network`,icon:L($e,{class:`size-5`}),linkLabel:N?`해결하기 →`:`자세히 보기 →`,meta:fe,metaState:pe,state:ue,value:de}),L(Qp,{detail:te.detail,eyebrow:`SafeShield`,href:`#safeshield`,icon:L(Ue,{class:`size-5`}),meta:tm(ge?`차단 목록 갱신 지연`:`차단 목록 갱신`,b?.timestamps.lastSuccess,j,S?`확인 중`:`기록 없음`),metaWarning:ge,state:te.state===`healthy`&&ge?`warning`:te.state,value:te.value}),L(Qp,{detail:se,eyebrow:`Connected devices`,href:`#devices`,icon:L(Ge,{class:`size-5`}),meta:tm(`목록 확인`,a?.generatedAt,j,s?`확인 중`:`기록 없음`),state:a?`healthy`:o?`warning`:`neutral`,value:oe}),L(Qp,{detail:ae,eyebrow:`Software update`,href:`#system`,icon:L(Je,{class:`size-5`}),meta:D&&!D.settings.checkEnabled?tm(`자동 확인 꺼짐 · 마지막 확인`,D.lastCheckAt,j,`확인 기록 없음`):tm(_e?`업데이트 확인 지연`:`마지막 확인`,D?.lastCheckAt,j,k?`확인 중`:`아직 없음`),metaWarning:_e,state:O||_e||re?`warning`:D?.lastCheckAt?`healthy`:`neutral`,value:ie})]}),e===!1?L(`div`,{class:`ssh-dashboard-account-banner mt-4 flex flex-col gap-4 rounded-2xl border p-4 sm:flex-row sm:items-center sm:justify-between sm:p-5`,role:`status`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3`,children:[L(`span`,{class:`ssh-dashboard-account-banner-icon grid size-10 shrink-0 place-items-center rounded-xl`,"aria-hidden":`true`,children:L(qe,{class:`size-5`})}),L(`div`,{class:`min-w-0`,children:[L(`div`,{class:`flex flex-wrap items-center gap-2`,children:[L(`p`,{class:`ssh-dashboard-account-banner-title m-0 font-bold`,children:`SmartSafeHub 계정을 연결해 주세요`}),L(`span`,{class:`ssh-dashboard-account-banner-badge rounded-full px-2 py-0.5 text-[0.7rem] font-bold`,children:`연결 필요`})]}),L(`p`,{class:`ssh-dashboard-account-banner-description mt-1 mb-0 text-sm leading-relaxed`,children:`계정을 연결하면 웹사이트에서 기기를 관리하고 구독 정보를 확인할 수 있습니다. DNS 보호는 연결하지 않아도 계속 작동합니다.`})]})]}),L(`a`,{href:`#account`,class:`ssh-dashboard-account-banner-action inline-flex min-h-10 shrink-0 items-center justify-center rounded-xl px-4 py-2 text-sm font-bold no-underline transition-colors focus-visible:outline-2 focus-visible:outline-offset-2`,children:`계정 연결하기 →`})]}):null]}),L(`section`,{"aria-labelledby":`dashboard-activity-title`,children:[L(V,{description:`최근 SafeShield 차단 활동과 현재 네트워크 연결 구성을 함께 확인합니다.`,eyebrow:`Activity`,id:`dashboard-activity-title`,title:`네트워크 보호 활동`}),L(`div`,{class:`grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1.55fr)_minmax(18rem,0.65fr)]`,children:[L(Xp,{data:C,error:w,loading:T,nowTimestamp:j,refreshing:E}),L(`article`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:[L(`div`,{class:`flex items-start justify-between gap-4`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Network`}),L(`h3`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`연결 상태`})]}),L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl ${i.network.up&&!N?`bg-emerald-50 text-emerald-700`:`bg-amber-50 text-amber-800`}`,children:L($e,{class:`size-5`})})]}),L(`dl`,{class:`mt-4 mb-0`,children:[L(im,{label:`인터넷`,value:N?`네트워크 충돌`:i.network.up?`정상 연결`:`연결 확인`}),L(im,{label:`WAN IP`,value:me}),L(im,{label:`프로토콜`,value:ce||`확인되지 않음`}),L(im,{label:`상위 네트워크`,value:h?.wan.subnet||(_?`확인 중`:g?`확인 필요`:`확인되지 않음`)}),L(im,{label:`LAN 네트워크`,value:h?.lan.subnet||(_?`확인 중`:g?`확인 필요`:`확인되지 않음`)}),L(im,{label:`대역 충돌`,value:h?L(`span`,{class:h.conflict.detected?`text-amber-700`:`text-emerald-700`,children:h.conflict.detected?`충돌 감지`:`없음`}):_?`확인 중`:`확인 필요`})]}),L(`a`,{class:`mt-5 inline-flex min-h-10 w-full items-center justify-center rounded-xl border border-slate-200 bg-slate-50 px-4 py-2 text-sm font-extrabold text-slate-700 no-underline transition hover:border-teal-200 hover:bg-teal-50 hover:text-teal-800`,href:`#network`,children:`네트워크 보기`})]})]})]}),L(`section`,{"aria-labelledby":`dashboard-recent-activity-title`,children:[L(V,{action:L(`a`,{class:`inline-flex whitespace-nowrap text-xs font-extrabold text-teal-700 no-underline hover:text-teal-900`,href:`#activity`,children:`전체 보기 →`}),description:`인터넷, 보호, 업데이트와 진단에서 실제 상태가 바뀐 시점을 확인합니다.`,eyebrow:`History`,id:`dashboard-recent-activity-title`,title:`최근 활동`}),L(`article`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:r||n&&!t?L(dn,{error:n,loading:r}):L(un,{compact:!0,events:(t?.events??[]).slice(0,3)})})]}),L(`section`,{"aria-labelledby":`dashboard-system-title`,children:[L(V,{description:`메모리, 시스템 부하와 장치 정보를 빠르게 점검합니다.`,eyebrow:`System`,id:`dashboard-system-title`,title:`시스템 상태`}),L(`div`,{class:`grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1.45fr)_minmax(20rem,0.75fr)]`,children:[L(`article`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:[L(`div`,{class:`flex flex-wrap items-start justify-between gap-4`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Runtime`}),L(`h3`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`리소스 사용량`})]}),L(`span`,{class:`inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-black ring-1 ring-inset ${ee>=90?`bg-amber-50 text-amber-800 ring-amber-200`:`bg-emerald-50 text-emerald-700 ring-emerald-200`}`,children:[L(`span`,{class:`size-2 rounded-full ${ee>=90?`bg-amber-500`:`bg-emerald-500`}`}),ee>=90?`메모리 확인`:`정상 실행`]})]}),L(`div`,{class:`mt-5 grid gap-3 sm:grid-cols-3`,children:[L(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[L(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[L(nt,{class:`size-4`}),L(`span`,{class:`text-xs font-extrabold`,children:`메모리`})]}),L(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:[ee,`%`]}),L(`p`,{class:`mt-1 mb-0 text-xs font-semibold text-slate-500`,children:[A?Vt(A.used):`-`,` /`,` `,Vt(i.runtime.memory.total)]}),L(`div`,{"aria-label":`메모리 사용률 ${ee}%`,class:`mt-3 h-1.5 overflow-hidden rounded-full bg-slate-200`,role:`progressbar`,"aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":ee,children:L(`div`,{class:`h-full rounded-full transition-[width] duration-300 ${ee>=90?`bg-amber-500`:`bg-teal-600`}`,style:{width:`${Math.min(100,ee)}%`}})})]}),L(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[L(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[L(We,{class:`size-4`}),L(`span`,{class:`text-xs font-extrabold`,children:`시스템 부하`})]}),L(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:Ht(i.runtime.load[0])}),L(`p`,{class:`mt-1 mb-0 text-xs font-semibold text-slate-500`,children:[`5분 `,Ht(i.runtime.load[1]),` · 15분`,` `,Ht(i.runtime.load[2])]})]}),L(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[L(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[L(tt,{class:`size-4`}),L(`span`,{class:`text-xs font-extrabold`,children:`실행 시간`})]}),L(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:Bt(i.runtime.uptime)}),L(`p`,{class:`mt-1 mb-0 text-xs font-semibold text-slate-500`,children:[`부팅 `,Ut(i.runtime.localtime,i.runtime.uptime)]})]})]}),L(om,{data:f,error:p,loading:m,nowTimestamp:j})]}),L(`article`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:[L(`div`,{class:`flex items-start justify-between gap-4`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Device`}),L(`h3`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`장치 정보`})]}),L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:L(We,{class:`size-5`})})]}),L(`dl`,{class:`mt-4 mb-0`,children:[L(im,{label:`모델`,value:i.device.model}),L(im,{label:`호스트 이름`,value:i.device.hostname}),L(im,{label:`펌웨어`,value:L(`span`,{title:ve?l?.current.releaseVersion?`SmartSafeHub 펌웨어 ${l.current.releaseVersion}`:`SmartSafeHub 커스텀 펌웨어`:u||void 0,children:ye})}),L(im,{label:xe,value:be}),L(im,{label:`커널`,value:i.software.kernel}),L(im,{label:`WAN IP`,value:i.network.ipv4Address||`할당되지 않음`})]})]})]})]})]})}var lm=3,um=10;function dm(e,t){return e<=0?`0.0%`:`${(t/e*100).toFixed(1)}%`}function fm(e){return e.id===`other`?`기타 기기`:e.hostname?e.hostname:e.identified?`이름 없는 기기`:`미식별 기기`}function pm(e){return[...e].sort((e,t)=>e.id===`other`?1:t.id===`other`?-1:t.blocked-e.blocked||t.queries-e.queries||fm(e).localeCompare(fm(t),`ko`))}function mm({devices:e,deviceLimit:t,truncated:n}){let r=pm(e),[i,a]=F(!1),[o,s]=F(1),c=Math.max(1,Math.ceil(r.length/um)),l=Math.min(o,c),u=(l-1)*um,d=Math.min(u+um,r.length),f=i?r.slice(u,d):r.slice(0,lm),p=r.length>lm;return I(()=>{s(e=>Math.min(e,c))},[c]),L(`div`,{class:`mt-6 rounded-2xl border border-slate-100 bg-slate-50/70 p-4 sm:p-5`,children:[L(`div`,{class:`flex flex-wrap items-start justify-between gap-3`,children:[L(`div`,{children:[L(`div`,{class:`flex flex-wrap items-center gap-2`,children:[L(`p`,{class:`m-0 text-xs font-bold text-slate-500`,children:`기기별 통계`}),p&&!i?L(`span`,{class:`inline-flex items-center rounded-full bg-teal-50 px-2 py-1 text-[10px] font-extrabold tracking-wide text-teal-700 ring-1 ring-teal-100`,children:`차단 TOP 3`}):null]}),L(`h3`,{class:`mt-1 mb-0 text-base font-extrabold text-slate-950`,children:`어떤 기기에서 차단되었는지 확인`}),L(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-500`,children:`DHCP 정보가 있는 기기는 MAC 주소를 기준으로 식별하며, 기기별 숫자만 현재 부팅 세션 동안 메모리에 집계합니다.`})]}),L(`div`,{class:`flex flex-wrap items-center justify-end gap-2`,children:[L(`span`,{class:`inline-flex items-center gap-2 rounded-full bg-white px-3 py-1.5 text-xs font-extrabold text-slate-600 ring-1 ring-slate-200`,children:[L(Ge,{class:`size-4 text-teal-700`}),H(r.length),`개 항목`]}),p?L(`button`,{"aria-controls":`safeshield-device-statistics-list`,"aria-expanded":i,class:`rounded-full bg-white px-3 py-1.5 text-xs font-extrabold text-teal-700 ring-1 ring-slate-200 transition hover:bg-slate-50`,onClick:()=>{a(e=>!e),s(1)},type:`button`,children:i?`간단히 보기 ↑`:`전체 ${H(r.length)}개 기기 보기 ↓`}):null]})]}),r.length===0?L(`p`,{class:`mt-4 mb-0 rounded-xl bg-white p-4 text-sm leading-6 text-slate-500 ring-1 ring-slate-100`,children:`아직 기기별 DNS 통계가 없습니다. 클라이언트에서 DNS 요청이 발생하면 다음 통계 갱신 시 표시됩니다.`}):L(`div`,{class:`mt-4 grid gap-3`,id:`safeshield-device-statistics-list`,children:f.map(e=>L(`article`,{class:`grid gap-4 rounded-xl bg-white p-4 ring-1 ring-slate-100 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center`,children:[L(`div`,{class:`min-w-0`,children:L(`div`,{class:`flex min-w-0 flex-wrap items-center gap-2`,children:[L(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg bg-teal-50 text-teal-700`,children:L(Ge,{class:`size-4`})}),L(`div`,{class:`min-w-0`,children:[L(`h4`,{class:`m-0 truncate text-sm font-extrabold text-slate-950`,children:fm(e)}),L(`p`,{class:`mt-1 mb-0 break-all text-xs text-slate-500`,children:[e.ip||`IP 미확인`,e.mac?` · ${e.mac}`:``]})]}),L(`span`,{class:`rounded-full px-2 py-1 text-[11px] font-extrabold ${e.identified?`bg-emerald-50 text-emerald-700`:`bg-slate-100 text-slate-600`}`,children:e.id===`other`?`합산`:e.identified?`DHCP 식별`:`IP 임시 식별`})]})}),L(`dl`,{class:`m-0 grid grid-cols-3 gap-2 text-right`,children:[L(`div`,{class:`min-w-20`,children:[L(`dt`,{class:`text-[11px] font-bold text-slate-400`,children:`DNS 요청`}),L(`dd`,{class:`mt-1 mb-0 ml-0 text-sm font-extrabold text-slate-900`,children:H(e.queries)})]}),L(`div`,{class:`min-w-20`,children:[L(`dt`,{class:`text-[11px] font-bold text-slate-400`,children:`차단`}),L(`dd`,{class:`mt-1 mb-0 ml-0 text-sm font-extrabold text-teal-700`,children:H(e.blocked)})]}),L(`div`,{class:`min-w-20`,children:[L(`dt`,{class:`text-[11px] font-bold text-slate-400`,children:`차단율`}),L(`dd`,{class:`mt-1 mb-0 ml-0 text-sm font-extrabold text-slate-900`,children:dm(e.queries,e.blocked)})]})]})]},e.id))}),p&&!i?L(`p`,{class:`mt-3 mb-0 text-xs font-semibold leading-5 text-slate-500`,children:[`전체 `,H(r.length),`개 기기 중 차단 수 기준 상위 `,lm,`개를 표시하고 있습니다.`]}):null,i&&r.length>um?L(`nav`,{"aria-label":`기기별 통계 페이지`,class:`mt-4 flex flex-wrap items-center justify-between gap-3`,children:[L(`p`,{class:`m-0 text-xs font-semibold text-slate-500`,children:[H(u+1),`–`,H(d),` /`,` `,H(r.length),`개`]}),L(`div`,{class:`flex items-center gap-2`,children:[L(`button`,{class:`rounded-lg bg-white px-3 py-2 text-xs font-extrabold text-slate-700 ring-1 ring-slate-200 transition hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50`,disabled:l<=1,onClick:()=>s(e=>Math.max(1,e-1)),type:`button`,children:`이전`}),L(`span`,{"aria-current":`page`,class:`min-w-20 text-center text-xs font-extrabold text-slate-600`,children:[H(l),` / `,H(c)]}),L(`button`,{class:`rounded-lg bg-white px-3 py-2 text-xs font-extrabold text-slate-700 ring-1 ring-slate-200 transition hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50`,disabled:l>=c,onClick:()=>s(e=>Math.min(c,e+1)),type:`button`,children:`다음`})]})]}):null,n?L(`p`,{class:`mt-4 mb-0 rounded-xl border border-amber-200 bg-amber-50 p-3 text-xs font-semibold leading-5 text-amber-800`,children:[`개별 통계는 최대 `,H(t),`개 기기까지 추적합니다. 한도를 초과한 기기의 요청은 기타 기기로 합산됩니다.`]}):null]})}var hm=3600,gm=24;function _m(e,t){return e<=0?`0.0%`:`${(t/e*100).toFixed(1)}%`}function vm(e){let t=e.hourly[e.hourly.length-1];if(t?.bucketStart)return t.bucketStart;let n=e.updatedAt>0?e.updatedAt:Math.floor(Date.now()/1e3);return Math.floor(n/hm)*hm}function ym(e){let t=new Map(e.hourly.map(e=>[e.bucketStart,e])),n=vm(e)-23*hm;return Array.from({length:gm},(e,r)=>{let i=n+r*hm;return t.get(i)??{bucketStart:i,queries:0,blocked:0}})}function bm({emphasized:e=!1,label:t,value:n}){return L(`div`,{class:`rounded-xl bg-slate-50 p-4 sm:p-5`,children:[L(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:t}),L(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight ${e?`text-teal-700`:`text-slate-950`}`,children:n})]})}function xm({action:e,data:t,error:n,loading:r,onRetry:i,onSetEnabled:a,refreshing:o}){if(r&&t===null)return L(v,{children:[L(V,{eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청과 차단 현황을 확인합니다.`}),L(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:L(`p`,{class:`m-0 text-sm font-bold text-slate-500`,children:`차단 통계를 불러오는 중입니다…`})})]});if(n&&t===null)return L(v,{children:[L(V,{eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청과 차단 현황을 확인합니다.`}),L(`section`,{class:`rounded-2xl border border-amber-200 bg-amber-50 p-5 sm:p-6`,children:[L(`p`,{class:`m-0 text-sm font-extrabold text-amber-950`,children:`차단 통계를 불러오지 못했습니다.`}),L(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-amber-800`,children:n}),L(`button`,{class:`mt-4 inline-flex min-h-9 items-center justify-center rounded-xl border border-amber-300 bg-white px-3 py-2 text-xs font-extrabold text-amber-900 transition hover:bg-amber-100 focus:outline-none focus-visible:ring-4 focus-visible:ring-amber-100`,onClick:i,type:`button`,children:`다시 시도`})]})]});if(!t)return L(v,{children:[L(V,{eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청과 차단 현황을 확인합니다.`}),L(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:L(`p`,{class:`m-0 text-sm leading-6 text-slate-600`,children:`현재 SafeShield에서 통계 데이터를 제공하지 않습니다. SafeShield 0.3.15 버전 이상이 설치되어 있고, 통계 수집이 활성화되어 있는지 확인해 주세요.`})})]});let s=ym(t),c=s[s.length-1],l=s.reduce((e,t)=>({queries:e.queries+t.queries,blocked:e.blocked+t.blocked}),{queries:0,blocked:0}),u=e===`statistics-enable`||e===`statistics-disable`,d=e===`statistics-enable`||e!==`statistics-disable`&&t.enabled,f=u?d?`활성화하는 중…`:`비활성화하는 중…`:t.enabled?t.collectorRunning?`활성화 · 수집 중`:`활성화 · 시작 중`:`비활성화`;return L(v,{children:[L(V,{action:L(`div`,{class:`flex items-center gap-3 rounded-xl border border-slate-100 bg-slate-50 px-3 py-2.5`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-500`,children:L(at,{class:`size-5`})}),L(`div`,{class:`text-right`,children:[L(`p`,{class:`m-0 text-[0.68rem] font-bold text-slate-400`,children:`통계 수집`}),L(`p`,{class:`mt-1 mb-0 text-xs font-extrabold ${u||d?`text-emerald-700`:`text-slate-500`}`,role:`status`,children:f})]}),L(`button`,{"aria-checked":d,"aria-label":d?`차단 통계 수집 끄기`:`차단 통계 수집 켜기`,class:`ssh-switch-control relative inline-flex shrink-0 items-center rounded-full border-0 p-0 transition focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 ${u?`cursor-wait opacity-80`:`disabled:cursor-not-allowed disabled:opacity-50`} ${d?`bg-teal-600`:`bg-slate-300`}`,disabled:e!==null,onClick:()=>a(!t.enabled),role:`switch`,type:`button`,children:L(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 grid place-items-center shadow-sm transition-all ${d?`left-6`:`left-1`}`,children:u?L(z,{"aria-hidden":`true`,class:`size-3 animate-spin text-teal-600`}):null})})]}),eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청 원본은 저장하지 않고 숫자만 로컬 메모리에 집계합니다.`}),L(`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:L(`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 요청 원본은 저장하지 않고 요청 수와 차단 수만 로컬 메모리에 집계합니다.`}),L(`dl`,{class:`mt-2 grid gap-3 sm:grid-cols-2 xl:grid-cols-4`,children:[L(bm,{label:`최근 24시간 DNS 요청`,value:H(l.queries)}),L(bm,{emphasized:!0,label:`최근 24시간 차단`,value:H(l.blocked)}),L(bm,{label:`최근 24시간 차단율`,value:_m(l.queries,l.blocked)}),L(bm,{label:`현재 시간 차단`,value:H(c?.blocked??0)})]}),L(`div`,{class:`mt-5 rounded-xl bg-slate-50/70 p-4 sm:p-5`,children:[L(`div`,{class:`flex flex-wrap items-end justify-between gap-3`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-xs font-bold text-slate-500`,children:`최근 24시간`}),L(`h3`,{class:`mt-1 mb-0 text-base font-extrabold text-slate-950`,children:`시간대별 차단 요청`})]}),L(`span`,{class:`text-xs font-bold text-slate-500`,children:o?`갱신 중…`:`마지막 집계 ${Wt(t.updatedAt)}`})]}),L(Up,{buckets:s})]}),L(mm,{deviceLimit:t.deviceLimit,devices:t.devices,truncated:t.devicesTruncated}),L(`div`,{class:`mt-5 flex flex-wrap gap-x-5 gap-y-2 border-t border-slate-100 pt-4 text-xs leading-5 text-slate-500`,children:[L(`span`,{children:[`수집 누적 DNS 요청 `,H(t.totals.queries)]}),L(`span`,{children:[`수집 누적 차단 `,H(t.totals.blocked)]}),L(`span`,{children:[`수집 시작 `,Wt(t.startedAt)]}),L(`span`,{children:[`보존 `,H(t.retentionHours),`시간`]}),t.volatile?L(`span`,{children:`재부팅 시 초기화되는 메모리 통계`}):null]}),n?L(`p`,{class:`mt-3 mb-0 text-xs font-bold text-amber-700`,children:`최근 자동 갱신에 실패했습니다. 현재 표시된 마지막 정상 통계를 유지합니다.`}):null]})]})}var Sm=[{label:`갱신 준비`,description:`차단 목록 갱신을 준비하고 있습니다.`,stages:[`starting`,`boot_delay`,`boot_refresh`,`boot_refresh_skipped`,`scheduled_refresh`,`init`]},{label:`최신 차단 목록 확인`,description:`서버에서 최신 차단 목록 정보를 확인하고 있습니다.`,stages:[`resolve_api`]},{label:`차단 목록 다운로드`,description:`최신 차단 목록을 안전하게 다운로드하고 있습니다.`,stages:[`download_artifact`]},{label:`사용자 규칙 적용`,description:`허용·차단 사용자 규칙을 새 차단 목록에 반영하고 있습니다.`,stages:[`local_overrides`,`merge`]},{label:`보호 규칙 적용`,description:`새 차단 규칙을 설치하고 DNS 보호 서비스를 갱신하고 있습니다.`,stages:[`install`,`restart_dnsmasq`]},{label:`보호 상태 확인`,description:`새 차단 목록과 DNS 보호가 정상적으로 동작하는지 확인하고 있습니다.`,stages:[`runtime_check`,`blocklist_verify`]}],Cm=new Map;Sm.forEach((e,t)=>{e.stages.forEach(e=>Cm.set(e,t))});function wm(e,t){return e===`running`||t!==null&&Cm.has(t)}function Tm(e){let t=e===null?0:Cm.get(e)??0,n=Sm[t];return{description:n.description,label:n.label,number:t+1,total:Sm.length}}function Em(e){return(e??``).trim().toLowerCase()}function Dm(e,t){let n=Em(e);if(n.includes(`upgrade`)||n.includes(`version`))return`현재 SafeShield 버전으로는 갱신을 계속할 수 없습니다. 시스템 업데이트에서 SafeShield를 최신 버전으로 업데이트해 주세요.`;if(n.includes(`entitlement`)||n.includes(`license`)||n.includes(`unauthorized`)||n.includes(`forbidden`))return`차단 목록을 사용할 권한을 확인하지 못했습니다. SmartSafeHub 계정과 요금제 상태를 확인해 주세요.`;if(n.includes(`dnsmasq`)||n.includes(`restart`)||n.includes(`runtime`))return`DNS 보호 서비스를 정상적으로 갱신하지 못했습니다. 잠시 후 다시 갱신해 보고 문제가 계속되면 상태 점검 정보를 확인해 주세요.`;if(n.includes(`verify`)||n.includes(`sanity`))return`새 차단 목록을 적용한 뒤 보호 상태 확인을 완료하지 못했습니다. 다시 갱신해 주세요.`;if(n.includes(`download`)||n.includes(`fetch`)||n.includes(`timeout`)||n.includes(`network`)||n.includes(`wan`))return`차단 목록을 다운로드하지 못했습니다. 인터넷 연결을 확인한 뒤 다시 갱신해 주세요.`;if(n.includes(`resolve`)||n.includes(`manifest`)||n.includes(`artifact`)||n.includes(`api`))return`최신 차단 목록 정보를 확인하지 못했습니다. 인터넷 연결을 확인한 뒤 다시 갱신해 주세요.`;if(n.includes(`override`)||n.includes(`allowlist`)||n.includes(`merge`)||n.includes(`rule`))return`사용자 허용·차단 규칙을 새 차단 목록에 반영하지 못했습니다. 사용자 규칙을 확인한 뒤 다시 갱신해 주세요.`;if(n.includes(`install`)||n.includes(`write`)||n.includes(`space`))return`새 차단 규칙을 장치에 적용하지 못했습니다. 저장 공간과 SafeShield 상태를 확인한 뒤 다시 갱신해 주세요.`;let r=Tm(t);return r.number===2?`최신 차단 목록 정보를 확인하는 중 문제가 발생했습니다. 인터넷 연결을 확인한 뒤 다시 갱신해 주세요.`:r.number===3?`차단 목록을 다운로드하는 중 문제가 발생했습니다. 인터넷 연결을 확인한 뒤 다시 갱신해 주세요.`:r.number===4?`사용자 규칙을 적용하는 중 문제가 발생했습니다. 사용자 규칙을 확인한 뒤 다시 갱신해 주세요.`:r.number===5?`새 보호 규칙을 적용하는 중 문제가 발생했습니다. 잠시 후 다시 갱신해 주세요.`:r.number===6?`새 차단 목록의 보호 상태를 확인하지 못했습니다. 다시 갱신해 주세요.`:`차단 목록 갱신을 완료하지 못했습니다. 잠시 후 다시 갱신해 주세요.`}var Om=`https://www.smartsafehub.com/pricing/`;function km({value:e,trueLabel:t=`정상`,falseLabel:n=`확인 필요`}){return L(`span`,{class:`inline-flex items-center gap-2 text-sm font-extrabold ${e?`text-emerald-700`:`text-slate-500`}`,children:[L(`span`,{class:`size-2 rounded-full ${e?`bg-emerald-500`:`bg-slate-300`}`}),e?t:n]})}function Am(e){return!e.enabled||e.status===`disabled`?`disabled`:e.status===`running`?`refreshing`:e.status===`error`||e.health.overall===`error`||e.issueCounts.errors>0?`error`:e.status===`paused`?`paused`:e.active&&e.runtime.dnsmasqRunning&&e.runtime.dnsRuntimeOk?`protecting`:`attention`}function jm(e){return!e.enabled||e.status===`disabled`?`비활성화`:e.active&&e.runtime.dnsmasqRunning&&e.runtime.dnsRuntimeOk?`보호 중`:e.status===`paused`?`일시 중지`:`확인 필요`}function Mm(e){let t=Am(e);return t===`error`&&jm(e)===`보호 중`&&e.runtime.lastErrorCode?`갱신 실패`:{disabled:`비활성화`,refreshing:`갱신 중`,protecting:`보호 중`,paused:`일시 중지`,error:`오류`,attention:`확인 필요`}[t]}function Nm(e){return e?`자동 최적화`:`확인되지 않음`}function Pm(e){let t=qt(e);return t===null?`확인되지 않음`:L(Dr,{timestamp:t})}function Fm(e){let t=Am(e);return t===`protecting`?e.blocklist.validLineCount>0?`${H(e.blocklist.validLineCount)}개 도메인 차단 규칙으로 DNS를 보호하고 있습니다.`:e.blocklist.installed?`차단 목록이 설치되어 있으며 DNS 보호가 정상적으로 동작하고 있습니다.`:`SafeShield 서비스와 DNS 런타임이 정상적으로 동작하고 있습니다.`:t===`refreshing`?`차단 목록을 최신 상태로 갱신하고 있습니다.`:t===`disabled`?`SafeShield DNS 보호가 비활성화되어 있습니다.`:t===`paused`?`SafeShield 작업이 일시 중지되어 있습니다.`:t===`error`?jm(e)===`보호 중`?`기존 DNS 보호는 유지되고 있지만 최근 차단 목록 갱신을 완료하지 못했습니다.`:`SafeShield 보호 상태에 문제가 발생했습니다. 아래 오류 내용을 확인해 주세요.`:e.active?e.runtime.dnsmasqRunning?e.runtime.dnsRuntimeOk?e.summary.message:`SafeShield DNS 런타임 상태를 확인해야 합니다.`:`dnsmasq가 실행 중이 아니어서 DNS 보호 상태를 확인해야 합니다.`:`SafeShield가 활성화되어 있지만 서비스가 실행 중이 아닙니다.`}function Im(e){return{ok:`정상`,warning:`경고`,error:`오류`,unavailable:`사용 불가`}[e]??e}function Lm({data:e}){let t=Am(e);return L(`span`,{class:`inline-flex items-center rounded-full px-3 py-1.5 text-xs font-extrabold ring-1 ring-inset ${t===`error`?`bg-red-50 text-red-700 ring-red-200`:t===`disabled`?`bg-slate-100 text-slate-700 ring-slate-200`:t===`refreshing`?`bg-blue-50 text-blue-700 ring-blue-200`:t===`paused`||t===`attention`?`bg-amber-50 text-amber-800 ring-amber-200`:`bg-emerald-50 text-emerald-700 ring-emerald-200`}`,children:Mm(e)})}function Rm({failed:e,step:t,total:n}){let r=2*Math.PI*17,i=r*.22,a=r-i;return L(`span`,{"aria-label":e?`갱신 실패 · ${t}/${n} 단계`:`갱신 ${t}/${n} 단계`,class:`ssh-safeshield-refresh-donut`,"data-state":e?`error`:`active`,role:`progressbar`,"aria-valuemin":0,"aria-valuemax":n,"aria-valuenow":t,children:[L(`svg`,{"aria-hidden":`true`,class:e?`ssh-safeshield-refresh-loader-svg ssh-safeshield-refresh-loader-svg-error`:`ssh-safeshield-refresh-loader-svg`,viewBox:`0 0 48 48`,children:[L(`circle`,{class:`ssh-safeshield-refresh-loader-track`,cx:`24`,cy:`24`,fill:`none`,r:17,stroke:`currentColor`,strokeWidth:2.5}),e?null:L(`circle`,{class:`ssh-safeshield-refresh-loader-arc`,cx:`24`,cy:`24`,fill:`none`,r:17,stroke:`currentColor`,strokeDasharray:`${i} ${a}`,strokeLinecap:`round`,strokeWidth:3.5})]}),L(`span`,{class:`ssh-safeshield-refresh-donut-label`,children:e?`!`:`${t}/${n}`})]})}function zm({data:e}){let t=wm(e.status,e.stage),n=Am(e)===`error`;if(!t&&!n)return null;let r=Tm(e.stage),i=n?Dm(e.runtime.lastErrorCode,e.stage):null;return L(`div`,{"aria-live":n?`assertive`:`polite`,class:`ssh-safeshield-refresh`,"data-state":n?`error`:`active`,role:n?`alert`:`status`,children:[L(Rm,{failed:n,step:r.number,total:r.total}),L(`div`,{class:`ssh-safeshield-refresh-copy`,children:[L(`p`,{class:`ssh-safeshield-refresh-kicker`,children:n?`갱신 실패 · ${r.number}/${r.total}`:`갱신 ${r.number}/${r.total}`}),L(`p`,{class:`ssh-safeshield-refresh-title`,children:n?`${r.label} 단계에서 문제가 발생했습니다.`:r.label}),L(`p`,{class:`ssh-safeshield-refresh-description`,children:i??r.description}),n&&e.runtime.lastErrorCode?L(`p`,{class:`ssh-safeshield-refresh-error-code`,children:[`오류 코드: `,L(`code`,{children:e.runtime.lastErrorCode})]}):null]})]})}function Bm({label:e,value:t}){return L(`div`,{class:`bg-white px-5 py-4 sm:px-6`,children:[L(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.14em] text-slate-400`,children:e}),L(`dd`,{class:`mt-2 mb-0 ml-0 text-sm font-extrabold text-slate-950`,title:typeof t==`string`?t:void 0,children:t})]})}function Vm(e){return e?.trim().toUpperCase()||`FREE`}function Hm(e){return e===`FREE`?`free`:e===`PRO`?`pro`:e===`ULTIMATE`?`ultimate`:`paid`}function Um({compact:e=!1,plan:t}){let n=Hm(t),r=n!==`free`;return L(`span`,{class:`ssh-safeshield-plan-badge`,"data-compact":e?`true`:`false`,"data-tier":n,title:r?`${t} 멤버십`:`무료 플랜`,children:[L(`span`,{"aria-hidden":`true`,class:`ssh-safeshield-plan-badge-mark`,children:r?`✦`:`•`}),L(`span`,{children:t})]})}function Wm({plan:e}){let t=Hm(e)===`free`;return L(`div`,{class:`bg-white px-5 py-4 sm:px-6`,children:[L(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.14em] text-slate-400`,children:`Plan`}),L(`dd`,{class:`mt-2 mb-0 ml-0 flex min-w-0 flex-wrap items-center gap-2`,children:[L(Um,{plan:e}),t?L(`span`,{class:`ssh-safeshield-plan-caption`,children:`기본 플랜`}):null]})]})}function Gm(){return L(`div`,{class:`ssh-safeshield-upgrade-card`,children:[L(`span`,{"aria-hidden":`true`,class:`ssh-safeshield-upgrade-mark`,children:`✦`}),L(`div`,{class:`ssh-safeshield-upgrade-copy`,children:[L(`div`,{class:`ssh-safeshield-upgrade-meta`,children:[L(`span`,{children:`SmartSafeHub Membership`}),L(`span`,{class:`ssh-safeshield-upgrade-soon`,children:`Coming soon`})]}),L(`strong`,{children:`더 강력한 보호 기능을 준비하고 있습니다.`}),L(`span`,{children:`프리미엄 보호와 확장된 관리 기능을 미리 확인해 보세요.`})]}),L(`a`,{class:`ssh-safeshield-upgrade-action`,href:Om,rel:`noopener noreferrer`,target:`_blank`,children:[`요금제 보기`,L(`span`,{"aria-hidden":`true`,children:`→`})]})]})}function Km({children:e,eyebrow:t,icon:n,title:r}){return L(`article`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6 xl:p-4 2xl:p-5`,children:[L(`div`,{class:`flex min-w-0 items-start justify-between gap-4 xl:gap-2`,children:[L(`div`,{children:[L(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.16em] text-slate-400`,children:t}),L(`h3`,{class:`mt-2 mb-0 text-lg font-black tracking-tight text-slate-950 xl:text-base 2xl:text-lg`,children:r})]}),L(`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})]}),L(`dl`,{class:`mt-5 mb-0 grid gap-3`,children:e})]})}function qm({label:e,value:t}){return L(`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:[L(`dt`,{class:`text-sm font-semibold text-slate-500`,children:e}),L(`dd`,{class:`m-0 min-w-0 break-words text-right text-sm font-extrabold text-slate-950`,children:t})]})}function Jm({error:e,message:t,onDismiss:n}){if(!e&&!t)return null;let r=e!==null;return L(`div`,{"aria-live":r?`assertive`:`polite`,class:`mt-4 flex flex-col items-start gap-3 rounded-2xl border px-4 py-3 text-sm sm:flex-row sm:justify-between ${r?`border-red-200 bg-red-50 text-red-900`:`border-emerald-200 bg-emerald-50 text-emerald-900`}`,role:r?`alert`:`status`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3`,children:[L(r?B:ut,{class:`mt-0.5 size-5 shrink-0`}),L(`p`,{class:`m-0 font-semibold leading-6`,children:e??t})]}),L(`button`,{"aria-label":`알림 닫기`,class:`shrink-0 rounded-lg border-0 bg-transparent px-2 py-1 font-black text-current opacity-60 transition hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-current`,onClick:n,type:`button`,children:`×`})]})}function Ym({action:e,actionError:t,actionMessage:n,data:r,error:i,loading:a,statistics:o,statisticsError:s,statisticsLoading:c,statisticsRefreshing:l,onDismissFeedback:u,onRefreshBlocklist:d,onRetry:f,onRetryStatistics:p,onSetEnabled:m,onSetStatisticsEnabled:h}){let[g,_]=F(null);if(I(()=>{r?.available&&r.blocklist.validLineCount>0&&!wm(r.status,r.stage)&&_(r.blocklist.validLineCount)},[r?.available,r?.blocklist.validLineCount,r?.stage,r?.status]),a)return L(zn,{});if(i)return L(Bn,{message:i,onRetry:f});if(!r)return L(zn,{});if(!r.available)return L(`section`,{class:`rounded-2xl border border-amber-200 bg-amber-50 p-5 sm:p-6`,children:[L(`span`,{class:`grid size-11 place-items-center rounded-xl bg-amber-100 text-amber-800`,children:L(B,{class:`size-6`})}),L(`h2`,{class:`mt-5 mb-0 text-xl font-extrabold text-amber-950`,children:`SafeShield 상태 API를 찾을 수 없습니다`}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-amber-900`,children:`safeshield 패키지와 rpcd ucode 플러그인이 설치되어 있는지 확인해 주세요.`})]});let y=r.enabled,b=Vm(r.entitlement.plan),x=r.status===`running`,S=e!==null,C=(wm(r.status,r.stage)||Am(r)===`error`)&&r.blocklist.validLineCount===0&&g!==null?g:r.blocklist.validLineCount,w=y?e===`disable`?`끄는 중…`:`보호 끄기`:e===`enable`?`켜는 중…`:`보호 켜기`;function T(){(!y||window.confirm(`보호를 끄면, 현재 차단 목록이 제거되어 위험에 노출될 수 있습니다. 그래도 계속하시겠습니까?`))&&m(!y)}return L(v,{children:[L(V,{eyebrow:`Protection`,title:`SafeShield 보호 상태`,description:`DNS 보호 상태와 적용 중인 차단 목록을 확인합니다.`}),L(`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:[L(`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:[L(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between xl:flex-col 2xl:flex-row`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:L(Ue,{class:`size-5`})}),L(`div`,{class:`min-w-0`,children:[L(`div`,{class:`flex flex-wrap items-center gap-2`,children:[L(`h3`,{class:`m-0 text-base font-black text-slate-950`,children:`현재 보호 상태`}),L(Lm,{data:r})]}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:Fm(r)})]})]}),L(`div`,{class:`flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap xl:justify-end`,children:[L(`button`,{class:`inline-flex min-h-11 items-center justify-center gap-2 rounded-xl border px-4 py-2.5 text-sm font-extrabold transition focus:outline-none focus-visible:ring-4 disabled:cursor-not-allowed disabled:opacity-50 ${r.enabled?`border-red-200 bg-white text-red-700 hover:border-red-300 hover:bg-red-50 focus-visible:ring-red-100`:`border-teal-700 bg-teal-700 text-white hover:border-teal-800 hover:bg-teal-800 focus-visible:ring-teal-100`}`,disabled:S||x,onClick:T,type:`button`,children:[L(rt,{class:`size-4 ${e===`enable`||e===`disable`?`animate-pulse`:``}`}),w]}),L(`button`,{class:`inline-flex min-h-11 items-center justify-center gap-2 rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-extrabold text-slate-700 transition hover:border-teal-300 hover:bg-teal-50 hover:text-teal-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-not-allowed disabled:opacity-50`,disabled:!r.enabled||S||x,onClick:d,type:`button`,children:[L(it,{class:`size-4 ${e===`refresh`||x?`animate-bounce`:``}`}),e===`refresh`?`시작 중…`:x?`갱신 중…`:r.status===`error`?`다시 갱신`:`지금 갱신`]})]})]}),L(zm,{data:r}),L(`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:[L(Bm,{label:`SafeShield`,value:r.version??`확인되지 않음`}),L(Bm,{label:`Blocklist`,value:`${H(C)}개 도메인`}),L(Bm,{label:`Last refresh`,value:L(Dr,{timestamp:r.timestamps.lastSuccess})}),L(Wm,{plan:b})]}),b===`FREE`?L(Gm,{}):null]}),L(`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:[L(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[L(`div`,{class:`flex min-w-0 items-center gap-3`,children:[L(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-500`,children:L(at,{class:`size-5`})}),L(`div`,{children:[L(`p`,{class:`m-0 text-[0.65rem] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Protection data`}),L(`h3`,{class:`mt-1 mb-0 text-sm font-black text-slate-950`,children:`보호 데이터`})]})]}),L(`span`,{"aria-label":`보호 데이터 상태: ${r.artifact.resolved&&r.blocklist.installed&&r.blocklist.verificationOk?`최신`:`확인 필요`}`,class:`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-extrabold ${r.artifact.resolved&&r.blocklist.installed&&r.blocklist.verificationOk?`bg-emerald-50 text-emerald-700`:`bg-amber-50 text-amber-700`}`,children:[r.artifact.resolved&&r.blocklist.installed&&r.blocklist.verificationOk?L(ut,{class:`size-3.5 shrink-0`}):L(B,{class:`size-3.5 shrink-0`}),r.artifact.resolved&&r.blocklist.installed&&r.blocklist.verificationOk?`최신`:`확인 필요`]})]}),L(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-500`,children:`SafeShield가 기기 성능에 맞춰 사용하는 보호 목록의 구성과 최신 상태를 확인합니다.`}),L(`dl`,{class:`mt-3 mb-0 grid grid-cols-1 gap-2 sm:grid-cols-2`,children:[L(`div`,{class:`min-w-0 rounded-lg border border-slate-200 bg-white px-3 py-2.5`,children:[L(`dt`,{class:`text-xs font-bold text-slate-500`,children:`기기 최적화`}),L(`dd`,{class:`mt-1 mb-0 text-sm font-extrabold text-slate-950`,children:Nm(r.artifact.tier)})]}),L(`div`,{class:`min-w-0 rounded-lg border border-slate-200 bg-white px-3 py-2.5`,children:[L(`dt`,{class:`text-xs font-bold text-slate-500`,children:`마지막 업데이트`}),L(`dd`,{class:`mt-1 mb-0 text-sm font-extrabold leading-5 text-slate-950`,children:Pm(r.artifact.version)})]})]}),L(`section`,{"aria-label":`보호 데이터 세부 정보`,class:`mt-3 border-t border-slate-200 pt-3`,children:[L(`h4`,{class:`m-0 text-xs font-extrabold text-slate-600`,children:`세부 정보`}),L(`dl`,{class:`mt-3 mb-0 grid gap-2`,children:[L(qm,{label:`데이터 프로필`,value:r.artifact.tier||`확인되지 않음`}),L(qm,{label:`데이터 버전`,value:r.artifact.version||`확인되지 않음`})]})]})]})]}),L(Jm,{error:t,message:n,onDismiss:u}),L(xm,{action:e,data:o,error:s,loading:c,onRetry:p,onSetEnabled:h,refreshing:l}),L(`section`,{class:`mt-7`,children:[L(V,{description:`DNS 보호 런타임, 차단 목록, 갱신 일정과 상태 점검 결과를 한곳에서 확인합니다.`,eyebrow:`Protection details`,title:`보호 구성`}),L(`div`,{class:`grid min-w-0 grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4`,children:[L(Km,{eyebrow:`Runtime`,icon:L(ut,{class:`size-5`}),title:`DNS 보호 런타임`,children:[L(qm,{label:`SafeShield 서비스`,value:L(km,{falseLabel:`중지됨`,trueLabel:`동작 중`,value:r.active})}),L(qm,{label:`DNS 서비스`,value:L(km,{falseLabel:`중지됨`,value:r.runtime.dnsmasqRunning})}),L(qm,{label:`SafeShield DNS 연동`,value:L(km,{value:r.runtime.dnsRuntimeOk})}),L(qm,{label:`차단 목록 갱신`,value:L(km,{falseLabel:`중지됨`,value:r.runtime.refreshdRunning})})]}),L(Km,{eyebrow:`Blocklist`,icon:L(at,{class:`size-5`}),title:`차단 목록`,children:[L(qm,{label:`적용 규칙`,value:`${H(r.blocklist.validLineCount)}개 도메인`}),L(qm,{label:`파일 크기`,value:Vt(r.blocklist.fileSizeKb*1024)}),L(qm,{label:`설치 상태`,value:L(km,{falseLabel:`미설치`,trueLabel:`설치됨`,value:r.blocklist.installed})}),L(qm,{label:`검증 상태`,value:L(km,{falseLabel:`확인 필요`,value:r.blocklist.verificationOk})})]}),L(Km,{eyebrow:`Refresh schedule`,icon:L(lt,{class:`size-5`}),title:`차단 목록 갱신`,children:[L(qm,{label:`마지막 성공`,value:L(Dr,{timestamp:r.timestamps.lastSuccess})}),L(qm,{label:`다음 갱신`,value:Wt(r.timestamps.nextRefreshAt)}),L(qm,{label:`갱신 주기`,value:Jt(r.timestamps.refreshIntervalS)}),L(qm,{label:`최근 결과`,value:r.runtime.lastResult||(x?`갱신 중`:`확인되지 않음`)})]}),L(Km,{eyebrow:`Health`,icon:L(Ue,{class:`size-5`}),title:`상태 점검`,children:[L(qm,{label:`전체 상태`,value:Im(r.health.overall)}),L(qm,{label:`경고`,value:L(`span`,{class:`text-amber-700`,children:H(r.issueCounts.warnings)})}),L(qm,{label:`오류`,value:L(`span`,{class:`text-red-700`,children:H(r.issueCounts.errors)})}),L(qm,{label:`최근 오류 코드`,value:r.runtime.lastErrorCode||`없음`})]})]})]})]})}var Xm=Math.floor(Date.now()/1e3),Zm=3600,Qm={generatedAt:Xm,devices:[{id:`demo-phone`,hostname:`거실 스마트폰`,mac:`02:00:00:00:00:01`,ipv4Address:`192.0.2.101`,connection:`wifi`,online:!0,leaseActive:!0,leaseExpiresAt:Xm+6*Zm,interface:`wlan0`,ssid:`SmartSafeHub`,radio:`radio0`,band:`5g`,bandLabel:`5 GHz`,signalDbm:-43,inactiveMs:250,connectedSeconds:8300},{id:`demo-laptop`,hostname:`작업용 노트북`,mac:`02:00:00:00:00:02`,ipv4Address:`192.0.2.102`,connection:`wifi`,online:!0,leaseActive:!0,leaseExpiresAt:Xm+8*Zm,interface:`wlan0`,ssid:`SmartSafeHub`,radio:`radio0`,band:`5g`,bandLabel:`5 GHz`,signalDbm:-57,inactiveMs:720,connectedSeconds:29200},{id:`demo-tv`,hostname:`거실 TV`,mac:`02:00:00:00:00:03`,ipv4Address:`192.0.2.103`,connection:`ethernet`,online:!0,leaseActive:!0,leaseExpiresAt:Xm+12*Zm,interface:`lan1`,ssid:null,radio:null,band:null,bandLabel:null,signalDbm:null,inactiveMs:null,connectedSeconds:14e4},{id:`demo-tablet`,hostname:`태블릿`,mac:`02:00:00:00:00:04`,ipv4Address:`192.0.2.104`,connection:`wifi`,online:!1,leaseActive:!1,leaseExpiresAt:null,interface:null,ssid:`SmartSafeHub`,radio:`radio1`,band:`2g`,bandLabel:`2.4 GHz`,signalDbm:null,inactiveMs:null,connectedSeconds:null}],totals:{known:4,online:3,wireless:2,ethernet:1,offline:1}},$m={schema:1,scope:`current_boot`,volatile:!0,maxEvents:100,cloud:{enabled:!1,phase:`disabled`,eligible:!1,plan:`free`,retentionDays:0,pendingEvents:0,lastAttemptAt:0,lastSuccessAt:0,lastUploadedCount:0,lastErrorCode:null,nextSyncAt:0},events:[{schema:1,eventId:`demo-event-1`,eventType:`safeshield.blocklist.updated`,severity:`success`,occurredAt:Xm-7200,deviceUuid:null,source:`safeshield`,metadata:{}},{schema:1,eventId:`demo-event-2`,eventType:`system.boot`,severity:`info`,occurredAt:Xm-86400,deviceUuid:null,source:`system`,metadata:{}}]},eh={device:{hostname:`SmartRouter`,model:`ipTIME AX3000SM`,boardName:`iptime,ax3000sm`},software:{distribution:`OpenWrt`,version:`25.12`,revision:`demo`,kernel:`6.12`},runtime:{uptime:86400,localtime:Xm,load:[.08,.12,.05],memory:{total:268435456,free:83886080,shared:4194304,buffered:41943040,available:171966464,cached:41943040}},network:{available:!0,up:!0,protocol:`dhcp`,ipv4Address:`198.51.100.20`},activityHistory:$m},th={lan:{address:`192.168.1.1`,prefixLength:24,netmask:`255.255.255.0`,subnet:`192.168.1.0/24`},dhcp:{enabled:!0,start:`192.168.1.100`,end:`192.168.1.249`,leaseTime:`12h`},wan:{connected:!0,protocol:`dhcp`,address:`198.51.100.20`,prefixLength:24,subnet:`198.51.100.0/24`},conflict:{detected:!1,wanSubnet:null},recommendation:null},nh={available:!0,version:`0.3.25`,enabled:!0,active:!0,status:`idle`,stage:null,summary:{label:`보호 중`,message:`DNS 보호가 정상적으로 동작하고 있습니다.`,severity:`info`},runtime:{refreshdRunning:!0,dnsmasqRunning:!0,dnsRuntimeOk:!0,lastResult:`success`,lastErrorCode:null},entitlement:{plan:`free`,status:`active`},artifact:{resolved:!0,tier:`light`,version:`20261010T010000Z`,uniqueDomains:48e3,rules:48e3},localOverrides:{enabled:!0,allowlistPath:`/etc/safeshield/allowlist`,blocklistPath:`/etc/safeshield/blocklist`},blocklist:{installed:!0,validLineCount:48e3,fileSizeKb:4700,verificationOk:!0},health:{overall:`ok`},issueCounts:{warnings:0,errors:0},timestamps:{lastAttempt:Xm-2*Zm,lastSuccess:Xm-2*Zm,lastFailure:0,lastLocalApply:Xm-2*Zm,lastLocalApplyFailure:0,nextRefreshAt:Xm+21*Zm,refreshIntervalS:82800}},rh=Math.floor(Xm/Zm)*Zm,ih=[{queries:351,blocked:18},{queries:297,blocked:15},{queries:243,blocked:11},{queries:216,blocked:9},{queries:203,blocked:8},{queries:230,blocked:10},{queries:324,blocked:17},{queries:567,blocked:35},{queries:783,blocked:54},{queries:864,blocked:61},{queries:756,blocked:50},{queries:702,blocked:43},{queries:635,blocked:36},{queries:580,blocked:30},{queries:608,blocked:33},{queries:689,blocked:40},{queries:837,blocked:54},{queries:1026,blocked:73},{queries:1256,blocked:99},{queries:1458,blocked:125},{queries:1512,blocked:135},{queries:1323,blocked:111},{queries:1026,blocked:75},{queries:648,blocked:39}],ah=Array.from({length:24},(e,t)=>{let n=rh-(23-t)*Zm,r=ih[new Date(n*1e3).getHours()];return{bucketStart:n,queries:r.queries,blocked:r.blocked}}),oh={enabled:!0,collectorRunning:!0,available:!0,schemaVersion:1,volatile:!0,startedAt:Xm-604800,updatedAt:Xm-30,retentionHours:168,totals:{queries:ah.reduce((e,t)=>e+t.queries,0),blocked:ah.reduce((e,t)=>e+t.blocked,0)},hourly:ah,deviceLimit:128,devicesTruncated:!1,devices:[{id:`demo-phone`,mac:`02:00:00:00:00:01`,ip:`192.0.2.101`,hostname:`거실 스마트폰`,identified:!0,queries:3400,blocked:470},{id:`demo-laptop`,mac:`02:00:00:00:00:02`,ip:`192.0.2.102`,hostname:`작업용 노트북`,identified:!0,queries:2900,blocked:345},{id:`demo-tv`,mac:`02:00:00:00:00:03`,ip:`192.0.2.103`,hostname:`거실 TV`,identified:!0,queries:1200,blocked:185}]},sh={schema:1,generatedAt:Xm,overall:`ok`,summary:{message:`모든 주요 항목이 정상입니다.`,total:3,ok:3,warning:0,critical:0,unknown:0},metrics:{memoryAvailablePercent:64,load1m:.08,overlayAvailablePercent:80},checks:[],reporter:{enabled:!1,eligible:!1,plan:`free`,entitlementStatus:`active`,lastReportAt:0,nextReportAt:0,lastResult:``,lastErrorCode:null}},ch={phase:`idle`,updateCount:0,packages:[],lastCheckAt:Xm-Zm,lastInstallAt:null,lastError:null,releaseNotes:[],releaseNotesComplete:!0,settings:{channel:`stable`,checkEnabled:!0,checkIntervalSeconds:21600,autoInstall:!0,autoInstallTime:`03:00`,repositoryHost:`example.invalid`,updatePackage:`luci-app-smartsafehub`}},lh={phase:`idle`,source:null,lastCheckAt:Xm-Zm,lastPrepareAt:null,lastError:null,prepared:null,current:{deviceCode:`iptime-ax3000sm`,boardName:`iptime,ax3000sm`,buildId:`demo`,releaseVersion:`1.0.2`,openwrtVersion:`25.12`,metadataAvailable:!0},updateAvailable:!1,release:null,settings:{channel:`stable`,apiBaseUrl:`https://www.smartsafehub.com`,checkIntervalSeconds:21600,autoInstall:!1}},uh={configuration:{protocol:`dhcp`,supported:!0,pppoe:{username:``,passwordConfigured:!1},static:{address:null,prefixLength:null,netmask:null,gateway:null,dns:[]}},status:{connected:!0,pending:!1,protocol:`dhcp`,device:`wan`,address:`198.51.100.20`,prefixLength:24,gateway:`198.51.100.1`,dns:[`1.1.1.1`,`8.8.8.8`],uptimeSeconds:86400}},dh={networks:[{section:`demo_5g`,device:`radio0`,band:`5g`,bandLabel:`5 GHz`,ssid:`SmartSafeHub`,enabled:!0,runtimeUp:!0,security:`sae-mixed`,securityRaw:`sae-mixed`,passwordConfigured:!0,channel:`36`,networks:[`lan`],clientCount:2},{section:`demo_2g`,device:`radio1`,band:`2g`,bandLabel:`2.4 GHz`,ssid:`SmartSafeHub-2G`,enabled:!0,runtimeUp:!0,security:`sae-mixed`,securityRaw:`sae-mixed`,passwordConfigured:!0,channel:`6`,networks:[`lan`],clientCount:0}],totalClients:2,guest:{supported:!0,configured:!0,enabled:!0,ssid:`SmartSafeHub-Guest`,passwordConfigured:!0,subnet:`192.0.2.0/24`,radio:`radio1`}},fh={allow:[`trusted.example`,`updates.example`],block:[`ads.example`,`tracker.example`],counts:{allow:2,block:2,total:4},safeshieldEnabled:!0,applyLocalOverrides:!0},ph=async e=>({ssid:dh.networks.find(t=>t.section===e)?.ssid??`SmartSafeHub`,security:`sae-mixed`,password:`demo-only-2026`}),mh=async()=>({ssid:`SmartSafeHub-Guest`,security:`sae-mixed`,password:`guest-demo-only-2026`}),hh=new Set(Le);function gh(){let e=ai(),t=li(),n=hh.has(e)?e:`home`,[r,i]=F(``);I(()=>{hh.has(e)||(window.history.replaceState(null,``,`${window.location.pathname}${window.location.search}#home`),i(`해당 기능은 현재 데모에서 제공하지 않습니다.`))},[e]);let a=()=>i(`데모에서는 실제 공유기 설정을 변경할 수 없습니다.`),o=()=>i(``),s=()=>i(`예시 데이터가 표시되고 있습니다. 실제 공유기에는 연결되지 않습니다.`),c=async()=>(a(),!1),l={action:null,actionError:null,data:lh,error:null,loading:!1,message:null,reconnecting:!1,uploadProgress:null,check:a,discard:c,dismissFeedback:o,install:c,prepare:a,refresh:s,upload:c},u;switch(n){case`activity`:u=L(hn,{actionError:null,actionMessage:null,data:$m,error:null,loading:!1,onDismissActionFeedback:o,onRetry:s,onSetCloudSync:a,savingCloudSync:!1});break;case`network`:u=L(`div`,{class:`min-w-0`,children:[L(ui,{active:t.network,label:`네트워크 설정 메뉴`,tabs:[{id:`internet`,label:`인터넷 연결`,href:`#network`},{id:`lan`,label:`LAN 및 DHCP`,href:`#network/lan`}]}),L(`div`,{"aria-label":`인터넷 연결 설정`,"aria-labelledby":`internet-tab`,hidden:t.network!==`internet`,id:`internet-panel`,role:`tabpanel`,children:L(er,{action:null,data:uh,error:null,feedback:null,loading:!1,onDismissFeedback:o,onReconnect:a,onRetry:s,onSave:c})}),L(`div`,{"aria-label":`내부 네트워크 설정`,"aria-labelledby":`lan-tab`,hidden:t.network!==`lan`,id:`lan-panel`,role:`tabpanel`,children:L(dr,{action:null,data:th,error:null,feedback:null,loading:!1,onApplyRecommendation:c,onDismissFeedback:o,onRetry:s,onSave:c})})]});break;case`wifi`:u=L(Er,{data:dh,error:null,feedback:null,loading:!1,updatingSection:null,onDismissFeedback:o,onRetry:s,onUpdate:c,onUpdateGuest:c,onLoadQr:ph,onLoadGuestQr:mh});break;case`rules`:u=L(jr,{action:null,data:fh,error:null,feedback:null,loading:!1,onAddRule:c,onDeleteRule:c,onDismissFeedback:o,onRetry:s});break;case`system`:u=L(ni,{action:null,actionError:null,data:ch,error:null,firmware:l,loading:!1,message:null,onCheck:a,onDismissFeedback:o,onInstall:a,onRetry:s,onSaveSettings:c});break;case`devices`:u=L(Si,{data:Qm,error:null,loading:!1,onRetry:s});break;case`safeshield`:u=L(Ym,{action:null,actionError:null,actionMessage:null,data:nh,error:null,loading:!1,statistics:oh,statisticsError:null,statisticsLoading:!1,statisticsRefreshing:!1,onDismissFeedback:o,onRefreshBlocklist:a,onRetry:s,onRetryStatistics:s,onSetEnabled:a,onSetStatisticsEnabled:a});break;default:u=L(cm,{accountRegistered:!1,activity:$m,activityError:null,activityLoading:!1,data:eh,devices:Qm,devicesError:null,devicesLoading:!1,error:null,firmware:lh,firmwareError:null,firmwareLoading:!1,health:sh,healthError:null,healthLoading:!1,lan:th,lanError:null,lanLoading:!1,loading:!1,onRetry:s,safeshield:nh,safeshieldError:null,safeshieldLoading:!1,statistics:oh,statisticsError:null,statisticsLoading:!1,statisticsRefreshing:!1,updates:ch,updatesError:null,updatesLoading:!1})}return L(Lt,{accountRegistered:!1,demo:!0,loading:!1,onRefresh:s,refreshing:!1,route:n,updateCount:0,children:[L(`div`,{role:`status`,class:`mb-5 flex flex-wrap items-center justify-between gap-3 rounded-xl border border-teal-200 bg-teal-50 px-4 py-3 text-sm font-semibold text-teal-800`,children:[L(`span`,{children:`체험 모드 · 표시되는 기기와 보호 통계는 모두 예시입니다.`}),L(`a`,{class:`font-extrabold underline`,href:`/devices/`,children:`지원 기기 살펴보기 →`})]}),r?L(`div`,{role:`alert`,class:`mb-5 flex items-center justify-between gap-3 rounded-xl border border-amber-300 bg-amber-50 p-4 text-sm font-semibold text-amber-900`,children:[r,L(`button`,{"aria-label":`안내 닫기`,class:`font-black`,onClick:o,type:`button`,children:`✕`})]}):null,u]})}var _h=document.getElementById(`smartsafehub-demo-root`);if(_h){let e=_h.attachShadow({mode:`open`}),t=document.createElement(`link`);t.rel=`stylesheet`,t.href=_h.dataset.demoCss||`/static/router-demo/demo.css`,e.appendChild(t);let n=document.createElement(`div`);n.className=`smartsafehub-shadow-root`,e.appendChild(n),oe(L(gh,{}),n)} \ No newline at end of file +`):e}function _p(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 vp(e,t){let n=e.chart.ctx,{body:r,footer:i,title:a}=e,{boxWidth:o,boxHeight:s}=t,c=oc(t.bodyFont),l=oc(t.titleFont),u=oc(t.footerFont),d=a.length,f=i.length,p=r.length,m=ac(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,X(e.title,y),n.font=c.string,X(e.beforeBody.concat(e.afterBody),y),v=t.displayColors?o+2+t.boxPadding:0,X(r,e=>{X(e.before,y),X(e.lines,y),X(e.after,y)}),v=0,n.font=u.string,X(e.footer,y),n.restore(),g+=m.width,{width:g,height:h}}function yp(e,t){let{y:n,height:r}=t;return ne.height-r/2?`bottom`:`center`}function bp(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 xp(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`),bp(l,e,t,n)&&(l=`center`),l}function Sp(e,t,n){let r=n.yAlign||t.yAlign||yp(e,n);return{xAlign:n.xAlign||t.xAlign||xp(e,t,n,r),yAlign:r}}function Cp(e,t){let{x:n,width:r}=e;return t===`right`?n-=r:t===`center`&&(n-=r/2),n}function wp(e,t,n){let{y:r,height:i}=e;return t===`top`?r+=n:r-=t===`bottom`?i+n:i/2,r}function Tp(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}=ic(o),m=Cp(t,s),h=wp(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:Jo(m,0,r.width-t.width),y:Jo(h,0,r.height-t.height)}}function Ep(e,t,n){let r=ac(n.padding);return t===`center`?e.x+e.width/2:t===`right`?e.x+e.width-r.right:e.x+r.left}function Dp(e){return hp([],gp(e))}function Op(e,t,n){return lc(e,{tooltip:t,tooltipItems:n,type:`tooltip`})}function kp(e,t){let n=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return n?e.override(n):e}var Ap={beforeTitle:to,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=kp(n,e);hp(t.before,gp(jp(i,`beforeLabel`,this,e))),hp(t.lines,jp(i,`label`,this,e)),hp(t.after,gp(jp(i,`afterLabel`,this,e))),r.push(t)}),r}getAfterBody(e,t){return Dp(jp(t.callbacks,`afterBody`,this,e))}getFooter(e,t){let{callbacks:n}=t,r=jp(n,`beforeFooter`,this,e),i=jp(n,`footer`,this,e),a=jp(n,`afterFooter`,this,e),o=[];return o=hp(o,gp(r)),o=hp(o,gp(i)),o=hp(o,gp(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))),X(o,t=>{let n=kp(e.callbacks,t);r.push(jp(n,`labelColor`,this,t)),i.push(jp(n,`labelPointStyle`,this,t)),a.push(jp(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=mp[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=vp(this,n),o=Object.assign({},e,t),s=Sp(this.chart,n,o),c=Tp(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}=ic(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=sl(n.rtl,this.x,this.width);for(e.x=Ep(this,n.titleAlign,n),t.textAlign=c.textAlign(n.titleAlign),t.textBaseline=`middle`,a=oc(n.titleFont),o=n.titleSpacing,t.fillStyle=n.titleColor,t.font=a.string,s=0;se!==0)?(e.beginPath(),e.fillStyle=i.multiKeyBackground,Zs(e,{x:t,y:p,w:c,h:s,radius:o}),e.fill(),e.stroke(),e.fillStyle=a.backgroundColor,e.beginPath(),Zs(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=oc(n.bodyFont),d=u.lineHeight,f=0,p=sl(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=Ep(this,h,n),t.fillStyle=n.bodyColor,X(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=mp[e.position].call(this,this._active,this._eventPosition);if(!n)return;let a=this._size=vp(this,e),o=Object.assign({},n,this._size),s=Sp(t,e,o),c=Tp(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=ac(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),cl(e,t.textDirection),i.y+=a.top,this.drawTitle(i,e,t),this.drawBody(i,e,t),this.drawFooter(i,e,t),ll(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=!co(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||!co(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=mp[i.position].call(this,e,t);return a!==!1&&(n!==a.x||r!==a.y)}};$(Mp,`positioners`,mp);var Np={id:`tooltip`,_element:Mp,positioners:mp,afterInit(e,t,n){n&&(e.tooltip=new Mp({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:Ap},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`]},Pp=(e,t,n,r)=>(typeof t==`string`?(n=e.push(t)-1,r.unshift({index:n,label:t})):isNaN(t)&&(n=null),n);function Fp(e,t,n,r){let i=e.indexOf(t);return i===-1?Pp(e,t,n,r):i===e.lastIndexOf(t)?i:n}var Ip=(e,t)=>e===null?null:Jo(Math.round(e),0,t);function Lp(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}};$(Rp,`id`,`category`),$(Rp,`defaults`,{ticks:{callback:Lp}});function zp(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=!K(a),_=!K(o),v=!K(c),y=(h-m)/(u+1),b=Po((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=Po(w*b/p/f)*f),K(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&&Ro((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=No(w,Math.round(w),b/1e3)?Math.round(w):Math.ceil(w));let T=Math.max(Ho(b),Ho(S));x=10**(K(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&&No(n[n.length-1].value,o,Bp(o,y,e))?n[n.length-1].value=o:n.push({value:o}):_&&C!==o||n.push({value:C}),n}function Bp(e,t,{horizontal:n,minRotation:r}){let i=Bo(r),a=(n?Math.sin(i):Math.cos(i))||.001,o=.75*t*(``+e).length;return Math.min(t/a,o)}var Vp=class extends Hd{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 K(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=Mo(r),t=Mo(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=zp({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`&&zo(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 Ts(e,this.chart.options.locale,this.options.ticks.format)}},Hp=class extends Vp{determineDataLimits(){let{min:e,max:t}=this.getMinMax(!0);this.min=ro(e)?e:0,this.max=ro(t)?t:1,this.handleTickRangeOptions()}computeTickLimit(){let e=this.isHorizontal(),t=e?this.width:this.height,n=Bo(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}};$(Hp,`id`,`linear`),$(Hp,`defaults`,{ticks:{callback:Os.formatters.numeric}});var Up=e=>Math.floor(jo(e)),Wp=(e,t)=>10**(Up(e)+t);function Gp(e){return e/10**Up(e)==1}function Kp(e,t,n){let r=10**n,i=Math.floor(e/r);return Math.ceil(t/r)-i}function qp(e,t){let n=Up(t-e);for(;Kp(e,t,n)>10;)n++;for(;Kp(e,t,n)<10;)n--;return Math.min(n,Up(e))}function Jp(e,{min:t,max:n}){t=io(e.min,t);let r=[],i=Up(t),a=qp(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=io(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=io(e.max,f);return r.push({value:p,major:Gp(p),significand:d}),r}var Yp=class extends Hd{constructor(e){super(e),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(e,t){let n=Vp.prototype.parse.apply(this,[e,t]);if(n===0)this._zero=!0;else return ro(n)&&n>0?n:null}determineDataLimits(){let{min:e,max:t}=this.getMinMax(!0);this.min=ro(e)?Math.max(0,e):null,this.max=ro(t)?Math.max(0,t):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!ro(this._userMin)&&(this.min=e===Wp(this.min,0)?Wp(this.min,-1):Wp(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(Wp(n,-1)),a(Wp(r,1)))),n<=0&&i(Wp(r,-1)),r<=0&&a(Wp(n,1)),this.min=n,this.max=r}buildTicks(){let e=this.options,t=Jp({min:this._userMin,max:this._userMax},this);return e.bounds===`ticks`&&zo(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`:Ts(e,this.chart.options.locale,this.options.ticks.format)}configure(){let e=this.min;super.configure(),this._startValue=jo(e),this._valueRange=jo(this.max)-jo(e)}getPixelForValue(e){return(e===void 0||e===0)&&(e=this.min),e===null||isNaN(e)?NaN:this.getPixelForDecimal(e===this.min?0:(jo(e)-this._startValue)/this._valueRange)}getValueForPixel(e){let t=this.getDecimalForPixel(e);return 10**(this._startValue+t*this._valueRange)}};$(Yp,`id`,`logarithmic`),$(Yp,`defaults`,{ticks:{callback:Os.formatters.logarithmic,major:{enabled:!0}}});function Xp(e){let t=e.ticks;if(t.display&&e.display){let e=ac(t.backdropPadding);return Y(t.font&&t.font.size,Ps.font.size)+e.height}return 0}function Zp(e,t,n){return n=q(n)?n:[n],{w:Ls(e,t.string,n),h:n.length*t.lineHeight}}function Qp(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 $p(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?Z/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 tm(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(Vo(Ko(c.angle+Oo))),u=om(c.y,s.h,l),d=im(l),f=am(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 nm(e,t){if(!t)return!0;let{left:n,top:r,right:i,bottom:a}=e;return!(Hs({x:n,y:r},t)||Hs({x:n,y:a},t)||Hs({x:i,y:r},t)||Hs({x:i,y:a},t))}function rm(e,t,n){let r=[],i=e._pointLabels.length,a=e.options,{centerPointLabels:o,display:s}=a.pointLabels,c={extra:Xp(a)/2,additionalAngle:o?Z/i:0},l;for(let a=0;a270||n<90)&&(e-=t),e}function sm(e,t,n){let{left:r,top:i,right:a,bottom:o}=n,{backdropColor:s}=t;if(!K(s)){let n=ic(t.borderRadius),c=ac(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(),Zs(e,{x:l,y:u,w:d,h:f,radius:n}),e.fill()):e.fillRect(l,u,d,f)}}function cm(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));sm(n,a,t);let o=oc(a.font),{x:s,y:c,textAlign:l}=t;Xs(n,e._pointLabels[i],s,c+o.lineHeight/2,o,{color:a.color,textAlign:l,textBaseline:`middle`})}}function lm(e,t,n,r){let{ctx:i}=e;if(n)i.arc(e.xCenter,e.yCenter,t,0,Q);else{let n=e.getPointPosition(0,t);i.moveTo(n.x,n.y);for(let a=1;a{let n=so(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?$p(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=Q/(this._pointLabels.length||1),n=this.options.startAngle||0;return Ko(e*t+Bo(n))}getDistanceFromCenterForValue(e){if(K(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(K(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);um(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=oc(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=ac(s.backdropPadding);e.fillRect(-a/2-t.left,-i-c.size/2-t.top,a+t.width,c.size+t.height)}Xs(e,r.label,0,-i,c,{color:s.color,strokeColor:s.textStrokeColor,strokeWidth:s.textStrokeWidth})}),e.restore()}drawTitle(){}};$(fm,`id`,`radialLinear`),$(fm,`defaults`,{display:!0,animate:!0,position:`chartArea`,angleLines:{display:!0,lineWidth:1,borderDash:[],borderDashOffset:0},grid:{circular:!1},startAngle:0,ticks:{showLabelBackdrop:!0,callback:Os.formatters.numeric},pointLabels:{backdropColor:void 0,backdropPadding:2,display:!0,font:{size:10},callback(e){return e},padding:5,centerPointLabels:!1}}),$(fm,`defaultRoutes`,{"angleLines.color":`borderColor`,"pointLabels.color":`color`,"ticks.color":`color`}),$(fm,`descriptors`,{angleLines:{_fallback:`grid`}});var pm={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}},mm=Object.keys(pm);function hm(e,t){return e-t}function gm(e,t){if(K(t))return null;let n=e._adapter,{parser:r,round:i,isoWeekday:a}=e._parseOpts,o=t;return typeof r==`function`&&(o=r(o)),ro(o)||(o=typeof r==`string`?n.parse(o,r):n.parse(o)),o===null?null:(i&&(o=i===`week`&&(Lo(a)||a===!0)?n.startOf(o,`isoWeek`,a):n.startOf(o,i)),+o)}function _m(e,t,n,r){let i=mm.length;for(let a=mm.indexOf(e);a=mm.indexOf(n);a--){let n=mm[a];if(pm[n].common&&e._adapter.diff(i,r,n)>=t-1)return n}return mm[n?mm.indexOf(n):0]}function ym(e){for(let t=mm.indexOf(e)+1,n=mm.length;t=t?n[r]:n[i];e[a]=!0}}function xm(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 Sm(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=Jo(t,0,a),n=Jo(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||_m(i.minUnit,t,n,this._getLabelCapacity(t)),o=Y(r.ticks.stepSize,1),s=a===`week`&&i.isoWeekday,c=Lo(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 so(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}=Qo(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}=Qo(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 Tm=class extends Cm{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=wm(t,this.min),this._tableRange=wm(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(wm(this._table,e)-this._minPos)/this._tableRange}getValueForPixel(e){let t=this._offsets,n=this.getDecimalForPixel(e)/t.factor-t.end;return wm(this._table,n*this._tableRange+this._minPos,!0)}};$(Tm,`id`,`timeseries`),$(Tm,`defaults`,Cm.defaults),jf.register(gu,pp,Rp,Hp,Np);var Em=new Intl.DateTimeFormat(`ko-KR`,{hour:`2-digit`,hour12:!1}),Dm=`#94a3b8`,Om=`rgba(148, 163, 184, 0.18)`,km=`#0d9488`,Am=`#0f766e`;function jm(e){return Em.format(new Date(e*1e3))}function Mm(e){return e.map((t,n)=>n%3==0||n===e.length-1?jm(t.bucketStart):``)}function Nm({buckets:e}){let t=we(null),n=we(null),r=we(e);return r.current=e,z(()=>{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?jm(n.bucketStart):``},label:e=>`차단 ${G(Number(e.raw??0))}`,afterLabel:e=>{let t=r.current[e.dataIndex];return t?`DNS 요청 ${G(t.queries)}`:``}}}},scales:{x:{border:{display:!1},grid:{display:!1},ticks:{autoSkip:!1,color:Dm,font:{family:a,size:11,weight:700},maxRotation:0,minRotation:0}},y:{beginAtZero:!0,border:{display:!1},grid:{color:Om},ticks:{color:Dm,font:{family:a,size:11,weight:700},maxTicksLimit:4,precision:0}}}};return n.current=new jf(e,{type:`bar`,data:{labels:Mm(r.current),datasets:[{data:r.current.map(e=>e.blocked),backgroundColor:km,hoverBackgroundColor:Am,borderRadius:4,borderSkipped:!1,barPercentage:.82,categoryPercentage:.9,maxBarThickness:24}]},options:o}),()=>{n.current?.destroy(),n.current=null}},[]),z(()=>{let t=n.current;t&&(t.data.labels=Mm(e),t.data.datasets[0].data=e.map(e=>e.blocked),t.update())},[e]),B(`div`,{class:`mt-5 h-44 sm:h-52`,children:B(`canvas`,{"aria-label":`최근 24시간 시간대별 SafeShield 차단 요청`,ref:t,role:`img`,children:`최근 24시간 시간대별 SafeShield 차단 요청 차트`})})}var Pm=3600,Fm=24;function Im(e,t){return e<=0?`0.0%`:`${(t/e*100).toFixed(1)}%`}function Lm(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/Pm)*Pm}function Rm(e){let t=new Map(e.hourly.map(e=>[e.bucketStart,e])),n=Lm(e)-23*Pm;return Array.from({length:Fm},(e,r)=>{let i=n+r*Pm;return t.get(i)??{bucketStart:i,queries:0,blocked:0}})}function zm({children:e}){return B(`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:B(`p`,{class:`m-0 max-w-lg text-sm font-semibold leading-6 text-slate-500`,children:e})})}function Bm({data:e,error:t,loading:n,nowTimestamp:r,refreshing:i}){let a;if(n&&!e)a=B(zm,{children:`SafeShield 보호 활동을 불러오는 중입니다.`});else if(t&&!e)a=B(zm,{children:`SafeShield 보호 활동을 불러오지 못했습니다. SafeShield 페이지에서 상태를 확인해 주세요.`});else if(!e||!e.available)a=B(zm,{children:`현재 SafeShield에서 차단 통계를 제공하지 않습니다. 통계 기능을 확인해 주세요.`});else if(!e.enabled)a=B(zm,{children:`통계 수집이 꺼져 있습니다. SafeShield 페이지에서 통계를 켜면 최근 24시간 활동이 표시됩니다.`});else{let t=Rm(e),n=t.reduce((e,t)=>({queries:e.queries+t.queries,blocked:e.blocked+t.blocked}),{queries:0,blocked:0});a=B(b,{children:[B(`dl`,{class:`mt-5 grid gap-3 sm:grid-cols-3`,children:[B(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3`,children:[B(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:`DNS 요청`}),B(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-slate-950`,children:G(n.queries)})]}),B(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3`,children:[B(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:`차단`}),B(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-teal-700`,children:G(n.blocked)})]}),B(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3`,children:[B(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:`차단율`}),B(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-slate-950`,children:Im(n.queries,n.blocked)})]})]}),B(Nm,{buckets:t})]})}return B(`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:[B(`div`,{class:`min-w-0`,children:[B(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`SafeShield activity`}),B(`div`,{class:`mt-2 flex min-w-0 items-center justify-between gap-3`,children:[B(`h3`,{class:`m-0 min-w-0 text-xl font-black text-slate-950`,children:`최근 24시간 보호 활동`}),B(`a`,{class:`shrink-0 whitespace-nowrap text-xs font-extrabold text-teal-700 no-underline hover:text-teal-800`,href:`#safeshield`,children:`상세 통계 →`})]}),e?.updatedAt?B(`p`,{class:`mt-2 mb-0 text-[0.68rem] font-semibold text-slate-400`,title:Qt(e.updatedAt),children:i?`갱신 중…`:`집계: ${$t(e.updatedAt,r)}`}):null]}),a,t&&e?B(`p`,{class:`mt-3 mb-0 text-xs font-bold text-amber-700`,children:`최근 통계 갱신에 실패해 마지막 정상 데이터를 표시하고 있습니다.`}):null]})}var Vm=2*Math.PI*39;function Hm(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 Um({detail:e,eyebrow:t,href:n,icon:r,linkLabel:i=`자세히 보기 →`,meta:a,metaState:o,metaWarning:s=!1,state:c=`neutral`,value:l}){let u=B(b,{children:[B(`div`,{class:`flex items-start justify-between gap-4`,children:[B(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.16em] text-slate-500`,children:t}),B(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl ring-1 ring-inset ${Hm(c)}`,children:r})]}),B(`p`,{class:`mt-4 mb-0 break-words text-2xl font-black tracking-tight text-slate-950`,children:l}),B(`p`,{class:`mt-2 mb-0 min-h-10 text-sm font-medium leading-5 text-slate-500`,children:e}),a?B(`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?B(`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?B(`a`,{class:`${d} no-underline hover:border-slate-300 hover:shadow-md`,href:n,children:u}):B(`article`,{class:d,children:u})}var Wm=6e4;function Gm(e,t){return!e||!Number.isFinite(e)||e<=0?null:Math.max(0,t-e)}function Km(e,t,n,r){return!t||t<=0?`${e}: ${r}`:B(`span`,{title:Qt(t),children:[e,`: `,$t(t,n)]})}function qm(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 Jm(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 Ym({label:e,value:t}){return B(`div`,{class:`flex min-w-0 items-start justify-between gap-4 border-b border-slate-100 py-3 last:border-b-0`,children:[B(`dt`,{class:`text-sm font-semibold text-slate-500`,children:e}),B(`dd`,{class:`m-0 min-w-0 max-w-[68%] break-words text-right text-sm font-extrabold text-slate-950`,children:t})]})}function Xm(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 Zm({data:e,error:t,loading:n,nowTimestamp:r}){let i=!!(e&&e.generatedAt>0),a=Xm(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 B(`div`,{class:`mt-5 border-t border-slate-200 pt-5`,children:[B(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[B(`div`,{class:`flex items-center gap-2`,children:[B(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:`장치 진단`}),i?B(`span`,{class:`rounded-full px-2.5 py-1 text-[11px] font-extrabold ${a.badge}`,children:a.label}):null]}),B(`a`,{class:`text-xs font-extrabold text-teal-700 no-underline hover:text-teal-900`,href:`#settings`,children:`상세 보기 →`})]}),t&&!i?B(`div`,{class:`mt-3 rounded-xl border border-amber-200 bg-amber-50/70 px-4 py-3`,children:B(`div`,{class:`flex items-start gap-2`,children:[B(U,{class:`mt-0.5 size-4 shrink-0 text-amber-700`}),B(`div`,{class:`min-w-0`,children:[B(`p`,{class:`m-0 text-sm font-extrabold text-slate-900`,children:`진단 상태를 확인하지 못했습니다.`}),B(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`설정에서 장치 진단 상태를 다시 확인해 주세요.`})]})]})}):i&&e?B(`div`,{class:`mt-3 rounded-xl border px-4 py-3 ${a.panel}`,children:[B(`div`,{class:`flex min-w-0 items-start gap-3`,children:[e.overall===`ok`?B(_t,{class:`mt-0.5 size-5 shrink-0 ${a.icon}`}):e.overall===`initializing`?B(st,{class:`mt-0.5 size-5 shrink-0 ${a.icon}`}):B(U,{class:`mt-0.5 size-5 shrink-0 ${a.icon}`}),B(`div`,{class:`min-w-0 flex-1`,children:[B(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:e.summary.message}),B(`p`,{class:`mt-1 mb-0 text-xs font-semibold leading-5 text-slate-500`,children:[`마지막 진단: `,$t(e.generatedAt,r),` · 검사 항목:`,` `,e.summary.total,`개`]})]})]}),s.length>0?B(`div`,{class:`mt-3 grid gap-2 sm:grid-cols-2`,children:s.map(e=>B(`div`,{class:`min-w-0 rounded-lg border border-white/80 bg-white/70 px-3 py-2`,children:[B(`p`,{class:`m-0 text-xs font-extrabold text-slate-900`,children:e.label}),B(`p`,{class:`mt-1 mb-0 line-clamp-2 text-xs leading-5 text-slate-500`,children:e.message})]},e.id))}):null,c>0?B(`p`,{class:`mt-2 mb-0 text-xs font-extrabold text-slate-600`,children:[`외 `,c,`건의 확인 항목이 있습니다.`]}):null]}):B(`div`,{class:`mt-3 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3`,children:[B(`p`,{class:`m-0 text-sm font-extrabold text-slate-900`,children:n?`진단 상태를 확인하고 있습니다.`:`아직 생성된 진단 결과가 없습니다.`}),B(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`진단 결과가 생성되면 현재 장치 상태를 이곳에 요약해서 표시합니다.`})]})]})}function Qm(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?`${G(e.blocklist.validLineCount)}개 차단 규칙`:`차단 목록 적용됨`;return{detail:r?`${i} · ${r.toUpperCase()}`:i,state:`healthy`,value:`보호 중`}}function $m({accountRegistered:e,activity:t,activityError:n,activityLoading:r,data:i,devices:a,devicesError:o,devicesLoading:s,error:c,firmware:l,firmwareError:u,firmwareLoading:d,health:f,healthError:p,healthLoading:m,lan:h,lanError:g,lanLoading:_,loading:v,onRetry:y,safeshield:b,safeshieldError:x,safeshieldLoading:S,statistics:C,statisticsError:w,statisticsLoading:T,statisticsRefreshing:E,updates:D,updatesError:O,updatesLoading:k}){let A=Te(()=>i?rn(i.runtime.memory):null,[i]),[j,M]=R(()=>Math.floor(Date.now()/1e3));if(z(()=>{let e=window.setInterval(()=>{M(Math.floor(Date.now()/1e3))},Wm);return()=>window.clearInterval(e)},[]),v&&!i)return B(Bn,{});if(c&&!i)return B(Vn,{message:c,onRetry:y});if(!i)return null;let N=A?Math.round(A.percent):0,P=Tn(i.runtime.storage),F=P?(P.availableRatio*Vm).toFixed(2):`0`,ee=Qm(b,S,x),te=D?.packages.find(e=>e.name===`luci-app-smartsafehub`)??D?.packages[0]??null,ne=!!(D&&D.updateCount>0),re=D?ne?`${D.updateCount}개 업데이트`:D.lastCheckAt?`최신 상태`:`확인 전`:O?`확인 필요`:`확인 중`,ie=te?ne&&te.availableVersion?`${te.installedVersion} → ${te.availableVersion}`:`설치 버전 ${te.installedVersion}`:O?`업데이트 상태를 불러오지 못했습니다.`:k?`업데이트 상태를 확인하고 있습니다.`:`업데이트 상태를 확인할 수 없습니다.`,ae=a?`${G(a.totals.online)}대`:s?`확인 중`:`확인 필요`,oe=a?`Wi-Fi ${G(a.totals.wireless)}대 · 유선/기타 ${G(a.totals.ethernet)}대`:o||`연결 기기 요약을 불러오고 있습니다.`,se=qm(i.network.protocol),ce=i.network.available?i.network.ipv4Address?`WAN ${i.network.ipv4Address}${se?` · ${se}`:``}`:se?`${se} · WAN 주소를 받지 못했습니다.`:`WAN 주소를 받지 못했습니다.`:`WAN 인터페이스를 찾을 수 없습니다.`,le=!!h?.conflict.detected,I=!i.network.up||le||g!==null&&!h?`warning`:h?`healthy`:`neutral`,ue=le?`네트워크 충돌`:i.network.up?`정상 연결`:`연결 확인`,de=i.network.up?le?`⚠ LAN 대역과 충돌합니다`:_&&!h?`네트워크 대역 확인 중`:g&&!h?`네트워크 대역을 확인하지 못했습니다.`:h?`✓ 네트워크 충돌 없음`:`네트워크 대역 확인 필요`:`WAN 연결 상태를 확인해 주세요.`,fe=!i.network.up||le||g!==null&&!h?`warning`:h?`healthy`:`neutral`,pe=i.network.ipv4Address?Jm(i.network.ipv4Address)?`${i.network.ipv4Address} · 사설 네트워크`:i.network.ipv4Address:`할당되지 않음`,me=Gm(b?.timestamps.lastSuccess,j),he=b?Math.max(b.timestamps.refreshIntervalS*2,86400):null,L=!!(b?.enabled&&b.status!==`running`&&me!==null&&he!==null&&me>he),ge=Ai(D,j),_e=!!l?.current.metadataAvailable,ve=d&&!l?`확인 중`:u&&!l?`확인 필요`:_e?l?.current.releaseVersion?`SmartSafeHub ${l.current.releaseVersion}`:`SmartSafeHub 펌웨어`:`${i.software.distribution} ${i.software.version}`,ye=d&&!l?`확인 중`:u&&!l?`확인 필요`:_e&&l?.current.buildId||i.software.revision,be=_e?`빌드 ID`:`리비전`;return B(`div`,{class:`min-w-0 space-y-6`,children:[B(`section`,{"aria-labelledby":`dashboard-overview-title`,children:[B(W,{description:`인터넷, SafeShield, 연결 기기와 소프트웨어 상태를 한눈에 확인합니다.`,eyebrow:`Overview`,id:`dashboard-overview-title`,title:`시스템 개요`}),B(`div`,{class:`grid min-w-0 gap-4 sm:grid-cols-2 xl:grid-cols-4`,children:[B(Um,{detail:ce,eyebrow:`Internet`,href:`#network`,icon:B(at,{class:`size-5`}),linkLabel:le?`해결하기 →`:`자세히 보기 →`,meta:de,metaState:fe,state:I,value:ue}),B(Um,{detail:ee.detail,eyebrow:`SafeShield`,href:`#safeshield`,icon:B(We,{class:`size-5`}),meta:Km(L?`차단 목록 갱신 지연`:`차단 목록 갱신`,b?.timestamps.lastSuccess,j,S?`확인 중`:`기록 없음`),metaWarning:L,state:ee.state===`healthy`&&L?`warning`:ee.state,value:ee.value}),B(Um,{detail:oe,eyebrow:`Connected devices`,href:`#devices`,icon:B(Ke,{class:`size-5`}),meta:Km(`목록 확인`,a?.generatedAt,j,s?`확인 중`:`기록 없음`),state:a?`healthy`:o?`warning`:`neutral`,value:ae}),B(Um,{detail:ie,eyebrow:`Software update`,href:`#system`,icon:B(et,{class:`size-5`}),meta:D&&!D.settings.checkEnabled?Km(`자동 확인 꺼짐 · 마지막 확인`,D.lastCheckAt,j,`확인 기록 없음`):Km(ge?`업데이트 확인 지연`:`마지막 확인`,D?.lastCheckAt,j,k?`확인 중`:`아직 없음`),metaWarning:ge,state:O||ge||ne?`warning`:D?.lastCheckAt?`healthy`:`neutral`,value:re})]}),e===!1?B(`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:[B(`div`,{class:`flex min-w-0 items-start gap-3`,children:[B(`span`,{class:`ssh-dashboard-account-banner-icon grid size-10 shrink-0 place-items-center rounded-xl`,"aria-hidden":`true`,children:B(Je,{class:`size-5`})}),B(`div`,{class:`min-w-0`,children:[B(`div`,{class:`flex flex-wrap items-center gap-2`,children:[B(`p`,{class:`ssh-dashboard-account-banner-title m-0 font-bold`,children:`SmartSafeHub 계정을 연결해 주세요`}),B(`span`,{class:`ssh-dashboard-account-banner-badge rounded-full px-2 py-0.5 text-[0.7rem] font-bold`,children:`연결 필요`})]}),B(`p`,{class:`ssh-dashboard-account-banner-description mt-1 mb-0 text-sm leading-relaxed`,children:`계정을 연결하면 웹사이트에서 기기를 관리하고 구독 정보를 확인할 수 있습니다. DNS 보호는 연결하지 않아도 계속 작동합니다.`})]})]}),B(`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]}),B(`section`,{"aria-labelledby":`dashboard-activity-title`,children:[B(W,{description:`최근 SafeShield 차단 활동과 현재 네트워크 연결 구성을 함께 확인합니다.`,eyebrow:`Activity`,id:`dashboard-activity-title`,title:`네트워크 보호 활동`}),B(`div`,{class:`grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1.55fr)_minmax(18rem,0.65fr)]`,children:[B(Bm,{data:C,error:w,loading:T,nowTimestamp:j,refreshing:E}),B(`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:[B(`div`,{class:`flex items-start justify-between gap-4`,children:[B(`div`,{children:[B(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Network`}),B(`h3`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`연결 상태`})]}),B(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl ${i.network.up&&!le?`bg-emerald-50 text-emerald-700`:`bg-amber-50 text-amber-800`}`,children:B(at,{class:`size-5`})})]}),B(`dl`,{class:`mt-4 mb-0`,children:[B(Ym,{label:`인터넷`,value:le?`네트워크 충돌`:i.network.up?`정상 연결`:`연결 확인`}),B(Ym,{label:`WAN IP`,value:pe}),B(Ym,{label:`프로토콜`,value:se||`확인되지 않음`}),B(Ym,{label:`상위 네트워크`,value:h?.wan.subnet||(_?`확인 중`:g?`확인 필요`:`확인되지 않음`)}),B(Ym,{label:`LAN 네트워크`,value:h?.lan.subnet||(_?`확인 중`:g?`확인 필요`:`확인되지 않음`)}),B(Ym,{label:`대역 충돌`,value:h?B(`span`,{class:h.conflict.detected?`text-amber-700`:`text-emerald-700`,children:h.conflict.detected?`충돌 감지`:`없음`}):_?`확인 중`:`확인 필요`})]}),B(`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:`네트워크 보기`})]})]})]}),B(`section`,{"aria-labelledby":`dashboard-recent-activity-title`,children:[B(W,{action:B(`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:`최근 활동`}),B(`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?B(yn,{error:n,loading:r}):B(vn,{compact:!0,events:(t?.events??[]).slice(0,3)})})]}),B(`section`,{"aria-labelledby":`dashboard-system-title`,children:[B(W,{description:`메모리, 디스크, 시스템 부하와 장치 정보를 빠르게 점검합니다.`,eyebrow:`System`,id:`dashboard-system-title`,title:`시스템 상태`}),B(`div`,{class:`grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1.45fr)_minmax(20rem,0.75fr)]`,children:[B(`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:[B(`div`,{class:`flex flex-wrap items-start justify-between gap-4`,children:[B(`div`,{children:[B(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Runtime`}),B(`h3`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`리소스 사용량`})]}),B(`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:[B(`span`,{class:`size-2 rounded-full ${N>=90?`bg-amber-500`:`bg-emerald-500`}`}),N>=90?`메모리 확인`:`정상 실행`]})]}),B(`div`,{class:`mt-5 grid min-w-0 grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4`,children:[B(`div`,{class:`flex min-w-0 flex-col rounded-xl border border-slate-200 bg-slate-50 p-3 sm:p-4`,children:[B(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[B(ct,{class:`size-4`}),B(`span`,{class:`text-xs font-extrabold`,children:`메모리`})]}),B(`div`,{class:`flex min-h-0 flex-1 flex-col justify-center py-1`,children:[B(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:[N,`%`]}),B(`p`,{class:`mt-1 mb-0 text-xs font-semibold text-slate-500`,children:[A?Yt(A.used):`-`,` /`,` `,Yt(i.runtime.memory.total)]}),B(`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:B(`div`,{class:`h-full rounded-full transition-[width] duration-300 ${N>=90?`bg-amber-500`:`bg-teal-600`}`,style:{width:`${Math.min(100,N)}%`}})})]})]}),B(`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:[B(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[B(dt,{class:`size-4`}),B(`span`,{class:`text-xs font-extrabold`,children:`디스크`})]}),B(`div`,{class:`mt-1 flex justify-center`,role:`img`,"aria-label":P?`디스크 여유 공간 ${P.availablePercent}%, ${P.availableText} 남음, 사용 ${P.usedText} / 전체 ${P.totalText}`:`디스크 저장 공간 확인 불가`,children:B(`div`,{class:`ssh-apps-storage-ring relative grid size-[76px] shrink-0 place-items-center`,"data-level":P?.level??`unavailable`,children:[B(`svg`,{"aria-hidden":`true`,class:`size-[76px]`,viewBox:`0 0 96 96`,children:[B(`circle`,{class:`ssh-apps-storage-track`,cx:`48`,cy:`48`,r:`39`}),P&&P.availableRatio>0?B(`circle`,{class:`ssh-apps-storage-free`,cx:`48`,cy:`48`,r:`39`,"stroke-dasharray":`${F} ${Vm.toFixed(2)}`}):null]}),B(`div`,{class:`absolute inset-0 flex items-center justify-center text-center`,children:B(`strong`,{class:`text-base leading-none font-black text-slate-950`,children:P?`${P.availablePercent}%`:`—`})})]})}),B(`p`,{class:`ssh-apps-storage-available mt-1 mb-0 text-center text-xs font-extrabold text-teal-700`,children:P?`${P.availableText} 남음`:`저장 공간 확인 불가`}),P?B(`p`,{class:`mt-1 mb-0 text-center text-[11px] leading-4 font-semibold text-slate-500`,title:`앱 설치 가능 공간을 기준으로 계산하며 파일시스템 예약 공간이 사용량에 포함될 수 있습니다.`,children:[`사용 `,P.usedText,` / 전체 `,P.totalText]}):null]}),B(`div`,{class:`flex min-w-0 flex-col rounded-xl border border-slate-200 bg-slate-50 p-3 sm:p-4`,children:[B(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[B(Ge,{class:`size-4`}),B(`span`,{class:`text-xs font-extrabold`,children:`시스템 부하`})]}),B(`div`,{class:`flex min-h-0 flex-1 flex-col justify-center py-1`,children:[B(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:Xt(i.runtime.load[0])}),B(`p`,{class:`mt-1 mb-0 text-xs font-semibold text-slate-500`,children:[`5분 `,Xt(i.runtime.load[1]),` · 15분`,` `,Xt(i.runtime.load[2])]})]})]}),B(`div`,{class:`flex min-w-0 flex-col rounded-xl border border-slate-200 bg-slate-50 p-3 sm:p-4`,children:[B(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[B(st,{class:`size-4`}),B(`span`,{class:`text-xs font-extrabold`,children:`실행 시간`})]}),B(`div`,{class:`flex min-h-0 flex-1 flex-col justify-center py-1`,children:[B(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:Jt(i.runtime.uptime)}),B(`p`,{class:`mt-1 mb-0 text-xs font-semibold text-slate-500`,children:[`부팅 `,Zt(i.runtime.localtime,i.runtime.uptime)]})]})]})]}),B(Zm,{data:f,error:p,loading:m,nowTimestamp:j})]}),B(`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:[B(`div`,{class:`flex items-start justify-between gap-4`,children:[B(`div`,{children:[B(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Device`}),B(`h3`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`장치 정보`})]}),B(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:B(Ge,{class:`size-5`})})]}),B(`dl`,{class:`mt-4 mb-0`,children:[B(Ym,{label:`모델`,value:i.device.model}),B(Ym,{label:`호스트 이름`,value:i.device.hostname}),B(Ym,{label:`펌웨어`,value:B(`span`,{title:_e?l?.current.releaseVersion?`SmartSafeHub 펌웨어 ${l.current.releaseVersion}`:`SmartSafeHub 커스텀 펌웨어`:u||void 0,children:ve})}),B(Ym,{label:be,value:ye}),B(Ym,{label:`커널`,value:i.software.kernel}),B(Ym,{label:`WAN IP`,value:i.network.ipv4Address||`할당되지 않음`})]})]})]})]})]})}var eh=3,th=10;function nh(e,t){return e<=0?`0.0%`:`${(t/e*100).toFixed(1)}%`}function rh(e){return e.id===`other`?`기타 기기`:e.hostname?e.hostname:e.identified?`이름 없는 기기`:`미식별 기기`}function ih(e){return[...e].sort((e,t)=>e.id===`other`?1:t.id===`other`?-1:t.blocked-e.blocked||t.queries-e.queries||rh(e).localeCompare(rh(t),`ko`))}function ah({devices:e,deviceLimit:t,truncated:n}){let r=ih(e),[i,a]=R(!1),[o,s]=R(1),c=Math.max(1,Math.ceil(r.length/th)),l=Math.min(o,c),u=(l-1)*th,d=Math.min(u+th,r.length),f=i?r.slice(u,d):r.slice(0,eh),p=r.length>eh;return z(()=>{s(e=>Math.min(e,c))},[c]),B(`div`,{class:`mt-6 rounded-2xl border border-slate-100 bg-slate-50/70 p-4 sm:p-5`,children:[B(`div`,{class:`flex flex-wrap items-start justify-between gap-3`,children:[B(`div`,{children:[B(`div`,{class:`flex flex-wrap items-center gap-2`,children:[B(`p`,{class:`m-0 text-xs font-bold text-slate-500`,children:`기기별 통계`}),p&&!i?B(`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]}),B(`h3`,{class:`mt-1 mb-0 text-base font-extrabold text-slate-950`,children:`어떤 기기에서 차단되었는지 확인`}),B(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-500`,children:`DHCP 정보가 있는 기기는 MAC 주소를 기준으로 식별하며, 기기별 숫자만 현재 부팅 세션 동안 메모리에 집계합니다.`})]}),B(`div`,{class:`flex flex-wrap items-center justify-end gap-2`,children:[B(`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:[B(Ke,{class:`size-4 text-teal-700`}),G(r.length),`개 항목`]}),p?B(`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?`간단히 보기 ↑`:`전체 ${G(r.length)}개 기기 보기 ↓`}):null]})]}),r.length===0?B(`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 요청이 발생하면 다음 통계 갱신 시 표시됩니다.`}):B(`div`,{class:`mt-4 grid gap-3`,id:`safeshield-device-statistics-list`,children:f.map(e=>B(`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:[B(`div`,{class:`min-w-0`,children:B(`div`,{class:`flex min-w-0 flex-wrap items-center gap-2`,children:[B(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg bg-teal-50 text-teal-700`,children:B(Ke,{class:`size-4`})}),B(`div`,{class:`min-w-0`,children:[B(`h4`,{class:`m-0 truncate text-sm font-extrabold text-slate-950`,children:rh(e)}),B(`p`,{class:`mt-1 mb-0 break-all text-xs text-slate-500`,children:[e.ip||`IP 미확인`,e.mac?` · ${e.mac}`:``]})]}),B(`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 임시 식별`})]})}),B(`dl`,{class:`m-0 grid grid-cols-3 gap-2 text-right`,children:[B(`div`,{class:`min-w-20`,children:[B(`dt`,{class:`text-[11px] font-bold text-slate-400`,children:`DNS 요청`}),B(`dd`,{class:`mt-1 mb-0 ml-0 text-sm font-extrabold text-slate-900`,children:G(e.queries)})]}),B(`div`,{class:`min-w-20`,children:[B(`dt`,{class:`text-[11px] font-bold text-slate-400`,children:`차단`}),B(`dd`,{class:`mt-1 mb-0 ml-0 text-sm font-extrabold text-teal-700`,children:G(e.blocked)})]}),B(`div`,{class:`min-w-20`,children:[B(`dt`,{class:`text-[11px] font-bold text-slate-400`,children:`차단율`}),B(`dd`,{class:`mt-1 mb-0 ml-0 text-sm font-extrabold text-slate-900`,children:nh(e.queries,e.blocked)})]})]})]},e.id))}),p&&!i?B(`p`,{class:`mt-3 mb-0 text-xs font-semibold leading-5 text-slate-500`,children:[`전체 `,G(r.length),`개 기기 중 차단 수 기준 상위 `,eh,`개를 표시하고 있습니다.`]}):null,i&&r.length>th?B(`nav`,{"aria-label":`기기별 통계 페이지`,class:`mt-4 flex flex-wrap items-center justify-between gap-3`,children:[B(`p`,{class:`m-0 text-xs font-semibold text-slate-500`,children:[G(u+1),`–`,G(d),` /`,` `,G(r.length),`개`]}),B(`div`,{class:`flex items-center gap-2`,children:[B(`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:`이전`}),B(`span`,{"aria-current":`page`,class:`min-w-20 text-center text-xs font-extrabold text-slate-600`,children:[G(l),` / `,G(c)]}),B(`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?B(`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:[`개별 통계는 최대 `,G(t),`개 기기까지 추적합니다. 한도를 초과한 기기의 요청은 기타 기기로 합산됩니다.`]}):null]})}var oh=3600,sh=24;function ch(e,t){return e<=0?`0.0%`:`${(t/e*100).toFixed(1)}%`}function lh(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/oh)*oh}function uh(e){let t=new Map(e.hourly.map(e=>[e.bucketStart,e])),n=lh(e)-23*oh;return Array.from({length:sh},(e,r)=>{let i=n+r*oh;return t.get(i)??{bucketStart:i,queries:0,blocked:0}})}function dh({emphasized:e=!1,label:t,value:n}){return B(`div`,{class:`rounded-xl bg-slate-50 p-4 sm:p-5`,children:[B(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:t}),B(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight ${e?`text-teal-700`:`text-slate-950`}`,children:n})]})}function fh({action:e,data:t,error:n,loading:r,onRetry:i,onSetEnabled:a,refreshing:o}){if(r&&t===null)return B(b,{children:[B(W,{eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청과 차단 현황을 확인합니다.`}),B(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:B(`p`,{class:`m-0 text-sm font-bold text-slate-500`,children:`차단 통계를 불러오는 중입니다…`})})]});if(n&&t===null)return B(b,{children:[B(W,{eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청과 차단 현황을 확인합니다.`}),B(`section`,{class:`rounded-2xl border border-amber-200 bg-amber-50 p-5 sm:p-6`,children:[B(`p`,{class:`m-0 text-sm font-extrabold text-amber-950`,children:`차단 통계를 불러오지 못했습니다.`}),B(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-amber-800`,children:n}),B(`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 B(b,{children:[B(W,{eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청과 차단 현황을 확인합니다.`}),B(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:B(`p`,{class:`m-0 text-sm leading-6 text-slate-600`,children:`현재 SafeShield에서 통계 데이터를 제공하지 않습니다. SafeShield 0.3.15 버전 이상이 설치되어 있고, 통계 수집이 활성화되어 있는지 확인해 주세요.`})})]});let s=uh(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 B(b,{children:[B(W,{action:B(`div`,{class:`flex items-center gap-3 rounded-xl border border-slate-100 bg-slate-50 px-3 py-2.5`,children:[B(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-500`,children:B(dt,{class:`size-5`})}),B(`div`,{class:`text-right`,children:[B(`p`,{class:`m-0 text-[0.68rem] font-bold text-slate-400`,children:`통계 수집`}),B(`p`,{class:`mt-1 mb-0 text-xs font-extrabold ${u||d?`text-emerald-700`:`text-slate-500`}`,role:`status`,children:f})]}),B(`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:B(`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?B(H,{"aria-hidden":`true`,class:`size-3 animate-spin text-teal-600`}):null})})]}),eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청 원본은 저장하지 않고 숫자만 로컬 메모리에 집계합니다.`}),B(`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:B(`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 요청 원본은 저장하지 않고 요청 수와 차단 수만 로컬 메모리에 집계합니다.`}),B(`dl`,{class:`mt-2 grid gap-3 sm:grid-cols-2 xl:grid-cols-4`,children:[B(dh,{label:`최근 24시간 DNS 요청`,value:G(l.queries)}),B(dh,{emphasized:!0,label:`최근 24시간 차단`,value:G(l.blocked)}),B(dh,{label:`최근 24시간 차단율`,value:ch(l.queries,l.blocked)}),B(dh,{label:`현재 시간 차단`,value:G(c?.blocked??0)})]}),B(`div`,{class:`mt-5 rounded-xl bg-slate-50/70 p-4 sm:p-5`,children:[B(`div`,{class:`flex flex-wrap items-end justify-between gap-3`,children:[B(`div`,{children:[B(`p`,{class:`m-0 text-xs font-bold text-slate-500`,children:`최근 24시간`}),B(`h3`,{class:`mt-1 mb-0 text-base font-extrabold text-slate-950`,children:`시간대별 차단 요청`})]}),B(`span`,{class:`text-xs font-bold text-slate-500`,children:o?`갱신 중…`:`마지막 집계 ${Qt(t.updatedAt)}`})]}),B(Nm,{buckets:s})]}),B(ah,{deviceLimit:t.deviceLimit,devices:t.devices,truncated:t.devicesTruncated}),B(`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:[B(`span`,{children:[`수집 누적 DNS 요청 `,G(t.totals.queries)]}),B(`span`,{children:[`수집 누적 차단 `,G(t.totals.blocked)]}),B(`span`,{children:[`수집 시작 `,Qt(t.startedAt)]}),B(`span`,{children:[`보존 `,G(t.retentionHours),`시간`]}),t.volatile?B(`span`,{children:`재부팅 시 초기화되는 메모리 통계`}):null]}),n?B(`p`,{class:`mt-3 mb-0 text-xs font-bold text-amber-700`,children:`최근 자동 갱신에 실패했습니다. 현재 표시된 마지막 정상 통계를 유지합니다.`}):null]})]})}var ph=[{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`]}],mh=new Map;ph.forEach((e,t)=>{e.stages.forEach(e=>mh.set(e,t))});function hh(e,t){return e===`running`||t!==null&&mh.has(t)}function gh(e){let t=e===null?0:mh.get(e)??0,n=ph[t];return{description:n.description,label:n.label,number:t+1,total:ph.length}}function _h(e){return(e??``).trim().toLowerCase()}function vh(e,t){let n=_h(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=gh(t);return r.number===2?`최신 차단 목록 정보를 확인하는 중 문제가 발생했습니다. 인터넷 연결을 확인한 뒤 다시 갱신해 주세요.`:r.number===3?`차단 목록을 다운로드하는 중 문제가 발생했습니다. 인터넷 연결을 확인한 뒤 다시 갱신해 주세요.`:r.number===4?`사용자 규칙을 적용하는 중 문제가 발생했습니다. 사용자 규칙을 확인한 뒤 다시 갱신해 주세요.`:r.number===5?`새 보호 규칙을 적용하는 중 문제가 발생했습니다. 잠시 후 다시 갱신해 주세요.`:r.number===6?`새 차단 목록의 보호 상태를 확인하지 못했습니다. 다시 갱신해 주세요.`:`차단 목록 갱신을 완료하지 못했습니다. 잠시 후 다시 갱신해 주세요.`}var yh=`https://www.smartsafehub.com/pricing/`;function bh({value:e,trueLabel:t=`정상`,falseLabel:n=`확인 필요`}){return B(`span`,{class:`inline-flex items-center gap-2 text-sm font-extrabold ${e?`text-emerald-700`:`text-slate-500`}`,children:[B(`span`,{class:`size-2 rounded-full ${e?`bg-emerald-500`:`bg-slate-300`}`}),e?t:n]})}function xh(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 Sh(e){return!e.enabled||e.status===`disabled`?`비활성화`:e.active&&e.runtime.dnsmasqRunning&&e.runtime.dnsRuntimeOk?`보호 중`:e.status===`paused`?`일시 중지`:`확인 필요`}function Ch(e){let t=xh(e);return t===`error`&&Sh(e)===`보호 중`&&e.runtime.lastErrorCode?`갱신 실패`:{disabled:`비활성화`,refreshing:`갱신 중`,protecting:`보호 중`,paused:`일시 중지`,error:`오류`,attention:`확인 필요`}[t]}function wh(e){return e?`자동 최적화`:`확인되지 않음`}function Th(e){let t=tn(e);return t===null?`확인되지 않음`:B(oi,{timestamp:t})}function Eh(e){let t=xh(e);return t===`protecting`?e.blocklist.validLineCount>0?`${G(e.blocklist.validLineCount)}개 도메인 차단 규칙으로 DNS를 보호하고 있습니다.`:e.blocklist.installed?`차단 목록이 설치되어 있으며 DNS 보호가 정상적으로 동작하고 있습니다.`:`SafeShield 서비스와 DNS 런타임이 정상적으로 동작하고 있습니다.`:t===`refreshing`?`차단 목록을 최신 상태로 갱신하고 있습니다.`:t===`disabled`?`SafeShield DNS 보호가 비활성화되어 있습니다.`:t===`paused`?`SafeShield 작업이 일시 중지되어 있습니다.`:t===`error`?Sh(e)===`보호 중`?`기존 DNS 보호는 유지되고 있지만 최근 차단 목록 갱신을 완료하지 못했습니다.`:`SafeShield 보호 상태에 문제가 발생했습니다. 아래 오류 내용을 확인해 주세요.`:e.active?e.runtime.dnsmasqRunning?e.runtime.dnsRuntimeOk?e.summary.message:`SafeShield DNS 런타임 상태를 확인해야 합니다.`:`dnsmasq가 실행 중이 아니어서 DNS 보호 상태를 확인해야 합니다.`:`SafeShield가 활성화되어 있지만 서비스가 실행 중이 아닙니다.`}function Dh(e){return{ok:`정상`,warning:`경고`,error:`오류`,unavailable:`사용 불가`}[e]??e}function Oh({data:e}){let t=xh(e);return B(`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:Ch(e)})}function kh({failed:e,step:t,total:n}){let r=2*Math.PI*17,i=r*.22,a=r-i;return B(`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:[B(`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:[B(`circle`,{class:`ssh-safeshield-refresh-loader-track`,cx:`24`,cy:`24`,fill:`none`,r:17,stroke:`currentColor`,strokeWidth:2.5}),e?null:B(`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})]}),B(`span`,{class:`ssh-safeshield-refresh-donut-label`,children:e?`!`:`${t}/${n}`})]})}function Ah({data:e}){let t=hh(e.status,e.stage),n=xh(e)===`error`;if(!t&&!n)return null;let r=gh(e.stage),i=n?vh(e.runtime.lastErrorCode,e.stage):null;return B(`div`,{"aria-live":n?`assertive`:`polite`,class:`ssh-safeshield-refresh`,"data-state":n?`error`:`active`,role:n?`alert`:`status`,children:[B(kh,{failed:n,step:r.number,total:r.total}),B(`div`,{class:`ssh-safeshield-refresh-copy`,children:[B(`p`,{class:`ssh-safeshield-refresh-kicker`,children:n?`갱신 실패 · ${r.number}/${r.total}`:`갱신 ${r.number}/${r.total}`}),B(`p`,{class:`ssh-safeshield-refresh-title`,children:n?`${r.label} 단계에서 문제가 발생했습니다.`:r.label}),B(`p`,{class:`ssh-safeshield-refresh-description`,children:i??r.description}),n&&e.runtime.lastErrorCode?B(`p`,{class:`ssh-safeshield-refresh-error-code`,children:[`오류 코드: `,B(`code`,{children:e.runtime.lastErrorCode})]}):null]})]})}function jh({label:e,value:t}){return B(`div`,{class:`bg-white px-5 py-4 sm:px-6`,children:[B(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.14em] text-slate-400`,children:e}),B(`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 Mh(e){return e?.trim().toUpperCase()||`FREE`}function Nh(e){return e===`FREE`?`free`:e===`PRO`?`pro`:e===`ULTIMATE`?`ultimate`:`paid`}function Ph({compact:e=!1,plan:t}){let n=Nh(t),r=n!==`free`;return B(`span`,{class:`ssh-safeshield-plan-badge`,"data-compact":e?`true`:`false`,"data-tier":n,title:r?`${t} 멤버십`:`무료 플랜`,children:[B(`span`,{"aria-hidden":`true`,class:`ssh-safeshield-plan-badge-mark`,children:r?`✦`:`•`}),B(`span`,{children:t})]})}function Fh({plan:e}){let t=Nh(e)===`free`;return B(`div`,{class:`bg-white px-5 py-4 sm:px-6`,children:[B(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.14em] text-slate-400`,children:`Plan`}),B(`dd`,{class:`mt-2 mb-0 ml-0 flex min-w-0 flex-wrap items-center gap-2`,children:[B(Ph,{plan:e}),t?B(`span`,{class:`ssh-safeshield-plan-caption`,children:`기본 플랜`}):null]})]})}function Ih(){return B(`div`,{class:`ssh-safeshield-upgrade-card`,children:[B(`span`,{"aria-hidden":`true`,class:`ssh-safeshield-upgrade-mark`,children:`✦`}),B(`div`,{class:`ssh-safeshield-upgrade-copy`,children:[B(`div`,{class:`ssh-safeshield-upgrade-meta`,children:[B(`span`,{children:`SmartSafeHub Membership`}),B(`span`,{class:`ssh-safeshield-upgrade-soon`,children:`Coming soon`})]}),B(`strong`,{children:`더 강력한 보호 기능을 준비하고 있습니다.`}),B(`span`,{children:`프리미엄 보호와 확장된 관리 기능을 미리 확인해 보세요.`})]}),B(`a`,{class:`ssh-safeshield-upgrade-action`,href:yh,rel:`noopener noreferrer`,target:`_blank`,children:[`요금제 보기`,B(`span`,{"aria-hidden":`true`,children:`→`})]})]})}function Lh({children:e,eyebrow:t,icon:n,title:r}){return B(`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:[B(`div`,{class:`flex min-w-0 items-start justify-between gap-4 xl:gap-2`,children:[B(`div`,{children:[B(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.16em] text-slate-400`,children:t}),B(`h3`,{class:`mt-2 mb-0 text-lg font-black tracking-tight text-slate-950 xl:text-base 2xl:text-lg`,children:r})]}),B(`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})]}),B(`dl`,{class:`mt-5 mb-0 grid gap-3`,children:e})]})}function Rh({label:e,value:t}){return B(`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:[B(`dt`,{class:`text-sm font-semibold text-slate-500`,children:e}),B(`dd`,{class:`m-0 min-w-0 break-words text-right text-sm font-extrabold text-slate-950`,children:t})]})}function zh({error:e,message:t,onDismiss:n}){if(!e&&!t)return null;let r=e!==null;return B(`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:[B(`div`,{class:`flex min-w-0 items-start gap-3`,children:[B(r?U:_t,{class:`mt-0.5 size-5 shrink-0`}),B(`p`,{class:`m-0 font-semibold leading-6`,children:e??t})]}),B(`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 Bh({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,_]=R(null);if(z(()=>{r?.available&&r.blocklist.validLineCount>0&&!hh(r.status,r.stage)&&_(r.blocklist.validLineCount)},[r?.available,r?.blocklist.validLineCount,r?.stage,r?.status]),a)return B(Bn,{});if(i)return B(Vn,{message:i,onRetry:f});if(!r)return B(Bn,{});if(!r.available)return B(`section`,{class:`rounded-2xl border border-amber-200 bg-amber-50 p-5 sm:p-6`,children:[B(`span`,{class:`grid size-11 place-items-center rounded-xl bg-amber-100 text-amber-800`,children:B(U,{class:`size-6`})}),B(`h2`,{class:`mt-5 mb-0 text-xl font-extrabold text-amber-950`,children:`SafeShield 상태 API를 찾을 수 없습니다`}),B(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-amber-900`,children:`safeshield 패키지와 rpcd ucode 플러그인이 설치되어 있는지 확인해 주세요.`})]});let v=r.enabled,y=Mh(r.entitlement.plan),x=r.status===`running`,S=e!==null,C=(hh(r.status,r.stage)||xh(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 B(b,{children:[B(W,{eyebrow:`Protection`,title:`SafeShield 보호 상태`,description:`DNS 보호 상태와 적용 중인 차단 목록을 확인합니다.`}),B(`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:[B(`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:[B(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between xl:flex-col 2xl:flex-row`,children:[B(`div`,{class:`flex min-w-0 items-start gap-3`,children:[B(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:B(We,{class:`size-5`})}),B(`div`,{class:`min-w-0`,children:[B(`div`,{class:`flex flex-wrap items-center gap-2`,children:[B(`h3`,{class:`m-0 text-base font-black text-slate-950`,children:`현재 보호 상태`}),B(Oh,{data:r})]}),B(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:Eh(r)})]})]}),B(`div`,{class:`flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap xl:justify-end`,children:[B(`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:[B(lt,{class:`size-4 ${e===`enable`||e===`disable`?`animate-pulse`:``}`}),w]}),B(`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:[B(ut,{class:`size-4 ${e===`refresh`||x?`animate-bounce`:``}`}),e===`refresh`?`시작 중…`:x?`갱신 중…`:r.status===`error`?`다시 갱신`:`지금 갱신`]})]})]}),B(Ah,{data:r}),B(`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:[B(jh,{label:`SafeShield`,value:r.version??`확인되지 않음`}),B(jh,{label:`Blocklist`,value:`${G(C)}개 도메인`}),B(jh,{label:`Last refresh`,value:B(oi,{timestamp:r.timestamps.lastSuccess})}),B(Fh,{plan:y})]}),y===`FREE`?B(Ih,{}):null]}),B(`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:[B(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[B(`div`,{class:`flex min-w-0 items-center gap-3`,children:[B(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-500`,children:B(dt,{class:`size-5`})}),B(`div`,{children:[B(`p`,{class:`m-0 text-[0.65rem] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Protection data`}),B(`h3`,{class:`mt-1 mb-0 text-sm font-black text-slate-950`,children:`보호 데이터`})]})]}),B(`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?B(_t,{class:`size-3.5 shrink-0`}):B(U,{class:`size-3.5 shrink-0`}),r.artifact.resolved&&r.blocklist.installed&&r.blocklist.verificationOk?`최신`:`확인 필요`]})]}),B(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-500`,children:`SafeShield가 기기 성능에 맞춰 사용하는 보호 목록의 구성과 최신 상태를 확인합니다.`}),B(`dl`,{class:`mt-3 mb-0 grid grid-cols-1 gap-2 sm:grid-cols-2`,children:[B(`div`,{class:`min-w-0 rounded-lg border border-slate-200 bg-white px-3 py-2.5`,children:[B(`dt`,{class:`text-xs font-bold text-slate-500`,children:`기기 최적화`}),B(`dd`,{class:`mt-1 mb-0 text-sm font-extrabold text-slate-950`,children:wh(r.artifact.tier)})]}),B(`div`,{class:`min-w-0 rounded-lg border border-slate-200 bg-white px-3 py-2.5`,children:[B(`dt`,{class:`text-xs font-bold text-slate-500`,children:`마지막 업데이트`}),B(`dd`,{class:`mt-1 mb-0 text-sm font-extrabold leading-5 text-slate-950`,children:Th(r.artifact.version)})]})]}),B(`section`,{"aria-label":`보호 데이터 세부 정보`,class:`mt-3 border-t border-slate-200 pt-3`,children:[B(`h4`,{class:`m-0 text-xs font-extrabold text-slate-600`,children:`세부 정보`}),B(`dl`,{class:`mt-3 mb-0 grid gap-2`,children:[B(Rh,{label:`데이터 프로필`,value:r.artifact.tier||`확인되지 않음`}),B(Rh,{label:`데이터 버전`,value:r.artifact.version||`확인되지 않음`})]})]})]})]}),B(zh,{error:t,message:n,onDismiss:u}),B(fh,{action:e,data:o,error:s,loading:c,onRetry:p,onSetEnabled:h,refreshing:l}),B(`section`,{class:`mt-7`,children:[B(W,{description:`DNS 보호 런타임, 차단 목록, 갱신 일정과 상태 점검 결과를 한곳에서 확인합니다.`,eyebrow:`Protection details`,title:`보호 구성`}),B(`div`,{class:`grid min-w-0 grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4`,children:[B(Lh,{eyebrow:`Runtime`,icon:B(_t,{class:`size-5`}),title:`DNS 보호 런타임`,children:[B(Rh,{label:`SafeShield 서비스`,value:B(bh,{falseLabel:`중지됨`,trueLabel:`동작 중`,value:r.active})}),B(Rh,{label:`DNS 서비스`,value:B(bh,{falseLabel:`중지됨`,value:r.runtime.dnsmasqRunning})}),B(Rh,{label:`SafeShield DNS 연동`,value:B(bh,{value:r.runtime.dnsRuntimeOk})}),B(Rh,{label:`차단 목록 갱신`,value:B(bh,{falseLabel:`중지됨`,value:r.runtime.refreshdRunning})})]}),B(Lh,{eyebrow:`Blocklist`,icon:B(dt,{class:`size-5`}),title:`차단 목록`,children:[B(Rh,{label:`적용 규칙`,value:`${G(r.blocklist.validLineCount)}개 도메인`}),B(Rh,{label:`파일 크기`,value:Yt(r.blocklist.fileSizeKb*1024)}),B(Rh,{label:`설치 상태`,value:B(bh,{falseLabel:`미설치`,trueLabel:`설치됨`,value:r.blocklist.installed})}),B(Rh,{label:`검증 상태`,value:B(bh,{falseLabel:`확인 필요`,value:r.blocklist.verificationOk})})]}),B(Lh,{eyebrow:`Refresh schedule`,icon:B(gt,{class:`size-5`}),title:`차단 목록 갱신`,children:[B(Rh,{label:`마지막 성공`,value:B(oi,{timestamp:r.timestamps.lastSuccess})}),B(Rh,{label:`다음 갱신`,value:Qt(r.timestamps.nextRefreshAt)}),B(Rh,{label:`갱신 주기`,value:nn(r.timestamps.refreshIntervalS)}),B(Rh,{label:`최근 결과`,value:r.runtime.lastResult||(x?`갱신 중`:`확인되지 않음`)})]}),B(Lh,{eyebrow:`Health`,icon:B(We,{class:`size-5`}),title:`상태 점검`,children:[B(Rh,{label:`전체 상태`,value:Dh(r.health.overall)}),B(Rh,{label:`경고`,value:B(`span`,{class:`text-amber-700`,children:G(r.issueCounts.warnings)})}),B(Rh,{label:`오류`,value:B(`span`,{class:`text-red-700`,children:G(r.issueCounts.errors)})}),B(Rh,{label:`최근 오류 코드`,value:r.runtime.lastErrorCode||`없음`})]})]})]})]})}var Vh=Math.floor(Date.now()/1e3),Hh=3600,Uh={schema:1,component:`device`,phase:`registered`,lastResult:`success`,lastErrorCode:null,accountRegistered:!0,plan:`free`,pairingCode:null,pairingExpiresAt:null,nextSyncAt:0,lastSuccessAt:Vh-300},Wh={schema:1,deviceCode:`iptime-ax3000sm`,firmwareBuildId:`demo-build`,channel:`stable`,apps:[{id:`retro-arcade`,name:`레트로 아케이드`,summary:`브라우저에서 레트로 게임을 즐기는 엔터테인먼트 앱입니다.`,description:`사용자가 직접 준비한 게임 파일로 즐기는 레트로 아케이드입니다.`,category:`entertainment`,version:`1.1.0-r5`,webUi:{type:`static`,path:`/retro-arcade/`}},{id:`network-usage-monitor`,name:`네트워크 사용량 모니터`,summary:`기기별 네트워크 사용량과 통계를 확인하는 앱입니다.`,description:`네트워크 트래픽을 모니터링합니다.`,category:`network`,version:`2025.06.02~29236be6-r1`,webUi:null},{id:`wake-on-lan`,name:`Wake-on-LAN`,summary:`공유기에서 PC와 홈서버의 전원을 켤 수 있는 도구입니다.`,description:`Wake-on-LAN 지원 컴퓨터를 네트워크에서 깨우는 앱입니다.`,category:`utilities`,version:`26.288.56989~75a306d`,webUi:null}]},Gh={job:{phase:`idle`,action:``,appId:``,code:`NONE`},installed:{"retro-arcade":{installed:!0,partial:!1,managed:!0,updateAvailable:!0,installedVersion:`1.0.0-r1`,webUiReady:!0},"network-usage-monitor":{installed:!0,partial:!1,managed:!0,updateAvailable:!1,installedVersion:`2025.06.02~29236be6-r1`,webUiReady:!1},"wake-on-lan":{installed:!1,partial:!1,managed:!1,updateAvailable:!1,installedVersion:null,webUiReady:!1}}},Kh={storage:{mount:`/overlay`,totalBytes:268435456,availableBytes:193986560},installedSizes:{"retro-arcade":1394688,"network-usage-monitor":117760}},qh={generatedAt:Vh,devices:[{id:`demo-phone`,hostname:`거실 스마트폰`,mac:`02:00:00:00:00:01`,ipv4Address:`192.0.2.101`,connection:`wifi`,online:!0,leaseActive:!0,leaseExpiresAt:Vh+6*Hh,interface:`wlan0`,ssid:`SmartSafeHub`,radio:`radio0`,band:`5g`,bandLabel:`5 GHz`,signalDbm:-43,inactiveMs:250,connectedSeconds:8300},{id:`demo-laptop`,hostname:`작업용 노트북`,mac:`02:00:00:00:00:02`,ipv4Address:`192.0.2.102`,connection:`wifi`,online:!0,leaseActive:!0,leaseExpiresAt:Vh+8*Hh,interface:`wlan0`,ssid:`SmartSafeHub`,radio:`radio0`,band:`5g`,bandLabel:`5 GHz`,signalDbm:-57,inactiveMs:720,connectedSeconds:29200},{id:`demo-tv`,hostname:`거실 TV`,mac:`02:00:00:00:00:03`,ipv4Address:`192.0.2.103`,connection:`ethernet`,online:!0,leaseActive:!0,leaseExpiresAt:Vh+12*Hh,interface:`lan1`,ssid:null,radio:null,band:null,bandLabel:null,signalDbm:null,inactiveMs:null,connectedSeconds:14e4},{id:`demo-tablet`,hostname:`태블릿`,mac:`02:00:00:00:00:04`,ipv4Address:`192.0.2.104`,connection:`wifi`,online:!1,leaseActive:!1,leaseExpiresAt:null,interface:null,ssid:`SmartSafeHub`,radio:`radio1`,band:`2g`,bandLabel:`2.4 GHz`,signalDbm:null,inactiveMs:null,connectedSeconds:null}],totals:{known:4,online:3,wireless:2,ethernet:1,offline:1}},Jh={schema:1,scope:`current_boot`,volatile:!0,maxEvents:100,cloud:{enabled:!1,phase:`disabled`,eligible:!1,plan:`free`,retentionDays:0,pendingEvents:0,lastAttemptAt:0,lastSuccessAt:0,lastUploadedCount:0,lastErrorCode:null,nextSyncAt:0},events:[{schema:1,eventId:`demo-event-1`,eventType:`safeshield.blocklist.updated`,severity:`success`,occurredAt:Vh-7200,deviceUuid:null,source:`safeshield`,metadata:{}},{schema:1,eventId:`demo-event-2`,eventType:`system.boot`,severity:`info`,occurredAt:Vh-86400,deviceUuid:null,source:`system`,metadata:{}}]},Yh={device:{hostname:`SmartRouter`,model:`ipTIME AX3000SM`,boardName:`iptime,ax3000sm`},software:{distribution:`OpenWrt`,version:`25.12`,revision:`demo`,kernel:`6.12`},runtime:{uptime:86400,localtime:Vh,load:[.08,.12,.05],memory:{total:268435456,free:83886080,shared:4194304,buffered:41943040,available:171966464,cached:41943040},storage:Kh.storage},network:{available:!0,up:!0,protocol:`dhcp`,ipv4Address:`198.51.100.20`},activityHistory:Jh},Xh={lan:{address:`192.168.1.1`,prefixLength:24,netmask:`255.255.255.0`,subnet:`192.168.1.0/24`},dhcp:{enabled:!0,start:`192.168.1.100`,end:`192.168.1.249`,leaseTime:`12h`},wan:{connected:!0,protocol:`dhcp`,address:`198.51.100.20`,prefixLength:24,subnet:`198.51.100.0/24`},conflict:{detected:!1,wanSubnet:null},recommendation:null},Zh={available:!0,version:`0.3.25`,enabled:!0,active:!0,status:`idle`,stage:null,summary:{label:`보호 중`,message:`DNS 보호가 정상적으로 동작하고 있습니다.`,severity:`info`},runtime:{refreshdRunning:!0,dnsmasqRunning:!0,dnsRuntimeOk:!0,lastResult:`success`,lastErrorCode:null},entitlement:{plan:`free`,status:`active`},artifact:{resolved:!0,tier:`light`,version:`20261010T010000Z`,uniqueDomains:48e3,rules:48e3},localOverrides:{enabled:!0,allowlistPath:`/etc/safeshield/allowlist`,blocklistPath:`/etc/safeshield/blocklist`},blocklist:{installed:!0,validLineCount:48e3,fileSizeKb:4700,verificationOk:!0},health:{overall:`ok`},issueCounts:{warnings:0,errors:0},timestamps:{lastAttempt:Vh-2*Hh,lastSuccess:Vh-2*Hh,lastFailure:0,lastLocalApply:Vh-2*Hh,lastLocalApplyFailure:0,nextRefreshAt:Vh+21*Hh,refreshIntervalS:82800}},Qh=Math.floor(Vh/Hh)*Hh,$h=[{queries:351,blocked:18},{queries:297,blocked:15},{queries:243,blocked:11},{queries:216,blocked:9},{queries:203,blocked:8},{queries:230,blocked:10},{queries:324,blocked:17},{queries:567,blocked:35},{queries:783,blocked:54},{queries:864,blocked:61},{queries:756,blocked:50},{queries:702,blocked:43},{queries:635,blocked:36},{queries:580,blocked:30},{queries:608,blocked:33},{queries:689,blocked:40},{queries:837,blocked:54},{queries:1026,blocked:73},{queries:1256,blocked:99},{queries:1458,blocked:125},{queries:1512,blocked:135},{queries:1323,blocked:111},{queries:1026,blocked:75},{queries:648,blocked:39}],eg=Array.from({length:24},(e,t)=>{let n=Qh-(23-t)*Hh,r=$h[new Date(n*1e3).getHours()];return{bucketStart:n,queries:r.queries,blocked:r.blocked}}),tg={enabled:!0,collectorRunning:!0,available:!0,schemaVersion:1,volatile:!0,startedAt:Vh-604800,updatedAt:Vh-30,retentionHours:168,totals:{queries:eg.reduce((e,t)=>e+t.queries,0),blocked:eg.reduce((e,t)=>e+t.blocked,0)},hourly:eg,deviceLimit:128,devicesTruncated:!1,devices:[{id:`demo-phone`,mac:`02:00:00:00:00:01`,ip:`192.0.2.101`,hostname:`거실 스마트폰`,identified:!0,queries:3400,blocked:470},{id:`demo-laptop`,mac:`02:00:00:00:00:02`,ip:`192.0.2.102`,hostname:`작업용 노트북`,identified:!0,queries:2900,blocked:345},{id:`demo-tv`,mac:`02:00:00:00:00:03`,ip:`192.0.2.103`,hostname:`거실 TV`,identified:!0,queries:1200,blocked:185}]},ng={schema:1,generatedAt:Vh,overall:`ok`,summary:{message:`모든 주요 항목이 정상입니다.`,total:3,ok:3,warning:0,critical:0,unknown:0},metrics:{memoryAvailablePercent:64,load1m:.08,overlayAvailablePercent:80},checks:[],reporter:{enabled:!1,eligible:!1,plan:`free`,entitlementStatus:`active`,lastReportAt:0,nextReportAt:0,lastResult:``,lastErrorCode:null}},rg={phase:`idle`,updateCount:0,packages:[],lastCheckAt:Vh-Hh,lastInstallAt:null,lastError:null,releaseNotes:[],releaseNotesComplete:!0,settings:{channel:`stable`,checkEnabled:!0,checkIntervalSeconds:21600,autoInstall:!0,autoInstallTime:`03:00`,repositoryHost:`example.invalid`,updatePackage:`luci-app-smartsafehub`}},ig={phase:`idle`,source:null,lastCheckAt:Vh-Hh,lastPrepareAt:null,lastError:null,prepared:null,current:{deviceCode:`iptime-ax3000sm`,boardName:`iptime,ax3000sm`,buildId:`demo`,releaseVersion:`1.0.2`,openwrtVersion:`25.12`,metadataAvailable:!0},updateAvailable:!1,release:null,settings:{channel:`stable`,apiBaseUrl:`https://www.smartsafehub.com`,checkIntervalSeconds:21600,autoInstall:!1}},ag={configuration:{protocol:`dhcp`,supported:!0,pppoe:{username:``,passwordConfigured:!1},static:{address:null,prefixLength:null,netmask:null,gateway:null,dns:[]}},status:{connected:!0,pending:!1,protocol:`dhcp`,device:`wan`,address:`198.51.100.20`,prefixLength:24,gateway:`198.51.100.1`,dns:[`1.1.1.1`,`8.8.8.8`],uptimeSeconds:86400}},og={networks:[{section:`demo_5g`,device:`radio0`,band:`5g`,bandLabel:`5 GHz`,ssid:`SmartSafeHub`,enabled:!0,runtimeUp:!0,security:`sae-mixed`,securityRaw:`sae-mixed`,passwordConfigured:!0,channel:`36`,networks:[`lan`],clientCount:2},{section:`demo_2g`,device:`radio1`,band:`2g`,bandLabel:`2.4 GHz`,ssid:`SmartSafeHub-2G`,enabled:!0,runtimeUp:!0,security:`sae-mixed`,securityRaw:`sae-mixed`,passwordConfigured:!0,channel:`6`,networks:[`lan`],clientCount:0}],totalClients:2,guest:{supported:!0,configured:!0,enabled:!0,ssid:`SmartSafeHub-Guest`,passwordConfigured:!0,subnet:`192.0.2.0/24`,radio:`radio1`}},sg={allow:[`trusted.example`,`updates.example`],block:[`ads.example`,`tracker.example`],counts:{allow:2,block:2,total:4},safeshieldEnabled:!0,applyLocalOverrides:!0},cg=async e=>({ssid:og.networks.find(t=>t.section===e)?.ssid??`SmartSafeHub`,security:`sae-mixed`,password:`demo-only-2026`}),lg=async()=>({ssid:`SmartSafeHub-Guest`,security:`sae-mixed`,password:`guest-demo-only-2026`}),ug=new Set(Le);function dg(){let e=Gi(),t=Yi(),n=ug.has(e)?e:`home`,[r,i]=R(``);z(()=>{ug.has(e)||(window.history.replaceState(null,``,`${window.location.pathname}${window.location.search}#home`),i(`해당 기능은 현재 데모에서 제공하지 않습니다.`))},[e]);let a=()=>i(`체험 모드에서는 실제 계정 연결, 앱 설치·제거·실행 및 공유기 설정 변경을 수행하지 않습니다.`),o=()=>i(``),s=()=>i(`예시 데이터가 표시되고 있습니다. 실제 공유기에는 연결되지 않습니다.`),c=async()=>(a(),!1),l={action:null,actionError:null,data:ig,error:null,loading:!1,message:null,reconnecting:!1,uploadProgress:null,check:a,discard:c,dismissFeedback:o,install:c,prepare:a,refresh:s,upload:c},u;switch(n){case`account`:u=B(Jn,{data:Uh,error:null,loading:!1,pairingBusy:!1,refreshing:!1,syncError:null,demo:!0,onDemoAction:a,onRequestPairing:async()=>(a(),null),onRetry:s});break;case`apps`:u=B(zn,{data:Wh,error:null,loading:!1,refreshing:!1,installStatus:Gh,metrics:Kh,statusError:null,demo:!0,onDemoAction:a,onRefreshStatus:s,onRetry:s,onInstall:async()=>{a()},onRemove:async()=>{a()},onUpdate:async()=>{a()}});break;case`activity`:u=B(Cn,{actionError:null,actionMessage:null,data:Jh,error:null,loading:!1,onDismissActionFeedback:o,onRetry:s,onSetCloudSync:a,savingCloudSync:!1});break;case`network`:u=B(`div`,{class:`min-w-0`,children:[B(Xi,{active:t.network,label:`네트워크 설정 메뉴`,tabs:[{id:`internet`,label:`인터넷 연결`,href:`#network`},{id:`lan`,label:`LAN 및 DHCP`,href:`#network/lan`}]}),B(`div`,{"aria-label":`인터넷 연결 설정`,"aria-labelledby":`internet-tab`,hidden:t.network!==`internet`,id:`internet-panel`,role:`tabpanel`,children:B(Nr,{action:null,data:ag,error:null,feedback:null,loading:!1,onDismissFeedback:o,onReconnect:a,onRetry:s,onSave:c})}),B(`div`,{"aria-label":`내부 네트워크 설정`,"aria-labelledby":`lan-tab`,hidden:t.network!==`lan`,id:`lan-panel`,role:`tabpanel`,children:B(Wr,{action:null,data:Xh,error:null,feedback:null,loading:!1,onApplyRecommendation:c,onDismissFeedback:o,onRetry:s,onSave:c})})]});break;case`wifi`:u=B(ai,{data:og,error:null,feedback:null,loading:!1,updatingSection:null,onDismissFeedback:o,onRetry:s,onUpdate:c,onUpdateGuest:c,onLoadQr:cg,onLoadGuestQr:lg});break;case`rules`:u=B(_i,{action:null,data:sg,error:null,feedback:null,loading:!1,onAddRule:c,onDeleteRule:c,onDismissFeedback:o,onRetry:s});break;case`system`:u=B(Hi,{action:null,actionError:null,data:rg,error:null,firmware:l,loading:!1,message:null,onCheck:a,onDismissFeedback:o,onInstall:a,onRetry:s,onSaveSettings:c});break;case`devices`:u=B(ca,{data:qh,error:null,loading:!1,onRetry:s});break;case`safeshield`:u=B(Bh,{action:null,actionError:null,actionMessage:null,data:Zh,error:null,loading:!1,statistics:tg,statisticsError:null,statisticsLoading:!1,statisticsRefreshing:!1,onDismissFeedback:o,onRefreshBlocklist:a,onRetry:s,onRetryStatistics:s,onSetEnabled:a,onSetStatisticsEnabled:a});break;default:u=B($m,{accountRegistered:Uh.accountRegistered,activity:Jh,activityError:null,activityLoading:!1,data:Yh,devices:qh,devicesError:null,devicesLoading:!1,error:null,firmware:ig,firmwareError:null,firmwareLoading:!1,health:ng,healthError:null,healthLoading:!1,lan:Xh,lanError:null,lanLoading:!1,loading:!1,onRetry:s,safeshield:Zh,safeshieldError:null,safeshieldLoading:!1,statistics:tg,statisticsError:null,statisticsLoading:!1,statisticsRefreshing:!1,updates:rg,updatesError:null,updatesLoading:!1})}return B(Gt,{accountRegistered:Uh.accountRegistered,demo:!0,loading:!1,onRefresh:s,refreshing:!1,route:n,updateCount:0,children:[B(`div`,{role:`status`,class:`mb-5 flex flex-wrap items-center justify-between gap-3 rounded-xl border border-teal-200 bg-teal-50 px-4 py-3 text-sm font-semibold text-teal-800`,children:[B(`span`,{children:`체험 모드 · 표시되는 기기와 보호 통계는 모두 예시입니다.`}),B(`a`,{class:`font-extrabold underline`,href:`/devices/`,children:`지원 기기 살펴보기 →`})]}),r?B(`div`,{role:`alert`,class:`mb-5 flex items-center justify-between gap-3 rounded-xl border border-amber-300 bg-amber-50 p-4 text-sm font-semibold text-amber-900`,children:[r,B(`button`,{"aria-label":`안내 닫기`,class:`font-black`,onClick:o,type:`button`,children:`✕`})]}):null,u]})}var fg=document.getElementById(`smartsafehub-demo-root`);if(fg){let e=fg.attachShadow({mode:`open`}),t=document.createElement(`link`);t.rel=`stylesheet`,t.href=fg.dataset.demoCss||`/static/router-demo/demo.css`,e.appendChild(t);let n=document.createElement(`div`);n.className=`smartsafehub-shadow-root`,e.appendChild(n),oe(B(dg,{}),n)} \ No newline at end of file diff --git a/luci-app-smartsafehub/frontend/package-lock.json b/luci-app-smartsafehub/frontend/package-lock.json index 6a4ee073c..19c7c3c2b 100644 --- a/luci-app-smartsafehub/frontend/package-lock.json +++ b/luci-app-smartsafehub/frontend/package-lock.json @@ -1,23 +1,23 @@ { "name": "luci-app-smartsafehub-frontend", - "version": "0.2.30", + "version": "0.2.31", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "luci-app-smartsafehub-frontend", - "version": "0.2.30", + "version": "0.2.31", "dependencies": { - "chart.js": "4.5.1", - "preact": "11.0.1" + "chart.js": "4.6.1", + "preact": "11.1.0" }, "devDependencies": { "@preact/preset-vite": "2.10.6", "@tailwindcss/vite": "4.3.3", - "@types/node": "26.6.4", + "@types/node": "26.6.5", "tailwindcss": "4.3.3", "typescript": "7.0.2", - "vite": "8.3.4" + "vite": "8.3.5" }, "engines": { "node": ">=24.0" @@ -448,9 +448,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.153.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.153.0.tgz", - "integrity": "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz", + "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==", "dev": true, "license": "MIT", "funding": { @@ -550,9 +550,9 @@ } }, "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.13.tgz", - "integrity": "sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", + "integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==", "cpu": [ "arm" ], @@ -567,9 +567,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.13.tgz", - "integrity": "sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz", + "integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==", "cpu": [ "arm64" ], @@ -584,9 +584,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.13.tgz", - "integrity": "sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz", + "integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==", "cpu": [ "arm64" ], @@ -601,9 +601,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.13.tgz", - "integrity": "sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz", + "integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==", "cpu": [ "x64" ], @@ -618,9 +618,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.13.tgz", - "integrity": "sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz", + "integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==", "cpu": [ "x64" ], @@ -635,9 +635,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.13.tgz", - "integrity": "sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz", + "integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==", "cpu": [ "arm" ], @@ -652,9 +652,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.13.tgz", - "integrity": "sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz", + "integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==", "cpu": [ "arm64" ], @@ -669,9 +669,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.13.tgz", - "integrity": "sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz", + "integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==", "cpu": [ "arm64" ], @@ -686,9 +686,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.13.tgz", - "integrity": "sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz", + "integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==", "cpu": [ "ppc64" ], @@ -703,9 +703,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.13.tgz", - "integrity": "sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz", + "integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==", "cpu": [ "s390x" ], @@ -720,9 +720,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.13.tgz", - "integrity": "sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz", + "integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==", "cpu": [ "x64" ], @@ -737,9 +737,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.13.tgz", - "integrity": "sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz", + "integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==", "cpu": [ "x64" ], @@ -754,9 +754,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.13.tgz", - "integrity": "sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz", + "integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==", "cpu": [ "arm64" ], @@ -771,9 +771,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.13.tgz", - "integrity": "sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz", + "integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==", "cpu": [ "arm64" ], @@ -788,9 +788,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.13.tgz", - "integrity": "sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz", + "integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==", "cpu": [ "x64" ], @@ -1125,9 +1125,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.6.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", - "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", "dev": true, "license": "MIT", "dependencies": { @@ -1560,9 +1560,9 @@ "license": "CC-BY-4.0" }, "node_modules/chart.js": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", - "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.6.1.tgz", + "integrity": "sha512-hJyniucsFmq//EvXYvJ6c6mahPoI3kaxdjVH8awTaJ/fDNWeSqqqArixMJWkWNN6fcYvPU3HneSANFNYtRi61g==", "license": "MIT", "dependencies": { "@kurkle/color": "^0.3.0" @@ -2218,9 +2218,9 @@ } }, "node_modules/postcss": { - "version": "8.5.29", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", - "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", "dev": true, "funding": [ { @@ -2238,7 +2238,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.19", + "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" }, @@ -2247,15 +2247,16 @@ } }, "node_modules/preact": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/preact/-/preact-11.0.1.tgz", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-11.1.0.tgz", + "integrity": "sha512-hz1VsjWx5pSYPDVEzb9w+XyOmQpP1qW/ZEaMiYm+C8ykJr8KP+TKGZIbCW0QmZ/Yr/jg3XkZoziEWn9iWHZ9Mw==", "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" }, "peerDependencies": { - "preact-render-to-string": ">=5" + "preact-render-to-string": ">=6.7.0" }, "peerDependenciesMeta": { "preact-render-to-string": { @@ -2264,13 +2265,13 @@ } }, "node_modules/rolldown": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.13.tgz", - "integrity": "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz", + "integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.153.0", + "@oxc-project/types": "=0.155.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -2280,21 +2281,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.13", - "@rolldown/binding-android-arm64": "1.2.13", - "@rolldown/binding-darwin-arm64": "1.2.13", - "@rolldown/binding-darwin-x64": "1.2.13", - "@rolldown/binding-freebsd-x64": "1.2.13", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.13", - "@rolldown/binding-linux-arm64-gnu": "1.2.13", - "@rolldown/binding-linux-arm64-musl": "1.2.13", - "@rolldown/binding-linux-ppc64-gnu": "1.2.13", - "@rolldown/binding-linux-s390x-gnu": "1.2.13", - "@rolldown/binding-linux-x64-gnu": "1.2.13", - "@rolldown/binding-linux-x64-musl": "1.2.13", - "@rolldown/binding-openharmony-arm64": "1.2.13", - "@rolldown/binding-win32-arm64-msvc": "1.2.13", - "@rolldown/binding-win32-x64-msvc": "1.2.13" + "@rolldown/binding-android-arm-eabi": "1.2.15", + "@rolldown/binding-android-arm64": "1.2.15", + "@rolldown/binding-darwin-arm64": "1.2.15", + "@rolldown/binding-darwin-x64": "1.2.15", + "@rolldown/binding-freebsd-x64": "1.2.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.15", + "@rolldown/binding-linux-arm64-gnu": "1.2.15", + "@rolldown/binding-linux-arm64-musl": "1.2.15", + "@rolldown/binding-linux-ppc64-gnu": "1.2.15", + "@rolldown/binding-linux-s390x-gnu": "1.2.15", + "@rolldown/binding-linux-x64-gnu": "1.2.15", + "@rolldown/binding-linux-x64-musl": "1.2.15", + "@rolldown/binding-openharmony-arm64": "1.2.15", + "@rolldown/binding-win32-arm64-msvc": "1.2.15", + "@rolldown/binding-win32-x64-msvc": "1.2.15" } }, "node_modules/semver": { @@ -2467,16 +2468,16 @@ } }, "node_modules/vite": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", - "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz", + "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", - "postcss": "^8.5.29", - "rolldown": "~1.2.12", + "postcss": "^8.5.30", + "rolldown": "~1.2.14", "tinyglobby": "^0.2.17" }, "bin": { @@ -2493,7 +2494,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.7.1", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", diff --git a/luci-app-smartsafehub/frontend/package.json b/luci-app-smartsafehub/frontend/package.json index 7be743399..7664ced3c 100644 --- a/luci-app-smartsafehub/frontend/package.json +++ b/luci-app-smartsafehub/frontend/package.json @@ -1,7 +1,7 @@ { "name": "luci-app-smartsafehub-frontend", "private": true, - "version": "0.2.30", + "version": "0.2.31", "type": "module", "engines": { "node": ">=24.0" @@ -14,15 +14,15 @@ "preview": "vite preview --host 0.0.0.0" }, "dependencies": { - "chart.js": "4.5.1", - "preact": "11.0.1" + "chart.js": "4.6.1", + "preact": "11.1.0" }, "devDependencies": { "@preact/preset-vite": "2.10.6", "@tailwindcss/vite": "4.3.3", - "@types/node": "26.6.4", + "@types/node": "26.6.5", "tailwindcss": "4.3.3", "typescript": "7.0.2", - "vite": "8.3.4" + "vite": "8.3.5" } } diff --git a/luci-app-smartsafehub/frontend/src/api/rpc.ts b/luci-app-smartsafehub/frontend/src/api/rpc.ts index b9b44ce4b..5c57e3a06 100644 --- a/luci-app-smartsafehub/frontend/src/api/rpc.ts +++ b/luci-app-smartsafehub/frontend/src/api/rpc.ts @@ -4,6 +4,8 @@ import { } from '../auth/sessionEvents'; import type { SmartSafeHubBootstrap } from '../types/bootstrap'; import type { ApiResponse } from '../types/status'; +import { ensureRpcDiagnostics, recordRpcTiming } from './rpcDiagnostics'; +import { acquireRpcSlot, type RpcPriority } from './rpcScheduler'; const UBUS_STATUS_TEXT: Readonly> = { 1: '잘못된 명령입니다.', @@ -27,6 +29,8 @@ let requestId = 1; export interface RpcCallOptions { timeoutMs?: number; + signal?: AbortSignal; + priority?: RpcPriority; } export class RpcError extends Error { @@ -258,15 +262,25 @@ export async function callRpc( options: RpcCallOptions = {}, ): Promise { const bootstrap = getBootstrap(); + ensureRpcDiagnostics(); const startedAt = performance.now(); + let responseMs: number | null = null; + let decodeMs: number | null = null; + let resultCode = 'ok'; const id = requestId++; + // Cancellation while queued never occupies a network slot. Start the + // response timeout only after a slot is acquired, not while waiting. + let release: (() => void) | null = null; + let timeout: number | null = null; const controller = new AbortController(); - const timeout = window.setTimeout( - () => controller.abort(), - normalizedTimeout(options.timeoutMs), - ); + let cancelled = false; + const cancel = () => { cancelled = true; controller.abort(); }; + options.signal?.addEventListener('abort', cancel, { once: true }); try { + release = await acquireRpcSlot(options.priority ?? 'normal', options.signal); + if (cancelled || options.signal?.aborted) throw new RpcError('RPC_CANCELLED', '요청이 취소되었습니다.'); + timeout = window.setTimeout(() => controller.abort(), normalizedTimeout(options.timeoutMs)); const response = await fetch(bootstrap.rpcUrl, { method: 'POST', credentials: 'same-origin', @@ -281,6 +295,7 @@ export async function callRpc( }), signal: controller.signal, }); + responseMs = performance.now() - startedAt; if (!response.ok) { throw new RpcError( @@ -293,33 +308,52 @@ export async function callRpc( try { payload = await response.json(); + decodeMs = performance.now() - startedAt - (responseMs ?? 0); } catch { throw invalidResponse(); } return parseJsonRpcResponse(payload, id); } catch (error) { + if (cancelled || options.signal?.aborted) { + resultCode = 'RPC_CANCELLED'; + throw new RpcError('RPC_CANCELLED', '요청이 취소되었습니다.'); + } if (controller.signal.aborted) { + resultCode = 'RPC_TIMEOUT'; throw new RpcError( 'RPC_TIMEOUT', '장치 응답 시간이 초과되었습니다. 잠시 후 다시 시도해 주세요.', ); } + if (error instanceof DOMException && error.name === 'AbortError') { + resultCode = 'RPC_CANCELLED'; + throw new RpcError('RPC_CANCELLED', '요청이 취소되었습니다.'); + } if (error instanceof RpcError) { - throw await sessionExpiryError(error, bootstrap); + const resolved = await sessionExpiryError(error, bootstrap); + resultCode = resolved.code; + throw resolved; } + resultCode = 'NETWORK_ERROR'; throw new RpcError( 'NETWORK_ERROR', '공유기와 통신할 수 없습니다. 네트워크 연결을 확인해 주세요.', ); } finally { - window.clearTimeout(timeout); + if (timeout !== null) window.clearTimeout(timeout); + options.signal?.removeEventListener('abort', cancel); + release?.(); const elapsedMs = Math.round(performance.now() - startedAt); - // Development diagnostics only: never record parameters, tokens, or responses. - if (import.meta.env.DEV && elapsedMs >= 1000) { - console.warn(`[RPC slow] ${object}.${method}: ${elapsedMs}ms`); + const slow = recordRpcTiming({ + object, method, elapsedMs, responseMs, decodeMs, result: resultCode, + }); + // One concise warning only for a slow call or a timeout; full bounded + // timings are available in DevTools even on a production router. + if (slow || resultCode === 'RPC_TIMEOUT') { + console.warn(`[SmartSafeHub RPC] ${object}.${method}: ${elapsedMs}ms (${resultCode})`); } } } diff --git a/luci-app-smartsafehub/frontend/src/api/rpcDiagnostics.ts b/luci-app-smartsafehub/frontend/src/api/rpcDiagnostics.ts new file mode 100644 index 000000000..57700dc18 --- /dev/null +++ b/luci-app-smartsafehub/frontend/src/api/rpcDiagnostics.ts @@ -0,0 +1,112 @@ +// Browser-local, bounded RPC diagnostics. Never store URL, session, request +// arguments, headers, response data, or error messages. +export const RPC_SLOW_THRESHOLD_MS = 1_500; +const MAX_RECENT = 60; +const MAX_METHODS = 64; + +export interface RpcTiming { + object: string; + method: string; + elapsedMs: number; + responseMs: number | null; + decodeMs: number | null; + result: string; +} + +interface RpcEvent extends RpcTiming { + at: string; +} + +interface RpcSummary { + method: string; + calls: number; + slow: number; + errors: number; + lastMs: number; + maxMs: number; + averageMs: number; +} + +interface MutableSummary extends RpcSummary { totalMs: number } + +export interface RpcDiagnosticsSnapshot { + slowThresholdMs: number; + summary: RpcSummary[]; + recent: RpcEvent[]; +} + +export interface RpcDiagnosticsApi { + snapshot: () => RpcDiagnosticsSnapshot; + reset: () => void; +} + +declare global { + interface Window { + __SMARTHUB_RPC_DIAGNOSTICS__?: RpcDiagnosticsApi; + } +} + +const summaries = new Map(); +const recent: RpcEvent[] = []; + +function rounded(milliseconds: number | null): number | null { + return milliseconds === null ? null : Math.max(0, Math.round(milliseconds)); +} + +export function recordRpcTiming(timing: RpcTiming): boolean { + const method = `${timing.object}.${timing.method}`; + const elapsedMs = rounded(timing.elapsedMs) ?? 0; + const slow = elapsedMs >= RPC_SLOW_THRESHOLD_MS; + const failed = timing.result !== 'ok'; + let summary = summaries.get(method); + if (!summary && summaries.size < MAX_METHODS) { + summary = { method, calls: 0, slow: 0, errors: 0, lastMs: 0, maxMs: 0, averageMs: 0, totalMs: 0 }; + summaries.set(method, summary); + } + if (summary) { + summary.calls++; + summary.slow += Number(slow); + summary.errors += Number(failed); + summary.totalMs += elapsedMs; + summary.lastMs = elapsedMs; + summary.maxMs = Math.max(summary.maxMs, elapsedMs); + summary.averageMs = Math.round(summary.totalMs / summary.calls); + } + + if (slow || failed) { + recent.push({ + at: new Date().toISOString(), + object: timing.object, + method: timing.method, + elapsedMs, + responseMs: rounded(timing.responseMs), + decodeMs: rounded(timing.decodeMs), + result: timing.result, + }); + if (recent.length > MAX_RECENT) recent.shift(); + } + return slow; +} + +export function snapshotRpcDiagnostics(): RpcDiagnosticsSnapshot { + return { + slowThresholdMs: RPC_SLOW_THRESHOLD_MS, + summary: [...summaries.values()].map(({ totalMs: _totalMs, ...summary }) => ({ ...summary })), + recent: recent.map((entry) => ({ ...entry })), + }; +} + +export function resetRpcDiagnostics(): void { + summaries.clear(); + recent.length = 0; +} + +const diagnosticsApi: RpcDiagnosticsApi = { + snapshot: snapshotRpcDiagnostics, + reset: resetRpcDiagnostics, +}; + +export function ensureRpcDiagnostics(): void { + // Enable in production as well, without persistent storage or remote transfer. + window.__SMARTHUB_RPC_DIAGNOSTICS__ ??= diagnosticsApi; +} diff --git a/luci-app-smartsafehub/frontend/src/api/rpcScheduler.ts b/luci-app-smartsafehub/frontend/src/api/rpcScheduler.ts new file mode 100644 index 000000000..041ff0e95 --- /dev/null +++ b/luci-app-smartsafehub/frontend/src/api/rpcScheduler.ts @@ -0,0 +1,52 @@ +/** Shared bounded queue. Reserve one connection for user-triggered requests. */ +export type RpcPriority = 'high' | 'normal' | 'low'; +interface Waiter { + priority: RpcPriority; + start: (release: () => void) => void; + abort: () => void; +} +const MAX_RUNNING = 4; +const MAX_BACKGROUND = 3; +let running = 0; +let background = 0; +const queue: Waiter[] = []; + +function drain() { + while (running < MAX_RUNNING) { + const index = queue.findIndex((waiter) => waiter.priority !== 'low' || background < MAX_BACKGROUND); + if (index === -1) return; + const waiter = queue.splice(index, 1)[0]; + if (!waiter) continue; + running += 1; + if (waiter.priority === 'low') background += 1; + waiter.start(() => { + running -= 1; + if (waiter.priority === 'low') background -= 1; + drain(); + }); + } +} + +export function acquireRpcSlot(priority: RpcPriority, signal?: AbortSignal): Promise<() => void> { + if (signal?.aborted) return Promise.reject(new DOMException('Cancelled', 'AbortError')); + return new Promise((resolve, reject) => { + const waiter: Waiter = { + priority, + start: (release) => { signal?.removeEventListener('abort', waiter.abort); resolve(release); }, + abort: () => { + const index = queue.indexOf(waiter); + if (index !== -1) queue.splice(index, 1); + signal?.removeEventListener('abort', waiter.abort); + reject(new DOMException('Cancelled', 'AbortError')); + }, + }; + signal?.addEventListener('abort', waiter.abort, { once: true }); + // Preserve FIFO within priority while permitting interactive RPC ahead of polls. + const last = queue.findIndex((item) => + (priority === 'high' && item.priority !== 'high') || + (priority === 'normal' && item.priority === 'low')); + if (last === -1) queue.push(waiter); + else queue.splice(last, 0, waiter); + drain(); + }); +} diff --git a/luci-app-smartsafehub/frontend/src/api/safeshield.ts b/luci-app-smartsafehub/frontend/src/api/safeshield.ts index 8d8c512b7..71eead0b5 100644 --- a/luci-app-smartsafehub/frontend/src/api/safeshield.ts +++ b/luci-app-smartsafehub/frontend/src/api/safeshield.ts @@ -125,8 +125,9 @@ function apiError(response: Record): RpcError { async function callSafeShield( method: string, params: Record = {}, + signal?: AbortSignal, ): Promise { - const response = await callRpc(API_OBJECT, method, params); + const response = await callRpc(API_OBJECT, method, params, { ...(signal ? { signal } : {}), priority: signal ? 'low' : 'normal' }); const object = objectValue(response); if (object.ok === false) { @@ -455,8 +456,8 @@ export async function fetchSafeShieldStatus(): Promise { } } -async function fetchSafeShieldRuleConfig(): Promise { - const response = await callSafeShield('config'); +async function fetchSafeShieldRuleConfig(signal?: AbortSignal): Promise { + const response = await callSafeShield('config', {}, signal); return normalizeRuleConfig(response); } @@ -491,10 +492,10 @@ export async function requestSafeShieldRefresh(): Promise { +export async function fetchSafeShieldRules(signal?: AbortSignal): Promise { const [rulesResponse, config] = await Promise.all([ - callSafeShield('rules_list'), - fetchSafeShieldRuleConfig(), + callSafeShield('rules_list', {}, signal), + fetchSafeShieldRuleConfig(signal), ]); const allow = stringArray(rulesResponse.allow); const block = stringArray(rulesResponse.block); diff --git a/luci-app-smartsafehub/frontend/src/api/smartsafehub.ts b/luci-app-smartsafehub/frontend/src/api/smartsafehub.ts index dad5c86bf..dfb80d206 100644 --- a/luci-app-smartsafehub/frontend/src/api/smartsafehub.ts +++ b/luci-app-smartsafehub/frontend/src/api/smartsafehub.ts @@ -1,4 +1,5 @@ import type { ActivityHistory } from '../types/activity'; +import type { AppsCatalog, AppsInstallStatus, AppsMetrics, AppsOperationAccepted } from '../types/apps'; import type { ConfigurationBackupDiscardResult, ConfigurationBackupRestoreResult, @@ -87,30 +88,39 @@ export interface DeviceRegistrationStatus { pairingExpiresAt: string | null; nextSyncAt: number; lastSuccessAt: number; + operation?: BackgroundOperation; + pairingOperation?: BackgroundOperation; } -export function fetchRecommendedRulesStatus(): Promise { - return callApi(API_OBJECT, 'device_recommended_rules_status', {}, { timeoutMs: 5000 }); +export interface BackgroundOperation { + phase: 'idle' | 'starting' | 'running' | 'success' | 'failed'; + code: string; + argument: string; + updatedAt: number; +} + +export function fetchRecommendedRulesStatus(signal?: AbortSignal): Promise { + return callApi(API_OBJECT, 'device_recommended_rules_status', {}, { ...(signal ? { signal } : {}), priority: 'low', timeoutMs: 5000 }); } export function refreshRecommendedRulesStatus(): Promise { - return callApi(API_OBJECT, 'device_recommended_rules_refresh', {}, { timeoutMs: 30000 }); + return callApi(API_OBJECT, 'device_recommended_rules_refresh', {}, { priority: 'high', timeoutMs: 6000 }); } export function revokeRecommendedRule(ruleId: number): Promise { - return callApi(API_OBJECT, 'device_recommended_rules_revoke', { rule_id: ruleId }, { timeoutMs: 30000 }); + return callApi(API_OBJECT, 'device_recommended_rules_revoke', { rule_id: ruleId }, { priority: 'high', timeoutMs: 6000 }); } -export function fetchDeviceRegistrationStatus(): Promise { - return callApi(API_OBJECT, 'device_registration_status', {}, { timeoutMs: 5000 }); +export function fetchDeviceRegistrationStatus(signal?: AbortSignal): Promise { + return callApi(API_OBJECT, 'device_registration_status', {}, { ...(signal ? { signal } : {}), priority: 'low', timeoutMs: 5000 }); } export function refreshDeviceRegistrationStatus(): Promise { - return callApi(API_OBJECT, 'device_registration_refresh', {}, { timeoutMs: 30000 }); + return callApi(API_OBJECT, 'device_registration_refresh', {}, { priority: 'high', timeoutMs: 6000 }); } export function requestDevicePairingCode(): Promise { - return callApi(API_OBJECT, 'device_pairing_refresh', {}, { timeoutMs: 15000 }); + return callApi(API_OBJECT, 'device_pairing_refresh', {}, { priority: 'high', timeoutMs: 6000 }); } export function fetchHealthStatus(): Promise { @@ -125,6 +135,42 @@ export function updateHealthReporter(enabled: boolean): Promise { return callApi(API_OBJECT, 'health_reporter_update', { enabled }); } +export function fetchAppsCatalog(signal?: AbortSignal): Promise { + return callApi(API_OBJECT, 'apps_catalog', {}, { ...(signal ? { signal } : {}), priority: 'low', timeoutMs: 6000 }); +} + +export function refreshAppsCatalog(): Promise { + return callApi(API_OBJECT, 'apps_catalog', { refresh: true }, { priority: 'high', timeoutMs: 6000 }); +} + +export function fetchAppsStatus(signal?: AbortSignal): Promise { + return callApi(API_OBJECT, 'apps_status', {}, { ...(signal ? { signal } : {}), priority: 'low', timeoutMs: 6000 }); +} + +export function refreshAppsStatus(): Promise { + return callApi(API_OBJECT, 'apps_status', { refresh: true }, { priority: 'high', timeoutMs: 6000 }); +} + +export function fetchAppsMetrics(signal?: AbortSignal): Promise { + return callApi(API_OBJECT, 'apps_metrics', {}, { ...(signal ? { signal } : {}), priority: 'low', timeoutMs: 6000 }); +} + +export function refreshAppsMetrics(): Promise { + return callApi(API_OBJECT, 'apps_metrics', { refresh: true }, { priority: 'high', timeoutMs: 6000 }); +} + +export function installVerifiedApp(appId: string): Promise { + return callApi(API_OBJECT, 'apps_install', { app_id: appId }, { timeoutMs: 8000 }); +} + +export function updateVerifiedApp(appId: string): Promise { + return callApi(API_OBJECT, 'apps_update', { app_id: appId }, { timeoutMs: 8000 }); +} + +export function removeVerifiedApp(appId: string): Promise { + return callApi(API_OBJECT, 'apps_remove', { app_id: appId }, { timeoutMs: 8000 }); +} + export function fetchSoftwareUpdates(): Promise { return callApi(API_OBJECT, 'updates_status'); } diff --git a/luci-app-smartsafehub/frontend/src/app/App.tsx b/luci-app-smartsafehub/frontend/src/app/App.tsx index 2c3528e90..78ac85347 100644 --- a/luci-app-smartsafehub/frontend/src/app/App.tsx +++ b/luci-app-smartsafehub/frontend/src/app/App.tsx @@ -1,12 +1,14 @@ import type { ComponentChildren } from 'preact'; -import { useEffect } from 'preact/hooks'; +import { useEffect, useRef } from 'preact/hooks'; -import { fetchGuestWifiQr, fetchRecommendedRulesStatus, refreshRecommendedRulesStatus, +import { fetchAppsCatalog, fetchAppsStatus, fetchAppsMetrics, refreshAppsCatalog, refreshAppsStatus, refreshAppsMetrics, installVerifiedApp, updateVerifiedApp, removeVerifiedApp, fetchGuestWifiQr, fetchRecommendedRulesStatus, refreshRecommendedRulesStatus, revokeRecommendedRule, fetchWifiQr, initializeSystemTimezone } from '../api/smartsafehub'; import { AppShell } from '../components/AppShell'; import { PageTabs } from '../components/PageTabs'; import { useActivityHistory } from '../hooks/useActivityHistory'; +import { useAsyncResource } from '../hooks/useAsyncResource'; +import type { AppsCatalog, AppsInstallStatus, AppsMetrics } from '../types/apps'; import { useConfigurationBackup } from '../hooks/useConfigurationBackup'; import { useConnectedDevices } from '../hooks/useConnectedDevices'; import { useFirmwareUpdates } from '../hooks/useFirmwareUpdates'; @@ -29,6 +31,7 @@ import { useDeviceRegistration } from '../hooks/useDeviceRegistration'; import { useDashboardAccountStatus } from '../hooks/useDashboardAccountStatus'; import { useWifi } from '../hooks/useWifi'; import { ActivityPage } from '../pages/ActivityPage'; +import { AppsPage } from '../pages/AppsPage'; import { ConnectedDevicesPage } from '../pages/ConnectedDevicesPage'; import { HomePage } from '../pages/HomePage'; import { IptvPage } from '../pages/IptvPage'; @@ -51,6 +54,28 @@ export function App({ onAdministratorPasswordChanged }: AppProps) { const tabs = usePageTabs(); const configurationBackup = useConfigurationBackup(); const activity = useActivityHistory(route === 'activity'); + const apps = useAsyncResource({ + active: route === 'apps', + fallbackError: '검증된 앱 목록을 불러오지 못했습니다.', + loader: fetchAppsCatalog, + pollInterval: (data) => data?.refreshing ? 2_000 : !data?.ready ? (data?.refreshFailed ? 15_000 : 2_000) : 60_000, + staleTimeMs: 2_000, + }); + const appsStatus = useAsyncResource({ + active: route === 'apps', + fallbackError: '앱 설치 상태를 불러오지 못했습니다.', + loader: fetchAppsStatus, + pollInterval: (data) => !data?.ready || data?.refreshing ? 2_000 : data?.job.phase === 'running' || data?.job.phase === 'starting' || data?.restore?.phase === 'running' || data?.restore?.phase === 'starting' ? 3000 : 12000, + staleTimeMs: 3000, + }); + // Optional metrics never participate in the status/error guard for buttons. + const appsMetrics = useAsyncResource({ + active: route === 'apps', + fallbackError: '저장 공간 정보를 확인하지 못했습니다.', + loader: fetchAppsMetrics, + pollInterval: (data) => data?.refreshing || !data?.ready ? 3_000 : 60000, + staleTimeMs: 5000, + }); const status = useStatus(route === 'home' || route === 'settings'); const updates = useSoftwareUpdates(true); const firmware = useFirmwareUpdates(true); @@ -95,6 +120,23 @@ export function App({ onAdministratorPasswordChanged }: AppProps) { }); }, []); + // A completed install/remove invalidates both APK snapshots even if their + // background TTL has not elapsed. Never force them on every render/poll. + const appsJobId = appsStatus.data?.job.operationId; + const appsJobPhase = appsStatus.data?.job.phase; + const appsRestorePhase = appsStatus.data?.restore?.phase; + const previousAppsRefreshKey = useRef(''); + useEffect(() => { + if (route !== 'apps' || (appsJobPhase !== 'success' && appsRestorePhase !== 'success')) return; + const key = `${appsJobId ?? ''}:${appsJobPhase}:${appsRestorePhase ?? ''}`; + if (key === previousAppsRefreshKey.current) return; + previousAppsRefreshKey.current = key; + void Promise.allSettled([refreshAppsStatus(), refreshAppsMetrics()]).then(() => { + void appsStatus.refresh(); + void appsMetrics.refresh(); + }); + }, [route, appsJobId, appsJobPhase, appsRestorePhase]); + const safeshieldActions = useSafeShieldActions( safeshield.refresh, safeshieldStatistics.refresh, @@ -117,13 +159,44 @@ export function App({ onAdministratorPasswordChanged }: AppProps) { ? rules : route === 'account' ? deviceRegistration - : route === 'system' - ? updates - : status; + : route === 'apps' + ? apps + : route === 'system' + ? updates + : status; let content: ComponentChildren; switch (route) { + case 'apps': + content = ( + void refreshAppsCatalog().then(() => apps.refresh()).then(() => + Promise.all([appsStatus.refresh(), appsMetrics.refresh()])).catch(() => apps.refresh())} + installStatus={appsStatus.data?.ready === false ? null : appsStatus.data} + metrics={appsMetrics.data} + statusError={appsStatus.error ?? (appsStatus.data?.ready === false ? '설치 상태를 확인하고 있습니다.' : null)} + onRefreshStatus={() => void appsStatus.refresh()} + onInstall={async (id) => { + await installVerifiedApp(id); + await appsStatus.refresh(); + }} + onUpdate={async (id) => { + await updateVerifiedApp(id); + await appsStatus.refresh(); + }} + onRemove={async (id) => { + await removeVerifiedApp(id); + await appsStatus.refresh(); + }} + /> + ); + break; + case 'network': content = (
diff --git a/luci-app-smartsafehub/frontend/src/app/routes.ts b/luci-app-smartsafehub/frontend/src/app/routes.ts index 7496d3a48..524df6280 100644 --- a/luci-app-smartsafehub/frontend/src/app/routes.ts +++ b/luci-app-smartsafehub/frontend/src/app/routes.ts @@ -8,6 +8,7 @@ export type AppRoute = | 'safeshield' | 'rules' | 'account' + | 'apps' | 'system' | 'settings'; @@ -83,6 +84,13 @@ export const ROUTES: readonly RouteDefinition[] = [ title: 'SmartSafeHub 계정', description: '이 기기의 SmartSafeHub 계정 연결 상태와 구독 플랜을 확인합니다.', }, + { + route: 'apps', + hash: '#apps', + label: 'Apps', + title: 'SmartSafeHub Apps', + description: 'SmartSafeHub에서 검증한 앱과 확장 기능을 살펴봅니다.', + }, { route: 'system', hash: '#system', @@ -105,5 +113,5 @@ export const ROUTE_BY_NAME = Object.fromEntries( // Public demo exposes only these read-only, fixture-backed screens. export const DEMO_ROUTES: readonly AppRoute[] = [ - 'home', 'activity', 'network', 'wifi', 'devices', 'safeshield', 'rules', 'system', + 'home', 'activity', 'network', 'wifi', 'devices', 'safeshield', 'rules', 'account', 'apps', 'system', ]; diff --git a/luci-app-smartsafehub/frontend/src/components/AppsCategoryIcon.tsx b/luci-app-smartsafehub/frontend/src/components/AppsCategoryIcon.tsx new file mode 100644 index 000000000..63d66e771 --- /dev/null +++ b/luci-app-smartsafehub/frontend/src/components/AppsCategoryIcon.tsx @@ -0,0 +1,32 @@ +import { AppsIcon, CircleEllipsisIcon, GamepadIcon, HomeIcon, NetworkNodesIcon, ShieldIcon, WrenchIcon } from './Icons'; + +export type AppsCategory = 'all' | 'network' | 'security' | 'smart_home' | 'entertainment' | 'utilities' | 'other'; + +/** Normalize catalog categories once for both the filter and icon fallback. */ +export function appsCategoryOf(category: string): AppsCategory { + switch (category) { + 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'; + } +} + +/** Local SVG icons: never fetch or execute artwork from a catalog entry. + * When verified per-app icons are supported, use these as the fallback. */ +export function AppsCategoryIcon({ category, class: className }: { category: AppsCategory; class?: string }) { + switch (category) { + case 'all': return ; + case 'network': return ; + case 'security': return ; + case 'smart_home': return ; + case 'entertainment': return ; + case 'utilities': return ; + case 'other': return ; + default: return ; + } +} diff --git a/luci-app-smartsafehub/frontend/src/components/AppsStorageCard.tsx b/luci-app-smartsafehub/frontend/src/components/AppsStorageCard.tsx new file mode 100644 index 000000000..c2d0bb120 --- /dev/null +++ b/luci-app-smartsafehub/frontend/src/components/AppsStorageCard.tsx @@ -0,0 +1,39 @@ +import type { AppsStorage } from '../types/apps'; +import { summarizeAppsStorage } from '../utils/appsStorage'; + +const RING_CIRCUMFERENCE = 2 * Math.PI * 39; + +export function AppsStorageCard({ storage }: { storage: AppsStorage | null | undefined }) { + const summary = summarizeAppsStorage(storage); + const availableArc = summary ? (summary.availableRatio * RING_CIRCUMFERENCE).toFixed(2) : '0'; + return ( + + ); +} diff --git a/luci-app-smartsafehub/frontend/src/components/Icons.tsx b/luci-app-smartsafehub/frontend/src/components/Icons.tsx index 39d100d7d..5e4084df6 100644 --- a/luci-app-smartsafehub/frontend/src/components/Icons.tsx +++ b/luci-app-smartsafehub/frontend/src/components/Icons.tsx @@ -69,6 +69,59 @@ export function UserIcon(props: IconProps) { ); } +/** App modules icon; distinct from firmware update and system settings. */ +export function AppsIcon(props: IconProps) { + return ( + + + + + + + ); +} + +/** Connected nodes for network-monitoring Apps. */ +export function NetworkNodesIcon(props: IconProps) { + return ( + + + + + + + ); +} + +/** Controller outline for games and entertainment Apps. */ +export function GamepadIcon(props: IconProps) { + return ( + + + + + ); +} + +/** Tools and utilities: a wrench is distinct from the power control. */ +export function WrenchIcon(props: IconProps) { + return ( + + + + ); +} + +/** Other/unknown Apps: a simple ellipsis, distinct from network nodes. */ +export function CircleEllipsisIcon(props: IconProps) { + return ( + + + + + ); +} + export function UpdateIcon(props: IconProps) { return ( diff --git a/luci-app-smartsafehub/frontend/src/components/ProductNavigation.tsx b/luci-app-smartsafehub/frontend/src/components/ProductNavigation.tsx index db38670d3..0f549917a 100644 --- a/luci-app-smartsafehub/frontend/src/components/ProductNavigation.tsx +++ b/luci-app-smartsafehub/frontend/src/components/ProductNavigation.tsx @@ -4,6 +4,7 @@ import { DEMO_ROUTES, ROUTE_BY_NAME } from '../app/routes'; import type { AppRoute } from '../app/routes'; import { luciAdminUrl } from '../utils/luci'; import { + AppsIcon, CloseIcon, ClockIcon, DevicesIcon, @@ -47,6 +48,7 @@ const NAVIGATION_GROUPS: readonly { { 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'] }, ]; @@ -70,6 +72,8 @@ function NavigationIcon({ route }: { route: AppRoute }) { return ; case 'account': return ; + case 'apps': + return ; case 'system': return ; case 'settings': @@ -85,14 +89,25 @@ function themeActionLabel(theme: 'dark' | 'light'): string { return theme === 'dark' ? '라이트 모드' : '다크 모드'; } -function navigationClass(active: boolean, collapsed = false): string { - return `relative flex min-h-11 min-w-0 items-center rounded-xl py-2.5 text-sm font-extrabold no-underline transition ${ - collapsed ? 'justify-center px-2' : 'gap-3 px-3' - } ${ - active - ? 'bg-teal-50 text-teal-800 ring-1 ring-inset ring-teal-200' - : 'text-slate-600 hover:bg-slate-50 hover:text-slate-950' - }`; +function navigationClass(active: boolean, collapsed = false, routeName?: AppRoute): string { + const baseSpacing = collapsed ? 'justify-center px-2' : 'gap-3 px-3'; + if (active) { + return `relative flex min-h-11 min-w-0 items-center rounded-xl py-2.5 text-sm font-extrabold no-underline transition ${baseSpacing} bg-teal-50 text-teal-800 ring-1 ring-inset ring-teal-200`; + } + + if (routeName === 'apps') { + return `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 ${baseSpacing}`; + } + + return `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 ${baseSpacing} hover:bg-slate-50 hover:text-slate-950`; +} + +function navigationDescription(routeName: AppRoute): string | null { + if (routeName === 'apps') { + return '앱으로 확장하기'; + } + + return null; } function NavigationItems({ @@ -133,7 +148,7 @@ function NavigationItems({ aria-label={routeName === 'account' && accountRegistered === false ? `${item.label} (계정 연결 필요)` : collapsed ? item.label : undefined} - class={navigationClass(active, collapsed)} + class={navigationClass(active, collapsed, routeName)} href={item.hash} key={routeName} title={routeName === 'account' && accountRegistered === false @@ -149,7 +164,14 @@ function NavigationItems({ {collapsed ? null : ( - {item.label} + + {item.label} + {navigationDescription(routeName) ? ( + + {navigationDescription(routeName)} + + ) : null} + {routeName === 'iptv' ? ( Beta diff --git a/luci-app-smartsafehub/frontend/src/demo/data.ts b/luci-app-smartsafehub/frontend/src/demo/data.ts index 8c8d54150..8896bd029 100644 --- a/luci-app-smartsafehub/frontend/src/demo/data.ts +++ b/luci-app-smartsafehub/frontend/src/demo/data.ts @@ -10,10 +10,100 @@ import type { LanSettings } from '../types/lan'; import type { SafeShieldStatistics, SafeShieldStatus } from '../types/safeshield'; import type { SmartSafeHubStatus } from '../types/status'; import type { SoftwareUpdateStatus } from '../types/updates'; +import type { DeviceRegistrationStatus } from '../api/smartsafehub'; +import type { AppsCatalog, AppsInstallStatus, AppsMetrics } from '../types/apps'; const now = Math.floor(Date.now() / 1000); const hour = 3600; +// Demo-only account status: no user, device credential, pairing token or live Hub session. +export const demoAccount: DeviceRegistrationStatus = { + schema: 1, + component: 'device', + phase: 'registered', + lastResult: 'success', + lastErrorCode: null, + accountRegistered: true, + plan: 'free', + pairingCode: null, + pairingExpiresAt: null, + nextSyncAt: 0, + lastSuccessAt: now - 300, +}; + +// Demo-only catalog. No APK URLs or authenticated API responses are used. +export const demoApps: AppsCatalog = { + schema: 1, + deviceCode: 'iptime-ax3000sm', + firmwareBuildId: 'demo-build', + channel: 'stable', + apps: [ + { + id: 'retro-arcade', + name: '레트로 아케이드', + summary: '브라우저에서 레트로 게임을 즐기는 엔터테인먼트 앱입니다.', + description: '사용자가 직접 준비한 게임 파일로 즐기는 레트로 아케이드입니다.', + category: 'entertainment', + version: '1.1.0-r5', + webUi: { type: 'static', path: '/retro-arcade/' }, + }, + { + id: 'network-usage-monitor', + name: '네트워크 사용량 모니터', + summary: '기기별 네트워크 사용량과 통계를 확인하는 앱입니다.', + description: '네트워크 트래픽을 모니터링합니다.', + category: 'network', + version: '2025.06.02~29236be6-r1', + webUi: null, + }, + { + id: 'wake-on-lan', + name: 'Wake-on-LAN', + summary: '공유기에서 PC와 홈서버의 전원을 켤 수 있는 도구입니다.', + description: 'Wake-on-LAN 지원 컴퓨터를 네트워크에서 깨우는 앱입니다.', + category: 'utilities', + version: '26.288.56989~75a306d', + webUi: null, // Public demo must never link to an actual LuCI control page. + }, + ], +}; + +export const demoAppsStatus: AppsInstallStatus = { + job: { phase: 'idle', action: '', appId: '', code: 'NONE' }, + installed: { + 'retro-arcade': { + installed: true, + partial: false, + managed: true, + updateAvailable: true, + installedVersion: '1.0.0-r1', + webUiReady: true, + }, + 'network-usage-monitor': { + installed: true, + partial: false, + managed: true, + updateAvailable: false, + installedVersion: '2025.06.02~29236be6-r1', + webUiReady: false, + }, + 'wake-on-lan': { + installed: false, + partial: false, + managed: false, + updateAvailable: false, + installedVersion: null, + webUiReady: false, + }, + }, +}; + +export const demoAppsMetrics: AppsMetrics = { + storage: { mount: '/overlay', totalBytes: 256 * 1024 * 1024, availableBytes: 185 * 1024 * 1024 }, + // Installed APK metadata; neither app cache nor runtime data is included. + installedSizes: { 'retro-arcade': 1394688, 'network-usage-monitor': 117760 }, +}; + export const demoDevices: ConnectedDevicesSummary = { generatedAt: now, devices: [ @@ -37,7 +127,7 @@ export const demoActivity: ActivityHistory = { export const demoStatus: SmartSafeHubStatus = { device: { hostname: 'SmartRouter', model: 'ipTIME AX3000SM', boardName: 'iptime,ax3000sm' }, software: { distribution: 'OpenWrt', version: '25.12', revision: 'demo', kernel: '6.12' }, - runtime: { uptime: 86400, localtime: now, load: [0.08, 0.12, 0.05], memory: { total: 268435456, free: 83886080, shared: 4194304, buffered: 41943040, available: 171966464, cached: 41943040 } }, + runtime: { uptime: 86400, localtime: now, load: [0.08, 0.12, 0.05], memory: { total: 268435456, free: 83886080, shared: 4194304, buffered: 41943040, available: 171966464, cached: 41943040 }, storage: demoAppsMetrics.storage }, network: { available: true, up: true, protocol: 'dhcp', ipv4Address: '198.51.100.20' }, activityHistory: demoActivity, }; diff --git a/luci-app-smartsafehub/frontend/src/demo/main.tsx b/luci-app-smartsafehub/frontend/src/demo/main.tsx index ab024d8bf..47fd0578e 100644 --- a/luci-app-smartsafehub/frontend/src/demo/main.tsx +++ b/luci-app-smartsafehub/frontend/src/demo/main.tsx @@ -4,6 +4,8 @@ import { useEffect, useState } from 'preact/hooks'; import { AppShell } from '../components/AppShell'; import { DEMO_ROUTES } from '../app/routes'; import { ActivityPage } from '../pages/ActivityPage'; +import { AppsPage } from '../pages/AppsPage'; +import { SmartSafeHubAccountPage } from '../pages/SmartSafeHubAccountPage'; import { WanPage } from '../pages/WanPage'; import { LanPage } from '../pages/LanPage'; import { WifiPage } from '../pages/WifiPage'; @@ -19,6 +21,7 @@ import { demoActivity, demoDevices, demoFirmware, demoHealth, demoLan, demoSafeShield, demoStatistics, demoStatus, demoUpdates, demoWan, demoWifi, demoRules, demoWifiQr, demoGuestWifiQr, + demoAccount, demoApps, demoAppsStatus, demoAppsMetrics, } from './data'; import '../styles/app.css'; @@ -36,7 +39,7 @@ function DemoApp() { setNotice('해당 기능은 현재 데모에서 제공하지 않습니다.'); } }, [currentRoute]); - const readonlyNotice = () => setNotice('데모에서는 실제 공유기 설정을 변경할 수 없습니다.'); + const readonlyNotice = () => setNotice('체험 모드에서는 실제 계정 연결, 앱 설치·제거·실행 및 공유기 설정 변경을 수행하지 않습니다.'); const dismiss = () => setNotice(''); const reset = () => setNotice('예시 데이터가 표시되고 있습니다. 실제 공유기에는 연결되지 않습니다.'); @@ -54,6 +57,20 @@ function DemoApp() { let content; switch (route) { + case 'account': + content = { readonlyNotice(); return null; }} + onRetry={reset} />; + break; + case 'apps': + content = { readonlyNotice(); }} + onRemove={async () => { readonlyNotice(); }} + onUpdate={async () => { readonlyNotice(); }} />; + break; case 'activity': content = +
체험 모드 · 표시되는 기기와 보호 통계는 모두 예시입니다. 지원 기기 살펴보기 → diff --git a/luci-app-smartsafehub/frontend/src/hooks/useAsyncResource.ts b/luci-app-smartsafehub/frontend/src/hooks/useAsyncResource.ts index 47e923e5e..a7de37630 100644 --- a/luci-app-smartsafehub/frontend/src/hooks/useAsyncResource.ts +++ b/luci-app-smartsafehub/frontend/src/hooks/useAsyncResource.ts @@ -12,7 +12,7 @@ export interface AsyncResourceState { interface AsyncResourceOptions { active?: boolean; fallbackError: string; - loader: () => Promise; + loader: (signal?: AbortSignal) => Promise; pollInterval?: number | ((data: T | null) => number | null); refreshOnFocus?: boolean; refreshOnVisible?: boolean; @@ -37,6 +37,8 @@ export function useAsyncResource({ const requested = useRef(false); const mounted = useRef(true); const inFlight = useRef | null>(null); + const controller = useRef(null); + const generation = useRef(0); const lastAttemptAt = useRef(null); const lastSuccessAt = useRef(null); const failureCount = useRef(0); @@ -48,6 +50,8 @@ export function useAsyncResource({ return () => { mounted.current = false; + generation.current += 1; + controller.current?.abort(); }; }, []); @@ -67,10 +71,14 @@ export function useAsyncResource({ } let request: Promise; + const sequence = ++generation.current; + const activeController = new AbortController(); + controller.current = activeController; lastAttemptAt.current = Date.now(); - request = loader() + request = loader(activeController.signal) .then((data) => { + if (sequence !== generation.current || activeController.signal.aborted) return; lastSuccessAt.current = Date.now(); failureCount.current = 0; if (mounted.current) { @@ -78,6 +86,7 @@ export function useAsyncResource({ } }) .catch((error: unknown) => { + if (sequence !== generation.current || activeController.signal.aborted) return; failureCount.current += 1; if (mounted.current) { setState((current) => ({ @@ -89,9 +98,8 @@ export function useAsyncResource({ } }) .finally(() => { - if (inFlight.current === request) { - inFlight.current = null; - } + if (inFlight.current === request) inFlight.current = null; + if (controller.current === activeController) controller.current = null; }); inFlight.current = request; @@ -103,6 +111,10 @@ export function useAsyncResource({ useEffect(() => { if (!active) { requested.current = false; + generation.current += 1; + controller.current?.abort(); + controller.current = null; + inFlight.current = null; return; } diff --git a/luci-app-smartsafehub/frontend/src/hooks/useDeviceRegistration.ts b/luci-app-smartsafehub/frontend/src/hooks/useDeviceRegistration.ts index 3af344c06..4c93516ca 100644 --- a/luci-app-smartsafehub/frontend/src/hooks/useDeviceRegistration.ts +++ b/luci-app-smartsafehub/frontend/src/hooks/useDeviceRegistration.ts @@ -157,7 +157,8 @@ export function useDeviceRegistration(enabled: boolean) { useEffect(() => () => { mounted.current = false; }, []); - const shouldPoll = useMemo(() => pairingStillValid(data), [data]); + const shouldPoll = useMemo(() => pairingStillValid(data) || + data?.pairingOperation?.phase === 'starting' || data?.pairingOperation?.phase === 'running', [data]); useEffect(() => { if (!enabled || !shouldPoll) { @@ -168,10 +169,10 @@ export function useDeviceRegistration(enabled: boolean) { let timer = 0; const poll = async () => { - await refresh(); - if (active) { - timer = window.setTimeout(() => void poll(), PAIRING_POLL_INTERVAL_MS); - } + const generation = ++requestGeneration.current; + const status = await fetchDeviceRegistrationStatus().catch(() => null); + if (active && status) commitStatus(status, generation); + if (active) timer = window.setTimeout(() => void poll(), PAIRING_POLL_INTERVAL_MS); }; timer = window.setTimeout(() => void poll(), PAIRING_POLL_INTERVAL_MS); @@ -179,7 +180,7 @@ export function useDeviceRegistration(enabled: boolean) { active = false; window.clearTimeout(timer); }; - }, [enabled, refresh, shouldPoll]); + }, [enabled, commitStatus, shouldPoll]); return { data, diff --git a/luci-app-smartsafehub/frontend/src/hooks/useHashRoute.ts b/luci-app-smartsafehub/frontend/src/hooks/useHashRoute.ts index 1b17eba54..f0e56c156 100644 --- a/luci-app-smartsafehub/frontend/src/hooks/useHashRoute.ts +++ b/luci-app-smartsafehub/frontend/src/hooks/useHashRoute.ts @@ -16,6 +16,7 @@ const HASH_ROUTES: Readonly> = { '#safeshield': 'safeshield', '#rules': 'rules', '#account': 'account', + '#apps': 'apps', '#system': 'system', '#settings': 'settings', '#settings/diagnostics': 'settings', diff --git a/luci-app-smartsafehub/frontend/src/main.tsx b/luci-app-smartsafehub/frontend/src/main.tsx index 0b5c080df..b9edbdcc9 100644 --- a/luci-app-smartsafehub/frontend/src/main.tsx +++ b/luci-app-smartsafehub/frontend/src/main.tsx @@ -44,7 +44,7 @@ function installBootstrap(sessionId: string, host: HTMLElement): void { sessionId, rpcUrl: luciUrl('/admin/ubus'), assetBase: host.dataset.assetBase ?? '/luci-static/smartsafehub/', - assetVersion: host.dataset.assetVersion ?? '0.2.30-r1', + assetVersion: host.dataset.assetVersion ?? '0.2.31-r1', locale: document.documentElement.lang || 'ko', }); } diff --git a/luci-app-smartsafehub/frontend/src/pages/AppsPage.tsx b/luci-app-smartsafehub/frontend/src/pages/AppsPage.tsx new file mode 100644 index 000000000..219fce62c --- /dev/null +++ b/luci-app-smartsafehub/frontend/src/pages/AppsPage.tsx @@ -0,0 +1,436 @@ +import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; + +import { AppsIcon, ReloadIcon } from '../components/Icons'; +import { AppsStorageCard } from '../components/AppsStorageCard'; +import { AppsCategoryIcon, appsCategoryOf } from '../components/AppsCategoryIcon'; +import type { AppsCategory } from '../components/AppsCategoryIcon'; +import { SectionHeading } from '../components/SectionHeading'; +import type { AppsCatalog, AppsInstallStatus, AppsMetrics, VerifiedApp } from '../types/apps'; +import { errorMessage } from '../utils/errors'; +import { approvedAppPath } from '../utils/appsWebUi'; +import { formatStorageBytes } from '../utils/appsStorage'; +import { appsJobNoticeKey, readDismissedAppsJob, saveDismissedAppsJob } from '../utils/appsJobNotice'; + +interface AppsPageProps { + data: AppsCatalog | null; + error: string | null; + loading: boolean; + refreshing: boolean; + onRetry: () => void; + installStatus: AppsInstallStatus | null; + metrics: AppsMetrics | null; + statusError: string | null; + onRefreshStatus: () => void; + onInstall: (id: string) => Promise; + onRemove: (id: string) => Promise; + onUpdate: (id: string) => Promise; + /** All public demo actions are local-only; no installs or real app launch. */ + demo?: boolean; + onDemoAction?: () => void; +} + +const OPERATION_MESSAGES: Readonly> = { + 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: '앱 제거에 실패했습니다.', +}; + +const TABS: readonly { id: AppsCategory; label: string }[] = [ + { id: 'all', label: '전체' }, + { id: 'network', label: '네트워크' }, + { id: 'security', label: '보안' }, + { id: 'smart_home', label: '스마트홈' }, + { id: 'entertainment', label: '엔터테인먼트' }, + { id: 'utilities', label: '도구' }, + { id: 'other', label: '기타' }, +]; + +type AppAction = 'install' | 'update' | 'remove'; + +function OperationIndicator({ action }: { action: AppAction }) { + const removing = action === 'remove'; + return ( + + + {removing ? '제거 중…' : action === 'update' ? '업데이트 중…' : '설치 중…'} + + ); +} + +export function AppsPage({ data, error, loading, refreshing, onRetry, installStatus, metrics, statusError, + onRefreshStatus, onInstall, onUpdate, onRemove, demo = false, onDemoAction }: AppsPageProps) { + const [category, setCategory] = useState('all'); + const [query, setQuery] = useState(''); + const [pending, setPending] = useState<{ appId: string; action: AppAction } | null>(null); + const [removeTarget, setRemoveTarget] = useState(null); + const cancelRemoveRef = useRef(null); + const removeTriggerRef = useRef(null); + const [actionError, setActionError] = useState(null); + const [dismissedJob, setDismissedJob] = useState(readDismissedAppsJob); + // Keep the last successful catalog while a refresh fails. A separate status + // request may describe a different catalog generation, so a missing ID + // alone is NOT proof that an app has been discontinued. + const currentData = data; + const apps = currentData?.apps ?? []; + const orphans = currentData && !error && !loading && !refreshing && !statusError + ? Object.entries(installStatus?.installed ?? {}).filter(([id, state]) => + state.retired === true && state.managed && !apps.some((app) => app.id === id)) + : []; + const restore = installStatus?.restore; + const job = installStatus?.job; + const running = job?.phase === 'starting' || job?.phase === 'running'; + const restoring = restore?.phase === 'starting' || restore?.phase === 'running'; + const jobKey = appsJobNoticeKey(job); + const dismissJob = (key: string) => { + setDismissedJob(key); + saveDismissedAppsJob(key); + }; + const showJob = job && job.phase !== 'idle' && jobKey !== dismissedJob; + useEffect(() => { + if (job?.phase !== 'success' || !showJob) return; + const timeout = window.setTimeout(() => dismissJob(jobKey), 7000); + return () => window.clearTimeout(timeout); + }, [jobKey, job?.phase, showJob]); + const jobAction: AppAction | null = job?.action === 'install' || job?.action === 'update' || job?.action === 'remove' ? job.action : null; + const filtered = useMemo(() => apps.filter((app) => + (category === 'all' || appsCategoryOf(app.category) === category) && + `${app.name} ${app.summary}`.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()), + ), [apps, category, query]); + + useEffect(() => { + if (!removeTarget) return; + cancelRemoveRef.current?.focus(); + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + event.preventDefault(); + setRemoveTarget(null); + removeTriggerRef.current?.focus(); + } + }; + window.addEventListener('keydown', handleEscape); + return () => window.removeEventListener('keydown', handleEscape); + }, [removeTarget]); + + const closeRemoveDialog = () => { + setRemoveTarget(null); + removeTriggerRef.current?.focus(); + }; + + const openRemoveDialog = (app: VerifiedApp, trigger: HTMLButtonElement) => { + if (!installStatus || statusError || pending !== null || running || restoring) return; + removeTriggerRef.current = trigger; + setRemoveTarget(app); + }; + + const requestOperation = async (app: VerifiedApp, action: AppAction) => { + if (!installStatus || statusError || pending !== null || running || restoring) return; + if (demo) { + if (action === 'remove') closeRemoveDialog(); + onDemoAction?.(); + return; + } + if (action === 'update' && !window.confirm(`${app.name} 앱을 최신 승인 버전으로 업데이트하시겠습니까?\n\n업데이트 중 공유기를 재부팅하지 마세요.`)) return; + if (action === 'install' && !window.confirm(`${app.name} 앱을 설치하시겠습니까?\n\n앱을 변경하는 동안 공유기를 재부팅하지 마세요.`)) return; + if (action === 'remove') closeRemoveDialog(); + setActionError(null); + setPending({ appId: app.id, action }); + try { + if (action === 'install') await onInstall(app.id); + else if (action === 'update') await onUpdate(app.id); + else await onRemove(app.id); + onRefreshStatus(); + } catch (cause: unknown) { + setActionError(errorMessage(cause, `${app.name} 앱 작업을 시작하지 못했습니다.`)); + } finally { + setPending(null); + } + }; + + return ( +
+ {removeTarget ? ( +
{ if (event.target === event.currentTarget) closeRemoveDialog(); }} + role="presentation" + > +
{ + if (event.key !== 'Tab') return; + // Keep keyboard focus inside the confirmation dialog. + if (event.shiftKey && event.target === cancelRemoveRef.current) { + event.preventDefault(); + const confirmButton = event.currentTarget.querySelector('[data-confirm-remove]'); + confirmButton?.focus(); + } else if (!event.shiftKey && event.target instanceof HTMLButtonElement && event.target.hasAttribute('data-confirm-remove')) { + event.preventDefault(); + cancelRemoveRef.current?.focus(); + } + }} + role="dialog" + > +

앱 제거 확인

+

+ {removeTarget.name} 앱을 이 공유기에서 제거하시겠습니까? +

+

+ 앱에서 제공하던 기능을 더 이상 사용할 수 없습니다. 제거하는 동안 공유기를 재부팅하지 마세요. +

+
+ + +
+
+
+ ) : null} +
+ +
+
+
+
+ + + +
+

검증된 앱

+

현재 펌웨어에서 확인된 앱만 표시합니다.

+
+
+
+ + {currentData ? `${apps.length}개` : '확인 전'} + + {error && currentData ? ( + 이전 목록 + ) : null} + +
+
+

+ 검증된 APK를 설치·업데이트·제거할 수 있습니다. 작업 중에는 다른 앱 작업을 시작할 수 없습니다. +

+
+ +
+
+ + {restore && (restore.phase === 'starting' || restore.phase === 'running' || restore.phase === 'failed') ? ( +
+ {restore.phase === 'failed' ? 'Apps 자동 복원을 완료하지 못했습니다.' : '펌웨어 업데이트 후 Apps를 복원하는 중입니다.'} + {restore.appId ? · {restore.appId} : null} + {restore.phase === 'failed' ?

{OPERATION_MESSAGES[restore.code] ?? '승인·연결·설치 상태를 확인해 주세요.'} 필요한 경우 앱 카드에서 설치 마무리를 진행할 수 있습니다.

:

검증된 패키지만 다시 설치합니다. 완료될 때까지 앱 설치·제거를 시작하지 마세요.

} +
+ ) : null} + {showJob && job ? ( +
+
+
+ {running ? (job.action === 'remove' ? '앱 제거 중' : job.action === 'update' ? '앱 업데이트 중' : '앱 설치 중') : job.phase === 'success' ? '마지막 앱 작업 완료' : '마지막 앱 작업 실패'} + {job.appId ? ` · ${job.appId}` : ''} + {job.code && job.code !== 'NONE' ? ` · 오류 코드 ${job.code}` : ''} + {job.phase === 'failed' && job.code && OPERATION_MESSAGES[job.code] + ?

{OPERATION_MESSAGES[job.code]}

: null} + {running ?

{job.action === 'remove' ? '앱 제거와 패키지 정리가 진행 중입니다.' : job.action === 'update' ? '승인된 버전으로 앱을 업데이트하고 있습니다.' : '설치 확인과 패키지 처리가 진행 중입니다.'} 화면을 닫아도 작업은 계속됩니다.

: null} +
+ {!running ? : null} +
+
+ ) : null} + {actionError ? : null} + {statusError ? : null} + {error && !currentData ? ( + + ) : null} + {loading && !currentData ? ( +
검증된 앱 목록을 확인하는 중입니다.
+ ) : null} + {currentData ? ( + <> + {apps.length > 0 ? ( +
+
+
+ {TABS.map((tab) => ( + + ))} +
+ setQuery(event.currentTarget.value)} + placeholder="앱 검색" + type="search" + value={query} + /> +
+
+ {filtered.map((app) => { + const state = installStatus?.installed[app.id]; + const installed = state?.installed ?? false; + const bytes = metrics?.installedSizes?.[app.id]; + const sizeLabel = installed && typeof bytes === 'number' && Number.isSafeInteger(bytes) && bytes >= 0 + ? formatStorageBytes(bytes) : null; + const managed = state?.managed ?? false; + const updateAvailable = Boolean(installed && managed && state?.updateAvailable); + const installedVersion = installed ? state?.installedVersion : null; + const showOfferedVersion = Boolean(installed && installedVersion && (updateAvailable || installedVersion !== app.version)); + const activeAction = pending?.appId === app.id ? pending.action : running && job?.appId === app.id ? jobAction : null; + const disabled = !installStatus || Boolean(statusError) || pending !== null || running || restoring; + const removable = managed && (installed || Boolean(state?.partial)); + const launchPath = installed && state?.webUiReady ? approvedAppPath(app.webUi) : null; + return ( +
+
+ +
+
+

{app.name}

+ {installed && managed ? ✓ 설치됨 : null} +
+
+ + {installed ? installedVersion ? `설치 v${installedVersion}` : '설치 버전 확인 불가' : `v${app.version}`} + + {updateAvailable ? 업데이트 가능 : null} +
+ {showOfferedVersion || sizeLabel !== null ? ( +

+ {showOfferedVersion ? ( + 제공 v{app.version} + ) : null} + {showOfferedVersion && sizeLabel !== null ? : null} + {sizeLabel !== null ? ( + 설치 용량 약 {sizeLabel} + ) : null} +

+ ) : null} +
+
+

{app.summary}

+
+ ✓ 검증된 앱 + {activeAction ? : ( + + {managed && state?.partial && !installed ? ( + + ) : null} + {updateAvailable ? : null} + {launchPath ? (demo ? ( + + ) : ( + 열기 ↗ + )) : installed && app.webUi ? ( + 웹 UI 미설치 + ) : null} + {installed && !managed ? 기존에 설치됨 + : state?.partial && !managed ? 일부 패키지 설치됨 + : } + + )} +
+
+ ); + })} +
+ {filtered.length === 0 ?

검색 조건에 맞는 앱이 없습니다.

: null} +
+ ) : ( +
+ +

아직 등록된 앱이 없습니다.

+

현재 기기와 펌웨어에 대해 승인된 앱이 준비되면 이곳에서 확인할 수 있습니다.

+
+ )} +

기기 {currentData.deviceCode} · 펌웨어 {currentData.firmwareBuildId} · {currentData.channel} 채널

+ + ) : null} + {orphans.length > 0 ? ( +
+

제공이 중단된 앱

+

현재 승인 목록에서 제외되었지만 기기에 남아 있는 앱입니다. 설치는 불가능하며 제거할 수 있습니다.

+
+ {orphans.map(([id]) => { + const activeAction = pending?.appId === id ? pending.action : running && job?.appId === id ? jobAction : null; + return ( +
+ {id} + {activeAction ? : } +
+ ); + })} +
+
+ ) : null} +
+ ); +} diff --git a/luci-app-smartsafehub/frontend/src/pages/HomePage.tsx b/luci-app-smartsafehub/frontend/src/pages/HomePage.tsx index d7dae2a6f..1052edc3f 100644 --- a/luci-app-smartsafehub/frontend/src/pages/HomePage.tsx +++ b/luci-app-smartsafehub/frontend/src/pages/HomePage.tsx @@ -7,6 +7,7 @@ import { CheckCircleIcon, ClockIcon, DevicesIcon, + DatabaseIcon, GlobeIcon, MemoryIcon, RouterIcon, @@ -26,6 +27,7 @@ import type { SafeShieldStatistics, SafeShieldStatus } from '../types/safeshield import type { SmartSafeHubStatus } from '../types/status'; import type { SoftwareUpdateStatus } from '../types/updates'; import { isSoftwareUpdateCheckStale } from '../utils/softwareUpdates'; +import { summarizeAppsStorage } from '../utils/appsStorage'; import { formatBootTime, formatBytes, @@ -37,6 +39,9 @@ import { getMemoryUsage, } from '../app/format'; +// Keep the dashboard ring geometry identical to AppsStorageCard (96px SVG, r=39). +const DISK_RING_CIRCUMFERENCE = 2 * Math.PI * 39; + interface HomePageProps { accountRegistered: boolean | null; activity: ActivityHistory | null; @@ -514,6 +519,8 @@ export function HomePage({ } const memoryPercent = memory ? Math.round(memory.percent) : 0; + const disk = summarizeAppsStorage(data.runtime.storage); + const diskAvailableArc = disk ? (disk.availableRatio * DISK_RING_CIRCUMFERENCE).toFixed(2) : '0'; const safeShieldSummary = safeShieldOverview( safeshield, safeshieldLoading, @@ -857,7 +864,7 @@ export function HomePage({
-
-
+
+
메모리
-

{memoryPercent}%

-

- {memory ? formatBytes(memory.used) : '-'} /{' '} - {formatBytes(data.runtime.memory.total)} -

-
+
+

{memoryPercent}%

+

+ {memory ? formatBytes(memory.used) : '-'} /{' '} + {formatBytes(data.runtime.memory.total)} +

= 90 ? 'bg-amber-500' : 'bg-teal-600' - }`} - style={{ width: `${Math.min(100, memoryPercent)}%` }} - /> + aria-label={`메모리 사용률 ${memoryPercent}%`} + class="mt-3 h-1.5 overflow-hidden rounded-full bg-slate-200" + role="progressbar" + aria-valuemin={0} + aria-valuemax={100} + aria-valuenow={memoryPercent} + > +
= 90 ? 'bg-amber-500' : 'bg-teal-600' + }`} + style={{ width: `${Math.min(100, memoryPercent)}%` }} + /> +
-
+
+
+ + 디스크 +
+ +

+ {disk ? `${disk.availableText} 남음` : '저장 공간 확인 불가'} +

+ {disk ? ( +

+ 사용 {disk.usedText} / 전체 {disk.totalText} +

+ ) : null} +
+
시스템 부하
-

- {formatLoadAverage(data.runtime.load[0])} -

-

- 5분 {formatLoadAverage(data.runtime.load[1])} · 15분{' '} - {formatLoadAverage(data.runtime.load[2])} -

+
+

+ {formatLoadAverage(data.runtime.load[0])} +

+

+ 5분 {formatLoadAverage(data.runtime.load[1])} · 15분{' '} + {formatLoadAverage(data.runtime.load[2])} +

+
-
+
실행 시간
-

- {formatUptime(data.runtime.uptime)} -

-

- 부팅 {formatBootTime(data.runtime.localtime, data.runtime.uptime)} -

+
+

+ {formatUptime(data.runtime.uptime)} +

+

+ 부팅 {formatBootTime(data.runtime.localtime, data.runtime.uptime)} +

+
diff --git a/luci-app-smartsafehub/frontend/src/pages/SafeShieldRulesPage.tsx b/luci-app-smartsafehub/frontend/src/pages/SafeShieldRulesPage.tsx index e08a538ae..035f132e3 100644 --- a/luci-app-smartsafehub/frontend/src/pages/SafeShieldRulesPage.tsx +++ b/luci-app-smartsafehub/frontend/src/pages/SafeShieldRulesPage.tsx @@ -1,5 +1,6 @@ import { SectionHeading } from '../components/SectionHeading'; import type { TargetedEvent, TargetedSubmitEvent } from 'preact'; +import { memo } from 'preact/compat'; import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { RelativeTimestamp } from '../components/RelativeTimestamp'; @@ -16,6 +17,12 @@ import { import { ErrorPanel, LoadingPanel } from '../components/StatePanels'; import type { SafeShieldRulesAction } from '../hooks/useSafeShieldRules'; import type { RecommendedRuleAssignment, RecommendedRulesStatus, SafeShieldRuleAction, SafeShieldRules } from '../types/rules'; +import { filterRecommendedAssignments, filterRuleGroups, sameRecommendationContent } from '../utils/ruleLists'; + +// Stable fallbacks keep memoized rule cards from rebuilding while the first +// recommendation status request is still pending. +const EMPTY_RECOMMENDED_DOMAINS: string[] = []; +const EMPTY_RECOMMENDED_RULES: RecommendedRuleAssignment[] = []; interface SafeShieldRulesPageProps { action: SafeShieldRulesAction | null; @@ -53,7 +60,7 @@ interface RuleListCardProps { domain: string, ) => Promise; rules: string[]; - recommendedDomains: string[]; + recommendedDomainSet: ReadonlySet; recommendedKnown: boolean; recommendedRules: RecommendedRuleAssignment[]; onRequestRevoke?: (rule: RecommendedRuleAssignment) => void; @@ -81,13 +88,13 @@ function isValidDomain(value: string): boolean { ); } -function RuleListCard({ +const RuleListCard = memo(function RuleListCard({ action, kind, onAddRule, onDeleteRule, rules, - recommendedDomains, + recommendedDomainSet, recommendedKnown, recommendedRules, onRequestRevoke, @@ -102,22 +109,18 @@ function RuleListCard({ const [recommendedExpanded, setRecommendedExpanded] = useState(null); const isAllow = kind === 'allow'; const busy = action !== null; - const filteredRules = useMemo(() => { - const query = search.trim().toLowerCase(); - return query ? rules.filter((domain) => domain.includes(query)) : rules; - }, [rules, search]); - const searching = search.trim().length > 0; - // Reconciler-owned domains remain separate from user-managed manual entries. - const managedRules = isAllow && recommendedKnown - ? filteredRules.filter((domain) => recommendedDomains.includes(domain)) - : []; - const manualRules = isAllow && recommendedKnown - ? filteredRules.filter((domain) => !recommendedDomains.includes(domain)) - : filteredRules; - const visibleRecommendedRules = isAllow ? recommendedRules.filter((rule) => - !searching || rule.title.toLowerCase().includes(search.trim().toLowerCase()) || - rule.domains.some((domain) => domain.includes(search.trim().toLowerCase())) - ) : []; + const query = useMemo(() => search.trim().toLowerCase(), [search]); + const searching = query.length > 0; + // Filter and partition once per actual list/search/ownership change. Set + // membership avoids a quadratic scan across the recommended domains. + const { filteredRules, manualRules, managedRules } = useMemo( + () => filterRuleGroups(rules, query, isAllow && recommendedKnown, recommendedDomainSet), + [rules, query, isAllow, recommendedKnown, recommendedDomainSet], + ); + const visibleRecommendedRules = useMemo( + () => isAllow ? filterRecommendedAssignments(recommendedRules, query) : [], + [isAllow, recommendedRules, query], + ); const showRecommended = searching || (recommendedExpanded ?? (recommendedRules.length || managedRules.length) <= 3); const showManual = searching || manualExpanded; @@ -156,7 +159,7 @@ function RuleListCard({ function renderRule(domain: string) { // A manual entry that happens to match a cloud recommendation must // remain deletable. Only entries actually owned by the reconciler lock. - const cloudManaged = isAllow && recommendedKnown && recommendedDomains.includes(domain); + const cloudManaged = isAllow && recommendedKnown && recommendedDomainSet.has(domain); const ruleOrigin = !isAllow || recommendedKnown ? (cloudManaged ? '추천 규칙' : '직접 추가') : null; @@ -319,7 +322,7 @@ function RuleListCard({
); -} +}); export function SafeShieldRulesPage({ action, @@ -344,30 +347,54 @@ export function SafeShieldRulesPage({ const [revokeConfirmationDelayed, setRevokeConfirmationDelayed] = useState(false); const pendingRevokeId = useRef(null); const previousStatusSignature = useRef(null); + const lastConfirmedSignature = useRef(null); const onRetryRef = useRef(onRetry); onRetryRef.current = onRetry; const [syncMessage, setSyncMessage] = useState(null); const [syncError, setSyncError] = useState(null); - const recommendedDomains = recommendedStatus?.domains ?? []; - const recommendedRules = recommendedStatus?.rules ?? []; + const recommendedDomains = recommendedStatus?.domains ?? EMPTY_RECOMMENDED_DOMAINS; + const recommendedRules = recommendedStatus?.rules ?? EMPTY_RECOMMENDED_RULES; + const recommendedDomainSet = useMemo(() => new Set(recommendedDomains), [recommendedDomains]); // A revoke can reach Hub even if rpcd times out while SafeShield is applying // the new rules. Only a confirmed local read can settle its final outcome. // The local SafeShield list also needs a refresh when a background poll // observes a new recommendation revision, without reloading the whole page. - const acceptRecommendedStatus = useCallback((status: RecommendedRulesStatus): void => { - setRecommendedStatus(status); + const acceptRecommendedStatus = useCallback((status: RecommendedRulesStatus): boolean => { + setRecommendedStatus((previous) => { + if (!previous) return status; + // Poll responses are newly deserialized objects. Reuse unchanged list + // references so both memoized cards skip work on progress-only updates. + const { domains, rules } = sameRecommendationContent(previous, status); + return { ...status, domains, rules }; + }); setRecommendedKnown(true); - const signature = JSON.stringify([ - status.revision, status.domains, status.rules.map((rule) => rule.id), - status.syncStatus, - ]); - if (previousStatusSignature.current !== null && previousStatusSignature.current !== signature) { - onRetryRef.current(); + // Progress/status/metadata changes must not re-read the whole SafeShield + // rules list. After a completed reconcile, reload only once per actual + // recommendation revision/ownership change. The first pending -> confirmed + // transition also refreshes a list fetched while SafeShield was applying. + const signature = JSON.stringify([status.revision, status.domains]); + let reloaded = false; + if (status.syncStatus === 'confirmed' && lastConfirmedSignature.current !== signature) { + if (previousStatusSignature.current !== null) { + onRetryRef.current(); + reloaded = true; + } + lastConfirmedSignature.current = signature; } previousStatusSignature.current = signature; - if (status.syncStatus === 'confirmed' && pendingRevokeId.current === null) { + // A worker failure is not a successful revoke. Keep the previous rule + // visible and let the user retry instead of waiting indefinitely. + if (pendingRevokeId.current !== null && status.revokeOperation?.phase === 'failed' && + status.revokeOperation.argument === String(pendingRevokeId.current)) { + pendingRevokeId.current = null; + setRevokeConfirming(false); + setRevokeConfirmationDelayed(false); + setSyncError('추천 규칙 적용 해제를 완료하지 못했습니다. 연결 상태를 확인하고 다시 시도해 주세요.'); + } + if (status.syncStatus === 'confirmed' && pendingRevokeId.current === null && + status.revokeOperation?.phase !== 'failed') { setSyncError(null); } if ( @@ -381,6 +408,7 @@ export function SafeShieldRulesPage({ setSyncError(null); setSyncMessage('추천 규칙 적용 해제를 확인했습니다.'); } + return reloaded; }, []); useEffect(() => { @@ -415,7 +443,7 @@ export function SafeShieldRulesPage({ window.removeEventListener('focus', onFocus); document.removeEventListener('visibilitychange', onVisible); }; - }, [data?.allow.join('\n'), onLoadRecommendedRulesStatus, syncing, revokeBusy, acceptRecommendedStatus]); + }, [onLoadRecommendedRulesStatus, syncing, revokeBusy, acceptRecommendedStatus]); // An RPC timeout cannot prove whether Hub accepted the removal. After a // minute, explain the delay but continue watching for eventual confirmation. @@ -432,11 +460,13 @@ export function SafeShieldRulesPage({ setSyncMessage(null); try { const status = await onSyncRecommendedRules(); - acceptRecommendedStatus(status); + const reloaded = acceptRecommendedStatus(status); setSyncMessage(status.syncStatus === 'confirmed' ? '서버에서 규칙을 가져와 SafeShield 허용 목록을 확인했습니다.' : '추천 규칙 상태를 확인 중입니다. 진행 결과는 자동으로 갱신됩니다.'); - onRetry(); // Reload the actual SafeShield allow list after reconciliation. + // The user explicitly requested synchronization, so allow one manual + // refresh after confirmation but never queue a duplicate full RPC. + if (status.syncStatus === 'confirmed' && !reloaded) onRetry(); } catch { setSyncError('추천 규칙을 동기화하지 못했습니다. 서버 연결 및 기기 등록 상태를 확인해 주세요.'); } finally { @@ -460,7 +490,8 @@ export function SafeShieldRulesPage({ if (pendingRevokeId.current !== null) { setSyncMessage('적용 해제 요청을 처리했습니다. 공유기의 허용 목록을 확인하고 있습니다.'); } - onRetryRef.current(); + // A pending revoke is not yet a changed SafeShield rule list. The + // confirmed status poll refreshes once the local apply is complete. } catch { // Hub may have removed the assignment before the local SafeShield apply // timed out. Keep checking instead of permanently displaying a failure. @@ -677,7 +708,7 @@ export function SafeShieldRulesPage({ onAddRule={onAddRule} onDeleteRule={onDeleteRule} rules={data.allow} - recommendedDomains={recommendedDomains} + recommendedDomainSet={recommendedDomainSet} recommendedKnown={recommendedKnown} recommendedRules={recommendedRules} {...(onRevokeRecommendedRule ? { onRequestRevoke: setRevokeTarget } : {})} @@ -689,7 +720,7 @@ export function SafeShieldRulesPage({ onAddRule={onAddRule} onDeleteRule={onDeleteRule} rules={data.block} - recommendedDomains={recommendedDomains} + recommendedDomainSet={recommendedDomainSet} recommendedKnown={recommendedKnown} recommendedRules={recommendedRules} revokeBusy={revokeBusy} diff --git a/luci-app-smartsafehub/frontend/src/pages/SmartSafeHubAccountPage.tsx b/luci-app-smartsafehub/frontend/src/pages/SmartSafeHubAccountPage.tsx index 8fb41dad9..02d1099c0 100644 --- a/luci-app-smartsafehub/frontend/src/pages/SmartSafeHubAccountPage.tsx +++ b/luci-app-smartsafehub/frontend/src/pages/SmartSafeHubAccountPage.tsx @@ -80,6 +80,9 @@ interface SmartSafeHubAccountPageProps { syncError: string | null; onRequestPairing: () => Promise; onRetry: () => void; + /** Public router demo never navigates to a real device account console. */ + demo?: boolean; + onDemoAction?: () => void; } export function SmartSafeHubAccountPage({ @@ -91,6 +94,8 @@ export function SmartSafeHubAccountPage({ syncError, onRequestPairing, onRetry, + demo = false, + onDemoAction, }: SmartSafeHubAccountPageProps) { const [pairingError, setPairingError] = useState(null); const [pairingCopied, setPairingCopied] = useState(false); @@ -109,6 +114,8 @@ export function SmartSafeHubAccountPage({ const currentPlan = planLabel(status?.plan ?? null); const expired = status ? pairingExpired(status) : false; const pairingCode = !connected && !expired ? status?.pairingCode : null; + const issuingCode = pairingBusy || status?.pairingOperation?.phase === 'starting' || + status?.pairingOperation?.phase === 'running'; async function requestPairing(): Promise { setPairingError(null); @@ -154,14 +161,20 @@ export function SmartSafeHubAccountPage({

이 기기가 계정과 정상적으로 연결되어 있습니다. 연결 코드는 더 이상 필요하지 않습니다.

- - 웹사이트에서 기기 관리 - + {demo ? ( + + ) : ( + 웹사이트에서 기기 관리 + )}

웹사이트에서 기기 등록을 해제하면 공유기도 다음 계정 상태 동기화에서 자동으로 연결 해제를 반영합니다.

@@ -205,13 +218,16 @@ export function SmartSafeHubAccountPage({ + {status?.pairingOperation?.phase === 'failed' ? ( +

연결 코드 발급에 실패했습니다. 다시 시도해 주세요.

+ ) : null} {pairingError ?

{pairingError}

: null}
)} diff --git a/luci-app-smartsafehub/frontend/src/styles/app.css b/luci-app-smartsafehub/frontend/src/styles/app.css index b8ebf045f..3e134c661 100644 --- a/luci-app-smartsafehub/frontend/src/styles/app.css +++ b/luci-app-smartsafehub/frontend/src/styles/app.css @@ -259,6 +259,45 @@ } +/* Apps is an optional expansion area, not the current page by default. + * Avoid translucent *light* teal utilities on dark backgrounds: the 40% white + * mix makes an unselected Apps link look like a grey selected overlay. */ +.ssh-apps-nav-link:not([aria-current='page']) { + border: 1px solid #99f6e4; + background-color: #f0fdfa; + color: #115e59; +} + +.ssh-apps-nav-link:not([aria-current='page']):hover { + border-color: #5eead4; + background-color: #ccfbf1; +} + +.ssh-apps-nav-description { + color: #0f766e; +} + +.ssh-app[data-theme='dark'] .ssh-apps-nav-link:not([aria-current='page']) { + border-color: #134e4a; + background-color: rgb(20 184 166 / 0.09); + color: #e2e8f0; +} + +.ssh-app[data-theme='dark'] .ssh-apps-nav-link:not([aria-current='page']):hover { + border-color: #0f766e; + background-color: rgb(20 184 166 / 0.16); + color: #5eead4; +} + +.ssh-app[data-theme='dark'] .ssh-apps-nav-description { + color: #5eead4; +} + +.ssh-apps-nav-link:focus-visible { + outline: 2px solid #0d9488; + outline-offset: 2px; +} + .ssh-sidebar-toggle { border: 1px solid #e2e8f0; background: #ffffff; @@ -606,6 +645,7 @@ .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; } @@ -757,6 +797,11 @@ background-color: #1e293b; } +/* Apps removal stays understated at rest; dark mode uses a muted red hover. */ +.ssh-app[data-theme='dark'] .hover\:bg-red-50:hover { + background-color: rgb(239 68 68 / 0.16); +} + .ssh-app[data-theme='dark'] .hover\:bg-rose-50:hover { background-color: rgb(244 63 94 / 0.18); } @@ -2275,3 +2320,71 @@ background-color: #fbbf24; --ssh-account-dot-ring: #0f172a; } + +/* Apps storage: a small SVG donut, without Chart.js or expensive redraws. */ +.ssh-apps-storage-track, +.ssh-apps-storage-free { + fill: none; + stroke-width: 9; +} + +.ssh-apps-storage-track { stroke: #e2e8f0; } +.ssh-apps-storage-free { + stroke: #0d9488; + transform: rotate(-90deg); + transform-origin: 50% 50%; +} +.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; } + +/* Identical low-space warnings in Apps and Dashboard, including the capacity label. */ +.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; } + +/* Apps cards: installed is a quiet state, updates are an amber action. */ +.ssh-apps-installed-badge { + border-color: #cbd5e1; + background: #f1f5f9; + color: #475569; +} +.ssh-apps-update-badge { + border-color: #fcd34d; + background: #fffbeb; + color: #92400e; +} +.ssh-apps-update-button { + border-color: #d97706; + background: #d97706; + color: #fff; +} +.ssh-apps-update-button:hover:not(:disabled) { background: #b45309; border-color: #b45309; } +.ssh-apps-update-button:focus-visible { outline: 2px solid #d97706; outline-offset: 2px; } +.ssh-app[data-theme='dark'] .ssh-apps-installed-badge { + border-color: #475569; + background: #1e293b; + color: #cbd5e1; +} +.ssh-app[data-theme='dark'] .ssh-apps-update-badge { + border-color: #92400e; + background: #451a03; + color: #fcd34d; +} +.ssh-app[data-theme='dark'] .ssh-apps-update-button { + border-color: #b45309; + background: #b45309; + color: #fff; +} +.ssh-app[data-theme='dark'] .ssh-apps-update-button:hover:not(:disabled) { + background: #d97706; + border-color: #d97706; +} diff --git a/luci-app-smartsafehub/frontend/src/types/apps.ts b/luci-app-smartsafehub/frontend/src/types/apps.ts new file mode 100644 index 000000000..6e3408864 --- /dev/null +++ b/luci-app-smartsafehub/frontend/src/types/apps.ts @@ -0,0 +1,74 @@ +/** Read-only SmartSafeHub Apps catalog schema 1 from the local router. */ +export interface VerifiedApp { + id: string; + name: string; + summary: string; + description: string; + category: string; + version: string; + /** Approved same-origin LuCI page or first-party static app; never a remote URL. */ + webUi?: { type: 'luci' | 'static'; path: string } | null; +} + +export interface AppsCatalog { + schema: 1; + deviceCode: string; + firmwareBuildId: string; + channel: string; + apps: VerifiedApp[]; + /** False until the first immutable, firmware-matched snapshot is available. */ + ready?: boolean; + refreshing?: boolean; + refreshFailed?: boolean; +} + +/** Writable filesystem used for installing APKs (not RAM or squashfs). */ +export interface AppsStorage { + mount: '/overlay' | '/'; + totalBytes: number; + availableBytes: number; +} + +/** Optional metrics, independent from installation and removal state. */ +export interface AppsMetrics { + storage: AppsStorage | null; + installedSizes: Record; + ready?: boolean; + refreshing?: boolean; +} + +/** Local APK adapter state, independent from the future IoT / AI runtimes. */ +export interface AppsInstallStatus { + job: { + phase: 'idle' | 'starting' | 'running' | 'success' | 'failed'; + action: 'install' | 'update' | 'remove' | ''; + appId: string; + code: string; + operationId?: string; + }; + restore?: { + phase: 'idle' | 'starting' | 'running' | 'success' | 'failed'; + appId: string; + code: string; + }; + ready?: boolean; + refreshing?: boolean; + refreshFailed?: boolean; + installed: Record; +} + +export interface AppsOperationAccepted { + accepted: boolean; + appId: string; + action: 'install' | 'update' | 'remove'; +} diff --git a/luci-app-smartsafehub/frontend/src/types/rules.ts b/luci-app-smartsafehub/frontend/src/types/rules.ts index 142bde6da..aa94fb40e 100644 --- a/luci-app-smartsafehub/frontend/src/types/rules.ts +++ b/luci-app-smartsafehub/frontend/src/types/rules.ts @@ -57,4 +57,6 @@ export interface RecommendedRulesStatus { lastDeviceSyncAt: number; nextDeviceSyncAt: number; accountRegistered: boolean; + operation?: { phase: string; code: string; updatedAt: number }; + revokeOperation?: { phase: string; code: string; argument: string; updatedAt: number }; } diff --git a/luci-app-smartsafehub/frontend/src/types/status.ts b/luci-app-smartsafehub/frontend/src/types/status.ts index 70a626b55..a4d64fb14 100644 --- a/luci-app-smartsafehub/frontend/src/types/status.ts +++ b/luci-app-smartsafehub/frontend/src/types/status.ts @@ -1,4 +1,5 @@ import type { ActivityHistory } from './activity'; +import type { AppsStorage } from './apps'; export interface DeviceStatus { hostname: string; model: string; @@ -26,6 +27,8 @@ export interface RuntimeStatus { localtime: number; load: [number, number, number]; memory: RuntimeMemory; + /** Writable root, using the same units and definition as Apps. Missing on older firmware. */ + storage?: AppsStorage | null; } export interface NetworkStatus { diff --git a/luci-app-smartsafehub/frontend/src/utils/appsJobNotice.ts b/luci-app-smartsafehub/frontend/src/utils/appsJobNotice.ts new file mode 100644 index 000000000..253fa8762 --- /dev/null +++ b/luci-app-smartsafehub/frontend/src/utils/appsJobNotice.ts @@ -0,0 +1,32 @@ +import type { AppsInstallStatus } from '../types/apps'; + +const STORAGE_KEY = 'smartsafehub:apps:dismissed-operation:v1'; + +type AppsJob = AppsInstallStatus['job']; + +/** A job ID is preserved through running/success/failure on the router. */ +export function appsJobNoticeKey(job: AppsJob | null | undefined): string { + if (!job || job.phase === 'idle') return ''; + // Backwards compatibility while router frontend and RPC versions differ. + return job.operationId + ? `operation:${job.operationId}` + : `legacy:${job.phase}:${job.action}:${job.appId}:${job.code}`; +} + +/** Browser storage is optional (private browsing or hardened browsers). */ +export function readDismissedAppsJob(): string | null { + try { + return typeof window === 'undefined' ? null : window.localStorage.getItem(STORAGE_KEY); + } catch { + return null; + } +} + +export function saveDismissedAppsJob(key: string): void { + if (!key) return; + try { + if (typeof window !== 'undefined') window.localStorage.setItem(STORAGE_KEY, key); + } catch { + // In-memory dismissal remains usable if storage is blocked. + } +} diff --git a/luci-app-smartsafehub/frontend/src/utils/appsStorage.ts b/luci-app-smartsafehub/frontend/src/utils/appsStorage.ts new file mode 100644 index 000000000..b07c9661b --- /dev/null +++ b/luci-app-smartsafehub/frontend/src/utils/appsStorage.ts @@ -0,0 +1,37 @@ +import type { AppsStorage } from '../types/apps'; + +export interface StorageSummary { + availablePercent: number; + availableRatio: number; + availableText: string; + usedText: string; + totalText: string; + level: 'normal' | 'warning' | 'critical'; +} + +/** Base-2 units match the BusyBox df -h figures used on OpenWrt. */ +export function formatStorageBytes(bytes: number): string { + if (!Number.isFinite(bytes) || bytes < 0) return '—'; + if (bytes < 1024) return `${Math.round(bytes)} B`; + if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(1)} KB`; + if (bytes < 1024 ** 3) return `${(bytes / (1024 ** 2)).toFixed(1)} MB`; + return `${(bytes / (1024 ** 3)).toFixed(1)} GB`; +} + +/** Reserved blocks count as unavailable, not as space for APK installation. */ +export function summarizeAppsStorage(storage: AppsStorage | null | undefined): StorageSummary | null { + if (!storage || !Number.isSafeInteger(storage.totalBytes) || + !Number.isSafeInteger(storage.availableBytes) || storage.totalBytes <= 0 || + storage.availableBytes < 0 || storage.availableBytes > storage.totalBytes || + (storage.mount !== '/overlay' && storage.mount !== '/')) return null; + const availableRatio = storage.availableBytes / storage.totalBytes; + return { + availablePercent: Math.round(availableRatio * 100), + availableRatio, + availableText: formatStorageBytes(storage.availableBytes), + // Includes filesystem-reserved space, not only files written by apps. + usedText: formatStorageBytes(storage.totalBytes - storage.availableBytes), + totalText: formatStorageBytes(storage.totalBytes), + level: availableRatio <= 0.1 ? 'critical' : availableRatio <= 0.2 ? 'warning' : 'normal', + }; +} diff --git a/luci-app-smartsafehub/frontend/src/utils/appsWebUi.ts b/luci-app-smartsafehub/frontend/src/utils/appsWebUi.ts new file mode 100644 index 000000000..f78432306 --- /dev/null +++ b/luci-app-smartsafehub/frontend/src/utils/appsWebUi.ts @@ -0,0 +1,7 @@ +/** Only approved same-origin LuCI pages or the first-party Retro Arcade entry point. */ +export function approvedAppPath(webUi: { type: string; path: string } | null | undefined): string | null { + if (!webUi || typeof webUi.path !== 'string' || webUi.path.length > 160) return null; + if (webUi.type === 'static') return webUi.path === '/retro-arcade/' ? webUi.path : null; + if (webUi.type !== 'luci') return null; + return /^\/cgi-bin\/luci\/admin\/[a-z0-9_-]+(?:\/[a-z0-9_-]+)*$/.test(webUi.path) ? webUi.path : null; +} diff --git a/luci-app-smartsafehub/frontend/src/utils/errors.ts b/luci-app-smartsafehub/frontend/src/utils/errors.ts index 44d25bd17..3e3c75bb1 100644 --- a/luci-app-smartsafehub/frontend/src/utils/errors.ts +++ b/luci-app-smartsafehub/frontend/src/utils/errors.ts @@ -1,7 +1,5 @@ -import { RpcError } from '../api/rpc'; - export function errorMessage(error: unknown, fallback: string): string { - return error instanceof RpcError || error instanceof Error + return error instanceof Error ? error.message : fallback; } diff --git a/luci-app-smartsafehub/frontend/src/utils/ruleLists.ts b/luci-app-smartsafehub/frontend/src/utils/ruleLists.ts new file mode 100644 index 000000000..71da3196f --- /dev/null +++ b/luci-app-smartsafehub/frontend/src/utils/ruleLists.ts @@ -0,0 +1,56 @@ +import type { RecommendedRuleAssignment, RecommendedRulesStatus } from '../types/rules'; + +export function filterRuleGroups( + rules: readonly string[], + query: string, + splitOwnership: boolean, + recommendedDomainSet: ReadonlySet, +): { filteredRules: readonly string[]; manualRules: readonly string[]; managedRules: readonly string[] } { + const filteredRules = query ? rules.filter((domain) => domain.includes(query)) : rules; + if (!splitOwnership) return { filteredRules, manualRules: filteredRules, managedRules: [] }; + + const manualRules: string[] = []; + const managedRules: string[] = []; + for (const domain of filteredRules) { + (recommendedDomainSet.has(domain) ? managedRules : manualRules).push(domain); + } + return { filteredRules, manualRules, managedRules }; +} + +export function filterRecommendedAssignments( + rules: readonly RecommendedRuleAssignment[], + query: string, +): readonly RecommendedRuleAssignment[] { + if (!query) return rules; + return rules.filter((rule) => + rule.title.toLowerCase().includes(query) || + rule.domains.some((domain) => domain.includes(query)), + ); +} + +function sameStrings(left: readonly string[], right: readonly string[]): boolean { + return left === right || (left.length === right.length && left.every((value, index) => value === right[index])); +} + +function sameAssignments( + left: readonly RecommendedRuleAssignment[], + right: readonly RecommendedRuleAssignment[], +): boolean { + return left === right || (left.length === right.length && left.every((rule, index) => { + const other = right[index]; + return other !== undefined && rule.id === other.id && rule.title === other.title && + sameStrings(rule.domains, other.domains); + })); +} + +// Preserve only the arrays that feed the expensive rule cards. Other status +// fields (including stage and timestamps) must always be allowed to update. +export function sameRecommendationContent( + previous: RecommendedRulesStatus, + next: RecommendedRulesStatus, +): Pick { + return { + domains: sameStrings(previous.domains, next.domains) ? previous.domains : next.domains, + rules: sameAssignments(previous.rules, next.rules) ? previous.rules : next.rules, + }; +} diff --git a/luci-app-smartsafehub/root/etc/hotplug.d/iface/98-smartsafehub-apps-restore b/luci-app-smartsafehub/root/etc/hotplug.d/iface/98-smartsafehub-apps-restore new file mode 100755 index 000000000..261c92351 --- /dev/null +++ b/luci-app-smartsafehub/root/etc/hotplug.d/iface/98-smartsafehub-apps-restore @@ -0,0 +1,16 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +# Resume a bounded restore after a late WAN reconnection, not on LAN churn. +[ "${ACTION:-}" = ifup ] || exit 0 +case "${INTERFACE:-}" in wan|wan6) ;; *) exit 0;; esac +[ -d /etc/smartsafehub/apps-installed ] || exit 0 +# A live restore already retries transient connectivity failures. +[ ! -d /tmp/smartsafehub/apps-restore.lock ] || exit 0 +[ -r /tmp/smartsafehub/apps-restore.state ] || exit 0 +# Never auto-repeat compatibility, integrity, or signing failures. +grep -Eq '^code[[:space:]]+(E21|E25|E31|E35|BUSY)$' \ + /tmp/smartsafehub/apps-restore.state || exit 0 +# Network retries run a finite worker, never launch another service. +RUNNER=/usr/libexec/smartsafehub-apps-restore +[ -x "$RUNNER" ] || exit 0 +"$RUNNER" run /dev/null 2>&1 & diff --git a/luci-app-smartsafehub/root/lib/upgrade/keep.d/smartsafehub-apps b/luci-app-smartsafehub/root/lib/upgrade/keep.d/smartsafehub-apps new file mode 100644 index 000000000..61b6914ca --- /dev/null +++ b/luci-app-smartsafehub/root/lib/upgrade/keep.d/smartsafehub-apps @@ -0,0 +1,2 @@ +/etc/smartsafehub/apps-installed/ +/etc/smartsafehub/apps-metrics-pending/ diff --git a/luci-app-smartsafehub/root/usr/libexec/smartsafehub-apps b/luci-app-smartsafehub/root/usr/libexec/smartsafehub-apps new file mode 100755 index 000000000..ebcccace2 --- /dev/null +++ b/luci-app-smartsafehub/root/usr/libexec/smartsafehub-apps @@ -0,0 +1,49 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +# Read-only, firmware-pinned SmartSafeHub Apps catalog fetch. +set -eu + +RUNTIME_DIR="${SMARTSAFEHUB_APPS_RUNTIME_DIR:-/tmp/smartsafehub}" +ROM_METADATA="${SMARTSAFEHUB_APPS_ROM_METADATA:-/rom/usr/share/smartsafehub/firmware.json}" +FIRMWARE_METADATA="${SMARTSAFEHUB_APPS_METADATA:-/usr/share/smartsafehub/firmware.json}" +JSONFILTER="${SMARTSAFEHUB_APPS_JSONFILTER_BIN:-jsonfilter}" +UCLIENT="${SMARTSAFEHUB_APPS_UCLIENT_BIN:-uclient-fetch}" +CATALOG_URL='https://www.smartsafehub.com/api/v1/app-store/catalog' + +json_field() { "$JSONFILTER" -i "$1" -e "$2" 2>/dev/null | head -n 1; } + +# The ROM image is authoritative when available. Do not accept a device code, +# build ID or catalog URL from RPC/browser input (or from an untrusted feed). +metadata="$ROM_METADATA" +[ -s "$metadata" ] || metadata="$FIRMWARE_METADATA" +[ -s "$metadata" ] || exit 2 + +device_code="$(json_field "$metadata" '@.device_code')" +build_id="$(json_field "$metadata" '@.build_id')" +channel="$(json_field "$metadata" '@.channel')" +case "$device_code" in + iptime-ax3000sm|iptime-ax3000se|iptime-a3004t|gl-mt300n-v2|xiaomi-ax3000t) ;; + *) exit 2 ;; +esac +case "$build_id" in + ''|[!a-zA-Z0-9]*|*[!a-zA-Z0-9._-]*) exit 2 ;; +esac +[ "${#build_id}" -le 64 ] || exit 2 +case "$channel" in stable|beta) ;; *) channel=stable ;; esac + +mkdir -p "$RUNTIME_DIR" || exit 1 +umask 077 +tmp="$(mktemp "$RUNTIME_DIR/apps-catalog.XXXXXXXX")" || exit 1 +trap 'rm -f "$tmp"' EXIT HUP INT TERM +url="$CATALOG_URL?schema=1&device_code=$device_code&firmware_build_id=$build_id&channel=$channel" +"$UCLIENT" -q -T 8 -O "$tmp" "$url" >/dev/null 2>&1 || exit 1 +bytes="$(wc -c < "$tmp" | tr -d ' ')" +[ "$bytes" -gt 1 ] && [ "$bytes" -le 131072 ] || exit 1 +[ "$(json_field "$tmp" '@.schema')" = 1 ] || exit 1 +[ "$(json_field "$tmp" '@.device_code')" = "$device_code" ] || exit 1 +[ "$(json_field "$tmp" '@.firmware_build_id')" = "$build_id" ] || exit 1 +[ "$(json_field "$tmp" '@.channel')" = "$channel" ] || exit 1 +# Atomically replace the previous (display-only) snapshot. If the network is +# unavailable, rpcd rejects this invocation; no stale approval is returned. +mv -f "$tmp" "$RUNTIME_DIR/apps-catalog.json" +exit 0 diff --git a/luci-app-smartsafehub/root/usr/libexec/smartsafehub-apps-manager b/luci-app-smartsafehub/root/usr/libexec/smartsafehub-apps-manager new file mode 100755 index 000000000..3e6f7b0d5 --- /dev/null +++ b/luci-app-smartsafehub/root/usr/libexec/smartsafehub-apps-manager @@ -0,0 +1,776 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +# Privileged, opt-in APK adapter for SmartSafeHub Apps fixed-release protocol. +# No caller-supplied package, URL, shell command or repository is accepted. +set -eu + +SELF="${SMARTSAFEHUB_APPS_MANAGER_BIN:-/usr/libexec/smartsafehub-apps-manager}" +CATALOG_HELPER="${SMARTSAFEHUB_APPS_HELPER_BIN:-/usr/libexec/smartsafehub-apps}" +RUNTIME="${SMARTSAFEHUB_APPS_RUNTIME_DIR:-/tmp/smartsafehub}" +OWNED="${SMARTSAFEHUB_APPS_OWNED_DIR:-/etc/smartsafehub/apps-installed}" +# Fixed first-party host. Only the Hub-approved channel/architecture determine +# the repository path; callers cannot submit a repository URL. +REPO_BASE="${SMARTSAFEHUB_APPS_REPO_BASE:-https://repo.smartsafehub.com}" +UCLIENT="${SMARTSAFEHUB_APPS_UCLIENT_BIN:-uclient-fetch}" +OVERLAY="${SMARTSAFEHUB_APPS_OVERLAY_PATH:-/overlay}" +# OpenWrt uses /proc/meminfo; tests on non-Linux hosts supply a fixture. +MEMINFO="${SMARTSAFEHUB_APPS_MEMINFO_PATH:-/proc/meminfo}" +JSONFILTER="${SMARTSAFEHUB_APPS_JSONFILTER_BIN:-jsonfilter}" +APK="${SMARTSAFEHUB_APPS_APK_BIN:-apk}" +ARCH_FILE="${SMARTSAFEHUB_APPS_ARCH_FILE:-/etc/apk/arch}" +SHA256="${SMARTSAFEHUB_APPS_SHA256_BIN:-sha256sum}" +DAEMON="${SMARTSAFEHUB_APPS_DAEMON_BIN:-start-stop-daemon}" +METRICS="${SMARTSAFEHUB_APPS_METRICS_BIN:-/usr/libexec/smartsafehub-apps-metrics}" +LUCI_INDEX_CACHE="${SMARTSAFEHUB_APPS_LUCI_INDEX_CACHE:-/tmp/luci-indexcache}" +LOCK="$RUNTIME/apps-operation.lock" +STATE="$RUNTIME/apps-operation.state" +CATALOG="$RUNTIME/apps-catalog.json" + +field() { "$JSONFILTER" -i "$1" -e "$2" 2>/dev/null | head -n 1; } +# A wildcard expression returns multiple app IDs. Scalar fields above must +# remain single-valued, but catalog iteration must not truncate the results. +catalog_ids() { "$JSONFILTER" -i "$CATALOG" -e '@.apps[*].id' 2>/dev/null; } +valid_id() { case "$1" in ''|*[!a-z0-9-]*|-*|*-) return 1;; esac; [ "${#1}" -le 80 ]; } +valid_package() { + case "$1" in ''|*[!a-zA-Z0-9_.+-]*|[-.+]*|*[-.+]) return 1;; esac + [ "${#1}" -le 100 ] +} +valid_version() { + case "$1" in ''|*[!a-zA-Z0-9._+~-]*|[-.+~]*|*[-.+~]) return 1;; esac + [ "${#1}" -le 100 ] +} +valid_sha() { + case "$1" in *[!a-f0-9]*|'') return 1;; esac + [ "${#1}" -eq 64 ] +} +protected_package() { + case "$1" in + base-files|busybox|apk|procd|netifd|dnsmasq*|firewall*|odhcp*|dropbear|ubus*|rpcd*|uhttpd*|safeshield*|luci-app-smartsafehub|kernel|kmod-*) return 0;; + esac + return 1 +} +write_state() { + phase="$1"; action="$2"; app="$3"; code="$4"; operation_id="$5"; source="${6:-user}" + tmp="$(mktemp "$RUNTIME/apps-state.XXXXXXXX")" || return 1 + printf 'phase\t%s\naction\t%s\napp_id\t%s\ncode\t%s\noperation_id\t%s\nsource\t%s\n' "$phase" "$action" "$app" "$code" "$operation_id" "$source" > "$tmp" + mv -f "$tmp" "$STATE" +} +# Persist *ownership*, not merely an installed flag, so removal never deletes +# unrelated packages installed through LuCI, ssh or another app. +owned_manifest() { printf '%s/%s' "$OWNED" "$1"; } + +# Dump only simple validated package pins. Indexed JSON filter expressions are +# created exclusively from a numeric index resolved from the approved catalog. +app_index() { + id="$1"; n=0 + for candidate in $(catalog_ids); do + if [ "$candidate" = "$id" ]; then printf '%s\n' "$n"; return 0; fi + n=$((n + 1)) + [ "$n" -le 100 ] || break + done + return 1 +} +extract_pins() { + index="$1"; out="$2" + : > "$out"; n=0 + while :; do + name="$(field "$CATALOG" "@.apps[$index].packages[$n].name")" + [ -n "$name" ] || break + version="$(field "$CATALOG" "@.apps[$index].packages[$n].version")" + hash="$(field "$CATALOG" "@.apps[$index].packages[$n].sha256")" + valid_package "$name" && valid_version "$version" && valid_sha "$hash" || return 1 + protected_package "$name" && return 1 + grep -Fq "$(printf '%s\t' "$name")" "$out" && return 1 + printf '%s\t%s\t%s\n' "$name" "$version" "$hash" >> "$out" + n=$((n + 1)) + [ "$n" -le 32 ] || return 1 + done + [ "$n" -gt 0 ] +} +# A dry-run may install only explicitly reviewed packages. Never allow any +# upgrades, downgrades, removals or automatic dependency additions. Missing +# prerequisites cause a safe failure. +review_install_plan() { + plan="$1"; allow="$2" + awk -v allowed="$allow" ' + BEGIN { split(allowed, names, " "); for (i in names) if (names[i] != "") ok[names[i]]=1 } + { + line=$0 + sub(/^\([0-9]+\/[0-9]+\) /, "", line) + if (line ~ /^(Installing|Upgrading|Downgrading|Purging|Removing) /) { + split(line, fields, " ") + if (fields[1] != "Installing" || !(fields[2] in ok) || ++seen[fields[2]] != 1) bad=1 + } + } + END { + for (name in ok) if (seen[name] != 1) bad=1 + exit bad ? 1 : 0 + } + ' "$plan" +} + +# Only explicitly approved, already-owned APKs may be upgraded. Never let +# apk upgrade the firmware, pull new dependencies, remove packages or downgrade. +review_update_plan() { + plan="$1"; allow="$2" + awk -v allowed="$allow" ' + BEGIN { split(allowed, names, " "); for (i in names) if (names[i] != "") ok[names[i]]=1 } + { + line=$0 + sub(/^\([0-9]+\/[0-9]+\) /, "", line) + if (line ~ /^(Installing|Upgrading|Downgrading|Purging|Removing|Replacing) /) { + split(line, fields, " ") + if (fields[1] != "Upgrading" || !(fields[2] in ok) || ++seen[fields[2]] != 1) bad=1 + } + } + END { + for (name in ok) if (seen[name] != 1) bad=1 + exit bad ? 1 : 0 + } + ' "$plan" +} + +# Likewise, removal may touch only package names in the Apps-owned manifest. +# Shared dependencies, manually installed packages and system packages must +# remain untouched even if apk considers them orphaned. +review_remove_plan() { + plan="$1"; allow="$2" + awk -v allowed="$allow" ' + BEGIN { split(allowed, names, " "); for (i in names) if (names[i] != "") ok[names[i]]=1 } + { + line=$0 + sub(/^\([0-9]+\/[0-9]+\) /, "", line) + if (line ~ /^(Purging|Removing|Upgrading|Downgrading) /) { + split(line, fields, " ") + if ((fields[1] != "Purging" && fields[1] != "Removing") || !(fields[2] in ok)) bad=1 + } + } + END { exit bad ? 1 : 0 } + ' "$plan" +} + +# Preserve recovery of partial operations: an owned manifest may contain a +# not-yet-installed package, but must never adopt a package that was present +# before this transaction. A later retry can finish it safely. +append_owned_pin() { + name="$1"; version="$2"; hash="$3" + manifest="$(owned_manifest "$app")" + mkdir -p "$OWNED" || return 1 + if [ -e "$manifest" ] && grep -Fqx "$(printf '%s\t%s\t%s' "$name" "$version" "$hash")" "$manifest"; then + return 0 + fi + tmp_manifest="$stage/owned-next" + if [ -e "$manifest" ]; then cp "$manifest" "$tmp_manifest" || return 1; else : > "$tmp_manifest"; fi + printf '%s\t%s\t%s\n' "$name" "$version" "$hash" >> "$tmp_manifest" + cp "$tmp_manifest" "$manifest.new" && mv -f "$manifest.new" "$manifest" +} + +installed() { "$APK" info -e "$1" >/dev/null 2>&1; } +# apk info -e verifies only the name. Version checks must use the installed +# database or an old, manually changed APK could be mistaken for our release. +installed_version() { + "$APK" list -I "$1" 2>/dev/null | awk -v prefix="$1-" ' + index($1, prefix) == 1 { print substr($1, length(prefix) + 1); exit } + ' +} +# Use APK's installed-size metadata, not archive/download size or a slow +# recursive scan of /overlay. apk versions may put the number on the same +# line as the "installed size" heading or on the next line. Do not guess +# missing metadata, and do not accept a size from a different package. +parse_installed_apk_size() { + awk -v name="$1" -v expected="$2" ' + function number_of_bytes(value, parts, count, factor, bytes) { + gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) + # Accept the standard "123 KiB" and compact "123KiB" forms. + sub(/(B|KiB|MiB|GiB)$/, " &", value) + count=split(value, parts, /[[:space:]]+/) + if (count != 2 || parts[1] !~ /^[0-9]+([.][0-9]+)?$/) return -1 + if (parts[2] == "B") factor=1 + else if (parts[2] == "KiB") factor=1024 + else if (parts[2] == "MiB") factor=1048576 + else if (parts[2] == "GiB") factor=1073741824 + else return -1 + bytes=parts[1]*factor + if (bytes < 0 || bytes > 1099511627776) return -1 + return bytes + } + # apk v3 may print the exact same installed-size record more than once + # (for instance, when package names are returned from multiple indexes). + # Accept only consistent repetitions; never sum duplicate records. + function record_size(value, version, parsed_bytes) { + parsed_bytes=number_of_bytes(value) + # An APK upgrade can leave multiple versions in index-derived info. + # Prefer the exact locally installed release over older/newer entries. + if (expected != "" && version == expected) { + matched=1 + if (parsed_bytes < 0 || (selected && parsed_bytes != selected_bytes)) { + selected_invalid=1 + return + } + selected_bytes=parsed_bytes + selected=1 + return + } + if (parsed_bytes < 0 || (parsed && parsed_bytes != bytes)) { + invalid=1 + return + } + bytes=parsed_bytes + parsed=1 + } + { + line=$0 + sub(/\r$/, "", line) + heading=" installed size:" + marker=index(line, heading) + if (marker > 0 && (substr(line, 1, length(name) + 1) == name "-" || + substr(line, 1, length(name) + length(heading)) == name heading)) { + if (waiting) { + if (expected != "" && pending_version == expected) selected_invalid=1 + else invalid=1 + } + seen++ + prefix=substr(line, 1, marker-1) + version=prefix == name ? "" : substr(prefix, length(name)+2) + value=substr(line, marker+length(heading)) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) + waiting=0 + if (value == "") { + waiting=1 + pending_version=version + } else record_size(value, version) + next + } + if (waiting) { + gsub(/^[[:space:]]+|[[:space:]]+$/, "", line) + if (line == "") next + record_size(line, pending_version) + waiting=0 + } + } + END { + if (waiting) { + if (expected != "" && pending_version == expected) selected_invalid=1 + else invalid=1 + } + if (matched) { + if (!selected_invalid && selected) + printf "%.0f\n", selected_bytes + else exit 1 + } else if (!invalid && seen > 0 && parsed == 1) { + # Keep supporting APK variants without exact version records. + printf "%.0f\n", bytes + } else exit 1 + } + ' +} +installed_size_bytes() { + [ "$#" -eq 1 ] || return 1 + name="$1" + # Match the package release actually installed, not a stale APK index hit. + current_version="$(installed_version "$name" || :)" + valid_version "$current_version" || current_version='' + # Try APK's size-only view first; fall back to its full information view. + # Do not attempt a potentially expensive recursive disk scan. + bytes="$("$APK" info -s "$name" 2>/dev/null | parse_installed_apk_size "$name" "$current_version")" || { + bytes="$("$APK" info "$name" 2>/dev/null | parse_installed_apk_size "$name" "$current_version")" || return 1 + } + case "$bytes" in ''|*[!0-9]*) return 1;; esac + printf '%s\n' "$bytes" +} +# Refuse a downgrade, a same-version rebuild with a different digest, any +# package-set change, or an APK that no longer matches our ownership record. +# The approved replacement must be strictly newer according to apk itself. +select_upgrades() { + old_manifest="$1"; approved_pins="$2"; changed_pins="$3" + [ -s "$old_manifest" ] || return 1 + : > "$changed_pins" + old_count=0 + while IFS="$(printf '\t')" read -r name old_version old_hash; do + valid_package "$name" && valid_version "$old_version" && valid_sha "$old_hash" || return 1 + protected_package "$name" && return 1 + new_version="$(awk -F '\t' -v name="$name" '$1 == name { print $2; exit }' "$approved_pins")" + new_hash="$(awk -F '\t' -v name="$name" '$1 == name { print $3; exit }' "$approved_pins")" + [ -n "$new_version" ] && valid_sha "$new_hash" || return 1 + [ "$(installed_version "$name")" = "$old_version" ] || return 1 + if [ "$old_version" = "$new_version" ]; then + [ "$old_hash" = "$new_hash" ] || return 1 + else + [ "$("$APK" version -t "$old_version" "$new_version" 2>/dev/null)" = '<' ] || return 1 + printf '%s\t%s\t%s\n' "$name" "$new_version" "$new_hash" >> "$changed_pins" + fi + old_count=$((old_count + 1)) + [ "$old_count" -le 32 ] || return 1 + done < "$old_manifest" + [ "$old_count" -gt 0 ] || return 1 + [ "$old_count" -eq "$(wc -l < "$approved_pins" | tr -d ' ')" ] || return 1 + [ -s "$changed_pins" ] +} +# Prefer OpenWrt's configured apk architecture. Some package-manager builds +# do not support --print-arch consistently. Never guess based on the CPU type, +# device name, or the channel: the Hub approval must match an exact native arch. +local_package_arch() { + native='' + if [ -r "$ARCH_FILE" ]; then + native="$(awk '$1 ~ /^(aarch64_cortex-a53|mipsel_24kc|x86_64)$/ {print $1; exit}' "$ARCH_FILE" 2>/dev/null)" + fi + if [ -z "$native" ]; then + native="$("$APK" --print-arch 2>/dev/null || :)" + fi + case "$native" in aarch64_cortex-a53|mipsel_24kc|x86_64) printf '%s\n' "$native";; esac +} + +# Web UI is never considered ready based only on the daemon package. +# Network usage is explicitly known to require a separate LuCI package. +# Other apps opt in via an approved web_ui.package that is also in their +# approved package list, preventing an unrelated local app from being opened. +web_ui_package() { + id="$1"; idx="$2" + if [ "$id" = network-usage-monitor ]; then + printf '%s\n' luci-app-nlbwmon + return 0 + fi + if [ "$id" = wake-on-lan ]; then + # The approved app has exactly one LuCI entry point. Do not mistake the + # backend etherwake package (or a different LuCI page) for that UI. + [ "$(field "$CATALOG" "@.apps[$idx].web_ui.type")" = luci ] || return 1 + [ "$(field "$CATALOG" "@.apps[$idx].web_ui.path")" = /cgi-bin/luci/admin/services/wol ] || return 1 + [ "$(field "$CATALOG" "@.apps[$idx].web_ui.package")" = luci-app-wol ] || return 1 + fi + if [ "$id" = retro-arcade ]; then + [ "$(field "$CATALOG" "@.apps[$idx].web_ui.type")" = static ] || return 1 + [ "$(field "$CATALOG" "@.apps[$idx].web_ui.path")" = /retro-arcade/ ] || return 1 + [ "$(field "$CATALOG" "@.apps[$idx].web_ui.package")" = smartsafehub-app-retro-arcade ] || return 1 + else + [ "$(field "$CATALOG" "@.apps[$idx].web_ui.type")" = luci ] || return 1 + fi + name="$(field "$CATALOG" "@.apps[$idx].web_ui.package")" + valid_package "$name" || return 1 + p=0 + while [ "$p" -le 32 ]; do + candidate="$(field "$CATALOG" "@.apps[$idx].packages[$p].name")" + [ -n "$candidate" ] || break + if [ "$candidate" = "$name" ]; then printf '%s\n' "$name"; return 0; fi + p=$((p + 1)) + done + return 1 +} + +status() { + tmp="$(mktemp "$RUNTIME/apps-status.XXXXXXXX")" || return 1 + { + # A disconnected device can still report its local installation state. + if [ ! -r "$CATALOG" ]; then : > "$tmp"; mv -f "$tmp" "$RUNTIME/apps-status.tsv"; return 0; fi + # Snapshot approved IDs once. Both catalog membership and local ownership + # checks below refer to the same on-disk approved generation. + approved_ids="$(catalog_ids)" + n=0 + for id in $approved_ids; do + valid_id "$id" || continue + idx="$n"; n=$((n + 1)) + [ "$n" -le 100 ] || break + total=0; present=0; p=0; primary_package='' + while :; do + name="$(field "$CATALOG" "@.apps[$idx].packages[$p].name")" + [ -n "$name" ] || break + valid_package "$name" || break + [ -n "$primary_package" ] || primary_package="$name" + total=$((total + 1)); installed "$name" && present=$((present + 1)) + p=$((p + 1)); [ "$p" -le 32 ] || break + done + # WOL's public release version belongs to its LuCI APK, not the + # etherwake backend. Avoid showing a false installed/offered mismatch. + if [ "$id" = wake-on-lan ] && [ "$total" -eq 2 ]; then + primary_package=luci-app-wol + fi + managed=0 + [ -f "$(owned_manifest "$id")" ] && managed=1 + ui_ready=0 + ui_package="$(web_ui_package "$id" "$idx" || :)" + if [ -n "$ui_package" ] && installed "$ui_package"; then + if [ "$id" != retro-arcade ] || [ -r "${SMARTSAFEHUB_APPS_WEB_ROOT:-/www}/retro-arcade/index.html" ]; then + ui_ready=1 + fi + fi + # Update availability requires a complete Apps-owned install and a newer + # Hub-approved exact package set; it never uses an untrusted URL or version. + update_ready=0 + if [ "$managed" -eq 1 ] && [ "$total" -gt 0 ] && [ "$total" -eq "$present" ]; then + status_pins="$(mktemp "$RUNTIME/apps-status-pins.XXXXXXXX")" || return 1 + status_changes="$(mktemp "$RUNTIME/apps-status-changes.XXXXXXXX")" || { rm -f "$status_pins"; return 1; } + if extract_pins "$idx" "$status_pins" && select_upgrades "$(owned_manifest "$id")" "$status_pins" "$status_changes"; then + update_ready=1 + fi + rm -f "$status_pins" "$status_changes" + fi + # The catalog version is the latest approved release, not the APK + # currently installed. Read the actual version of the first approved + # package as a representative version; never infer it from the catalog. + # Use a fixed APK command and validate the field before putting it in TSV. + installed_release='' + if [ -n "$primary_package" ] && installed "$primary_package"; then + installed_release="$(installed_version "$primary_package" || :)" + valid_version "$installed_release" || installed_release='' + fi + if [ "$total" -gt 0 ] && [ "$total" -eq "$present" ]; then full=1; else full=0; fi + if [ "$present" -gt 0 ]; then partial=1; else partial=0; fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$id" "$full" "$partial" "$managed" "$ui_ready" "$update_ready" "$installed_release" + done + # Removed approvals must be proven by a valid Apps-owned manifest, not + # inferred from racing UI/catalog RPC responses. Manual apk add does not + # write an Apps manifest and therefore must never appear as retired. + retired_count=0 + for manifest in "$OWNED"/*; do + [ -f "$manifest" ] || continue + id="${manifest##*/}" + valid_id "$id" || continue + printf '%s\n' "$approved_ids" | grep -Fxq "$id" && continue + retired_count=$((retired_count + 1)) + [ "$retired_count" -le 100 ] || break + count=0; present=0; valid=1 + while IFS="$(printf '\t')" read -r name version hash; do + valid_package "$name" && valid_version "$version" && valid_sha "$hash" || { valid=0; break; } + protected_package "$name" && { valid=0; break; } + count=$((count + 1)) + [ "$count" -le 32 ] || { valid=0; break; } + installed "$name" && present=$((present + 1)) + done < "$manifest" + [ "$valid" -eq 1 ] && [ "$count" -gt 0 ] && [ "$present" -gt 0 ] || continue + # Eighth column explicitly marks the verified retired ownership record. + printf '%s\t0\t1\t1\t0\t0\t\t1\n' "$id" + done + } > "$tmp" + mv -f "$tmp" "$RUNTIME/apps-status.tsv" +} + +# Optional read-only sizes. Consult the *last known installed* status and +# approved catalog. For incomplete metadata, omit the total rather than show +# a misleading partial sum. +sizes() { + tmp="$(mktemp "$RUNTIME/apps-size.XXXXXXXX")" || return 1 + { + if [ -r "$CATALOG" ] && [ -r "$RUNTIME/apps-status.tsv" ]; then + for id in $(awk -F '\t' '$2 == "1" {print $1}' "$RUNTIME/apps-status.tsv"); do + valid_id "$id" || continue + idx="$(app_index "$id")" || continue + set -- + p=0 + while :; do + name="$(field "$CATALOG" "@.apps[$idx].packages[$p].name")" + [ -n "$name" ] || break + valid_package "$name" || break + set -- "$@" "$name" + p=$((p + 1)); [ "$p" -le 32 ] || break + done + [ "$#" -gt 0 ] || continue + complete=1; total_bytes=0 + for name in "$@"; do + bytes="$(installed_size_bytes "$name" || :)" + case "$bytes" in + ''|*[!0-9]*) + complete=0;; + *) + # Never overflow signed shell arithmetic or report an impossible total. + if [ "${#bytes}" -gt 13 ] || [ "$bytes" -gt 1099511627776 ] || + [ "$total_bytes" -gt $((1099511627776 - bytes)) ]; then + complete=0 + else + total_bytes=$((total_bytes + bytes)) + fi;; + esac + done + [ "$complete" -eq 1 ] || continue + printf '%s\t%s\n' "$id" "$total_bytes" + done + fi + } > "$tmp" + mv -f "$tmp" "$RUNTIME/apps-size.tsv" +} +# Release names from the Hub have zero command authority. We cross-check the +# fresh exact-build approval, locally trusted immutable index, architecture and +# resource requirements before invoking the fixed APK binary. +preflight_install() { + "$CATALOG_HELPER" || return 21 + index="$(app_index "$app")" || return 22 + [ "$(field "$CATALOG" "@.apps[$index].runtime")" = router ] || return 23 + [ "$(field "$CATALOG" "@.apps[$index].installer")" = apk ] || return 23 + arch="$(field "$CATALOG" "@.apps[$index].package_arch")" + native_arch="$(local_package_arch)" + if [ -z "$arch" ] || [ "$arch" != "$native_arch" ]; then + # E24 is an approval mismatch, not a generic download failure. Keep a + # non-sensitive diagnostic for devices with different apk CLI behaviors. + command -v logger >/dev/null 2>&1 && logger -t smartsafehub-apps "E24: approved=${arch:-missing} local=${native_arch:-unknown}" || : + return 24 + fi + approved_hash="$(field "$CATALOG" "@.apps[$index].repository_index_sha256")" + valid_sha "$approved_hash" || return 25 + # Build an allowlisted repository URL from immutable local firmware identity + # and the approved catalog response. The Hub pins the exact index digest. + channel="$(field "$CATALOG" '@.channel')" + case "$channel" in stable|beta) ;; *) return 25;; esac + case "$arch" in aarch64_cortex-a53|mipsel_24kc|x86_64) ;; *) return 24;; esac + publication="$(field "$CATALOG" "@.apps[$index].repository_release_id")" + printf '%s\n' "$publication" | grep -Eq '^[0-9]{8}T[0-9]{6}([0-9]{1,6})?$' || return 25 + repo_url="$REPO_BASE/$channel/packages/$arch/smartsafehub/$publication" + index_url="$repo_url/packages.adb" + # Always fetch the current published index. If a channel is republished after + # review, fail closed until Hub has reviewed the new index hash. + "$UCLIENT" -q -T 15 -O "$stage/packages.adb" "$index_url" > "$stage/index-fetch.log" 2>&1 || return 25 + [ -s "$stage/packages.adb" ] || return 25 + index_bytes="$(wc -c < "$stage/packages.adb" | tr -d ' ')" + [ "$index_bytes" -le 8388608 ] || return 25 + actual_hash="$("$SHA256" "$stage/packages.adb" | cut -d ' ' -f 1)" + [ "$approved_hash" = "$actual_hash" ] || return 25 + memory="$(field "$CATALOG" "@.apps[$index].requirements.min_memory_mb")" + storage="$(field "$CATALOG" "@.apps[$index].requirements.min_storage_mb")" + case "$memory" in ''|*[!0-9]*) return 26;; esac + case "$storage" in ''|*[!0-9]*) return 26;; esac + [ "$memory" -le 131072 ] && [ "$storage" -le 131072 ] || return 26 + available_memory="$(awk '/^MemTotal:/ {print int($2/1024); exit}' "$MEMINFO" 2>/dev/null)" + available_storage="$(df -Pk "$OVERLAY" 2>/dev/null | awk 'END {print int($4/1024)}')" + [ -n "$available_memory" ] && [ "$available_memory" -ge "$memory" ] || return 26 + [ -n "$available_storage" ] && [ "$available_storage" -ge "$storage" ] || return 26 + extract_pins "$index" "$pins" || return 27 + if [ "$app" = network-usage-monitor ]; then + [ "$(wc -l < "$pins" | tr -d ' ')" -eq 3 ] || return 27 + for required in nlbwmon luci-app-nlbwmon luci-lib-chartjs; do + grep -q "^${required}$(printf '\t')" "$pins" || return 27 + done + elif [ "$app" = wake-on-lan ]; then + # WOL always requires both the signal sender and its LuCI user interface. + # Never install extra packages just because a catalog lists them. + [ "$(wc -l < "$pins" | tr -d ' ')" -eq 2 ] || return 27 + for required in etherwake luci-app-wol; do + grep -q "^${required}$(printf '\t')" "$pins" || return 27 + done + elif [ "$app" = retro-arcade ]; then + # The approved first-party game app owns exactly one APK. Keep the + # package name fixed without freezing its future upgrade versions. + [ "$(wc -l < "$pins" | tr -d ' ')" -eq 1 ] || return 27 + grep -q "^smartsafehub-app-retro-arcade$(printf '\t')" "$pins" || return 27 + fi + return 0 +} + +# Download and hash-check all approved archives prior to any APK mutation. +# Both initial installation and a selected-app update use this same check. +fetch_approved_archives() { + mkdir -p "$stage/packages" || return 30 + while IFS="$(printf '\t')" read -r name version hash; do + archive="$stage/packages/$name-$version.apk" + "$UCLIENT" -q -T 30 -O "$archive" "$repo_url/$name-$version.apk" > "$stage/fetch.log" 2>&1 || return 31 + [ -s "$archive" ] && [ ! -L "$archive" ] || return 32 + archive_bytes="$(wc -c < "$archive" | tr -d ' ')" + [ "$archive_bytes" -le 33554432 ] || return 32 + [ "$("$SHA256" "$archive" | cut -d ' ' -f 1)" = "$hash" ] || return 33 + done < "$pins" +} +prepare_signed_index() { + cp "$stage/packages.adb" "$stage/packages/packages.adb" || return 34 + [ "$("$SHA256" "$stage/packages/packages.adb" | cut -d ' ' -f 1)" = "$approved_hash" ] || return 34 + local_index="file://$stage/packages/packages.adb" + "$APK" --repositories-file /dev/null -X "$local_index" update > "$stage/repo-update.log" 2>&1 || return 34 +} + +# All packages come from one SmartSafeHub-signed immutable release. +install_app() { + preflight_install || return $? + manifest="$(owned_manifest "$app")" + # Existing app ownership must match the reviewed pins. + if [ -e "$manifest" ]; then + [ -s "$manifest" ] || return 28 + while IFS="$(printf '\t')" read -r name version hash; do + valid_package "$name" && valid_version "$version" && valid_sha "$hash" || return 28 + pin="$(printf '%s\t%s\t%s' "$name" "$version" "$hash")" + grep -Fqx "$pin" "$pins" || return 28 + done < "$manifest" + fi + need_install=0 + # Never adopt a package previously installed outside SmartSafeHub Apps. + while IFS="$(printf '\t')" read -r name version hash; do + pin="$(printf '%s\t%s\t%s' "$name" "$version" "$hash")" + if [ -e "$manifest" ] && grep -Fqx "$pin" "$manifest"; then + installed "$name" || need_install=1 + elif installed "$name"; then + return 29 + else + need_install=1 + fi + done < "$pins" + [ "$need_install" -eq 1 ] || return 28 + fetch_approved_archives || return $? + # Only missing packages enter the single signed repository transaction. + set --; base_names='' + while IFS="$(printf '\t')" read -r name version hash; do + if ! installed "$name"; then set -- "$@" "$name=$version"; base_names="${base_names:+$base_names }$name"; fi + done < "$pins" + if [ "$#" -gt 0 ]; then + # Only the verified immutable index can supply packages. + prepare_signed_index || return $? + "$APK" --repositories-file /dev/null -X "$local_index" add --simulate "$@" > "$stage/plan.log" 2>&1 || return 34 + review_install_plan "$stage/plan.log" "$base_names" || return 34 + # Write ownership immediately before modifying packages. If APK partially + # succeeds, the UI offers repair/removal without adopting unrelated packages. + while IFS="$(printf '\t')" read -r name version hash; do + if ! installed "$name"; then append_owned_pin "$name" "$version" "$hash" || return 36; fi + done < "$pins" + "$APK" --repositories-file /dev/null -X "$local_index" add "$@" > "$stage/install.log" 2>&1 || return 35 + fi + # A failed or incomplete add never counts as a successful application. + while IFS="$(printf '\t')" read -r name version hash; do + installed "$name" || return 35 + done < "$pins" + return 0 +} + +# Upgrade only packages already owned by this app, and only when the newly +# approved version is strictly newer. Re-fetch approval at click time, use a +# signed immutable repository, and reject any unrelated solver action. +update_app() { + preflight_install || return $? + manifest="$(owned_manifest "$app")" + [ -s "$manifest" ] || return 51 + changes="$stage/changes" + select_upgrades "$manifest" "$pins" "$changes" || return 52 + # No package installation has been changed at this point. Keep the previous + # manifest intact until every targeted APK has been upgraded and verified. + fetch_approved_archives || return $? + prepare_signed_index || return $? + set --; update_names='' + while IFS="$(printf '\t')" read -r name version hash; do + set -- "$@" "$name=$version" + update_names="${update_names:+$update_names }$name" + done < "$changes" + [ "$#" -gt 0 ] || return 52 + "$APK" --repositories-file /dev/null -X "$local_index" add --upgrade --simulate "$@" > "$stage/plan.log" 2>&1 || return 53 + review_update_plan "$stage/plan.log" "$update_names" || return 53 + "$APK" --repositories-file /dev/null -X "$local_index" add --upgrade "$@" > "$stage/install.log" 2>&1 || return 54 + while IFS="$(printf '\t')" read -r name version hash; do + [ "$(installed_version "$name")" = "$version" ] || return 54 + done < "$pins" + # Preserve ownership atomically; future removal and firmware restore now use + # the new pins. Never record an update as a first-time download receipt. + cp "$pins" "$manifest.new" || return 55 + mv -f "$manifest.new" "$manifest" || return 55 + return 0 +} + +remove_app() { + manifest="$(owned_manifest "$app")" + [ -f "$manifest" ] || return 41 + set --; count=0 + while IFS="$(printf '\t')" read -r name version hash; do + valid_package "$name" && valid_version "$version" && valid_sha "$hash" || return 42 + protected_package "$name" && return 42 + # Reject packages owned by any other Apps installation. + for other in "$OWNED"/*; do + [ -f "$other" ] || continue + [ "$other" = "$manifest" ] && continue + grep -Fq "${name}$(printf '\t')" "$other" && return 43 + done + # A failed transaction may have recorded ownership before apk completed. + # Only pass actually installed packages to apk del; still clear the + # manifest once all remaining owned packages are safely removed. + installed "$name" && set -- "$@" "$name" + count=$((count + 1)); [ "$count" -le 32 ] || return 42 + done < "$manifest" + [ "$count" -gt 0 ] || return 42 + any_installed=0 + for name in "$@"; do installed "$name" && any_installed=1; done + if [ "$any_installed" -eq 1 ]; then + "$APK" del --simulate "$@" > "$stage/plan.log" 2>&1 || return 44 + # Removing an optional app must never implicitly remove system/network, + # SafeShield or firmware kernel-module packages. + if grep -Ei '(Purging|Removing|Upgrading|Downgrading) (base-files|busybox|apk|procd|netifd|dnsmasq[^ ]*|firewall[^ ]*|odhcp[^ ]*|dropbear|ubus[^ ]*|rpcd[^ ]*|uhttpd[^ ]*|safeshield[^ ]*|luci-app-smartsafehub|kernel|kmod-[^ ]*) ' "$stage/plan.log" >/dev/null; then + return 44 + fi + review_remove_plan "$stage/plan.log" "$*" || return 44 + "$APK" del "$@" > "$stage/remove.log" 2>&1 || return 45 + fi + rm -f "$manifest" + return 0 +} + +worker() { + action="$1"; app="$2" + # Keep the ID from the queued job across running/success/failed states. + # The browser uses it to distinguish a new failure from a dismissed one. + operation_id="$(awk -F '\t' '$1 == "operation_id" { print $2; exit }' "$STATE" 2>/dev/null || :)" + source="$(awk -F '\t' '$1 == "source" { print $2; exit }' "$STATE" 2>/dev/null || :)" + case "$source" in user|restore) ;; *) source=user ;; esac + trap 'rm -rf "$stage"; rmdir "$LOCK" 2>/dev/null || true' EXIT HUP INT TERM + stage="$(mktemp -d "$RUNTIME/apps-work.XXXXXXXX")" || exit 1 + pins="$stage/pins" + # Never expose error text from a previous install as the current result. + rm -f "$RUNTIME/apps-last-error.log" + write_state running "$action" "$app" NONE "$operation_id" "$source" + rc=0 + case "$action" in + install) install_app || rc=$?;; + update) update_app || rc=$?;; + remove) remove_app || rc=$?;; + esac + if [ "$rc" -eq 0 ]; then + # The LuCI menu tree may be cached before an optional UI is installed. + # Invalidate the cache only after a successful package transaction. + if [ "$app" = wake-on-lan ]; then + rm -f "$LUCI_INDEX_CACHE" + fi + write_state success "$action" "$app" NONE "$operation_id" "$source" + # Count successful user installs and updates separately. Restores, + # removals and failed/no-op operations never become receipts. Telemetry + # must not affect the success of an APK transaction. + if [ "$source" = user ] && { [ "$action" = install ] || [ "$action" = update ]; }; then + version="$(field "$CATALOG" "@.apps[$index].version")" + "$METRICS" enqueue "$app" "$version" "$operation_id" "$action" >/dev/null 2>&1 || : + fi + else + # Keep only the latest package-manager diagnostic after the workspace is + # removed. Never record an auth token, catalog JSON, or package contents. + for log in "$stage/repo-update.log" "$stage/plan.log" "$stage/install.log" "$stage/remove.log"; do + if [ -s "$log" ]; then + tail -c 4096 "$log" > "$RUNTIME/apps-last-error.log" 2>/dev/null || : + fi + done + write_state failed "$action" "$app" "E$rc" "$operation_id" "$source" + fi +} + +start() { + action="$1"; app="$2"; source="${3:-user}" + case "$source" in user|restore) ;; *) exit 2;; esac + [ "$action" = install ] || [ "$source" = user ] || exit 2 + case "$action" in install|update|remove) ;; *) exit 2;; esac + valid_id "$app" || exit 2 + mkdir -p "$RUNTIME" || exit 1 + mkdir "$LOCK" 2>/dev/null || exit 3 + # The ID is unique to each invocation, even if the same error repeats. + # OpenWrt's default BusyBox image does not include the od applet. + # dd and sha256sum are available; hash 8 random bytes into a 16-hex nonce. + # Fail closed if the entropy read fails rather than reusing a fixed ID. + random_file="$(mktemp "$RUNTIME/apps-random.XXXXXXXX")" || { rmdir "$LOCK" 2>/dev/null || :; exit 1; } + if ! dd if=/dev/urandom of="$random_file" bs=8 count=1 2>/dev/null || + [ "$(wc -c < "$random_file" | tr -d ' ')" -ne 8 ]; then + rm -f "$random_file" + rmdir "$LOCK" 2>/dev/null || : + exit 1 + fi + random="$("$SHA256" "$random_file" | cut -c 1-16)" + rm -f "$random_file" + [ "${#random}" -eq 16 ] || { rmdir "$LOCK" 2>/dev/null || :; exit 1; } + operation_id="$(date +%s)-$$-$random" + write_state starting "$action" "$app" NONE "$operation_id" "$source" + if ! "$DAEMON" -S -b -x "$SELF" -- worker "$action" "$app" >/dev/null 2>&1; then + write_state failed "$action" "$app" LAUNCH "$operation_id" "$source" + rmdir "$LOCK" 2>/dev/null || true + exit 1 + fi +} + +case "${1:-}" in + status) mkdir -p "$RUNTIME"; status;; + sizes) mkdir -p "$RUNTIME"; sizes;; + start) [ "$#" -ge 3 ] && [ "$#" -le 4 ] || exit 2; start "$2" "$3" "${4:-user}";; + worker) [ "$#" -eq 3 ] || exit 2; worker "$2" "$3";; + *) exit 2;; +esac diff --git a/luci-app-smartsafehub/root/usr/libexec/smartsafehub-apps-metrics b/luci-app-smartsafehub/root/usr/libexec/smartsafehub-apps-metrics new file mode 100755 index 000000000..1470d13aa --- /dev/null +++ b/luci-app-smartsafehub/root/usr/libexec/smartsafehub-apps-metrics @@ -0,0 +1,74 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +# Durable outbox for authenticated successful user installs and updates. +# Never affects APK transactions; send only app, release, kind and receipt ID. +set -eu +umask 077 +QUEUE="${SMARTSAFEHUB_APPS_METRICS_DIR:-/etc/smartsafehub/apps-metrics-pending}" +CREDENTIAL="${SMARTSAFEHUB_APPS_METRICS_CREDENTIAL:-/etc/smartsafehub/device-credential.json}" +JSONFILTER="${SMARTSAFEHUB_APPS_JSONFILTER_BIN:-jsonfilter}" +FETCH="${SMARTSAFEHUB_APPS_METRICS_FETCH_BIN:-uclient-fetch}" +URL="${SMARTSAFEHUB_APPS_METRICS_URL:-https://www.smartsafehub.com/api/v1/app-store/installs}" +MAX_PENDING=500 +MAX_FLUSH=3 + +json_field() { "$JSONFILTER" -i "$1" -e "$2" 2>/dev/null | head -n 1; } +valid_id() { case "$1" in ''|*[!a-z0-9-]*|-*|*-) return 1;; esac; [ "${#1}" -le 80 ]; } +valid_version() { case "$1" in ''|*[!a-zA-Z0-9._+~:-]*|[-.+~:]*) return 1;; esac; [ "${#1}" -le 64 ]; } +valid_event() { + # Timestamp, process ID and 64-bit random component (no user data). + printf '%s\n' "$1" | grep -Eq '^[0-9]{10}-[0-9]+-[0-9a-f]{16}$' && [ "${#1}" -le 64 ] +} +enqueue() { + [ "$#" -eq 3 ] || [ "$#" -eq 4 ] || exit 2 + valid_id "$1" && valid_version "$2" && valid_event "$3" || exit 2 + kind="${4:-install}" + case "$kind" in install|update) ;; *) exit 2;; esac + mkdir -p "$QUEUE" || exit 1 + [ -d "$QUEUE" ] && [ ! -L "$QUEUE" ] || exit 1 + file="$QUEUE/$3.json" + [ ! -e "$file" ] || return 0 + # Bounded flash use. A full outbox never blocks installations. + count="$(find "$QUEUE" -maxdepth 1 -type f -name '*.json' | wc -l | tr -d ' ')" + [ "$count" -lt "$MAX_PENDING" ] || exit 1 + tmp="$(mktemp "$QUEUE/.pending.XXXXXXXX")" || exit 1 + if ! printf '{"app_id":"%s","version":"%s","event_id":"%s","kind":"%s"}\n' "$1" "$2" "$3" "$kind" > "$tmp"; then + rm -f "$tmp"; exit 1 + fi + mv -f "$tmp" "$file" +} +flush() { + [ -d "$QUEUE" ] || return 0 + [ -f "$CREDENTIAL" ] && [ ! -L "$CREDENTIAL" ] || return 0 + token="$(json_field "$CREDENTIAL" '@.token')" + case "$token" in ssh_dev_*) ;; *) return 0;; esac + [ "${#token}" -eq 72 ] || return 0 + # Only lower-case hex credentials can enter HTTP headers. + case "${token#ssh_dev_}" in *[!0-9a-f]*|'') return 0;; esac + mkdir "$QUEUE/.flush.lock" 2>/dev/null || return 0 + trap 'rmdir "$QUEUE/.flush.lock" 2>/dev/null || :' EXIT HUP INT TERM + n=0; failed=0 + for file in "$QUEUE"/*.json; do + [ -f "$file" ] && [ ! -L "$file" ] || continue + event="${file##*/}"; event="${event%.json}" + valid_event "$event" || continue + [ "$(json_field "$file" '@.event_id')" = "$event" ] || continue + response="$(mktemp "$QUEUE/.response.XXXXXXXX")" || return 1 + if "$FETCH" -q -T 8 -O "$response" \ + --header='Content-Type: application/json' \ + --header="Authorization: Device $token" --post-file="$file" "$URL" >/dev/null 2>&1; then + accepted="$(json_field "$response" '@.status')" + case "$accepted" in recorded|duplicate) rm -f "$file";; *) failed=1;; esac + else + failed=1 + fi + rm -f "$response" + n=$((n + 1)); [ "$n" -lt "$MAX_FLUSH" ] || break + done + [ "$failed" -eq 0 ] +} +case "${1:-}" in + enqueue) shift; enqueue "$@";; + flush) [ "$#" -eq 1 ] || exit 2; flush;; + *) exit 2;; +esac diff --git a/luci-app-smartsafehub/root/usr/libexec/smartsafehub-apps-restore b/luci-app-smartsafehub/root/usr/libexec/smartsafehub-apps-restore new file mode 100755 index 000000000..0b6337974 --- /dev/null +++ b/luci-app-smartsafehub/root/usr/libexec/smartsafehub-apps-restore @@ -0,0 +1,165 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +# Reconcile sysupgrade-preserved Apps ownership with the new firmware's APK DB. +# The privileged Apps manager alone decides which signed packages may be added. +set -eu + +MANAGER="${SMARTSAFEHUB_APPS_MANAGER_BIN:-/usr/libexec/smartsafehub-apps-manager}" +OWNED="${SMARTSAFEHUB_APPS_OWNED_DIR:-/etc/smartsafehub/apps-installed}" +RUNTIME="${SMARTSAFEHUB_APPS_RUNTIME_DIR:-/tmp/smartsafehub}" +APK="${SMARTSAFEHUB_APPS_APK_BIN:-apk}" +SLEEP="${SMARTSAFEHUB_APPS_RESTORE_SLEEP_BIN:-sleep}" +START_DELAY="${SMARTSAFEHUB_APPS_RESTORE_START_DELAY:-30}" +RETRY_DELAY="${SMARTSAFEHUB_APPS_RESTORE_RETRY_DELAY:-120}" +RETRIES="${SMARTSAFEHUB_APPS_RESTORE_RETRIES:-12}" +POLL_INTERVAL="${SMARTSAFEHUB_APPS_RESTORE_POLL_INTERVAL:-2}" +POLL_LIMIT="${SMARTSAFEHUB_APPS_RESTORE_POLL_LIMIT:-240}" +STATE="$RUNTIME/apps-restore.state" +JOB="$RUNTIME/apps-operation.state" +LOCK="$RUNTIME/apps-restore.lock" + +valid_id() { + case "$1" in ''|*[!a-z0-9-]*|-*|*-) return 1;; esac + [ "${#1}" -le 80 ] +} +valid_package() { + case "$1" in ''|*[!a-zA-Z0-9_.+-]*|[-.+]*|*[-.+]) return 1;; esac + [ "${#1}" -le 100 ] +} +valid_sha() { + case "$1" in ''|*[!a-f0-9]*) return 1;; esac + [ "${#1}" -eq 64 ] +} +write_state() { + phase="$1"; id="$2"; code="$3" + state_tmp="$(mktemp "$RUNTIME/apps-restore-state.XXXXXXXX")" || return 1 + printf 'phase\t%s\napp_id\t%s\ncode\t%s\n' "$phase" "$id" "$code" > "$state_tmp" + mv -f "$state_tmp" "$STATE" +} +log() { + command -v logger >/dev/null 2>&1 && logger -t smartsafehub-apps-restore "$*" || : +} +# A corrupt manifest must NOT grant an arbitrary app installation right. +# Existing packages are never replaced merely because an old manifest exists. +needs_restore() { + manifest="$1"; count=0; missing=0 + [ -f "$manifest" ] && [ ! -L "$manifest" ] && [ -s "$manifest" ] || return 2 + while IFS="$(printf '\t')" read -r name version hash; do + valid_package "$name" && [ -n "$version" ] && valid_sha "$hash" || return 2 + count=$((count + 1)) + [ "$count" -le 32 ] || return 2 + "$APK" info -e "$name" >/dev/null 2>&1 || missing=1 + done < "$manifest" + [ "$count" -gt 0 ] || return 2 + [ "$missing" -eq 1 ] +} +job_field() { + [ -r "$JOB" ] || return 0 + awk -F '\t' -v key="$1" '$1 == key {print $2; exit}' "$JOB" +} +# Starts exactly the same preflight/ownership/signature/plan checks as a user +# clicking Install. Never calls apk add directly, never reuses cached catalogs. +run_job() { + id="$1"; n=0 + if ! "$MANAGER" start install "$id" restore >/dev/null 2>&1; then + RESULT=BUSY + return 1 + fi + while [ "$n" -lt "$POLL_LIMIT" ]; do + phase="$(job_field phase)"; current="$(job_field app_id)" + if [ "$current" = "$id" ]; then + case "$phase" in + success) RESULT=NONE; return 0;; + failed) RESULT="$(job_field code)"; [ -n "$RESULT" ] || RESULT=UNKNOWN; return 1;; + esac + fi + "$SLEEP" "$POLL_INTERVAL" + n=$((n + 1)) + done + RESULT=TIMEOUT + return 1 +} +restore_one() { + id="$1"; attempt=1 + while [ "$attempt" -le "$RETRIES" ]; do + # User removal while waiting takes precedence; never reinstall a removed app. + [ -f "$OWNED/$id" ] || return 0 + if needs_restore "$OWNED/$id"; then :; else + result=$? + [ "$result" -eq 1 ] && return 0 + RESULT=INVALID_MANIFEST + return 1 + fi + write_state running "$id" NONE + if run_job "$id"; then + # Successful APK transaction may still have omitted an owned package. + if needs_restore "$OWNED/$id"; then + RESULT=INCOMPLETE + else + result=$? + [ "$result" -eq 1 ] && return 0 + RESULT=INVALID_MANIFEST + fi + fi + # Only networking failures or a concurrent manual operation are retried. + # Changed approval pins, revoked apps, bad signatures and unsafe dependency + # plans are never bypassed or retried indefinitely. + case "$RESULT" in BUSY|E21|E25|E31|E35) ;; *) return 1;; esac + [ "$attempt" -lt "$RETRIES" ] || return 1 + "$SLEEP" "$RETRY_DELAY" + attempt=$((attempt + 1)) + done + return 1 +} +restore() { + mkdir -p "$RUNTIME" || exit 1 + # procd only starts one instance, but an operator may invoke run manually. + mkdir "$LOCK" 2>/dev/null || exit 0 + trap 'rmdir "$LOCK" 2>/dev/null || :' EXIT + write_state idle '' NONE + # No persistent ownership means never install anything on a fresh router. + [ -d "$OWNED" ] || return 0 + pending=0 + for manifest in "$OWNED"/*; do + [ -f "$manifest" ] || continue + id="${manifest##*/}" + valid_id "$id" || continue + if needs_restore "$manifest"; then pending=1; break; else + result=$?; [ "$result" -eq 2 ] && { pending=1; break; } + fi + done + [ "$pending" -eq 1 ] || return 0 + write_state starting '' NONE + "$SLEEP" "$START_DELAY" + failed=0; last_app=''; last_code=NONE + for manifest in "$OWNED"/*; do + [ -f "$manifest" ] || continue + id="${manifest##*/}" + valid_id "$id" || continue + if needs_restore "$manifest"; then + RESULT=NONE + if restore_one "$id"; then + log "restored $id" + else + failed=1; last_app="$id"; last_code="$RESULT" + log "restore blocked for $id ($RESULT)" + fi + else + result=$? + if [ "$result" -eq 2 ]; then + failed=1; last_app="$id"; last_code=INVALID_MANIFEST + log "invalid owned manifest for $id" + fi + fi + done + if [ "$failed" -eq 1 ]; then + write_state failed "$last_app" "$last_code" + else + write_state success '' NONE + fi +} + +case "${1:-}" in + run) [ "$#" -eq 1 ] || exit 2; restore;; + *) exit 2;; +esac diff --git a/luci-app-smartsafehub/root/usr/libexec/smartsafehub-device b/luci-app-smartsafehub/root/usr/libexec/smartsafehub-device index 019333a50..49facb8d8 100755 --- a/luci-app-smartsafehub/root/usr/libexec/smartsafehub-device +++ b/luci-app-smartsafehub/root/usr/libexec/smartsafehub-device @@ -29,6 +29,7 @@ LOGGER_BIN="${SMARTSAFEHUB_DEVICE_LOGGER_BIN:-logger}" HEXDUMP_BIN="${SMARTSAFEHUB_DEVICE_HEXDUMP_BIN:-hexdump}" UBUS_BIN="${SMARTSAFEHUB_DEVICE_UBUS_BIN:-ubus}" REQUEST_TIMEOUT_S=10 +APPS_METRICS="${SMARTSAFEHUB_APPS_METRICS_BIN:-/usr/libexec/smartsafehub-apps-metrics}" CHECK_INTERVAL_S=300 RECOMMENDED_POLL_INTERVAL_S=30 RECOMMENDED_POLL_MAX_RETRY_S=300 @@ -498,6 +499,8 @@ daemon() { delay="$(retry_delay "$failures")" [ "$failures" -lt 4 ] && failures=$((failures + 1)) fi + # Backlogged Apps receipts are retried independently of account sync. + [ ! -x "$APPS_METRICS" ] || "$APPS_METRICS" flush >/dev/null 2>&1 || : now="$(now_epoch)" next_full=$((now + delay)) # Full sync already handled the recommended payload. diff --git a/luci-app-smartsafehub/root/usr/libexec/smartsafehub-firmware b/luci-app-smartsafehub/root/usr/libexec/smartsafehub-firmware index f0c4086a6..e1b66bea8 100755 --- a/luci-app-smartsafehub/root/usr/libexec/smartsafehub-firmware +++ b/luci-app-smartsafehub/root/usr/libexec/smartsafehub-firmware @@ -21,6 +21,7 @@ SHA256SUM_BIN="${SMARTSAFEHUB_FIRMWARE_SHA256SUM_BIN:-sha256sum}" UBUS_BIN="${SMARTSAFEHUB_FIRMWARE_UBUS_BIN:-ubus}" SYSUPGRADE_BIN="${SMARTSAFEHUB_FIRMWARE_SYSUPGRADE_BIN:-/sbin/sysupgrade}" EVENTS_BIN="${SMARTSAFEHUB_FIRMWARE_EVENTS_BIN:-/usr/libexec/smartsafehub-events}" +APPS_RESTORE_BIN="${SMARTSAFEHUB_APPS_RESTORE_BIN:-/usr/libexec/smartsafehub-apps-restore}" DEFAULT_API_BASE_URL="https://www.smartsafehub.com/api/v1" CHECK_TIMEOUT_S=20 DOWNLOAD_TIMEOUT_S=180 @@ -702,8 +703,18 @@ periodic_check_if_due() { fi } +# The existing firmware daemon schedules an independent, finite restore job +# once at boot. There is no new procd service, respawn loop or idle process. +# Restore worker itself checks the preserved ownership manifest before I/O. +start_apps_restore_once() { + [ -x "$APPS_RESTORE_BIN" ] || return 0 + [ -d "${SMARTSAFEHUB_APPS_OWNED_DIR:-/etc/smartsafehub/apps-installed}" ] || return 0 + "$APPS_RESTORE_BIN" run /dev/null 2>&1 & +} + run_daemon() { sync_firmware_identity + start_apps_restore_once load_daemon_settings sleep "$DAEMON_INITIAL_DELAY_S" boot_check_with_retry || true diff --git a/luci-app-smartsafehub/root/usr/libexec/smartsafehub-recommended-rules b/luci-app-smartsafehub/root/usr/libexec/smartsafehub-recommended-rules index 4965f9ac9..368917b63 100755 --- a/luci-app-smartsafehub/root/usr/libexec/smartsafehub-recommended-rules +++ b/luci-app-smartsafehub/root/usr/libexec/smartsafehub-recommended-rules @@ -30,7 +30,9 @@ SNAPSHOT="$RUNTIME/recommended-snapshot.$$" REPLY="$RUNTIME/recommended-reply.$$" ALLOW="$RUNTIME/recommended-allow.$$" VERIFY="$RUNTIME/recommended-verify.$$" -cleanup() { rm -f "$DESIRED" "$OWNED" "$SNAPSHOT" "$REPLY" "$ALLOW" "$VERIFY" "$LOCK/pid"; rmdir "$LOCK" 2>/dev/null || true; } +ADD="$RUNTIME/recommended-add.$$" +REMOVE="$RUNTIME/recommended-remove.$$" +cleanup() { rm -f "$DESIRED" "$OWNED" "$SNAPSHOT" "$REPLY" "$ALLOW" "$VERIFY" "$ADD" "$REMOVE" "$LOCK/pid"; rmdir "$LOCK" 2>/dev/null || true; } trap cleanup EXIT trap 'exit 1' HUP INT TERM @@ -48,16 +50,18 @@ extract_array() { [ "$status" -eq 1 ] && [ ! -s "$output" ] fi } -valid_domain() { - [ -n "$1" ] && [ "${#1}" -le 253 ] || return 1 - printf '%s\n' "$1" | LC_ALL=C grep -Eq '^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$' || return 1 - ! printf '%s\n' "$1" | LC_ALL=C grep -Eq '\.[0-9]+$' || return 1 - rest="$1" - while [ -n "$rest" ]; do - label="${rest%%.*}" - [ "${#label}" -le 63 ] || return 1 - case "$rest" in *.*) rest="${rest#*.}" ;; *) break ;; esac - done +# Validate whole sorted domain lists in one awk process. Per-domain grep and +# subshell creation previously scaled with every saved SafeShield allow entry. +valid_domains_file() { + LC_ALL=C awk ' + length($0) == 0 || length($0) > 253 { exit 1 } + $0 !~ /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/ { exit 1 } + $0 ~ /\.[0-9]+$/ { exit 1 } + { + count = split($0, labels, /\./) + for (i = 1; i <= count; i++) if (length(labels[i]) > 63) exit 1 + } + ' "$1" } revision="$(field "$RESPONSE" '@.recommended_rules.revision')" @@ -68,7 +72,7 @@ revision="$(field "$RESPONSE" '@.recommended_rules.revision')" valid_hash "$revision" || exit 1 extract_array "$RESPONSE" '@.recommended_rules.domains' "$DESIRED" || exit 1 [ "$(wc -l < "$DESIRED")" -le 100 ] || exit 1 -while IFS= read -r domain || [ -n "$domain" ]; do valid_domain "$domain" || exit 1; done < "$DESIRED" +valid_domains_file "$DESIRED" || exit 1 sort -u "$DESIRED" -o "$DESIRED" previous='' @@ -78,14 +82,19 @@ if [ -e "$STATE" ]; then previous="$(sed -n '1p' "$STATE")" [ "$previous" = '-' ] || valid_hash "$previous" || exit 1 sed '1d' "$STATE" > "$OWNED" - while IFS= read -r domain || [ -n "$domain" ]; do valid_domain "$domain" || exit 1; done < "$OWNED" + valid_domains_file "$OWNED" || exit 1 sort -u "$OWNED" -o "$OWNED" fi persist() { temp="${STATE}.tmp.$$" { printf '%s\n' "$1"; sort -u "$OWNED"; } > "$temp" chmod 600 "$temp" - mv -f "$temp" "$STATE" + # Avoid extra flash writes on a successful metadata-only reconciliation. + if [ -f "$STATE" ] && cmp -s "$STATE" "$temp"; then + rm -f "$temp" + else + mv -f "$temp" "$STATE" + fi } # Report progress from the worker itself. The parent daemon only commits a @@ -111,9 +120,11 @@ safeshield_refreshing() { [ "$status_value" = running ] } -# Defer a different desired revision before calling potentially busy SafeShield -# rules_list, which can itself be delayed during a long refresh. -if [ "$previous" != "$revision" ] && safeshield_refreshing; then +# Always defer a reconciliation during SafeShield's full-list refresh. Even +# a matching revision may need recovery if a prior apply lost its receipt. +# Check before dropping the old receipt so interrupted refreshes are not +# mistaken for failed or unverified rules. +if safeshield_refreshing; then phase safeshield_refreshing exit 75 fi @@ -129,26 +140,41 @@ fi # A broken or truncated rules_list must never be mistaken for an empty list. LC_ALL=C grep -Eq '"allow"[[:space:]]*:[[:space:]]*\[' "$REPLY" || exit 1 extract_array "$REPLY" '@.allow' "$ALLOW" || exit 1 -while IFS= read -r domain || [ -n "$domain" ]; do valid_domain "$domain" || exit 1; done < "$ALLOW" +valid_domains_file "$ALLOW" || exit 1 sort -u "$ALLOW" -o "$ALLOW" # Do not mutate overrides while a full protection refresh is already running. # Retain ownership and let the 30-second poll retry when SafeShield settles. +# Build the two mutation sets once. Owned entries absent from SafeShield do +# not need a delete, but their claims remain until final verification. +: > "$ADD" +: > "$REMOVE" +LC_ALL=C awk -v add_file="$ADD" -v remove_file="$REMOVE" ' + FILENAME == ARGV[1] { if ($0 != "") desired[$0] = 1; next } + FILENAME == ARGV[2] { if ($0 != "") allowed[$0] = 1; next } + FILENAME == ARGV[3] { + if ($0 != "" && !($0 in desired) && ($0 in allowed)) print $0 > remove_file + next + } + END { + for (domain in desired) if (!(domain in allowed)) print domain > add_file + } +' "$DESIRED" "$ALLOW" "$OWNED" || exit 1 +sort -u "$ADD" -o "$ADD" +sort -u "$REMOVE" -o "$REMOVE" changes_needed=0 -[ "$previous" = '-' ] && changes_needed=1 -while IFS= read -r domain; do - [ -n "$domain" ] || continue - if ! grep -Fxq "$domain" "$ALLOW"; then changes_needed=1; fi -done < "$DESIRED" -while IFS= read -r domain; do - [ -n "$domain" ] || continue - if ! grep -Fxq "$domain" "$DESIRED" && grep -Fxq "$domain" "$ALLOW"; then changes_needed=1; fi -done < "$OWNED" +if [ "$previous" = '-' ] || [ -s "$ADD" ] || [ -s "$REMOVE" ]; then changes_needed=1; fi if [ "$changes_needed" -eq 1 ] && safeshield_refreshing; then phase safeshield_refreshing exit 75 fi -if [ "$changes_needed" -eq 1 ]; then phase applying; fi +if [ "$changes_needed" -eq 1 ]; then + phase applying + # Mark the entire transaction unconfirmed once, before touching any rule. + # Keeping all ownership claims prevents a partial deletion from becoming a + # future manual-allow exemption after interruption. + persist '-' +fi mutate() { method="$1"; domain="$2" @@ -159,36 +185,24 @@ mutate() { # Refresh is retried after a partial or unsuccessful reconciliation. need_refresh=0 [ "$previous" = '-' ] && need_refresh=1 -cp "$OWNED" "$SNAPSHOT" while IFS= read -r domain; do [ -n "$domain" ] || continue - if ! grep -Fxq "$domain" "$DESIRED"; then - if grep -Fxq "$domain" "$ALLOW"; then - mutate rule_delete "$domain" || exit 1 - need_refresh=1 - fi - # Keep the ownership claim until the final SafeShield read confirms the - # domain is gone. A refresh failure must not turn a leftover cloud entry - # into an apparently user-created allow rule on the next assignment. - persist '-' - fi -done < "$SNAPSHOT" + mutate rule_delete "$domain" || exit 1 + need_refresh=1 + # Ownership remains in the pending state until final verification. +done < "$REMOVE" while IFS= read -r domain; do [ -n "$domain" ] || continue - if ! grep -Fxq "$domain" "$ALLOW"; then - # Write the ownership intent before creating the allow entry. If the - # process is interrupted after rule_add, the retry must still recognize - # the entry as cloud-managed rather than silently treating it as manual. - if ! grep -Fxq "$domain" "$OWNED"; then - printf '%s\n' "$domain" >> "$OWNED" - persist '-' - fi - mutate rule_add "$domain" || exit 1 - need_refresh=1 + # Record intent before rule_add so a crash cannot reclassify a managed rule + # as a manual allow entry. Preserve ownership on partial refresh failures. + if ! grep -Fxq "$domain" "$OWNED"; then + printf '%s\n' "$domain" >> "$OWNED" persist '-' fi -done < "$DESIRED" + mutate rule_add "$domain" || exit 1 + need_refresh=1 +done < "$ADD" if [ "$need_refresh" -eq 1 ]; then phase safeshield_refreshing @@ -201,54 +215,55 @@ if [ "$need_refresh" -eq 1 ]; then exit 1 fi fi -# Read the actual SafeShield rule list again rather than trusting rule_add results. +# Verify with a second RPC only when a mutation/refresh was requested. +# Otherwise the first rules_list response is already an authoritative read. if [ "$changes_needed" -eq 1 ] || [ "$previous" != "$revision" ]; then phase verifying; fi -if ! "$UBUS" call safeshield rules_list '{}' > "$REPLY"; then - if safeshield_refreshing; then phase safeshield_refreshing; exit 75; fi - exit 1 -fi -[ "$(field "$REPLY" '@.ok')" != false ] || exit 1 -LC_ALL=C grep -Eq '"allow"[[:space:]]*:[[:space:]]*\[' "$REPLY" || exit 1 -extract_array "$REPLY" '@.allow' "$VERIFY" || exit 1 -while IFS= read -r domain || [ -n "$domain" ]; do valid_domain "$domain" || exit 1; done < "$VERIFY" -sort -u "$VERIFY" -o "$VERIFY" -while IFS= read -r domain; do - [ -n "$domain" ] || continue - grep -Fxq "$domain" "$VERIFY" || exit 1 -done < "$DESIRED" -# Do not retire ownership if a previously managed, now-unassigned domain is -# still present in SafeShield. It may reappear during a failed refresh or an -# overlapping request, and a subsequent recommendation must not adopt it as a -# manual rule. Retry the deletion while the ownership marker remains intact. -while IFS= read -r domain; do - [ -n "$domain" ] || continue - if ! grep -Fxq "$domain" "$DESIRED" && grep -Fxq "$domain" "$VERIFY"; then +if [ "$need_refresh" -eq 1 ]; then + if ! "$UBUS" call safeshield rules_list '{}' > "$REPLY"; then + if safeshield_refreshing; then phase safeshield_refreshing; exit 75; fi exit 1 fi -done < "$OWNED" -# Only after successful reconciliation may we forget ownership of removed -# domains. The preserved marker survives a failed refresh and a reassignment. -cp "$OWNED" "$SNAPSHOT" -: > "$OWNED" -while IFS= read -r domain; do - [ -n "$domain" ] || continue - if grep -Fxq "$domain" "$DESIRED"; then - printf '%s\n' "$domain" >> "$OWNED" - fi -done < "$SNAPSHOT" + [ "$(field "$REPLY" '@.ok')" != false ] || exit 1 + LC_ALL=C grep -Eq '"allow"[[:space:]]*:[[:space:]]*\[' "$REPLY" || exit 1 + extract_array "$REPLY" '@.allow' "$VERIFY" || exit 1 + valid_domains_file "$VERIFY" || exit 1 + sort -u "$VERIFY" -o "$VERIFY" +else + cp "$ALLOW" "$VERIFY" +fi +# Check desired presence and pending managed deletions with one process. +LC_ALL=C awk ' + FILENAME == ARGV[1] { if ($0 != "") desired[$0] = 1; next } + FILENAME == ARGV[2] { if ($0 != "") verified[$0] = 1; next } + FILENAME == ARGV[3] { + if ($0 != "" && !($0 in desired) && ($0 in verified)) invalid = 1 + next + } + END { + for (domain in desired) if (!(domain in verified)) invalid = 1 + exit invalid + } +' "$DESIRED" "$VERIFY" "$OWNED" || exit 1 +# Retire ownership only after final verification, never on a failed refresh. +LC_ALL=C awk ' + FILENAME == ARGV[1] { if ($0 != "") desired[$0] = 1; next } + ($0 in desired) { print } +' "$DESIRED" "$OWNED" > "$SNAPSHOT" || exit 1 +sort -u "$SNAPSHOT" -o "$SNAPSHOT" +cp "$SNAPSHOT" "$OWNED" # The receipt describes *allow-list presence*, not proven DNS query results. # Domains not in OWNED existed before this recommendation (manual configuration). receipt_tmp="${RECEIPT}.tmp.$$" { printf '{"revision":"%s","status":"confirmed","domains":[' "$revision" - first=1 - while IFS= read -r domain; do - [ -n "$domain" ] || continue - if grep -Fxq "$domain" "$OWNED"; then source=managed; else source=manual; fi - [ "$first" -eq 1 ] || printf ',' - printf '{"domain":"%s","source":"%s"}' "$domain" "$source" - first=0 - done < "$DESIRED" + LC_ALL=C awk ' + FILENAME == ARGV[1] { if ($0 != "") owned[$0] = 1; next } + FILENAME == ARGV[2] { + if ($0 == "") next + if (count++) printf "," + printf "{\"domain\":\"%s\",\"source\":\"%s\"}", $0, (($0 in owned) ? "managed" : "manual") + } + ' "$OWNED" "$DESIRED" printf ']}\n' } > "$receipt_tmp" chmod 600 "$receipt_tmp" diff --git a/luci-app-smartsafehub/root/usr/libexec/smartsafehub-rpc-jobs b/luci-app-smartsafehub/root/usr/libexec/smartsafehub-rpc-jobs new file mode 100755 index 000000000..5d002743a --- /dev/null +++ b/luci-app-smartsafehub/root/usr/libexec/smartsafehub-rpc-jobs @@ -0,0 +1,121 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +# Bounded, single-flight subprocesses for slow read-only scans / Hub calls. +# RPC callers accept a job and read snapshots. No user-provided command/URL. +set -eu +umask 077 +RUNTIME="${SMARTSAFEHUB_RPC_JOBS_RUNTIME:-/tmp/smartsafehub/rpc-jobs}" +DAEMON="${SMARTSAFEHUB_RPC_JOBS_DAEMON:-start-stop-daemon}" +SELF="${SMARTSAFEHUB_RPC_JOBS_SELF:-/usr/libexec/smartsafehub-rpc-jobs}" +CATALOG="${SMARTSAFEHUB_RPC_JOBS_CATALOG:-/usr/libexec/smartsafehub-apps}" +MANAGER="${SMARTSAFEHUB_RPC_JOBS_MANAGER:-/usr/libexec/smartsafehub-apps-manager}" +DEVICE="${SMARTSAFEHUB_RPC_JOBS_DEVICE:-/usr/libexec/smartsafehub-device}" + +valid_job() { + case "$1" in catalog|status|sizes|device-sync|pairing|recommended-sync|recommended-revoke) return 0;; esac + return 1 +} +valid_arg() { + case "$1" in ''|0|*[!0-9]*) return 1;; esac + [ "${#1}" -le 10 ] +} +state_path() { printf '%s/%s.state' "$RUNTIME" "$1"; } +lock_path() { printf '%s/%s.lock' "$RUNTIME" "$1"; } +# All fields come from fixed enums, epoch seconds and validated digits. +write_state() { + job="$1"; phase="$2"; code="$3"; argument="$4"; started="$5" + tmp="$(mktemp "$RUNTIME/$job.XXXXXXXX")" || return 1 + printf 'phase\t%s\ncode\t%s\nargument\t%s\nstarted\t%s\nupdated\t%s\n' \ + "$phase" "$code" "$argument" "$started" "$(date +%s)" > "$tmp" + mv -f "$tmp" "$(state_path "$job")" +} +read_field() { [ -r "$1" ] && sed -n "s/^$2$(printf '\t')//p" "$1" | head -n 1 || :; } + +ensure() { + job="$1"; mode="$2"; argument="$3" + now="$(date +%s)" + state="$(state_path "$job")"; lock="$(lock_path "$job")" + ttl=0 + case "$job" in + catalog) ttl=600;; status) ttl=15;; sizes) ttl=120;; device-sync) ttl=30;; + *) ttl=0;; + esac + if [ "$mode" = auto ]; then + # Suppress redundant requests and short retry storms after errors. + stamp="$(read_field "$state" updated)" + case "$stamp" in ''|*[!0-9]*) stamp=0;; esac + if [ $((now - stamp)) -ge 0 ] && [ $((now - stamp)) -lt "$ttl" ]; then + exit 0 + fi + if [ "$ttl" -eq 0 ]; then exit 2; fi + fi + if ! mkdir "$lock" 2>/dev/null; then + # Keep a live worker. Recover only an abandoned lock, not a slow worker. + pid="$(read_field "$lock/pid" pid)" + stamp="$(read_field "$state" started)" + case "$stamp" in ''|*[!0-9]*) stamp="$now";; esac + if [ "$((now - stamp))" -gt 180 ] && { [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; }; then + rm -f "$lock/pid" + rmdir "$lock" 2>/dev/null || exit 0 + mkdir "$lock" 2>/dev/null || exit 0 + else + # Explicit duplicate actions are not silently counted as completed. + [ "$mode" = auto ] && exit 0 + existing="$(read_field "$state" argument)" + [ "$argument" = "$existing" ] && exit 0 + exit 3 + fi + fi + write_state "$job" starting NONE "$argument" "$now" + if ! "$DAEMON" -S -b -x "$SELF" -- worker "$job" "$argument" >/dev/null 2>&1; then + write_state "$job" failed LAUNCH "$argument" "$now" + rmdir "$lock" 2>/dev/null || : + exit 1 + fi +} +worker_cleanup() { + if [ "${finished:-0}" -ne 1 ]; then + write_state "$job" failed E70 "$argument" "$started" || : + fi + rm -f "$lock/pid" + rmdir "$lock" 2>/dev/null || : +} +worker() { + job="$1"; argument="$2" + lock="$(lock_path "$job")" + started="$(read_field "$(state_path "$job")" started)" + case "$started" in ''|*[!0-9]*) started="$(date +%s)";; esac + finished=0 + trap 'worker_cleanup' EXIT + trap 'exit 70' HUP INT TERM + printf 'pid\t%s\n' "$$" > "$lock/pid" + write_state "$job" running NONE "$argument" "$started" + rc=0 + case "$job" in + catalog) "$CATALOG" || rc=$?;; + status) "$MANAGER" status || rc=$?;; + sizes) "$MANAGER" sizes || rc=$?;; + device-sync) "$DEVICE" status-sync || rc=$?;; + pairing) "$DEVICE" pairing-session || rc=$?;; + recommended-sync) "$DEVICE" recommended-sync || rc=$?;; + recommended-revoke) "$DEVICE" recommended-revoke "$argument" || rc=$?;; + esac + if [ "$rc" -eq 0 ]; then + write_state "$job" success NONE "$argument" "$started" + else + write_state "$job" failed "E$rc" "$argument" "$started" + fi + finished=1 +} +[ "$#" -ge 2 ] && [ "$#" -le 3 ] || exit 2 +mode="$1"; job="$2"; argument="${3:-}" +valid_job "$job" || exit 2 +if [ "$job" = recommended-revoke ]; then valid_arg "$argument" || exit 2 +else [ -z "$argument" ] || exit 2; fi +mkdir -p "$RUNTIME" || exit 1 +case "$mode" in + ensure) ensure "$job" auto "$argument";; + request) ensure "$job" explicit "$argument";; + worker) worker "$job" "$argument";; + *) exit 2;; +esac diff --git a/luci-app-smartsafehub/root/usr/share/rpcd/acl.d/luci-app-smartsafehub.json b/luci-app-smartsafehub/root/usr/share/rpcd/acl.d/luci-app-smartsafehub.json index 0ffbd9e82..3ddf19f46 100644 --- a/luci-app-smartsafehub/root/usr/share/rpcd/acl.d/luci-app-smartsafehub.json +++ b/luci-app-smartsafehub/root/usr/share/rpcd/acl.d/luci-app-smartsafehub.json @@ -10,6 +10,9 @@ "wifi_summary", "updates_status", "firmware_status", + "apps_catalog", + "apps_status", + "apps_metrics", "system_time_settings", "system_scheduled_reboot_settings", "health_status", @@ -50,6 +53,9 @@ "system_reboot", "updates_check", "updates_install", + "apps_install", + "apps_update", + "apps_remove", "updates_settings_update", "firmware_check", "firmware_prepare", diff --git a/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub.uc b/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub.uc index 3d7e0e96d..4b2b5b26e 100644 --- a/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub.uc +++ b/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub.uc @@ -2,6 +2,8 @@ 'use strict'; import { failure } from './smartsafehub/core.uc'; +import { read_apps_catalog } from './smartsafehub/apps.uc'; +import { read_apps_status, read_apps_metrics, install_app, update_app, remove_app } from './smartsafehub/apps-manager.uc'; import { read_connected_devices } from './smartsafehub/devices.uc'; import { discard_uploaded_backup, @@ -173,6 +175,42 @@ const methods = { return update_update_settings(request); }), }, + apps_catalog: { + args: { refresh: false }, + call: require_root_password(function(request) { + return read_apps_catalog(request); + }), + }, + apps_status: { + args: { refresh: false }, + call: require_root_password(function(request) { + return read_apps_status(request); + }), + }, + apps_metrics: { + args: { refresh: false }, + call: require_root_password(function(request) { + return read_apps_metrics(request); + }), + }, + apps_install: { + args: { app_id: '' }, + call: require_root_password(function(request) { + return install_app(request); + }), + }, + apps_update: { + args: { app_id: '' }, + call: require_root_password(function(request) { + return update_app(request); + }), + }, + apps_remove: { + args: { app_id: '' }, + call: require_root_password(function(request) { + return remove_app(request); + }), + }, firmware_status: { call: require_root_password(function(request) { return read_firmware_status(request); diff --git a/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/apps-manager.uc b/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/apps-manager.uc new file mode 100644 index 000000000..e990d224e --- /dev/null +++ b/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/apps-manager.uc @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +'use strict'; + +import * as fs from 'fs'; +import { failure, run_command, safe_call, success } from './core.uc'; +import { from_system_root, from_statvfs } from './apps-storage.uc'; +import { ensure_job, job_state, request_job } from './rpc-jobs.uc'; + +const MANAGER = '/usr/libexec/smartsafehub-apps-manager'; +const STATE = '/tmp/smartsafehub/apps-operation.state'; +const STATUS = '/tmp/smartsafehub/apps-status.tsv'; +const SIZES = '/tmp/smartsafehub/apps-size.tsv'; +const RESTORE_STATE = '/tmp/smartsafehub/apps-restore.state'; + +// OpenWrt procd already reports the writable root filesystem in system.info. +// Those counters are KiB, unlike ucode fs.statvfs() which returns blocks. +// Prefer the existing ubus connection, then fall back to statvfs without +// spawning df or allowing optional metrics to affect core Apps status. +function writable_storage() { + const info = safe_call('system', 'info', {}); + const root_storage = from_system_root(info?.root); + if (root_storage != null) + return root_storage; + + // Do not assume /overlay is present (e.g. ext4 or extroot installs). + // Both paths are fixed trusted paths. Each call is inexpensive. + for (let mount in [ '/overlay', '/' ]) { + let stats = null; + try { + stats = fs.statvfs(mount); + } + catch (e) { + continue; + } + const storage = from_statvfs(stats, mount); + if (storage != null) + return storage; + } + return null; +} + +function job() { + const state = { phase: 'idle', action: '', appId: '', code: '', operationId: '' }; + const raw = fs.readfile(STATE); + if (type(raw) != 'string' || length(raw) > 4096) return state; + for (let line in split(type(raw) == 'string' ? raw : '', /\r?\n/)) { + const fields = split(line, /\t/); + if (length(fields) != 2) continue; + const key = fields[0]; + const value = fields[1]; + if (key == 'phase' && match(value, /^(idle|starting|running|success|failed)$/) != null) + state.phase = value; + else if (key == 'action' && (value == 'install' || value == 'update' || value == 'remove')) + state.action = value; + else if (key == 'app_id' && match(value, /^[a-z0-9][a-z0-9-]{0,79}$/) != null) + state.appId = value; + else if (key == 'code' && match(value, /^(NONE|LAUNCH|E[0-9]{1,3})$/) != null) + state.code = value; + else if (key == 'operation_id' && match(value, /^[0-9]{1,12}-[0-9]{1,12}(-[0-9a-f]{16})?$/) != null) + state.operationId = value; + } + return state; +} + +function restore_status() { + const result = { phase: 'idle', appId: '', code: '' }; + const raw = fs.readfile(RESTORE_STATE); + if (type(raw) != 'string' || length(raw) > 4096) return result; + for (let line in split(raw, /\r?\n/)) { + const fields = split(line, /\t/); + if (length(fields) != 2) continue; + if (fields[0] == 'phase' && match(fields[1], /^(idle|starting|running|success|failed)$/) != null) + result.phase = fields[1]; + else if (fields[0] == 'app_id' && (fields[1] == '' || match(fields[1], /^[a-z0-9][a-z0-9-]{0,79}$/) != null)) + result.appId = fields[1]; + else if (fields[0] == 'code' && match(fields[1], /^(NONE|BUSY|TIMEOUT|INCOMPLETE|INVALID_MANIFEST|UNKNOWN|E[0-9]{1,3})$/) != null) + result.code = fields[1]; + } + return result; +} + +export function read_apps_status(request) { + if (request?.args?.refresh == true) request_job('status'); + else ensure_job('status'); + const scan = job_state('status'); + const raw = fs.readfile(STATUS); + if (type(raw) != 'string' || length(raw) > 16384) + return failure('APPS_STATUS_INVALID', '설치 상태 데이터가 올바르지 않습니다.'); + const installed = {}; + for (let line in split(raw, /\r?\n/)) { + const fields = split(line, /\t/); + if ((length(fields) != 4 && length(fields) != 5 && length(fields) != 6 && length(fields) != 7 && length(fields) != 8) || + match(fields[0], /^[a-z0-9][a-z0-9-]{0,79}$/) == null) + continue; + installed[fields[0]] = { + installed: fields[1] == '1', + partial: fields[2] == '1', + managed: fields[3] == '1', + // Only the local manifest scanner can mark a removed approval as retired. + retired: length(fields) == 8 && fields[7] == '1', + webUiReady: length(fields) >= 5 && fields[4] == '1', + updateAvailable: length(fields) >= 6 && fields[5] == '1', + // This is a representative installed APK version, not the Hub's + // approved app/catalog version. Never guess when missing. + installedVersion: length(fields) >= 7 && fields[1] == '1' && + match(fields[6], /^[a-zA-Z0-9_][a-zA-Z0-9._+~-]{0,99}$/) != null + ? fields[6] : null, + }; + } + return success({ job: job(), restore: restore_status(), installed: installed, + ready: type(raw) == 'string', + refreshing: scan.phase == 'starting' || scan.phase == 'running', + refreshFailed: scan.phase == 'failed' }); +}; + +// Optional data lives behind an independent RPC. A slow APK database lookup +// or filesystem error must never prevent installation/removal status checks. +export function read_apps_metrics(request) { + let storage = null; + try { + storage = writable_storage(); + } + catch (e) { + // Older/broken filesystem modules cannot disable core Apps operations. + warn('smartsafehub: Apps storage statistics unavailable\n'); + } + + const installedSizes = {}; + // Disk accounting is optional and runs outside rpcd. Parse only the + // last atomically committed snapshot; it may be empty on first load. + if (request?.args?.refresh == true) request_job('sizes'); + else ensure_job('sizes'); + const sizesScan = job_state('sizes'); + const raw = fs.readfile(SIZES); + if (type(raw) == 'string' && length(raw) <= 8192) { + for (let line in split(raw, /\r?\n/)) { + const fields = split(line, /\t/); + if (length(fields) != 2 || + match(fields[0], /^[a-z0-9][a-z0-9-]{0,79}$/) == null || + match(fields[1], /^(0|[1-9][0-9]{0,12})$/) == null) + continue; + installedSizes[fields[0]] = int(fields[1]); + } + } + return success({ storage: storage, installedSizes: installedSizes, + ready: type(raw) == 'string', + refreshing: sizesScan.phase == 'starting' || sizesScan.phase == 'running' }); +}; + +function start_apps_operation(request, action) { + // rpcd passes declared method parameters in request.args, not request itself. + const app_id = request?.args?.app_id; + if (type(app_id) != 'string' || match(app_id, /^[a-z0-9][a-z0-9-]{0,79}$/) == null) + return failure('APPS_INVALID_ID', '올바르지 않은 앱입니다.'); + if (!run_command([ MANAGER, 'start', action, app_id ], 5000)) + return failure('APPS_OPERATION_REJECTED', '다른 작업이 진행 중이거나 요청을 시작하지 못했습니다.'); + return success({ accepted: true, appId: app_id, action: action }); +} + +export function install_app(request) { return start_apps_operation(request, 'install'); }; +export function update_app(request) { return start_apps_operation(request, 'update'); }; +export function remove_app(request) { return start_apps_operation(request, 'remove'); }; diff --git a/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/apps-storage.uc b/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/apps-storage.uc new file mode 100644 index 000000000..02539c163 --- /dev/null +++ b/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/apps-storage.uc @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +'use strict'; + +const MAX_BYTES = 9007199254740991; // JavaScript Number.MAX_SAFE_INTEGER + +function valid_counter(value) { + return (type(value) == 'int' || type(value) == 'double') && + value >= 0 && value <= MAX_BYTES; +} + +function storage_bytes(total, available, mount) { + if (!valid_counter(total) || !valid_counter(available) || + total <= 0 || available > total) + return null; + return { mount: mount, totalBytes: total, availableBytes: available }; +} + +// procd system.info.root.total/avail are KiB, NOT bytes. In OpenWrt +// with overlayfs, / is the writable union and its free blocks come +// from the underlying writable overlay filesystem. +export function from_system_root(root) { + if (!root || !valid_counter(root.total) || !valid_counter(root.avail) || + root.total > MAX_BYTES / 1024 || root.avail > root.total) + return null; + return storage_bytes(root.total * 1024, root.avail * 1024, '/'); +}; + +// Filesystem fallback for firmware without procd root statistics. +// The ucode type() builtin calls integers "int", not "integer". +export function from_statvfs(stats, mount) { + if (!stats || (mount != '/overlay' && mount != '/')) + return null; + const frsize = valid_counter(stats.frsize) && stats.frsize > 0 + ? stats.frsize : stats.bsize; + if (!valid_counter(frsize) || frsize <= 0 || + !valid_counter(stats.blocks) || !valid_counter(stats.bavail) || + stats.blocks <= 0 || stats.bavail > stats.blocks || + stats.blocks > MAX_BYTES / frsize) + return null; + return storage_bytes(stats.blocks * frsize, stats.bavail * frsize, mount); +}; diff --git a/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/apps.uc b/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/apps.uc new file mode 100644 index 000000000..5040035bf --- /dev/null +++ b/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/apps.uc @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +'use strict'; + +import * as fs from 'fs'; +import { failure, success } from './core.uc'; +import { ensure_job, job_state, request_job } from './rpc-jobs.uc'; + +const CATALOG_FILE = '/tmp/smartsafehub/apps-catalog.json'; + +function text(value, max_length) { + return type(value) == 'string' && length(value) <= max_length ? value : ''; +} + +// Only open LuCI pages served by this router. Reject query strings, fragments, +// percent escapes, scheme-relative URLs and path traversal from catalog data. +function luci_web_ui_path(value) { + if (!text(value, 160) || + match(value, /^\/cgi-bin\/luci\/admin\/[a-z0-9_-]+(\/[a-z0-9_-]+)*$/) == null) + return ''; + return value; +} + +// This endpoint is *read-only*. The catalog never supplies executable +// commands, repository URLs or permissions to a package installer. The +// separate privileged installer must revalidate every approval in the future. +export function read_apps_catalog(request) { + // Never perform an HTTP request on rpcd's event loop. Serve a last-known + // display-only snapshot while the detached worker updates it atomically. + if (request?.args?.refresh == true) request_job('catalog'); + else ensure_job('catalog'); + const operation = job_state('catalog'); + const raw = fs.readfile(CATALOG_FILE); + const pending = operation.phase == 'starting' || operation.phase == 'running'; + if (type(raw) != 'string' || length(raw) < 2) { + return success({ schema: 1, deviceCode: '', firmwareBuildId: '', channel: '', apps: [], + ready: false, refreshing: pending, refreshFailed: operation.phase == 'failed' }); + } + if (length(raw) > 131072) { + return failure('APPS_CATALOG_INVALID', '앱 목록 데이터가 올바르지 않습니다.'); + } + + let data; + try { + data = json(raw); + } + catch (e) { + return failure('APPS_CATALOG_INVALID', '앱 목록을 읽지 못했습니다.'); + } + if (type(data) != 'object' || data?.schema != 1 || type(data?.apps) != 'array' || + length(data.apps) > 100 || !text(data?.device_code, 80) || + !text(data?.firmware_build_id, 80)) { + return failure('APPS_CATALOG_INVALID', '앱 목록 형식이 올바르지 않습니다.'); + } + + // A firmware change invalidates the old catalog before any cards are shown. + // The ROM metadata takes precedence over writable overlay metadata. + let metadataRaw = fs.readfile('/rom/usr/share/smartsafehub/firmware.json'); + if (type(metadataRaw) != 'string' || !length(metadataRaw)) + metadataRaw = fs.readfile('/usr/share/smartsafehub/firmware.json'); + if (type(metadataRaw) == 'string' && length(metadataRaw)) { + let metadata = null; + try { metadata = json(metadataRaw); } catch (e) { /* reject stale catalog below */ } + if (metadata == null || metadata?.device_code != data.device_code || + metadata?.build_id != data.firmware_build_id || + (metadata?.channel ?? 'stable') != (data?.channel ?? 'stable')) + return success({ schema: 1, deviceCode: '', firmwareBuildId: '', channel: '', apps: [], + ready: false, refreshing: pending, refreshFailed: operation.phase == 'failed' }); + } + const apps = []; + for (let item in data.apps) { + if (type(item) != 'object' || item.runtime != 'router' || item.installer != 'apk') { + continue; + } + const id = text(item?.id, 80); + const name = text(item?.name, 120); + if (!length(id) || !length(name) || match(id, /^[a-z0-9][a-z0-9-]{0,79}$/) == null) { + continue; + } + // Backward-compatible route for the already approved nlbwmon catalog. + // The page is only launchable if its UI package is actually installed. + let path = ''; + if (id == 'wake-on-lan') { + if (item?.web_ui?.type == 'luci' && + item?.web_ui?.path == '/cgi-bin/luci/admin/services/wol' && + item?.web_ui?.package == 'luci-app-wol') + path = '/cgi-bin/luci/admin/services/wol'; + } + else if (item?.web_ui?.type == 'luci') + path = luci_web_ui_path(item.web_ui.path); + else if (id == 'retro-arcade' && item?.web_ui?.type == 'static' && + item?.web_ui?.path == '/retro-arcade/' && + item?.web_ui?.package == 'smartsafehub-app-retro-arcade') + path = '/retro-arcade/'; + else if (id == 'network-usage-monitor') + path = '/cgi-bin/luci/admin/services/nlbw/display'; + // Only UI copy crosses into the unprivileged browser. All package hashes, + // permissions and install descriptors remain server-side until an installer + // with independent verification is implemented. + push(apps, { + id: id, + name: name, + summary: text(item?.summary, 320), + description: text(item?.description, 2000), + category: text(item?.category, 64), + version: text(item?.version, 64), + webUi: length(path) ? { type: id == 'retro-arcade' && path == '/retro-arcade/' ? 'static' : 'luci', path: path } : null, + }); + } + return success({ + schema: 1, + deviceCode: data.device_code, + firmwareBuildId: data.firmware_build_id, + channel: text(data?.channel, 16), + apps: apps, + ready: true, + refreshing: pending, + refreshFailed: operation.phase == 'failed', + }); +}; diff --git a/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/device-registration.uc b/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/device-registration.uc index c1a013db3..6e0a4fce2 100644 --- a/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/device-registration.uc +++ b/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/device-registration.uc @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later 'use strict'; import * as fs from 'fs'; -import { failure, run_command, success } from './core.uc'; +import { failure, success } from './core.uc'; +import { ensure_job, job_state, request_job } from './rpc-jobs.uc'; const STATE_FILE = '/tmp/smartsafehub/device.json'; const HELPER = '/usr/libexec/smartsafehub-device'; function read_state() { @@ -48,7 +49,11 @@ export function read_recommended_rules_status(request) { // A busy SafeShield refresh is pending work, not an application failure. // Keep an older verified receipt for its revision without treating a newer // in-progress operation as already complete. - const syncStatus = sync?.status == 'pending' ? 'pending' : + const syncOperation = job_state('recommended-sync'); + const revokeOperation = job_state('recommended-revoke'); + const operationRunning = syncOperation.phase == 'starting' || syncOperation.phase == 'running' || + revokeOperation.phase == 'starting' || revokeOperation.phase == 'running'; + const syncStatus = operationRunning || sync?.status == 'pending' ? 'pending' : confirmed ? 'confirmed' : sync?.status == 'delayed' ? 'delayed' : sync?.status == 'failed' ? 'failed' : 'pending'; const syncWarning = confirmed && (sync?.status == 'failed' || sync?.status == 'delayed'); @@ -60,32 +65,45 @@ export function read_recommended_rules_status(request) { confirmedDomains: confirmedDomains, syncStatus: syncStatus, syncWarning: syncWarning, - syncStage: sync?.stage ?? null, + syncStage: operationRunning ? 'server_sync' : (sync?.stage ?? null), syncReason: sync?.reason ?? null, lastCheckedAt: sync?.checkedAt ?? 0, lastDeviceSyncAt: device?.lastSuccessAt ?? 0, nextDeviceSyncAt: device?.nextSyncAt ?? 0, - accountRegistered: device?.accountRegistered ?? false + accountRegistered: device?.accountRegistered ?? false, + operation: syncOperation, + revokeOperation: revokeOperation }); }; +// A request means "accepted", never "the remote rule has been applied". +// The local receipt and operation phase remain separate authoritative fields. export function refresh_recommended_rules_status(request) { - // The blocklist download and the Hub device sync are different operations. - // Reuse the authenticated device sync, never the SafeShield blocklist refresh. - if (!run_command([ HELPER, 'recommended-sync' ], 25000)) { - const latest = read_json_file(RECOMMENDED_SYNC_STATUS); - if (latest?.status != 'pending' && latest?.status != 'delayed') - return failure('RECOMMENDED_SYNC_UNAVAILABLE', 'SmartSafeHub 서버와 동기화하지 못했습니다. 잠시 후 다시 시도해 주세요.'); - } + if (!request_job('recommended-sync')) + return failure('RECOMMENDED_SYNC_BUSY', '이미 다른 추천 규칙 동기화 작업이 진행 중입니다.'); return read_recommended_rules_status(request); }; export function revoke_recommended_rule(request) { const rule_id = request?.args?.rule_id; if (rule_id == null || match(sprintf('%s', rule_id), /^[1-9][0-9]{0,9}$/) == null) return failure('INVALID_RULE_ID', '적용 해제할 추천 규칙을 선택해 주세요.'); - if (!run_command([ HELPER, 'recommended-revoke', sprintf('%s', rule_id) ], 25000)) - return failure('RECOMMENDED_REVOKE_FAILED', '규칙 해제 또는 SafeShield 반영에 실패했습니다. 서버 변경은 다음 동기화에서 반영될 수 있습니다.'); + if (!request_job('recommended-revoke', rule_id)) + return failure('RECOMMENDED_REVOKE_BUSY', '이미 추천 규칙 해제 작업이 진행 중입니다.'); return read_recommended_rules_status(request); }; -export function read_device_registration_status(request) { const state = read_state(); return state != null ? success(state) : failure('DEVICE_STATE_INVALID', '기기 등록 상태를 읽지 못했습니다.'); }; -export function refresh_device_registration_status(request) { if (!run_command([ HELPER, 'status-sync' ], 25000)) return failure('DEVICE_SYNC_FAILED', 'SmartSafeHub 계정 연결 상태를 확인하지 못했습니다.'); const state = read_state(); return state != null ? success(state) : failure('DEVICE_STATE_INVALID', '기기 등록 상태를 읽지 못했습니다.'); }; -export function refresh_device_pairing(request) { if (!run_command([ HELPER, 'pairing-session' ], 15000)) return failure('DEVICE_PAIRING_FAILED', 'SmartSafeHub 계정 연결 코드를 발급하지 못했습니다.'); const state = read_state(); return state != null ? success(state) : failure('DEVICE_STATE_INVALID', '기기 등록 상태를 읽지 못했습니다.'); }; +export function read_device_registration_status(request) { + const state = read_state(); + if (state == null) return failure('DEVICE_STATE_INVALID', '기기 등록 상태를 읽지 못했습니다.'); + state.operation = job_state('device-sync'); + state.pairingOperation = job_state('pairing'); + return success(state); +}; +export function refresh_device_registration_status(request) { + if (!ensure_job('device-sync')) + return failure('DEVICE_SYNC_LAUNCH_FAILED', '서버 상태 확인을 시작하지 못했습니다.'); + return read_device_registration_status(request); +}; +export function refresh_device_pairing(request) { + if (!request_job('pairing')) + return failure('DEVICE_PAIRING_BUSY', '연결 코드 발급을 시작하지 못했습니다. 잠시 후 다시 시도해 주세요.'); + return read_device_registration_status(request); +}; diff --git a/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/rpc-jobs.uc b/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/rpc-jobs.uc new file mode 100644 index 000000000..0ecc5985c --- /dev/null +++ b/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/rpc-jobs.uc @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +'use strict'; + +import * as fs from 'fs'; +import { run_command } from './core.uc'; + +const HELPER = '/usr/libexec/smartsafehub-rpc-jobs'; +const RUNTIME = '/tmp/smartsafehub/rpc-jobs/'; + +// Only fixed job names reach the privileged launcher. Never accept arbitrary +// executable paths, shell fragments or network locations from the browser. +export function job_state(name) { + const state = { phase: 'idle', code: '', argument: '', updatedAt: 0 }; + const raw = fs.readfile(RUNTIME + name + '.state'); + if (type(raw) != 'string' || length(raw) > 1024) return state; + for (let line in split(raw, /\r?\n/)) { + const fields = split(line, /\t/); + if (length(fields) != 2) continue; + if (fields[0] == 'phase' && match(fields[1], /^(starting|running|success|failed)$/) != null) + state.phase = fields[1]; + else if (fields[0] == 'code' && match(fields[1], /^(NONE|LAUNCH|E[0-9]{1,3})$/) != null) + state.code = fields[1]; + else if (fields[0] == 'argument' && match(fields[1], /^[0-9]{0,10}$/) != null) + state.argument = fields[1]; + else if (fields[0] == 'updated' && match(fields[1], /^[0-9]{1,12}$/) != null) + state.updatedAt = int(fields[1]); + } + return state; +}; + +export function ensure_job(name) { + return run_command([ HELPER, 'ensure', name ], 1500); +}; + +export function request_job(name, argument) { + const command = [ HELPER, 'request', name ]; + if (argument != null) push(command, sprintf('%s', argument)); + return run_command(command, 1500); +}; diff --git a/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/system.uc b/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/system.uc index 58936f14b..a6647d7da 100644 --- a/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/system.uc +++ b/luci-app-smartsafehub/root/usr/share/rpcd/ucode/smartsafehub/system.uc @@ -4,6 +4,7 @@ import * as fs from 'fs'; import { read_activity_history } from './activity.uc'; +import { from_system_root, from_statvfs } from './apps-storage.uc'; import { defer_call, @@ -563,6 +564,27 @@ export function reboot_system(request) { }); }; +// The dashboard already has system.info; some firmware images omit its root +// counters. Reuse Apps' statvfs fallback instead of displaying an empty donut. +// No extra ubus request or spawned process is required. +function dashboard_writable_storage(root) { + const from_root = from_system_root(root); + if (from_root != null) + return from_root; + + for (let mount in [ '/overlay', '/' ]) { + try { + const storage = from_statvfs(fs.statvfs(mount), mount); + if (storage != null) + return storage; + } + catch (e) { + // Try the next known writable root; failure is optional. + } + } + return null; +} + function system_status_payload(board, info, wan) { const release = board?.release ?? {}; const memory = info?.memory ?? {}; @@ -585,6 +607,8 @@ function system_status_payload(board, info, wan) { runtime: { uptime: number_value(info?.uptime), localtime: number_value(info?.localtime), + // Reuse the same writable root counters as Apps; no extra RPC or APK scan. + storage: dashboard_writable_storage(info?.root), load: [ number_value(load?.[0]), number_value(load?.[1]), diff --git a/luci-app-smartsafehub/root/usr/share/ucode/luci/template/smartsafehub/login.ut b/luci-app-smartsafehub/root/usr/share/ucode/luci/template/smartsafehub/login.ut index c479c13d1..0cc2ba22e 100644 --- a/luci-app-smartsafehub/root/usr/share/ucode/luci/template/smartsafehub/login.ut +++ b/luci-app-smartsafehub/root/usr/share/ucode/luci/template/smartsafehub/login.ut @@ -10,16 +10,16 @@ SmartSafeHub - +
- + 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 33ff16ce1..4958fa0ef 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-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-outline-style:solid;--tw-ease:initial}::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-outline-style:solid;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:#fef2f2;--color-red-100:#ffe2e2;--color-red-200:#ffcaca;--color-red-300:#ffa3a3;--color-red-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-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)}.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)}.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-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-4{column-gap:calc(var(--spacing) * 4)}.gap-x-5{column-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-700{border-color:var(--color-red-700)}.border-rose-200{border-color:var(--color-rose-200)}.border-rose-300{border-color:var(--color-rose-300)}.border-sky-100{border-color:var(--color-sky-100)}.border-sky-200{border-color:var(--color-sky-200)}.border-slate-100{border-color:var(--color-slate-100)}.border-slate-200{border-color:var(--color-slate-200)}.border-slate-300{border-color:var(--color-slate-300)}.border-teal-200{border-color:var(--color-teal-200)}.border-teal-300{border-color:var(--color-teal-300)}.border-teal-600{border-color:var(--color-teal-600)}.border-teal-700{border-color:var(--color-teal-700)}.border-white\/80{border-color:#fffc}@supports (color:color-mix(in lab, red, red)){.border-white\/80{border-color:color-mix(in oklab, var(--color-white) 80%, transparent)}}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-50\/70{background-color:#fffbebb3}@supports (color:color-mix(in lab, red, red)){.bg-amber-50\/70{background-color:color-mix(in oklab, var(--color-amber-50) 70%, transparent)}}.bg-amber-100{background-color:var(--color-amber-100)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-amber-600{background-color:var(--color-amber-600)}.bg-amber-700{background-color:var(--color-amber-700)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-emerald-50\/70{background-color:#ecfdf5b3}@supports (color:color-mix(in lab, red, red)){.bg-emerald-50\/70{background-color:color-mix(in oklab, var(--color-emerald-50) 70%, transparent)}}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-emerald-700{background-color:var(--color-emerald-700)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-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)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-10{padding-block:calc(var(--spacing) * 10)}.pt-1{padding-top:var(--spacing)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pr-12{padding-right:calc(var(--spacing) * 12)}.pb-1{padding-bottom:var(--spacing)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.pb-8{padding-bottom:calc(var(--spacing) * 8)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-9{padding-left:calc(var(--spacing) * 9)}.pl-11{padding-left:calc(var(--spacing) * 11)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.7rem\]{font-size:.7rem}.text-\[0\.65rem\]{font-size:.65rem}.text-\[0\.68rem\]{font-size:.68rem}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-6{--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6)}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.08em\]{--tw-tracking:.08em;letter-spacing:.08em}.tracking-\[0\.12em\]{--tw-tracking:.12em;letter-spacing:.12em}.tracking-\[0\.14em\]{--tw-tracking:.14em;letter-spacing:.14em}.tracking-\[0\.16em\]{--tw-tracking:.16em;letter-spacing:.16em}.tracking-\[0\.18em\]{--tw-tracking:.18em;letter-spacing:.18em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-amber-900{color:var(--color-amber-900)}.text-amber-900\/80{color:#7b3306cc}@supports (color:color-mix(in lab, red, red)){.text-amber-900\/80{color:color-mix(in oklab, var(--color-amber-900) 80%, transparent)}}.text-amber-950{color:var(--color-amber-950)}.text-blue-700{color:var(--color-blue-700)}.text-current{color:currentColor}.text-emerald-700{color:var(--color-emerald-700)}.text-emerald-800{color:var(--color-emerald-800)}.text-emerald-900{color:var(--color-emerald-900)}.text-emerald-950{color:var(--color-emerald-950)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-red-900{color:var(--color-red-900)}.text-red-950{color:var(--color-red-950)}.text-rose-600{color:var(--color-rose-600)}.text-rose-700{color:var(--color-rose-700)}.text-rose-800{color:var(--color-rose-800)}.text-rose-900{color:var(--color-rose-900)}.text-rose-950{color:var(--color-rose-950)}.text-sky-700{color:var(--color-sky-700)}.text-sky-800{color:var(--color-sky-800)}.text-sky-950{color:var(--color-sky-950)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-slate-700{color:var(--color-slate-700)}.text-slate-800{color:var(--color-slate-800)}.text-slate-900{color:var(--color-slate-900)}.text-slate-950{color:var(--color-slate-950)}.text-teal-300{color:var(--color-teal-300)}.text-teal-600{color:var(--color-teal-600)}.text-teal-700{color:var(--color-teal-700)}.text-teal-800{color:var(--color-teal-800)}.text-teal-950{color:var(--color-teal-950)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.accent-teal-700{accent-color:var(--color-teal-700)}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-slate-900\/5{--tw-shadow-color:#0f172b0d}@supports (color:color-mix(in lab, red, red)){.shadow-slate-900\/5{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-slate-900) 5%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-slate-950\/10{--tw-shadow-color:#0206181a}@supports (color:color-mix(in lab, red, red)){.shadow-slate-950\/10{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-slate-950) 10%, transparent) var(--tw-shadow-alpha), transparent)}}.ring-amber-200{--tw-ring-color:var(--color-amber-200)}.ring-blue-200{--tw-ring-color:var(--color-blue-200)}.ring-emerald-200{--tw-ring-color:var(--color-emerald-200)}.ring-red-200{--tw-ring-color:var(--color-red-200)}.ring-rose-200{--tw-ring-color:var(--color-rose-200)}.ring-sky-100{--tw-ring-color:var(--color-sky-100)}.ring-sky-200{--tw-ring-color:var(--color-sky-200)}.ring-slate-100{--tw-ring-color:var(--color-slate-100)}.ring-slate-200{--tw-ring-color:var(--color-slate-200)}.ring-teal-100{--tw-ring-color:var(--color-teal-100)}.ring-teal-200{--tw-ring-color:var(--color-teal-200)}.ring-white{--tw-ring-color:var(--color-white)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[left\]{transition-property:left;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.outline-none{--tw-outline-style:none;outline-style:none}.select-all{-webkit-user-select:all;user-select:all}.ring-inset{--tw-ring-inset:inset}.group-open\:hidden:-webkit-any(:where(.group):-webkit-any([open],:popover-open,:open) *){display:none}.group-open\:hidden:is(:where(.group):is([open],:popover-open,:open) *){display:none}.group-open\:inline-block:-webkit-any(:where(.group):-webkit-any([open],:popover-open,:open) *){display:inline-block}.group-open\:inline-block:is(:where(.group):is([open],:popover-open,:open) *){display:inline-block}.group-open\:inline-flex:-webkit-any(:where(.group):-webkit-any([open],:popover-open,:open) *){display:inline-flex}.group-open\:inline-flex:is(:where(.group):is([open],:popover-open,:open) *){display:inline-flex}.placeholder\:text-slate-400::placeholder{color:var(--color-slate-400)}.first\:mt-0:first-child{margin-top:0}.first\:pt-0:first-child{padding-top:0}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.last\:pb-0:last-child{padding-bottom:0}@media (hover:hover){.hover\:border-emerald-800:hover{border-color:var(--color-emerald-800)}.hover\:border-red-200:hover{border-color:var(--color-red-200)}.hover\:border-red-300:hover{border-color:var(--color-red-300)}.hover\:border-red-800:hover{border-color:var(--color-red-800)}.hover\:border-slate-300:hover{border-color:var(--color-slate-300)}.hover\:border-teal-200:hover{border-color:var(--color-teal-200)}.hover\:border-teal-300:hover{border-color:var(--color-teal-300)}.hover\:border-teal-400:hover{border-color:var(--color-teal-400)}.hover\:border-teal-800:hover{border-color:var(--color-teal-800)}.hover\:bg-amber-50:hover{background-color:var(--color-amber-50)}.hover\:bg-amber-100:hover{background-color:var(--color-amber-100)}.hover\:bg-amber-700:hover{background-color:var(--color-amber-700)}.hover\:bg-amber-800:hover{background-color:var(--color-amber-800)}.hover\:bg-black\/5:hover{background-color:#0000000d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-black\/5:hover{background-color:color-mix(in oklab, var(--color-black) 5%, transparent)}}.hover\:bg-emerald-800:hover{background-color:var(--color-emerald-800)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:bg-red-800:hover{background-color:var(--color-red-800)}.hover\:bg-rose-50:hover{background-color:var(--color-rose-50)}.hover\:bg-rose-100:hover{background-color:var(--color-rose-100)}.hover\:bg-rose-800:hover{background-color:var(--color-rose-800)}.hover\:bg-sky-800:hover{background-color:var(--color-sky-800)}.hover\:bg-slate-50:hover{background-color:var(--color-slate-50)}.hover\:bg-slate-100:hover{background-color:var(--color-slate-100)}.hover\:bg-slate-800:hover{background-color:var(--color-slate-800)}.hover\:bg-teal-50:hover{background-color:var(--color-teal-50)}.hover\:bg-teal-100:hover{background-color:var(--color-teal-100)}.hover\:bg-teal-800:hover{background-color:var(--color-teal-800)}.hover\:text-red-700:hover{color:var(--color-red-700)}.hover\:text-rose-800:hover{color:var(--color-rose-800)}.hover\:text-slate-800:hover{color:var(--color-slate-800)}.hover\:text-slate-950:hover{color:var(--color-slate-950)}.hover\:text-teal-700:hover{color:var(--color-teal-700)}.hover\:text-teal-800:hover{color:var(--color-teal-800)}.hover\:text-teal-900:hover{color:var(--color-teal-900)}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:border-teal-500:focus{border-color:var(--color-teal-500)}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-4:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-teal-100:focus{--tw-ring-color:var(--color-teal-100)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\: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-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-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\: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-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-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-950]{color:#5eead4}.ssh-app[data-theme=dark] [class~=border-teal-200],.ssh-app[data-theme=dark] [class~=border-teal-300]{border-color:#115e59}.ssh-app[data-theme=dark] [class~=ring-teal-100],.ssh-app[data-theme=dark] [class~=ring-teal-200]{--tw-ring-color:#134e4a}.ssh-app[data-theme=dark] [class~=bg-sky-50],.ssh-app[data-theme=dark] [class~=bg-sky-50\/60]{background-color:#0ea5e91f}.ssh-app[data-theme=dark] [class~=text-sky-700],.ssh-app[data-theme=dark] [class~=text-sky-800],.ssh-app[data-theme=dark] [class~=text-sky-950]{color:#7dd3fc}.ssh-app[data-theme=dark] [class~=border-sky-200]{border-color:#075985}.ssh-app[data-theme=dark] [class~=ring-sky-100],.ssh-app[data-theme=dark] [class~=ring-sky-200]{--tw-ring-color:#0c4a6e}.ssh-app[data-theme=dark] [class~=bg-emerald-50],.ssh-app[data-theme=dark] [class~=bg-emerald-100]{background-color:#10b98121}.ssh-app[data-theme=dark] [class~=bg-emerald-50\/70]{background-color:#10b9811a}.ssh-app[data-theme=dark] [class~=text-emerald-700],.ssh-app[data-theme=dark] [class~=text-emerald-800],.ssh-app[data-theme=dark] [class~=text-emerald-900],.ssh-app[data-theme=dark] [class~=text-emerald-950]{color:#6ee7b7}.ssh-app[data-theme=dark] [class~=border-emerald-200]{border-color:#065f46}.ssh-app[data-theme=dark] [class~=ring-emerald-100],.ssh-app[data-theme=dark] [class~=ring-emerald-200]{--tw-ring-color:#064e3b}.ssh-app[data-theme=dark] [class~=bg-amber-50],.ssh-app[data-theme=dark] [class~=bg-amber-50\/60],.ssh-app[data-theme=dark] [class~=bg-amber-100],.ssh-app[data-theme=dark] [class~=bg-amber-100\/70]{background-color:#f59e0b21}.ssh-app[data-theme=dark] [class~=bg-amber-50\/70]{background-color:#f59e0b1a}.ssh-app[data-theme=dark] [class~=text-amber-700],.ssh-app[data-theme=dark] [class~=text-amber-800],.ssh-app[data-theme=dark] [class~=text-amber-900],.ssh-app[data-theme=dark] [class~=text-amber-900\/80],.ssh-app[data-theme=dark] [class~=text-amber-950]{color:#fcd34d}.ssh-app[data-theme=dark] [class~=border-amber-200],.ssh-app[data-theme=dark] [class~=border-amber-300]{border-color:#92400e}.ssh-app[data-theme=dark] [class~=ring-amber-100],.ssh-app[data-theme=dark] [class~=ring-amber-200]{--tw-ring-color:#78350f}.ssh-app[data-theme=dark] [class~=bg-red-50],.ssh-app[data-theme=dark] [class~=bg-red-100]{background-color:#ef44441f}.ssh-app[data-theme=dark] [class~=text-red-700],.ssh-app[data-theme=dark] [class~=text-red-800],.ssh-app[data-theme=dark] [class~=text-red-900],.ssh-app[data-theme=dark] [class~=text-red-950]{color:#fca5a5}.ssh-app[data-theme=dark] [class~=border-red-200],.ssh-app[data-theme=dark] [class~=border-red-300]{border-color:#991b1b}.ssh-app[data-theme=dark] [class~=ring-red-100],.ssh-app[data-theme=dark] [class~=ring-red-200]{--tw-ring-color:#7f1d1d}.ssh-app[data-theme=dark] [class~=bg-rose-50]{background-color:#f43f5e1f}.ssh-app[data-theme=dark] [class~=bg-rose-50\/70]{background-color:#f43f5e1a}.ssh-app[data-theme=dark] [class~=text-rose-600],.ssh-app[data-theme=dark] [class~=text-rose-700],.ssh-app[data-theme=dark] [class~=text-rose-800],.ssh-app[data-theme=dark] [class~=text-rose-900],.ssh-app[data-theme=dark] [class~=text-rose-950]{color:#fda4af}.ssh-app[data-theme=dark] [class~=border-rose-200],.ssh-app[data-theme=dark] [class~=border-rose-300]{border-color:#9f1239}.ssh-app[data-theme=dark] [class~=ring-rose-200]{--tw-ring-color:#881337}.ssh-app[data-theme=dark] [class~=bg-blue-50]{background-color:#3b82f61f}.ssh-app[data-theme=dark] [class~=text-blue-700]{color:#93c5fd}.ssh-app[data-theme=dark] [class~=ring-blue-200]{--tw-ring-color:#1e3a8a}.ssh-app[data-theme=dark] .hover\:bg-slate-50:hover{background-color:#1e293b}.ssh-app[data-theme=dark] .hover\:bg-rose-50:hover{background-color:#f43f5e2e}.ssh-app[data-theme=dark] .hover\:text-slate-950:hover{color:#f8fafc}.ssh-app[data-theme=dark] .hover\:text-slate-700:hover{color:#e2e8f0}.ssh-app[data-theme=dark] .hover\:text-rose-800:hover{color:#fecdd3}.ssh-app[data-theme=dark] .hover\:border-slate-300:hover{border-color:#475569}.ssh-app[data-theme=dark] .ssh-product-header-action{color:#cbd5e1;background-color:#0f172a;border-color:#334155!important}.ssh-app[data-theme=dark] .ssh-product-header-action:hover{color:#5eead4;border-color:#0f766e!important}.ssh-app[data-theme=dark] .ssh-product-header-action:focus-visible{--tw-ring-color:#134e4a}.ssh-initial-setup-probe{color:#0f172a;background:#f8fafc;place-items:center;min-height:100dvh;padding:1.5rem;display:grid}.ssh-initial-setup-probe-card{text-align:center;background:#fff;border:1px solid #e2e8f0;border-radius:1.25rem;justify-items:center;gap:.7rem;width:min(100%,28rem);padding:2.25rem;display:grid;box-shadow:0 20px 55px #0f172a14}.ssh-initial-setup-probe-card>svg{color:#0f766e;width:2rem;height:2rem}.ssh-initial-setup-probe-card strong{font-size:1rem;font-weight:900}.ssh-initial-setup-probe-card span{color:#64748b;font-size:.84rem;line-height:1.55}.ssh-initial-setup-probe-card button{color:#fff;cursor:pointer;min-height:2.75rem;font:inherit;background:#0f766e;border:0;border-radius:.8rem;margin-top:.4rem;padding:0 1rem;font-size:.84rem;font-weight:900}.smartsafehub-login-shadow-root{background:#f8fafc;min-height:100dvh}.ssh-login-page,.ssh-login-page *,.ssh-login-page :before,.ssh-login-page :after{box-sizing:border-box}.ssh-login-page{--ssh-login-bg:#f8fafc;--ssh-login-surface:#fff;--ssh-login-surface-soft:#f8fafc;--ssh-login-border:#e2e8f0;--ssh-login-border-strong:#cbd5e1;--ssh-login-text:#0f172a;--ssh-login-muted:#64748b;--ssh-login-muted-soft:#94a3b8;--ssh-login-accent:#0f766e;--ssh-login-accent-hover:#115e59;--ssh-login-accent-soft:#f0fdfa;--ssh-login-accent-ring:#14b8a624;--ssh-login-card-shadow:0 24px 70px #0f172a14, 0 2px 10px #0f172a0a;background:var(--ssh-login-bg);min-height:100dvh;color:var(--ssh-login-text);grid-template-columns:minmax(0,1.08fr) minmax(420px,.92fr);display:grid}.ssh-login-page[data-theme=dark]{--ssh-login-bg:#020617;--ssh-login-surface:#0f172a;--ssh-login-surface-soft:#111827;--ssh-login-border:#1e293b;--ssh-login-border-strong:#334155;--ssh-login-text:#f8fafc;--ssh-login-muted:#94a3b8;--ssh-login-muted-soft:#64748b;--ssh-login-accent:#2dd4bf;--ssh-login-accent-hover:#5eead4;--ssh-login-accent-soft:#14b8a61a;--ssh-login-accent-ring:#2dd4bf2e;--ssh-login-card-shadow:0 28px 72px #0000003d, 0 0 0 1px #94a3b80a}.ssh-login-toast{top:max(1rem, env(safe-area-inset-top));z-index:20;color:#115e59;background:#f0fdfa;border:1px solid #99f6e4;border-radius:1rem;align-items:center;gap:.75rem;width:min(100% - 2rem,34rem);padding:.9rem 1rem;font-size:.88rem;font-weight:800;line-height:1.45;display:flex;position:fixed;left:50%;transform:translate(-50%);box-shadow:0 18px 45px #0f172a29}.ssh-login-toast svg{flex:none;width:1.25rem;height:1.25rem}.ssh-login-page[data-theme=dark] .ssh-login-toast{color:#99f6e4;background:#0f2f2e;border-color:#2dd4bf57;box-shadow:0 18px 45px #00000052}.ssh-login-brand{color:#f8fafc;background:radial-gradient(circle at 16% 14%,#2dd4bf29,#0000 28rem),linear-gradient(145deg,#020617 0%,#0f172a 56%,#042f2e 140%);justify-content:center;align-items:center;min-height:100dvh;padding:clamp(3rem,7vw,7rem);display:flex;position:relative;overflow:hidden}.ssh-login-brand:after{content:"";pointer-events:none;background-image:linear-gradient(#94a3b80e 1px,#0000 1px),linear-gradient(90deg,#94a3b80e 1px,#0000 1px);background-size:44px 44px;position:absolute;top:0;bottom:0;left:0;right:0;-webkit-mask-image:linear-gradient(to bottom right,#000,#0000 78%);mask-image:linear-gradient(to bottom right,#000,#0000 78%)}.ssh-login-brand-orb{filter:blur(1px);pointer-events:none;border-radius:9999px;position:absolute}.ssh-login-brand-orb-one{border:1px solid #2dd4bf29;width:30rem;height:30rem;top:-10rem;right:-8rem;box-shadow:inset 0 0 6rem #14b8a60d}.ssh-login-brand-orb-two{border:1px solid #5eead41c;width:36rem;height:36rem;bottom:-13rem;left:-10rem}.ssh-login-brand-content{z-index:1;width:min(100%,680px);position:relative}.ssh-login-brand-mark,.ssh-login-mobile-mark{color:#5eead4;background:#14b8a621;place-items:center;display:grid;box-shadow:inset 0 0 0 1px #5eead42e}.ssh-login-brand-mark{border-radius:1.5rem;width:4.5rem;height:4.5rem;margin-bottom:2rem}.ssh-login-brand-mark svg{width:2.25rem;height:2.25rem}.ssh-login-eyebrow,.ssh-login-kicker{color:#5eead4;letter-spacing:.22em;text-transform:uppercase;margin:0;font-size:.72rem;font-weight:900}.ssh-login-brand h1{letter-spacing:-.055em;margin:.7rem 0 0;font-size:clamp(2.7rem,5vw,5rem);font-weight:900;line-height:.98}.ssh-password-setup-brand-title-line{display:block}.ssh-login-brand-description{color:#cbd5e1;max-width:39rem;margin:1.6rem 0 0;font-size:clamp(1rem,1.5vw,1.16rem);line-height:1.85}.ssh-login-features{gap:.85rem;max-width:40rem;margin-top:3rem;display:grid}.ssh-login-feature{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background:#0f172a75;border:1px solid #94a3b81c;border-radius:1rem;align-items:center;gap:1rem;padding:.95rem 1rem;display:flex}.ssh-login-feature-icon{color:#5eead4;background:#14b8a61c;border-radius:.8rem;flex:none;place-items:center;width:2.65rem;height:2.65rem;display:grid}.ssh-login-feature-icon svg{width:1.25rem;height:1.25rem}.ssh-login-feature-copy{gap:.15rem;min-width:0;display:grid}.ssh-login-feature-copy strong{color:#f8fafc;font-size:.92rem;font-weight:800}.ssh-login-feature-copy>span{color:#94a3b8;font-size:.8rem;line-height:1.45}.ssh-login-panel{background:var(--ssh-login-bg);justify-content:center;align-items:center;min-height:100dvh;padding:clamp(2rem,5vw,5rem);transition:background-color .18s;display:flex;position:relative}.ssh-login-theme-toggle{top:max(1.25rem, env(safe-area-inset-top));right:max(1.25rem, env(safe-area-inset-right));z-index:2;border:1px solid var(--ssh-login-border);background:var(--ssh-login-surface);width:2.75rem;height:2.75rem;color:var(--ssh-login-muted);cursor:pointer;border-radius:.9rem;place-items:center;transition:border-color .15s,background-color .15s,color .15s,transform .15s;display:grid;position:absolute;box-shadow:0 5px 16px #0f172a0f}.ssh-login-theme-toggle svg{width:1.15rem;height:1.15rem}.ssh-login-theme-toggle:hover{border-color:var(--ssh-login-border-strong);color:var(--ssh-login-accent);transform:translateY(-1px)}.ssh-login-panel-inner{flex-direction:column;justify-content:center;width:min(100%,480px);min-height:min(40rem,100dvh - 4rem);display:flex}.ssh-login-card{border:1px solid var(--ssh-login-border);background:var(--ssh-login-surface);width:100%;box-shadow:var(--ssh-login-card-shadow);border-radius:1.5rem;padding:clamp(1.6rem,4vw,2.6rem);transition:background-color .18s,border-color .18s}.ssh-login-card-heading{align-items:flex-start;gap:1rem;display:flex}.ssh-login-card-heading>div{min-width:0}.ssh-login-mobile-mark{color:#fff;background:#0f766e;border-radius:1.1rem;flex:none;width:3.5rem;height:3.5rem;display:none;box-shadow:0 12px 28px #0f766e38}.ssh-login-mobile-mark svg{width:1.8rem;height:1.8rem}.ssh-login-kicker{color:var(--ssh-login-accent);letter-spacing:.18em}.ssh-login-card h2{color:var(--ssh-login-text);letter-spacing:-.035em;margin:.65rem 0 0;font-size:clamp(1.8rem,4vw,2.25rem);font-weight:900;line-height:1.15}.ssh-login-subtitle{color:var(--ssh-login-muted);margin:.7rem 0 0;font-size:.94rem;line-height:1.6}.ssh-login-alert{color:#9f1239;background:#fff1f2;border:1px solid #fecdd3;border-radius:.85rem;margin-top:1.3rem;padding:.85rem 1rem;font-size:.82rem;font-weight:700;line-height:1.55}.ssh-login-page[data-theme=dark] .ssh-login-alert{color:#fecdd3;background:#8813372e;border-color:#f43f5e59}.ssh-login-form{gap:1.1rem;margin-top:1.6rem;display:grid}.ssh-login-field{gap:.5rem;display:grid}.ssh-login-label{color:var(--ssh-login-text);font-size:.82rem;font-weight:800}.ssh-login-input-wrap{border:2px solid var(--ssh-login-border-strong);background:var(--ssh-login-surface-soft);min-height:3.3rem;color:var(--ssh-login-muted);border-radius:.9rem;align-items:center;transition:border-color .15s,background-color .15s,box-shadow .15s;display:flex;overflow:hidden;box-shadow:inset 0 1px 2px #0f172a09}.ssh-login-input-wrap:focus-within{border-color:var(--ssh-login-accent);background:var(--ssh-login-surface);box-shadow:0 0 0 4px var(--ssh-login-accent-ring)}.ssh-login-field-icon{background:var(--ssh-login-accent-soft);width:2.15rem;height:2.15rem;color:var(--ssh-login-accent);border-radius:.65rem;flex:none;place-items:center;margin-left:.55rem;transition:background-color .15s,color .15s,box-shadow .15s;display:grid}.ssh-login-field-icon svg{width:1.05rem;height:1.05rem}.ssh-login-input-wrap:focus-within .ssh-login-field-icon{box-shadow:inset 0 0 0 1px var(--ssh-login-accent-ring)}.ssh-login-input-wrap input{width:100%;min-width:0;height:3.15rem;color:var(--ssh-login-text);font:inherit;background:0 0;border:0;outline:0;padding:0 .8rem;font-size:.94rem;font-weight:700}.ssh-login-input-wrap input::placeholder{color:var(--ssh-login-muted-soft);opacity:.82;font-weight:600}.ssh-login-input-wrap input:-webkit-autofill{-webkit-text-fill-color:var(--ssh-login-text);caret-color:var(--ssh-login-text);box-shadow:0 0 0 1000px var(--ssh-login-surface-soft) inset}.ssh-login-input-wrap input:-webkit-autofill:hover{-webkit-text-fill-color:var(--ssh-login-text);caret-color:var(--ssh-login-text);box-shadow:0 0 0 1000px var(--ssh-login-surface-soft) inset}.ssh-login-input-wrap input:-webkit-autofill:focus{-webkit-text-fill-color:var(--ssh-login-text);caret-color:var(--ssh-login-text);box-shadow:0 0 0 1000px var(--ssh-login-surface-soft) inset}.ssh-login-input-wrap:focus-within input:-webkit-autofill{box-shadow:0 0 0 1000px var(--ssh-login-surface) inset}.ssh-login-input-wrap input:disabled{cursor:wait;opacity:.68}.ssh-login-password-toggle{width:2.35rem;height:2.35rem;color:var(--ssh-login-muted);cursor:pointer;background:0 0;border:1px solid #0000;border-radius:.7rem;flex:none;place-items:center;margin-right:.45rem;transition:border-color .15s,background-color .15s,color .15s;display:grid}.ssh-login-password-toggle svg{width:1.05rem;height:1.05rem}.ssh-login-password-toggle:hover:not(:disabled){border-color:var(--ssh-login-border);background:var(--ssh-login-accent-soft);color:var(--ssh-login-accent)}.ssh-login-password-toggle:disabled{cursor:wait;opacity:.6}.ssh-login-theme-toggle:focus-visible{outline:3px solid var(--ssh-login-accent-ring);outline-offset:2px}.ssh-login-password-toggle:focus-visible{outline:3px solid var(--ssh-login-accent-ring);outline-offset:2px}.ssh-login-submit:focus-visible{outline:3px solid var(--ssh-login-accent-ring);outline-offset:2px}.ssh-login-fallback:focus-visible{outline:3px solid var(--ssh-login-accent-ring);outline-offset:2px}.ssh-login-password-hint{color:var(--ssh-login-muted);margin:-.55rem 0 -.15rem;font-size:.76rem;line-height:1.5}.ssh-password-requirements{gap:.4rem;margin:-.25rem 0 0;padding:0;list-style:none;display:grid}.ssh-password-requirement{color:var(--ssh-login-muted);align-items:center;gap:.5rem;font-size:.78rem;font-weight:700;display:flex}.ssh-password-requirement svg{width:1rem;height:1rem;color:var(--ssh-login-muted-soft);flex:none}.ssh-password-requirement.is-met,.ssh-password-requirement.is-met svg,.ssh-password-match.is-met{color:var(--ssh-login-accent)}.ssh-password-match{color:#be123c;margin:-.5rem 0 -.1rem;font-size:.78rem;font-weight:800}.ssh-password-setup-summary{background:#14b8a617;border:1px solid #5eead42e;border-radius:1rem;align-items:flex-start;gap:.85rem;max-width:40rem;margin-top:2.4rem;padding:1rem 1.1rem;display:flex}.ssh-password-setup-summary>svg{color:#5eead4;flex:none;width:1.3rem;height:1.3rem;margin-top:.1rem}.ssh-password-setup-summary>div{gap:.25rem;display:grid}.ssh-password-setup-summary strong{color:#f8fafc;font-size:.9rem}.ssh-password-setup-summary span{color:#94a3b8;font-size:.8rem;line-height:1.5}.ssh-login-caps{color:#b45309;margin:-.45rem 0 -.15rem;font-size:.78rem;font-weight:800}.ssh-login-page[data-theme=dark] .ssh-login-caps{color:#fbbf24}.ssh-login-submit{color:#fff;cursor:pointer;min-height:3.3rem;font:inherit;background:#0f766e;border:0;border-radius:.9rem;justify-content:center;align-items:center;gap:.65rem;margin-top:.25rem;font-size:.94rem;font-weight:900;transition:background-color .15s,transform .15s,box-shadow .15s;display:inline-flex;box-shadow:0 12px 26px #0f766e33}.ssh-login-submit:hover:not(:disabled){background:#115e59;transform:translateY(-1px);box-shadow:0 16px 30px #0f766e40}.ssh-login-submit:disabled{cursor:wait;opacity:.72}.ssh-login-submit-spinner,.ssh-login-probe-spinner{flex:none;animation:.75s linear infinite ssh-login-spin;display:inline-block}.ssh-login-submit-spinner{color:#fff;width:1rem;height:1rem}.ssh-login-security-note{color:var(--ssh-login-muted);align-items:flex-start;gap:.55rem;margin-top:1.2rem;font-size:.76rem;line-height:1.5;display:flex}.ssh-login-security-note svg{width:1rem;height:1rem;color:var(--ssh-login-accent);flex:none;margin-top:.06rem}.ssh-login-fallback{border:1px solid var(--ssh-login-border-strong);background:var(--ssh-login-surface);min-height:2.7rem;color:var(--ssh-login-muted);border-radius:.8rem;justify-content:center;align-items:center;margin-top:.9rem;font-size:.8rem;font-weight:800;text-decoration:none;display:flex}.ssh-login-fallback:hover{border-color:var(--ssh-login-muted-soft);background:var(--ssh-login-surface-soft);color:var(--ssh-login-text)}.ssh-login-probe{color:var(--ssh-login-muted);text-align:center;justify-items:center;gap:.6rem;padding:2.5rem;display:grid}.ssh-login-probe-spinner{width:2rem;height:2rem;color:var(--ssh-login-accent);margin-bottom:.4rem}.ssh-login-probe strong{color:var(--ssh-login-text);font-size:1rem}.ssh-login-probe>span:last-child{font-size:.82rem}.ssh-login-footer{color:var(--ssh-login-muted-soft);flex-wrap:wrap;justify-content:center;gap:.4rem;margin-top:1.5rem;font-size:.72rem;display:flex}@keyframes ssh-login-spin{to{transform:rotate(360deg)}}@media (max-width:980px){.ssh-login-page{grid-template-columns:1fr}.ssh-login-brand{display:none}.ssh-login-panel{padding:max(1.25rem, env(safe-area-inset-top)) max(1.25rem, env(safe-area-inset-right)) max(1.25rem, env(safe-area-inset-bottom)) max(1.25rem, env(safe-area-inset-left))}.ssh-login-panel-inner{min-height:calc(100dvh - 2.5rem)}.ssh-login-mobile-mark{display:grid}}@media (max-width:520px){.ssh-login-panel{background:var(--ssh-login-surface);align-items:stretch;padding:0}.ssh-login-theme-toggle{top:max(1rem, env(safe-area-inset-top));right:max(1rem, env(safe-area-inset-right));box-shadow:none}.ssh-login-panel-inner{width:100%;min-height:100dvh}.ssh-login-card{min-height:100dvh;padding:max(4.75rem, calc(env(safe-area-inset-top) + 4.25rem)) max(1.25rem, env(safe-area-inset-right)) max(2rem, env(safe-area-inset-bottom)) max(1.25rem, env(safe-area-inset-left));box-shadow:none;border:0;border-radius:0;flex-direction:column;justify-content:center;display:flex}.ssh-login-card-heading{display:block}.ssh-login-mobile-mark{margin-bottom:1.25rem}.ssh-login-input-wrap input{font-size:16px}.ssh-login-footer{display:none}.ssh-login-probe{align-content:center;min-height:100dvh}}@media (prefers-reduced-motion:reduce){.ssh-login-panel,.ssh-login-card,.ssh-login-theme-toggle,.ssh-login-submit,.ssh-login-input-wrap{transition:none}.ssh-login-submit-spinner,.ssh-login-probe-spinner{animation-duration:1.4s}}.ssh-safeshield-plan-badge{isolation:isolate;letter-spacing:.085em;white-space:nowrap;border:1px solid #0000;border-radius:999px;align-items:center;gap:.5rem;min-height:2rem;padding:.3rem .82rem .3rem .36rem;font-size:.75rem;font-weight:950;line-height:1;display:inline-flex;position:relative;overflow:hidden}.ssh-safeshield-plan-badge[data-compact=true]{min-height:1.72rem;padding:.22rem .66rem .22rem .3rem;font-size:.69rem}.ssh-safeshield-plan-badge-mark{z-index:2;border-radius:999px;flex:none;place-items:center;width:1.2rem;height:1.2rem;font-size:.76rem;line-height:1;display:grid;position:relative}.ssh-safeshield-plan-badge[data-compact=true] .ssh-safeshield-plan-badge-mark{width:1.02rem;height:1.02rem;font-size:.66rem}.ssh-safeshield-plan-badge[data-tier=free]{color:#475569;background:linear-gradient(135deg,#fff 0%,#f1f5f9 100%);border-color:#cbd5e1;box-shadow:inset 0 1px #ffffffe6}.ssh-safeshield-plan-badge[data-tier=free] .ssh-safeshield-plan-badge-mark{color:#475569;background:#e2e8f0}.ssh-safeshield-plan-badge[data-tier=pro]{color:#f0fdfa;background:linear-gradient(135deg,#115e59 0%,#0f766e 62%,#0d9488 100%);border-color:#0f766e;box-shadow:0 1px 2px #0f766e29,inset 0 1px #ffffff24}.ssh-safeshield-plan-badge[data-tier=pro] .ssh-safeshield-plan-badge-mark{color:#ccfbf1;box-shadow:none;background:#f0fdfa1f;border:1px solid #99f6e475}.ssh-safeshield-plan-badge[data-tier=ultimate]{color:#fffbeb;background:linear-gradient(135deg,#78350f 0%,#92400e 58%,#b45309 100%);border-color:#b45309;box-shadow:0 1px 2px #78350f2e,inset 0 1px #ffffff1f}.ssh-safeshield-plan-badge[data-tier=ultimate] .ssh-safeshield-plan-badge-mark{color:#fde68a;box-shadow:none;background:#fffbeb1f;border:1px solid #fde68a75}.ssh-safeshield-plan-badge[data-tier=paid]{color:#f0f9ff;background:linear-gradient(135deg,#0c4a6e 0%,#075985 58%,#0369a1 100%);border-color:#0369a1;box-shadow:0 1px 2px #0369a129,inset 0 1px #ffffff1f}.ssh-safeshield-plan-badge[data-tier=paid] .ssh-safeshield-plan-badge-mark{color:#bae6fd;box-shadow:none;background:#f0f9ff1f;border:1px solid #bae6fd75}.ssh-safeshield-plan-caption{color:#64748b;white-space:nowrap;align-items:center;gap:.34rem;font-size:.68rem;font-weight:850;display:inline-flex}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=free]{color:#cbd5e1;background:#1e293b;border-color:#475569}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=free] .ssh-safeshield-plan-badge-mark{color:#e2e8f0;background:#475569}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=pro]{color:#99f6e4;background:linear-gradient(135deg,#042f2e 0%,#134e4a 100%);border-color:#0f766e;box-shadow:inset 0 1px #99f6e414}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=pro] .ssh-safeshield-plan-badge-mark{color:#99f6e4;background:#0d94881f;border-color:#2dd4bf61}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=ultimate]{color:#fde68a;background:linear-gradient(135deg,#451a03 0%,#78350f 100%);border-color:#b45309;box-shadow:inset 0 1px #fde68a14}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=ultimate] .ssh-safeshield-plan-badge-mark{color:#fde68a;background:#92400e29;border-color:#d9770675}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=paid]{color:#bae6fd;background:linear-gradient(135deg,#082f49 0%,#0c4a6e 100%);border-color:#0369a1;box-shadow:inset 0 1px #bae6fd14}.ssh-app[data-theme=dark] .ssh-safeshield-plan-badge[data-tier=paid] .ssh-safeshield-plan-badge-mark{color:#bae6fd;background:#0369a124;border-color:#0ea5e95c}.ssh-app[data-theme=dark] .ssh-safeshield-plan-caption{color:#94a3b8}.ssh-safeshield-upgrade-card{background:linear-gradient(120deg,#f0fdfa 0%,#ecfeff 58%,#fff7ed 100%);border:1px solid #99f6e4;border-radius:1rem;align-items:center;gap:.85rem;margin-top:.75rem;padding:.9rem 1rem;display:flex;box-shadow:0 10px 24px #0f766e14}.ssh-safeshield-upgrade-mark{color:#fff;background:linear-gradient(135deg,#0f766e,#0891b2);border-radius:.8rem;flex:none;place-items:center;width:2.5rem;height:2.5rem;font-size:1.05rem;display:grid;box-shadow:0 8px 18px #0d948838}.ssh-safeshield-upgrade-copy{flex:1;gap:.2rem;min-width:0;display:grid}.ssh-safeshield-upgrade-meta{color:#0f766e;letter-spacing:.12em;text-transform:uppercase;flex-wrap:wrap;align-items:center;gap:.45rem;font-size:.62rem;font-weight:900;display:flex}.ssh-safeshield-upgrade-soon{letter-spacing:.06em;background:#0f766e1a;border-radius:999px;padding:.18rem .45rem}.ssh-safeshield-upgrade-copy strong{color:#0f172a;font-size:.86rem;line-height:1.35rem}.ssh-safeshield-upgrade-copy>span{color:#64748b;font-size:.74rem;font-weight:600;line-height:1.15rem}.ssh-safeshield-upgrade-action{color:#fff;background:#0f766e;border:1px solid #0f766e;border-radius:.8rem;flex:none;justify-content:center;align-items:center;gap:.4rem;min-height:2.45rem;padding:.55rem .8rem;font-size:.74rem;font-weight:900;text-decoration:none;transition:background-color .15s,border-color .15s,transform .15s;display:inline-flex}.ssh-safeshield-upgrade-action:hover{background:#115e59;border-color:#115e59;transform:translateY(-1px)}.ssh-safeshield-upgrade-action:focus-visible{outline-offset:2px;outline:3px solid #14b8a63d}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-card{background:linear-gradient(120deg,#0d948821 0%,#0f172afa 58%,#92400e1f 100%);border-color:#134e4a;box-shadow:0 12px 28px #0000002e}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-mark{background:linear-gradient(135deg,#0d9488,#0891b2);box-shadow:0 8px 20px #2dd4bf21}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-meta{color:#5eead4}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-soon{background:#2dd4bf1f}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-copy strong{color:#f8fafc}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-copy>span{color:#94a3b8}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-action{color:#ccfbf1;background:#0d948838;border-color:#2dd4bf}.ssh-app[data-theme=dark] .ssh-safeshield-upgrade-action:hover{background:#0d948852;border-color:#5eead4}@media (max-width:640px){.ssh-safeshield-upgrade-card{flex-wrap:wrap;align-items:flex-start}.ssh-safeshield-upgrade-action{justify-content:center;width:100%}}.ssh-safeshield-refresh{background:#f0fdfab3;border:1px solid #ccfbf1;border-radius:1rem;align-items:center;gap:.875rem;width:27rem;max-width:100%;margin-top:1rem;padding:.75rem .875rem;display:flex}.ssh-safeshield-refresh[data-state=error]{background:#fef2f2c7;border-color:#fecaca}.ssh-safeshield-refresh-copy{min-width:0}.ssh-safeshield-refresh-kicker,.ssh-safeshield-refresh-title,.ssh-safeshield-refresh-description,.ssh-safeshield-refresh-error-code{margin:0}.ssh-safeshield-refresh-kicker{color:#0f766e;letter-spacing:.14em;text-transform:uppercase;font-size:.66rem;font-weight:900}.ssh-safeshield-refresh[data-state=error] .ssh-safeshield-refresh-kicker{color:#b91c1c}.ssh-safeshield-refresh-title{color:#0f172a;margin-top:.25rem;font-size:.875rem;font-weight:800}.ssh-safeshield-refresh-description{color:#475569;margin-top:.25rem;font-size:.75rem;font-weight:600;line-height:1.25rem}.ssh-safeshield-refresh-error-code{overflow-wrap:anywhere;color:#64748b;margin-top:.375rem;font-size:.68rem;font-weight:600}.ssh-safeshield-refresh-error-code code{font:inherit}.ssh-safeshield-refresh-donut{color:#0d9488;place-items:center;width:3.35rem;min-width:3.35rem;height:3.35rem;display:grid;position:relative}.ssh-safeshield-refresh-donut[data-state=error]{color:#dc2626}.ssh-safeshield-refresh-loader-svg{transform-origin:50%;width:100%;height:100%;animation:.82s linear infinite ssh-safeshield-refresh-loader-rotate;position:absolute;top:0;bottom:0;left:0;right:0;transform:rotate(-90deg)}.ssh-safeshield-refresh-loader-svg-error{animation:none}.ssh-safeshield-refresh-loader-track{color:#cbd5e1;stroke:#cbd5e1}.ssh-safeshield-refresh-loader-arc{color:#0f172a;stroke:#0f172a;filter:drop-shadow(0 0 .06rem #0f172a47)}.ssh-safeshield-refresh-donut[data-state=error] .ssh-safeshield-refresh-loader-track{color:#dc2626;stroke:#dc2626}.ssh-safeshield-refresh-donut-label{z-index:1;color:#0f766e;font-variant-numeric:tabular-nums;font-size:.68rem;font-weight:900;position:relative}.ssh-app[data-theme=dark] .ssh-safeshield-refresh{background:#0f766e1f;border-color:#134e4a}.ssh-app[data-theme=dark] .ssh-safeshield-refresh[data-state=error]{background:#7f1d1d2e;border-color:#7f1d1d}.ssh-app[data-theme=dark] .ssh-safeshield-refresh-title{color:#f8fafc}.ssh-app[data-theme=dark] .ssh-safeshield-refresh-description,.ssh-app[data-theme=dark] .ssh-safeshield-refresh-error-code{color:#94a3b8}.ssh-app[data-theme=dark] .ssh-safeshield-refresh-loader-track{color:#334155;stroke:#334155}.ssh-app[data-theme=dark] .ssh-safeshield-refresh-loader-arc{color:#fff;stroke:#fff;filter:drop-shadow(0 0 .08rem #ffffff8c)}.ssh-app[data-theme=dark] .ssh-safeshield-refresh-donut-label{color:#5eead4}.ssh-app[data-theme=dark] .ssh-safeshield-refresh-donut[data-state=error] .ssh-safeshield-refresh-loader-track{color:#ef4444;stroke:#ef4444}@keyframes ssh-safeshield-refresh-loader-rotate{to{transform:rotate(270deg)}}@media (prefers-reduced-motion:reduce){.ssh-safeshield-refresh-loader-svg{animation:none}}.ssh-dashboard-account-banner{background:#f0fdfa;border-color:#99e8df}.ssh-dashboard-account-banner-icon{color:#0f766e;background:#ccfbf1}.ssh-dashboard-account-banner-title{color:#0f172a}.ssh-dashboard-account-banner-description{color:#475569}.ssh-dashboard-account-banner-badge{color:#92400e;background:#fef3c7}.ssh-dashboard-account-banner-action{color:#fff;background:#0f766e}.ssh-dashboard-account-banner-action:hover{color:#fff;background:#115e59}.ssh-dashboard-account-banner-action:focus-visible{outline-color:#0d9488}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner{background:#102927;border-color:#115e59}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner-icon{color:#99f6e4;background:#134e4a}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner-title{color:#f1f5f9}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner-description{color:#cbd5e1}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner-badge{color:#fde68a;background:#78350f}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner-action{color:#fff;background:#0f766e}.ssh-app[data-theme=dark] .ssh-dashboard-account-banner-action:hover{background:#0d9488}.ssh-account-nav-dot{width:10px;height:10px;box-shadow:0 0 0 2px var(--ssh-account-dot-ring,#fff);background-color:#d97706;border-radius:50%;flex:none;margin-left:auto;display:block}.ssh-account-nav-dot-collapsed{margin-left:0;position:absolute;top:5px;right:7px}.ssh-app[data-theme=dark] .ssh-account-nav-dot{--ssh-account-dot-ring:#0f172a;background-color:#fbbf24}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}} +@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}} /*$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 71107a19d..2ba53ce00 100644 --- a/luci-app-smartsafehub/root/www/luci-static/smartsafehub/app.js +++ b/luci-app-smartsafehub/root/www/luci-static/smartsafehub/app.js @@ -1,4 +1,3 @@ -var e,t,n,r,i,a,o,s,c,l,u={},d=[],f=/^m(i|n|o|s|text|space)$/,p=Array.isArray,m=d.slice,h=Object.assign;function g(e){e&&e.parentNode&&e.remove()}function _(n,r,i,a,o){var s={type:n,props:r,key:i,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o||++t,__i:-1,__u:0};return!o&&e.vnode&&e.vnode(s),s}function v(e){return e.children}function y(e,t){this.props=e,this.context=t,this.__g=0}function b(e,t){if(t==null)return e.__?b(e.__,e.__i+1):null;for(var n;tr&&n.sort(a),t=n.shift(),r=n.length,8&t.__g&&(s=void 0,c=void 0,l=(c=(o=t).__v).__e,u=[],d=[],(f=o.__P)&&((s=h({constructor:void 0},c)).__v=c.__v+1,e.vnode&&e.vnode(s),ee(f,s,c,o.__n,f.namespaceURI,32&c.__u?[l]:null,u,l||b(c),32&c.__u,d),s.__v=c.__v,s.__.__k[s.__i]=s,ne(u,s,d),c.__=c.__e=null,s.__e!=l&&x(s)))}finally{n.length=i=0}}function w(e,t,n,r,i,a,o,s,c,l,f){var p,m,h,g,_,v,y=r.__k||d,b=t.length;for(c=T(n,t,y,c,b),p=0;pc?x--:x++,S=!0)):(~l||(i>g?x--:i>1]+!!u){for(i=n-1,a=n+1;i>=0||a=0?i--:a++])&&!(2&l.__u)&&s==l.key&&c==l.type)return o}return-1}function k(e,t,n){n??(n=``),t[0]==`-`?e.setProperty(t,n):e[t]=n}function A(e,t,n,r,i){var a,o;n:if(t==`style`){if(typeof n==`string`)e.style.cssText=n;else{if(typeof r==`string`&&(e.style.cssText=r=``),r)for(t in r)n&&t in n||k(e.style,t,``);if(n)for(t in n)r&&n[t]==r[t]||k(e.style,t,n[t])}}else if(t[0]==`o`&&t[1]==`n`)(e.__e||(e.__e={}))[t]=n,n&&r||(o=l[t]||(l[t]=j(t)),(e.__a||(e.__a={}))[t]=c,a=t!=(t=t.replace(s,`$1`)),(t=t.slice(2))[0]<`a`&&(t=t.toLowerCase()),n?e.addEventListener(t,o,a):e.removeEventListener(t,o,a));else{if(i==`http://www.w3.org/2000/svg`)t=t.replace(/xlink(H|:h)/,`h`).replace(/sName$/,`s`);else if(t!=`width`&&t!=`height`&&t!=`href`&&t!=`list`&&t!=`form`&&t!=`tabIndex`&&t!=`download`&&t!=`rowSpan`&&t!=`colSpan`&&t!=`role`&&t!=`popover`&&t in e)try{e[t]=n??``;break n}catch{}typeof n==`function`||(n==null||!1===n&&t[4]!=`-`?e.removeAttribute(t):e.setAttribute(t,t==`popover`&&n==1?``:n))}}function j(t){return function(n){if(this.__e){var r=this.__e[t];if(n[o]==null)n[o]=c++;else if(n[o]=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 M(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 N(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 P(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 F(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(n){t.some(function(e){e.__h&&(e.__h=[])}),t=[],Ue.__e(n,e.__v)}}),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}function ct(e){"@babel/helpers - typeof";return ct=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},ct(e)}function lt(e,t){if(ct(e)!=`object`||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||`default`);if(ct(r)!=`object`)return r;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(t===`string`?String:Number)(e)}function ut(e){var t=lt(e,`string`);return ct(t)==`symbol`?t:t+``}function I(e,t,n){return(t=ut(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:`장치 연결이 끊어졌습니다.`},ft=2e4,pt=3e3,mt=1e3,ht=6e4,gt=1,L=class extends Error{constructor(e,t){super(t),I(this,`code`,void 0),this.name=`RpcError`,this.code=e}};function _t(){let e=window.__SMARTHUB_BOOTSTRAP__;if(!e?.sessionId||!e.rpcUrl)throw new L(`BOOTSTRAP_MISSING`,`LuCI 세션 정보를 찾을 수 없습니다. 페이지를 새로고침해 주세요.`);return e}function vt(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function yt(){return new L(`INVALID_RESPONSE`,`장치 API가 올바르지 않은 응답을 반환했습니다.`)}function bt(e){return typeof e!=`number`||!Number.isFinite(e)?ft:Math.min(ht,Math.max(mt,Math.trunc(e)))}function xt(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 St(e){return window.__SMARTHUB_BOOTSTRAP__?.sessionId===e}async function Ct(e){let t=gt++,n=new AbortController,r=window.setTimeout(()=>n.abort(),pt);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(!vt(i)||i.jsonrpc!==`2.0`||i.id!==t)return`unknown`;if(`error`in i){let e=i.error;return vt(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 wt(e,t){if(!xt(e)||!St(t.sessionId))return e;let n=await Ct(t);return St(t.sessionId)?n===`active`?new L(`RPC_PERMISSION_DENIED`,`현재 로그인 세션에 이 기능 권한이 없습니다. SmartSafeHub 업데이트 직후라면 로그아웃 후 다시 로그인해 주세요.`):n===`expired`?(Pe(t.sessionId),new L(`SESSION_EXPIRED`,je)):e:e}function Tt(e,t){if(!vt(e)||e.jsonrpc!==`2.0`||e.id!==t)throw yt();if(`error`in e){let t=e.error;throw!vt(t)||typeof t.code!=`number`?yt():new L(`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 yt();let r=n[0];if(typeof r!=`number`||!Number.isInteger(r))throw yt();if(r!==0)throw new L(`UBUS_${r}`,dt[r]??`ubus 오류 코드 ${r}가 반환되었습니다.`);if(n.length<2||n[1]===void 0)throw new L(`EMPTY_RESPONSE`,`장치 API 응답이 비어 있습니다.`);return n[1]}async function Et(e,t,n={},r={}){let i=_t(),a=performance.now(),o=gt++,s=new AbortController,c=window.setTimeout(()=>s.abort(),bt(r.timeoutMs));try{let r=await fetch(i.rpcUrl,{method:`POST`,credentials:`same-origin`,headers:{"Content-Type":`application/json`},body:JSON.stringify({jsonrpc:`2.0`,id:o,method:`call`,params:[i.sessionId,e,t,n]}),signal:s.signal});if(!r.ok)throw new L(`HTTP_ERROR`,`장치 API가 HTTP ${r.status} 오류를 반환했습니다.`);let a;try{a=await r.json()}catch{throw yt()}return Tt(a,o)}catch(e){throw s.signal.aborted?new L(`RPC_TIMEOUT`,`장치 응답 시간이 초과되었습니다. 잠시 후 다시 시도해 주세요.`):e instanceof L?await wt(e,i):new L(`NETWORK_ERROR`,`공유기와 통신할 수 없습니다. 네트워크 연결을 확인해 주세요.`)}finally{window.clearTimeout(c),Math.round(performance.now()-a)}}async function R(e,t,n={},r={}){let i=await Et(e,t,n,r);if(!i.ok)throw new L(i.error.code,i.error.message);return i.data}function Dt(){return R(`smartsafehub`,`system_root_password_status`)}function Ot(e){return R(`smartsafehub`,`system_root_password_set`,{password:e})}var kt=0;Array.isArray;function z(t,n,r,i,a,o){n||(n={});var s,c,l=n;if(`ref`in l&&typeof t!=`function`)for(c in l={},n)c==`ref`?s=n[c]:l[c]=n[c];var u={type:t,props:l,key:r,ref:s,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--kt,__i:-1,__u:0};return(a||o)&&(u.__source=a,u.__self=o),e.vnode&&e.vnode(u),u}function B({children:e,title:t,...n}){return z(`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?z(`title`,{children:t}):null,e]})}function At(e){return z(B,{...e,children:[z(`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`}),z(`path`,{d:`m9 12 2 2 4-4`})]})}function jt(e){return z(B,{...e,children:[z(`rect`,{height:`7`,rx:`2`,width:`18`,x:`3`,y:`13`}),z(`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 Mt(e){return z(B,{...e,children:[z(`rect`,{height:`11`,rx:`2`,width:`16`,x:`4`,y:`3`}),z(`path`,{d:`M8 21h8M12 14v7`})]})}function Nt(e){return z(B,{...e,children:[z(`path`,{d:`m3 11 9-8 9 8`}),z(`path`,{d:`M5 10v10h14V10M9 20v-6h6v6`})]})}function Pt(e){return z(B,{...e,children:[z(`circle`,{cx:`12`,cy:`8`,r:`4`}),z(`path`,{d:`M4.5 21a7.5 7.5 0 0 1 15 0`})]})}function Ft(e){return z(B,{...e,children:[z(`path`,{d:`M12 16V4`}),z(`path`,{d:`m7 9 5-5 5 5`}),z(`path`,{d:`M5 14v5h14v-5`})]})}function It(e){return z(B,{...e,children:[z(`circle`,{cx:`12`,cy:`12`,r:`3`}),z(`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 Lt(e){return z(B,{...e,children:[z(`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`}),z(`path`,{d:`M12 20h.01`})]})}function Rt(e){return z(B,{...e,children:[z(`path`,{d:`M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4z`}),z(`path`,{d:`M6.5 6.5h1M16.5 6.5h1M6.5 16.5h1`}),z(`path`,{d:`M14 14h2v2h-2zM18 14h2v2h-2zM16 16h2v2h-2zM18 18h2v2h-2zM14 18h1`})]})}function zt(e){return z(B,{...e,children:[z(`path`,{d:`M7 3v4M17 3v4M5 7h14v4a7 7 0 0 1-14 0V7Z`}),z(`path`,{d:`M12 18v3`})]})}function Bt(e){return z(B,{...e,children:[z(`circle`,{cx:`12`,cy:`12`,r:`9`}),z(`path`,{d:`M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18`})]})}function Vt(e){return z(B,{...e,children:[z(`rect`,{height:`13`,rx:`2`,width:`18`,x:`3`,y:`6`}),z(`path`,{d:`m8 2 4 4 4-4M8 22h8`})]})}function Ht(e){return z(B,{...e,children:[z(`circle`,{cx:`12`,cy:`12`,r:`9`}),z(`path`,{d:`M12 7v5l3 2`})]})}function Ut(e){return z(B,{...e,children:[z(`rect`,{height:`14`,rx:`2`,width:`14`,x:`5`,y:`5`}),z(`path`,{d:`M9 9h6v6H9zM9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M2 15h3M19 9h3M19 15h3`})]})}function V(e){return z(B,{...e,children:[z(`path`,{d:`M20 11a8 8 0 1 0-2.3 5.7`}),z(`path`,{d:`M20 4v7h-7`})]})}function Wt(e){return z(B,{...e,children:[z(`path`,{d:`M12 2v10`}),z(`path`,{d:`M18.4 6.6a8 8 0 1 1-12.8 0`})]})}function Gt(e){return z(B,{...e,children:[z(`path`,{d:`M12 3v12`}),z(`path`,{d:`m7 10 5 5 5-5`}),z(`path`,{d:`M5 21h14`})]})}function Kt(e){return z(B,{...e,children:[z(`ellipse`,{cx:`12`,cy:`5`,rx:`8`,ry:`3`}),z(`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 qt(e){return z(B,{...e,children:[z(`circle`,{cx:`8`,cy:`15`,r:`4`}),z(`path`,{d:`m11 12 8-8M15 8l2 2M17 6l2 2`})]})}function Jt(e){return z(B,{...e,children:[z(`rect`,{height:`13`,rx:`2`,width:`13`,x:`8`,y:`8`}),z(`path`,{d:`M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3`})]})}function Yt(e){return z(B,{...e,children:[z(`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`}),z(`circle`,{cx:`12`,cy:`12`,r:`2.5`})]})}function Xt(e){return z(B,{...e,children:[z(`path`,{d:`m3 3 18 18`}),z(`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`}),z(`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`}),z(`path`,{d:`M10.3 10.3a2.5 2.5 0 0 0 3.4 3.4`})]})}function Zt(e){return z(B,{...e,children:[z(`rect`,{height:`17`,rx:`2`,width:`18`,x:`3`,y:`4`}),z(`path`,{d:`M8 2v4M16 2v4M3 9h18`})]})}function H(e){return z(B,{...e,children:[z(`circle`,{cx:`12`,cy:`12`,r:`9`}),z(`path`,{d:`m8 12 2.5 2.5L16 9`})]})}function U(e){return z(B,{...e,children:[z(`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`}),z(`path`,{d:`M12 9v4M12 17h.01`})]})}function Qt(e){return z(B,{...e,children:z(`path`,{d:`M12 5v14M5 12h14`})})}function $t(e){return z(B,{...e,children:z(`path`,{d:`M4 7h16M9 7V4h6v3M7 7l1 13h8l1-13M10 11v5M14 11v5`})})}function en(e){return z(B,{...e,children:[z(`circle`,{cx:`11`,cy:`11`,r:`7`}),z(`path`,{d:`m20 20-4-4`})]})}function tn(e){return z(B,{...e,children:[z(`path`,{d:`M8 6h12M8 12h12M8 18h12`}),z(`path`,{d:`M4 6h.01M4 12h.01M4 18h.01`})]})}function nn(e){return z(B,{...e,children:[z(`path`,{d:`M10 5H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h4`}),z(`path`,{d:`M14 8l4 4-4 4M8 12h10`})]})}function rn(e){return z(B,{...e,children:z(`path`,{d:`M4 7h16M4 12h16M4 17h16`})})}function an(e){return z(B,{...e,children:z(`path`,{d:`m6 9 6 6 6-6`})})}function on(e){return z(B,{...e,children:z(`path`,{d:`m6 6 12 12M18 6 6 18`})})}function sn(e){return z(B,{...e,children:z(`path`,{d:`m15 18-6-6 6-6`})})}function cn(e){return z(B,{...e,children:z(`path`,{d:`m9 18 6-6-6-6`})})}function ln(e){return z(B,{...e,children:z(`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 un(e){return z(B,{...e,children:[z(`circle`,{cx:`12`,cy:`12`,r:`4`}),z(`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 dn=`smartsafehub.theme`;function fn(){if(typeof window>`u`)return`light`;try{let e=window.localStorage.getItem(dn);if(e===`dark`||e===`light`)return e}catch{}return window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}function pn(e){try{window.localStorage.setItem(dn,e)}catch{}}function mn(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 hn(e){return{length:e.length>=8,letter:/[A-Za-z]/.test(e),number:/[0-9]/.test(e)}}function gn(e){return e.length&&e.letter&&e.number}function _n({theme:e}){return z(e===`dark`?un:ln,{})}function vn({met:e,children:t}){return z(`li`,{class:e?`ssh-password-requirement is-met`:`ssh-password-requirement`,children:[z(H,{"aria-hidden":`true`}),z(`span`,{children:t})]})}function yn({onCompleted:e,recovery:t}){let n=P(null),r=P(null),[i,a]=M(``),[o,s]=M(``),[c,l]=M(!1),[u,d]=M(!1),[f,p]=M(!1),[m,h]=M(null),[g,_]=M(fn),v=hn(i),y=gn(v),b=o.length>0&&i===o,x=g===`dark`?`라이트 모드로 전환`:`다크 모드로 전환`;N(()=>{pn(g),mn(g)},[g]),N(()=>{let e=window.requestAnimationFrame(()=>n.current?.focus());return()=>window.cancelAnimationFrame(e)},[]);let S=e=>{d(e.getModifierState?.(`CapsLock`)??!1)};return z(`main`,{class:`ssh-login-page`,"data-theme":g,children:[z(`section`,{class:`ssh-login-brand`,"aria-labelledby":`ssh-password-setup-brand-title`,children:[z(`div`,{class:`ssh-login-brand-orb ssh-login-brand-orb-one`,"aria-hidden":`true`}),z(`div`,{class:`ssh-login-brand-orb ssh-login-brand-orb-two`,"aria-hidden":`true`}),z(`div`,{class:`ssh-login-brand-content`,children:[z(`span`,{class:`ssh-login-brand-mark`,"aria-hidden":`true`,children:z(At,{})}),z(`p`,{class:`ssh-login-eyebrow`,children:t?`PASSWORD RECOVERY`:`FIRST SECURITY SETUP`}),z(`h1`,{id:`ssh-password-setup-brand-title`,children:[z(`span`,{class:`ssh-password-setup-brand-title-line`,children:`SmartSafeHub`}),z(`span`,{class:`ssh-password-setup-brand-title-line`,children:t?`비밀번호 복구`:`보호 시작`})]}),z(`p`,{class:`ssh-login-brand-description`,children:t?`물리 Reset 버튼으로 관리자 비밀번호 복구가 요청되었습니다. 네트워크, Wi-Fi, SafeShield 등 기존 설정은 그대로 유지됩니다.`:`관리자 비밀번호가 없는 상태에서는 SmartSafeHub 관리 기능을 사용할 수 없습니다. 먼저 이 공유기의 root 관리자 비밀번호를 설정해 주세요.`}),z(`div`,{class:`ssh-password-setup-summary`,children:[z(At,{"aria-hidden":`true`}),z(`div`,{children:[z(`strong`,{children:t?`새 관리자 비밀번호를 설정해 주세요`:`초기 보안 설정이 필요합니다`}),z(`span`,{children:t?`새 비밀번호를 설정하기 전까지 SSH 접근은 비활성화되고 다른 관리 기능도 잠겨 있습니다.`:`설정이 완료되기 전까지 다른 SmartSafeHub 기능은 잠겨 있습니다.`})]})]})]})]}),z(`section`,{class:`ssh-login-panel`,"aria-labelledby":`ssh-password-setup-title`,children:[z(`button`,{"aria-label":x,class:`ssh-login-theme-toggle`,onClick:()=>_(e=>e===`dark`?`light`:`dark`),title:x,type:`button`,children:z(_n,{theme:g})}),z(`div`,{class:`ssh-login-panel-inner`,children:[z(`div`,{class:`ssh-login-card`,children:[z(`div`,{class:`ssh-login-card-heading`,children:[z(`span`,{class:`ssh-login-mobile-mark`,"aria-hidden":`true`,children:z(qt,{})}),z(`div`,{children:[z(`p`,{class:`ssh-login-kicker`,children:`ADMIN PASSWORD`}),z(`h2`,{id:`ssh-password-setup-title`,children:t?`관리자 비밀번호 복구`:`관리자 비밀번호 설정`}),z(`p`,{class:`ssh-login-subtitle`,children:`비밀번호 설정 후 새 비밀번호로 다시 로그인해야 합니다.`})]})]}),m?z(`div`,{class:`ssh-login-alert`,role:`alert`,"aria-live":`polite`,children:m}):null,z(`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 Ot(i),a(``),s(``);try{await Oe()}catch{}e()}catch(e){h(e instanceof L?e.message:`관리자 비밀번호를 설정하지 못했습니다. 잠시 후 다시 시도해 주세요.`)}finally{p(!1)}}},noValidate:!0,children:[z(`label`,{class:`ssh-login-field`,for:`smartsafehub-new-root-password`,children:[z(`span`,{class:`ssh-login-label`,children:`새 비밀번호`}),z(`span`,{class:`ssh-login-input-wrap`,children:[z(`span`,{class:`ssh-login-field-icon`,"aria-hidden":`true`,children:z(qt,{})}),z(`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}),z(`button`,{"aria-label":c?`비밀번호 숨기기`:`비밀번호 표시`,"aria-pressed":c,class:`ssh-login-password-toggle`,disabled:f,onClick:()=>l(e=>!e),title:c?`비밀번호 숨기기`:`비밀번호 표시`,type:`button`,children:z(c?Xt:Yt,{})})]})]}),z(`ul`,{class:`ssh-password-requirements`,"aria-label":`비밀번호 요구 사항`,children:[z(vn,{met:v.length,children:`8자 이상`}),z(vn,{met:v.letter,children:`영문자 1자 이상 포함`}),z(vn,{met:v.number,children:`숫자 1자 이상 포함`})]}),z(`label`,{class:`ssh-login-field`,for:`smartsafehub-confirm-root-password`,children:[z(`span`,{class:`ssh-login-label`,children:`비밀번호 확인`}),z(`span`,{class:`ssh-login-input-wrap`,children:[z(`span`,{class:`ssh-login-field-icon`,"aria-hidden":`true`,children:z(qt,{})}),z(`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?z(`p`,{class:b?`ssh-password-match is-met`:`ssh-password-match`,role:`status`,children:b?`비밀번호가 일치합니다.`:`비밀번호가 일치하지 않습니다.`}):null,u?z(`p`,{class:`ssh-login-caps`,role:`status`,children:`Caps Lock이 켜져 있습니다.`}):null,z(`button`,{class:`ssh-login-submit`,disabled:f,type:`submit`,children:[f?z(V,{class:`ssh-login-submit-spinner`,"aria-hidden":`true`}):null,z(`span`,{children:f?`설정 중…`:`비밀번호 설정`})]})]}),z(`div`,{class:`ssh-login-security-note`,children:[z(At,{"aria-hidden":`true`}),z(`span`,{children:`비밀번호는 현재 공유기에 직접 설정되며 외부 서버로 전송되지 않습니다.`})]})]}),z(`footer`,{class:`ssh-login-footer`,children:[z(`span`,{children:`SmartSafeHub`}),z(`span`,{"aria-hidden":`true`,children:`·`}),z(`span`,{children:window.location.host})]})]})]})]})}var W=`smartsafehub`,bn=`smartsafehub_guest`,xn=`smartsafehub_network`;function Sn(){return R(W,`connected_devices`)}async function Cn(){let e=await R(W,`activity_history`);return{...e,cloud:{...e.cloud,enabled:e.cloud.enabled??!1}}}function wn(e){return R(W,`activity_cloud_sync_update`,{enabled:e})}function Tn(){return R(W,`status`)}function En(){return R(W,`device_recommended_rules_status`,{},{timeoutMs:5e3})}function Dn(){return R(W,`device_recommended_rules_refresh`,{},{timeoutMs:3e4})}function On(e){return R(W,`device_recommended_rules_revoke`,{rule_id:e},{timeoutMs:3e4})}function kn(){return R(W,`device_registration_status`,{},{timeoutMs:5e3})}function An(){return R(W,`device_registration_refresh`,{},{timeoutMs:3e4})}function jn(){return R(W,`device_pairing_refresh`,{},{timeoutMs:15e3})}function Mn(){return R(W,`health_status`)}function Nn(){return R(W,`health_run`)}function Pn(e){return R(W,`health_reporter_update`,{enabled:e})}function Fn(){return R(W,`updates_status`)}function In(){return R(W,`firmware_status`)}function Ln(){return R(W,`firmware_check`)}function Rn(){return R(W,`firmware_prepare`)}function zn(e){return R(W,`firmware_validate_upload`,{filename:e})}function Bn(e){return R(W,`firmware_install`,{confirm:`install`,keep_settings:e})}function Vn(){return R(W,`firmware_discard`)}function Hn(){return R(W,`updates_check`)}function Un(){return R(W,`updates_install`,{confirm:`install`})}function Wn(e){return R(W,`updates_settings_update`,{check_enabled:e.checkEnabled,check_interval_s:e.checkIntervalSeconds,auto_install:e.autoInstall,auto_install_time:e.autoInstallTime})}function Gn(){return R(W,`system_time_settings`)}function Kn(e){return R(W,`system_timezone_initialize`,{zonename:e})}function qn(e){return R(W,`system_timezone_update`,{zonename:e})}function Jn(){return R(W,`system_time_sync`)}function Yn(){return R(W,`system_scheduled_reboot_settings`)}function Xn(e){return R(W,`system_scheduled_reboot_update`,{enabled:e.enabled,frequency:e.frequency,day_of_week:e.dayOfWeek,time:e.time})}function Zn(e){return R(W,`system_backup_validate`,{filename:e})}function Qn(){return R(W,`system_backup_restore`,{confirm:`restore`},{timeoutMs:35e3})}function $n(){return R(W,`system_backup_discard`)}function er(){return R(W,`system_reboot`,{confirm:`reboot`})}function tr(){return R(xn,`wan_settings`)}function nr(e){return R(xn,`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 R(xn,`wan_reconnect`,{confirm:`reconnect`},{timeoutMs:1e4})}function ir(){return R(xn,`lan_settings`)}function ar(e){return R(xn,`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 or(){return R(xn,`lan_auto_subnet`,{confirm:`apply`},{timeoutMs:1e4})}function sr(){return R(xn,`iptv_settings`)}function cr(e){return R(xn,`iptv_update`,{enabled:e.enabled,provider:e.provider,confirm:`apply`},{timeoutMs:2e4})}function lr(e){return R(W,`wifi_qr`,{section:e},{timeoutMs:8e3})}function ur(){return R(bn,`wifi_guest_qr`,{},{timeoutMs:8e3})}async function dr(){let e=await R(W,`wifi_summary`);try{let t=await R(bn,`wifi_guest_summary`);return{...e,guest:t,guestError:!1}}catch{return{...e,guest:null,guestError:!0}}}function fr(e){return R(W,`wifi_update`,{...e},{timeoutMs:35e3})}function pr(e){return R(bn,`wifi_guest_update`,{...e},{timeoutMs:95e3})}var mr=Object.fromEntries([{route:`home`,hash:`#home`,label:`대시보드`,title:`대시보드`,description:`네트워크, 장치와 시스템 상태를 한눈에 확인합니다.`},{route:`activity`,hash:`#activity`,label:`최근 활동`,title:`최근 활동`,description:`현재 부팅 이후 인터넷, 보호, 업데이트와 진단 상태 변화를 확인합니다.`},{route:`network`,hash:`#network`,label:`네트워크`,title:`네트워크`,description:`인터넷 연결과 내부 네트워크 주소, DHCP 및 네트워크 충돌을 한 곳에서 관리합니다.`},{route:`wifi`,hash:`#wifi`,label:`Wi-Fi`,title:`Wi-Fi`,description:`기본 무선 네트워크의 이름, 보안과 사용 상태를 관리합니다.`},{route:`iptv`,hash:`#iptv`,label:`IPTV`,title:`IPTV`,description:`SK Broadband와 LG U+ 멀티캐스트 IPTV를 설정합니다.`},{route:`devices`,hash:`#devices`,label:`연결된 기기`,title:`연결된 기기`,description:`네트워크에서 확인된 기기와 연결 방식을 살펴봅니다.`},{route:`safeshield`,hash:`#safeshield`,label:`SafeShield`,title:`SafeShield`,description:`DNS 보호 상태를 확인하고 차단 목록을 관리합니다.`},{route:`rules`,hash:`#rules`,label:`사용자 규칙`,title:`사용자 규칙`,description:`직접 허용하거나 차단할 도메인을 관리합니다.`},{route:`account`,hash:`#account`,label:`SmartSafeHub 계정`,title:`SmartSafeHub 계정`,description:`이 기기의 SmartSafeHub 계정 연결 상태와 구독 플랜을 확인합니다.`},{route:`system`,hash:`#system`,label:`업데이트`,title:`업데이트`,description:`기기의 펌웨어와 관리 소프트웨어 업데이트를 관리합니다.`},{route:`settings`,hash:`#settings`,label:`설정`,title:`설정`,description:`시스템 상태와 시간대를 확인하고 장치 관리 및 진단 기능을 설정합니다.`}].map(e=>[e.route,e])),hr=[`home`,`activity`,`network`,`wifi`,`devices`,`safeshield`,`rules`,`system`];function gr({description:e,loading:t,onRefresh:n,onToggleTheme:r,refreshing:i,theme:a,title:o}){return z(`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:z(`div`,{class:`flex w-full items-center justify-between gap-4 md:h-full`,children:[z(`div`,{class:`min-w-0`,children:[z(`p`,{class:`m-0 text-[0.68rem] font-extrabold uppercase tracking-[0.18em] text-teal-700`,children:`SmartSafeHub`}),z(`div`,{class:`mt-1 flex min-w-0 items-baseline gap-3`,children:[z(`h1`,{class:`m-0 truncate text-xl font-black tracking-tight text-slate-950 sm:text-2xl`,children:o}),z(`p`,{class:`m-0 hidden truncate text-sm text-slate-500 lg:block`,children:e})]})]}),z(`div`,{class:`flex shrink-0 items-center gap-2`,children:[z(`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:z(a===`dark`?un:ln,{class:`size-5`})}),z(`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?z(V,{class:`size-5 animate-spin`}):z(V,{class:`size-5`})})]})]})})}var _r=[{label:`Overview`,routes:[`home`,`activity`]},{label:`Network`,routes:[`network`,`wifi`,`iptv`,`devices`]},{label:`Security`,routes:[`safeshield`,`rules`]},{label:`System`,routes:[`account`,`system`,`settings`]}];function vr({route:e}){switch(e){case`home`:return z(Nt,{class:`size-5`});case`activity`:return z(Ht,{class:`size-5`});case`network`:return z(Bt,{class:`size-5`});case`wifi`:return z(Lt,{class:`size-5`});case`iptv`:return z(Vt,{class:`size-5`});case`devices`:return z(Mt,{class:`size-5`});case`safeshield`:return z(At,{class:`size-5`});case`rules`:return z(Pt,{class:`size-5`});case`account`:return z(qt,{class:`size-5`});case`system`:return z(Ft,{class:`size-5`});case`settings`:return z(It,{class:`size-5`})}}function yr({theme:e}){return z(e===`dark`?un:ln,{class:`size-5`})}function br(e){return e===`dark`?`라이트 모드`:`다크 모드`}function xr(e,t=!1){return`relative flex min-h-11 min-w-0 items-center rounded-xl py-2.5 text-sm font-extrabold no-underline transition ${t?`justify-center px-2`:`gap-3 px-3`} ${e?`bg-teal-50 text-teal-800 ring-1 ring-inset ring-teal-200`:`text-slate-600 hover:bg-slate-50 hover:text-slate-950`}`}function Sr({accountRegistered:e,collapsed:t=!1,route:n,updateCount:r,demo:i=!1}){return z(`div`,{class:t?`space-y-3`:`space-y-5`,children:_r.filter(e=>!i||e.routes.some(e=>hr.includes(e))).map((a,o)=>z(`section`,{"aria-label":a.label,class:t&&o>0?`border-t border-slate-100 pt-3`:void 0,children:[t?null:z(`p`,{class:`mb-2 px-3 text-[0.65rem] font-black uppercase tracking-[0.18em] text-slate-400`,children:a.label}),z(`div`,{class:`space-y-1`,children:a.routes.filter(e=>!i||hr.includes(e)).map(i=>{let a=mr[i],o=n===i;return z(`a`,{"aria-current":o?`page`:void 0,"aria-label":i===`account`&&e===!1?`${a.label} (계정 연결 필요)`:t?a.label:void 0,class:xr(o,t),href:a.hash,title:i===`account`&&e===!1?`SmartSafeHub 계정 연결 필요`:t?a.label:void 0,children:[z(`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:z(vr,{route:i})}),t?null:z(`span`,{class:`flex min-w-0 flex-1 items-center gap-2`,children:[z(`span`,{class:`truncate`,children:a.label}),i===`iptv`?z(`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`?z(`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?z(`span`,{"aria-label":`계정 연결 필요`,class:`ssh-account-nav-dot ${t?`ssh-account-nav-dot-collapsed`:``}`,"aria-hidden":`true`,title:`계정 연결 필요`}):null,i===`system`&&r>0?t?z(`span`,{"aria-label":`${r}개의 업데이트 유형`,class:`ssh-update-nav-badge`,"data-collapsed":`true`,children:r}):z(`span`,{class:`ssh-update-nav-badge`,"data-collapsed":`false`,children:r}):null]},i)})})]},a.label))})}function Cr({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]=M(!1),m=br(c);return N(()=>{p(!1)},[s]),N(()=>{if(!f)return;let e=e=>{e.key===`Escape`&&p(!1)};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[f]),z(v,{children:[z(`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:[z(`div`,{class:`flex min-h-16 items-center justify-between gap-3`,children:[z(`a`,{class:`flex min-w-0 items-center gap-3 no-underline`,href:`#home`,children:[z(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-950 text-teal-300`,children:z(At,{class:`size-5`})}),z(`span`,{class:`min-w-0`,children:[z(`strong`,{class:`block truncate text-sm font-black text-slate-950`,children:`SmartSafeHub`}),z(`span`,{class:`block truncate text-xs font-semibold text-slate-500`,children:l})]})]}),z(`div`,{class:`flex shrink-0 items-center gap-2`,children:[z(`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:z(yr,{theme:c})}),z(`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?z(V,{class:`size-5 animate-spin`}):z(V,{class:`size-5`})}),z(`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:z(f?on:rn,{class:`size-5`})})]})]}),f?z(`div`,{class:`ssh-mobile-menu border-t border-slate-100 py-4`,id:`smartsafehub-mobile-menu`,children:[z(Sr,{accountRegistered:e,demo:d,route:s,updateCount:u}),d?null:z(`div`,{class:`mt-5 space-y-1 border-t border-slate-100 pt-4`,children:z(`a`,{class:`${xr(!1)} text-rose-700 hover:bg-rose-50 hover:text-rose-800`,href:be(`/admin/logout`),children:[z(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg bg-rose-50 text-rose-600`,children:z(nn,{class:`size-5`})}),`로그아웃`]})})]}):null]}),z(`aside`,{class:`hidden min-h-screen border-r border-slate-200 bg-white md:flex md:flex-col`,"data-collapsed":t?`true`:`false`,children:z(`div`,{class:`sticky top-0 flex h-screen flex-col`,children:[z(`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:[z(`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:[z(`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:z(At,{class:`size-5`})}),t?null:z(`div`,{class:`min-w-0`,children:[z(`strong`,{class:`block truncate text-sm font-black tracking-tight text-slate-950`,children:`SmartSafeHub`}),z(`span`,{class:`block text-[0.68rem] font-bold uppercase tracking-[0.14em] text-slate-400`,children:`Home Gateway`})]})]}),z(`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:z(t?cn:sn,{class:`size-4`})})]}),z(`nav`,{"aria-label":`SmartSafeHub 메뉴`,class:`flex-1 overflow-y-auto py-5 ${t?`px-2`:`px-3`}`,id:`smartsafehub-desktop-navigation`,children:z(Sr,{accountRegistered:e,collapsed:t,demo:d,route:s,updateCount:u})}),d?null:z(`div`,{class:`border-t border-slate-100 ${t?`p-2`:`p-3`}`,children:z(`a`,{"aria-label":`SmartSafeHub에서 로그아웃`,class:`${xr(!1,t)} mt-1 text-rose-700 hover:bg-rose-50 hover:text-rose-800`,href:be(`/admin/logout`),title:t?`로그아웃`:void 0,children:[z(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg bg-rose-50 text-rose-600`,children:z(nn,{class:`size-5`})}),t?null:`로그아웃`]})})]})})]})}var wr=`smartsafehub.sidebar.collapsed`;function Tr(){if(typeof window>`u`)return!1;try{return window.localStorage.getItem(wr)===`1`}catch{return!1}}function Er({accountRegistered:e,children:t,loading:n,onRefresh:r,refreshing:i,route:a,updateCount:o,demo:s=!1}){let c=mr[a],[l,u]=M(Tr),[d,f]=M(fn);return N(()=>{try{window.localStorage.setItem(wr,l?`1`:`0`)}catch{}},[l]),N(()=>{pn(d),mn(d)},[d]),z(`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:[z(Cr,{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}),z(`div`,{class:`min-w-0 md:min-h-screen`,children:[z(gr,{description:c.description,loading:n,onRefresh:r,onToggleTheme:()=>f(e=>e===`dark`?`light`:`dark`),refreshing:i,theme:d,title:c.title}),z(`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 Dr({active:e,label:t,tabs:n}){return z(`nav`,{"aria-label":t,class:`mb-5 min-w-0`,role:`tablist`,children:z(`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 z(`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 G(e,t){return e instanceof L||e instanceof Error?e.message:t}function Or({active:e=!0,fallbackError:t,loader:n,pollInterval:r,refreshOnFocus:i=!1,refreshOnVisible:a=!1,staleTimeMs:o=3e4}){let[s,c]=M({data:null,error:null,loading:e,refreshing:!1}),l=P(!1),u=P(!0),d=P(null),f=P(null),p=P(null),m=P(0);N(()=>(u.current=!0,()=>{u.current=!1}),[]);let h=F((e=!1)=>{if(d.current)return d.current;u.current&&c(t=>({...t,error:null,loading:t.data===null,refreshing:e}));let r;return f.current=Date.now(),r=n().then(e=>{p.current=Date.now(),m.current=0,u.current&&c({data:e,error:null,loading:!1,refreshing:!1})}).catch(e=>{m.current+=1,u.current&&c(n=>({...n,error:G(e,t),loading:!1,refreshing:!1}))}).finally(()=>{d.current===r&&(d.current=null)}),d.current=r,r},[t,n]);N(()=>{e?l.current||(l.current=!0,s.data!==null&&p.current!==null&&Date.now()-p.current{if(!e||g==null||g<=0)return;let t=!1,n=null,r=()=>{n!==null&&(window.clearTimeout(n),n=null)},o=()=>{let e=Date.now();if(m.current>0&&f.current!==null){let t=Math.min(3e4,3e3*2**Math.min(m.current-1,4));return Math.max(0,f.current+t-e)}return p.current===null?0:Math.max(0,p.current+g-e)},s=()=>{r(),!(t||document.visibilityState===`hidden`)&&(n=window.setTimeout(()=>{n=null,!(t||document.visibilityState===`hidden`)&&(o()>0?s():h(!1).finally(s))},o()))},c=()=>{r(),!(t||document.visibilityState===`hidden`)&&(o()>0?s():h(!1).finally(s))},l=()=>{document.visibilityState===`hidden`?r():a?h(!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,g,h,i,a]);let _=F(()=>h(!0),[h]),v=F(e=>{u.current&&(p.current=Date.now(),m.current=0,c(t=>({...t,data:e,error:null,loading:!1,refreshing:!1})))},[]);return{...s,refresh:_,replaceData:v}}var kr=6e4;function Ar(e){let t=Or({active:e,fallbackError:`최근 활동을 불러오지 못했습니다.`,loader:()=>Cn(),pollInterval:kr,refreshOnFocus:!0}),[n,r]=M({savingCloudSync:!1,actionError:null,actionMessage:null}),i=F(async e=>{r({savingCloudSync:!0,actionError:null,actionMessage:null});try{let n=await wn(e);return t.replaceData(n),r({savingCloudSync:!1,actionError:null,actionMessage:e?`Cloud 활동 기록 전송을 켰습니다. 지금부터 발생하는 활동을 Cloud에 전송합니다.`:`Cloud 활동 기록 전송을 껐습니다. 로컬 최근 활동은 계속 기록됩니다.`}),!0}catch(e){return r({savingCloudSync:!1,actionError:G(e,`Cloud 활동 기록 설정을 변경하지 못했습니다.`),actionMessage:null}),!1}},[t.replaceData]),a=F(()=>{r(e=>({...e,actionError:null,actionMessage:null}))},[]);return{...t,...n,dismissActionFeedback:a,setCloudSyncEnabled:i}}var jr=`/tmp/smartsafehub/config-backup.tar.gz`,Mr=`/cgi-backup`,Nr=`/cgi-upload`;function Pr(){let e=window.__SMARTHUB_BOOTSTRAP__?.sessionId;if(!e)throw new L(`BOOTSTRAP_MISSING`,`LuCI 세션 정보를 찾을 수 없습니다.`);return e}function Fr(e){return(e?.match(/filename="?([^";]+)"?/i))?.[1]?.trim()||`backup-openwrt.tar.gz`}function Ir(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 Lr(){let e=ve(Mr),t=await fetch(e,{method:`POST`,credentials:`same-origin`,headers:{"Content-Type":`application/x-www-form-urlencoded;charset=UTF-8`},body:new URLSearchParams({sessionid:Pr()}).toString()});if(!t.ok)throw new L(`SYSTEM_BACKUP_DOWNLOAD_FAILED`,`설정 백업 생성에 실패했습니다. (HTTP ${t.status})`);let n=await t.blob();if(n.size<=0)throw new L(`SYSTEM_BACKUP_DOWNLOAD_EMPTY`,`설정 백업 파일이 비어 있습니다.`);Ir(Fr(t.headers.get(`Content-Disposition`)),n)}function Rr(e,t){let n;try{n=Pr()}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`,jr),o.append(`filedata`,e),a.open(`POST`,ve(Nr)),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 L(`SYSTEM_BACKUP_UPLOAD_FAILED`,`설정 백업 파일을 공유기에 업로드하지 못했습니다.`))}),a.addEventListener(`load`,()=>{if(a.status<200||a.status>=300){i(new L(`SYSTEM_BACKUP_UPLOAD_HTTP_ERROR`,`설정 백업 업로드가 HTTP ${a.status} 오류를 반환했습니다.`));return}let n;try{n=JSON.parse(a.responseText)}catch{i(new L(`SYSTEM_BACKUP_UPLOAD_INVALID_RESPONSE`,`설정 백업 업로드 응답을 확인하지 못했습니다.`));return}n.failure?i(new L(n.failure,n.message||`설정 백업 파일 업로드가 거부되었습니다.`)):(t?.(100),r({...n,name:e.name}))}),a.send(o)})}var zr=16777216;function Br(){let[e,t]=M(null),[n,r]=M(null),[i,a]=M(null),[o,s]=M(null),[c,l]=M(null),[u,d]=M(!1),f=F(async()=>{t(`download`),r(null),a(null);try{await Lr(),a(`현재 설정 백업을 다운로드했습니다.`)}catch(e){r(G(e,`설정 백업을 다운로드하지 못했습니다.`))}finally{t(null)}},[]),p=F(async e=>{if(r(null),a(null),s(null),d(!1),e.size<=0||e.size>zr)return r(`설정 백업 파일은 16MB 이하여야 합니다.`),!1;try{t(`upload`),l(0),await Rr(e,l),t(`validate`);let n=await Zn(e.name);return s(n),a(`백업 파일 검증이 완료되었습니다. 복원 내용을 확인해 주세요.`),!0}catch(e){return r(G(e,`설정 백업 파일을 검증하지 못했습니다.`)),!1}finally{t(null),l(null)}},[]),m=F(async()=>{t(`discard`),r(null),a(null);try{return await $n(),s(null),d(!1),a(`업로드한 백업 파일을 삭제했습니다.`),!0}catch(e){return r(G(e,`업로드한 백업 파일을 삭제하지 못했습니다.`)),!1}finally{t(null)}},[]),h=F(async()=>{if(!o)return r(`먼저 복원할 백업 파일을 업로드하고 검증해 주세요.`),!1;t(`restore`),r(null),a(null);try{let e=await Qn();return d(e.accepted&&e.rebooting),s(null),a(e.accepted?`설정을 복원했습니다. 공유기가 재부팅되며 네트워크 설정에 따라 다시 연결해야 할 수 있습니다.`:`설정 복원 요청이 접수되지 않았습니다.`),e.accepted}catch(e){return r(G(e,`설정 백업을 복원하지 못했습니다.`)),!1}finally{t(null)}},[o]);return{action:e,discard:m,dismissFeedback:F(()=>{r(null),a(null)},[]),download:f,error:n,message:i,restore:h,restoreAccepted:u,upload:p,uploadProgress:c,validated:o}}var Vr=15e3;function Hr(e,t=!0){return Or({active:e,fallbackError:`연결된 기기 목록을 불러오지 못했습니다.`,loader:Sn,...t?{pollInterval:Vr}:{}})}var Ur=`/tmp/smartsafehub/firmware.bin`,Wr=`/cgi-upload`;function Gr(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`,Ur),o.append(`filedata`,e);let s=ve(Wr);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:Ur,fileName:e.name,fileSize:e.size,readyState:a.readyState,status:a.status}),i(new L(`FIRMWARE_UPLOAD_FAILED`,`펌웨어 파일 업로드에 실패했습니다. (${s}, 네트워크 오류)`))}),a.addEventListener(`load`,()=>{if(a.status<200||a.status>=300){console.error(`[SmartSafeHub] firmware upload HTTP error`,{endpoint:s,destination:Ur,fileName:e.name,fileSize:e.size,readyState:a.readyState,status:a.status,response:a.responseText}),i(new L(`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:Ur,fileName:e.name,fileSize:e.size,readyState:a.readyState,status:a.status,response:a.responseText}),i(new L(`FIRMWARE_UPLOAD_INVALID_RESPONSE`,`펌웨어 업로드 응답을 확인하지 못했습니다. (${s})`));return}n.failure?i(new L(n.failure,n.message||`펌웨어 파일 업로드가 거부되었습니다.`)):(t?.(100),r({...n,name:e.name}))}),a.send(o)}):Promise.reject(new L(`BOOTSTRAP_MISSING`,`LuCI 세션 정보를 찾을 수 없습니다.`))}var Kr=3e5,qr=1e3,Jr=500,Yr=3e3,Xr=3e4,Zr=15e3,Qr=5e3;function $r(e){return new Promise(t=>window.setTimeout(t,e))}function ei(e){return e===`checking`||e===`downloading`||e===`verifying`}function ti(e=!0){let[t,n]=M(null),r=Or({active:e||ei(t??void 0),fallbackError:`펌웨어 업데이트 상태를 불러오지 못했습니다.`,loader:In,pollInterval:e=>ei(e?.phase)?qr:Kr,refreshOnFocus:!0,refreshOnVisible:ei(t??void 0)});N(()=>{n(r.data?.phase??null)},[r.data?.phase]);let[i,a]=M(null),[o,s]=M(null),[c,l]=M(null),[u,d]=M(null),[f,p]=M(!1),m=P(null);N(()=>()=>{m.current!==null&&window.clearTimeout(m.current)},[]);let h=F(e=>{n(e),r.data&&r.replaceData({...r.data,phase:e,lastError:null})},[r]),g=F(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(G(e,i))}},[h,r]),_=F(()=>g(`check`,`checking`,Ln,`펌웨어 업데이트 확인을 시작하지 못했습니다.`),[g]),v=F(()=>g(`prepare`,`downloading`,Rn,`펌웨어 다운로드를 시작하지 못했습니다.`),[g]),y=F(async()=>{let e=Date.now(),t=null;for(;Date.now()-e{a(`upload`),s(null),l(null),d(0);try{if(await Gr(e,d),a(`validate-upload`),!(await zn(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(G(e,`펌웨어 파일을 업로드하지 못했습니다.`)),!1}},[h,y]),x=F(()=>{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,Qr)};m.current=window.setTimeout(e,Zr)},[]),S=F(async e=>{a(`install`),s(null),l(null);try{if(!(await Bn(e)).accepted)throw Error(`펌웨어 설치 요청이 접수되지 않았습니다.`);return h(`flashing`),a(null),x(),!0}catch(e){return a(null),s(G(e,`펌웨어 설치를 시작하지 못했습니다.`)),!1}},[h,x]),C=F(async()=>{a(`discard`),s(null),l(null);try{if(!(await Vn()).accepted)throw Error(`준비된 펌웨어 정리 요청이 접수되지 않았습니다.`);return a(null),l(`준비된 펌웨어 파일을 삭제했습니다.`),await r.refresh(),!0}catch(e){return a(null),s(G(e,`준비된 펌웨어를 정리하지 못했습니다.`)),!1}},[r]),w=F(()=>{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 ni={"#home":`home`,"#activity":`activity`,"#network":`network`,"#network/internet":`network`,"#network/lan":`network`,"#wan":`network`,"#lan":`network`,"#wifi":`wifi`,"#iptv":`iptv`,"#devices":`devices`,"#safeshield":`safeshield`,"#rules":`rules`,"#account":`account`,"#system":`system`,"#settings":`settings`,"#settings/diagnostics":`settings`,"#settings/time":`settings`,"#settings/management":`settings`};function ri(){return ni[window.location.hash]??`home`}function ii(){let[e,t]=M(ri);return N(()=>{let e=()=>t(ri());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),e}function ai(e){return e===`#lan`||e===`#network/lan`?`lan`:`internet`}function oi(e){return e===`#settings/time`?`time`:e===`#settings/management`?`management`:`diagnostics`}function si(){let e=window.location.hash;return{network:ai(e),settings:oi(e)}}function ci(){let[e,t]=M(si);return N(()=>{let e=()=>t(si());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),e}var li=6e4,ui=1e3,di=[400,800,1200,1600],fi=[400,800,1200,2e3,4e3];function pi(e){return new Promise(t=>window.setTimeout(t,e))}function mi(e){let t=Or({active:e,fallbackError:`장치 진단 상태를 불러오지 못했습니다.`,loader:Mn,pollInterval:e=>e?.generatedAt?li:ui,refreshOnFocus:!0}),n=P(0),[r,i]=M({running:!1,savingReporter:!1,actionError:null,actionMessage:null}),a=F(async()=>{i({running:!0,savingReporter:!1,actionError:null,actionMessage:null});try{let e=t.data?.generatedAt??0;if(!(await Nn()).accepted)throw Error(`장치가 진단 요청을 접수하지 않았습니다.`);let n=t.data;for(let t of di)if(await pi(t),n=await Mn(),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:G(e,`장치 상태 진단을 실행하지 못했습니다.`),actionMessage:null}),!1}},[t.data,t.replaceData]),o=F(async(e,r)=>{for(let i of fi){if(await pi(i),n.current!==r)return;try{let i=await Mn();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=F(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 Pn(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:G(e,`원격 상태 보고 설정을 변경하지 못했습니다.`),actionMessage:null})),!1}},[o,t.data,t.replaceData]),c=F(()=>{i(e=>({...e,actionError:null,actionMessage:null}))},[]);return{...t,...r,dismissActionFeedback:c,runDiagnostic:a,setReporterEnabled:s}}function hi(e){let t=Or({active:e,fallbackError:`IPTV 설정을 불러오지 못했습니다.`,loader:sr,refreshOnFocus:!0}),[n,r]=M(!1),[i,a]=M(null),o=F(async e=>{r(!0),a(null);try{let n=await cr(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:G(e,`IPTV 설정을 적용하지 못했습니다.`)}),!1}},[t.replaceData]),s=F(()=>a(null),[]);return{...t,saving:n,feedback:i,save:o,dismissFeedback:s}}var gi=4e3;function _i(e){let t=Or({active:e,fallbackError:`내부 네트워크 설정을 불러오지 못했습니다.`,loader:ir}),n=P(e),[r,i]=M({action:null,feedback:null});N(()=>{n.current=e},[e]);let a=F((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()},gi)):i({action:null,feedback:{kind:`success`,message:`변경된 내부 네트워크 설정이 없습니다.`}})},[t.refresh,t.replaceData]),o=F(async e=>{i({action:`saving`,feedback:null});try{let t=await ar(e);return a(t,!1),!0}catch(e){return i({action:null,feedback:{kind:`error`,message:G(e,`내부 네트워크 설정을 저장하지 못했습니다.`)}}),!1}},[a]),s=F(async()=>{i({action:`auto`,feedback:null});try{let e=await or();return a(e,!0),!0}catch(e){return i({action:null,feedback:{kind:`error`,message:G(e,`추천 내부 네트워크를 적용하지 못했습니다.`)}}),!1}},[a]),c=F(()=>{i(e=>({...e,feedback:null}))},[]);return{...t,...r,save:o,applyRecommendation:s,dismissFeedback:c}}var vi=5e3;function yi(e){let t=Or({active:e,fallbackError:`인터넷 연결 설정을 불러오지 못했습니다.`,loader:tr}),n=P(e),[r,i]=M({action:null,feedback:null});N(()=>{n.current=e},[e]);let a=F(()=>{window.setTimeout(()=>{n.current&&t.refresh()},vi)},[t.refresh]),o=F(async e=>{i({action:`saving`,feedback:null});try{let n=await nr(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:G(e,`인터넷 연결 설정을 저장하지 못했습니다.`)}}),!1}},[t.replaceData,a]),s=F(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:G(e,`WAN 재연결을 시작하지 못했습니다.`)}})}},[t.replaceData,a]),c=F(()=>{i(e=>({...e,feedback:null}))},[]);return{...t,...r,save:o,reconnect:s,dismissFeedback:c}}var bi=`safeshield`,xi=`smartsafehub`;function Si(e,t){return typeof e==`string`&&e.length>0?e:t}function Ci(e){return typeof e==`string`?e:``}function wi(e){return typeof e==`number`&&Number.isFinite(e)?e:0}function K(e){return e===!0}function Ti(e){return Array.isArray(e)?e.length:0}function Ei(e){return Array.isArray(e)?e.filter(e=>typeof e==`string`):[]}function Di(e){return typeof e==`object`&&e&&!Array.isArray(e)?e:{}}function Oi(e){let t=Di(e.error),n=Ci(t.code)||`UNKNOWN_ERROR`,r=Ci(t.message)||`SafeShield API 요청을 처리하지 못했습니다.`;return new L(`SAFESHIELD_${n.toUpperCase()}`,r)}async function ki(e,t={}){let n=await Et(bi,e,t),r=Di(n);if(r.ok===!1)throw Oi(r);return n}async function Ai(e,t,n,r=t){try{return await R(xi,e,t)}catch(e){if(e instanceof L&&[`RPC_PERMISSION_DENIED`,`UBUS_3`,`UBUS_6`].includes(e.code))return ki(n,r);throw e}}function ji(){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 Mi(e){let t=Di(e);if(Object.keys(t).length===0)return ji();let n=Di(t.schema),r=Di(t.totals),i=Array.isArray(t.hourly)?t.hourly.map(e=>Di(e)).map(e=>({bucketStart:wi(e.bucket_start),queries:wi(e.queries),blocked:wi(e.blocked)})).filter(e=>e.bucketStart>0).sort((e,t)=>e.bucketStart-t.bucketStart):[],a=Array.isArray(t.devices)?t.devices.map(e=>Di(e)).map(e=>{let t=Ci(e.ip),n=Ci(e.mac);return{id:Ci(e.id)||n||(t?`ip:${t}`:``),mac:n,ip:t,hostname:Ci(e.hostname),identified:K(e.identified),queries:wi(e.queries),blocked:wi(e.blocked)}}).filter(e=>e.id.length>0):[];return{enabled:K(t.enabled),collectorRunning:K(t.collector_running),available:t.available!==!1,schemaVersion:wi(n.version),volatile:t.volatile!==!1,startedAt:wi(t.started_at),updatedAt:wi(t.updated_at),retentionHours:wi(t.retention_hours),totals:{queries:wi(r.queries),blocked:wi(r.blocked)},hourly:i,deviceLimit:wi(t.device_limit),devicesTruncated:K(t.devices_truncated),devices:a}}function Ni(){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 Pi(e){let t=Di(e);if(Object.keys(t).length===0)return Ni();let n=Di(t.summary),r=Di(t.runtime),i=Di(t.entitlement),a=Di(t.artifact),o=Di(t.local_overrides),s=Di(t.blocklist),c=Di(t.health),l=Di(t.timestamps);return{available:!0,version:Si(t.version,null),enabled:K(t.enabled),active:K(t.active),status:Si(t.status,`unknown`)??`unknown`,stage:Si(t.stage,null),summary:{label:Si(n.label,`Unknown`)??`Unknown`,message:Si(n.message,`SafeShield status is unknown`)??`SafeShield status is unknown`,severity:Si(n.severity,`warning`)??`warning`},runtime:{refreshdRunning:K(r.refreshd_running),dnsmasqRunning:K(r.dnsmasq_running),dnsRuntimeOk:K(r.dns_runtime_ok),lastResult:Si(r.last_result,null),lastErrorCode:Si(r.last_error_code,null)},entitlement:{plan:Si(i.plan,null),status:Si(i.status,null)},artifact:{resolved:K(a.resolved),tier:Si(a.tier,null),version:Si(a.version,null),uniqueDomains:wi(a.unique_domains),rules:wi(a.rules)},localOverrides:{enabled:K(o.enabled),allowlistPath:Si(o.allowlist_path,null),blocklistPath:Si(o.blocklist_path,null)},blocklist:{installed:K(s.installed),validLineCount:wi(s.valid_line_count),fileSizeKb:wi(s.file_size_kb),verificationOk:K(s.verification_ok)},health:{overall:Si(c.overall,`unknown`)??`unknown`},issueCounts:{warnings:Ti(t.warnings),errors:Ti(t.errors)},timestamps:{lastAttempt:wi(l.last_attempt),lastSuccess:wi(l.last_success),lastFailure:wi(l.last_failure),lastLocalApply:wi(l.last_local_apply),lastLocalApplyFailure:wi(l.last_local_apply_failure),nextRefreshAt:wi(l.next_refresh_at),refreshIntervalS:wi(l.refresh_interval_s)}}}function Fi(e){let t=Di(Di(e).values);return{enabled:K(t.enabled),applyLocalOverrides:K(t.apply_local_overrides)}}function Ii(e){let t=Di(e);return{requested:K(t.requested),accepted:K(t.accepted),reason:Ci(t.reason)}}async function Li(){try{return Mi(await ki(`statistics`))}catch(e){if(e instanceof L&&[`UBUS_3`,`UBUS_4`,`UBUS_5`].includes(e.code))return ji();throw e}}async function Ri(e){let t=await Ai(`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:K(t.restarted),reconciled:K(t.reconciled)}}async function zi(){try{return Pi(await ki(`status`))}catch(e){if(e instanceof L&&[`UBUS_3`,`UBUS_4`,`UBUS_5`].includes(e.code))return Ni();throw e}}async function Bi(){return Fi(await ki(`config`))}async function Vi(e){let t=await Ai(`safeshield_set_enabled`,{enabled:e},`set_enabled`);return{changed:K(t.changed),accepted:K(t.accepted),targetEnabled:K(t.target_enabled),reconciled:K(t.reconciled)}}async function Hi(){let e=await Ai(`safeshield_refresh`,{},`refresh`);return{accepted:K(e.accepted),reason:Ci(e.reason),status:Pi(e.status)}}async function Ui(){let[e,t]=await Promise.all([ki(`rules_list`),Bi()]),n=Ei(e.allow),r=Ei(e.block);return{allow:n,block:r,counts:{allow:n.length,block:r.length,total:n.length+r.length},safeshieldEnabled:t.enabled,applyLocalOverrides:t.applyLocalOverrides}}async function Wi(e,t,n,r=!0){let i=await Ai(e===`rule_add`?`safeshield_rule_add`:`safeshield_rule_delete`,{action:t,domain:n,refresh:r},e),a=K(e===`rule_add`?i.added:i.deleted);return{action:t,domain:Ci(i.domain)||n,changed:a,refresh:Ii(i.refresh),rules:await Ui()}}function Gi(e,t,n=!0){return Wi(`rule_add`,e,t,n)}function Ki(e,t,n=!0){return Wi(`rule_delete`,e,t,n)}var qi=4500;function Ji(e,t){let[n,r]=M({action:null,error:null,message:null}),i=P([]),a=P([]),o=P(null),s=F(()=>{for(let e of i.current)window.clearTimeout(e);i.current=[]},[]),c=F(()=>{for(let e of a.current)window.clearTimeout(e);a.current=[]},[]),l=F(()=>{o.current!==null&&(window.clearTimeout(o.current),o.current=null)},[]),u=F(e=>{l(),r({action:e,error:null,message:null})},[l]),d=F(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})},qi)},[l]),f=F(t=>{s(),i.current=t.map(t=>window.setTimeout(()=>{e()},t))},[s,e]),p=F(e=>{c(),a.current=e.map(e=>window.setTimeout(()=>{t()},e))},[c,t]);N(()=>()=>{s(),c(),l()},[l,c,s]);let m=F(async t=>{u(t?`enable`:`disable`);try{let n=await Vi(t);d(n.changed?t?`SafeShield 보호 활성화 요청을 적용했습니다.`:`SafeShield 보호 비활성화 요청을 적용했습니다.`:t?`SafeShield 보호가 이미 활성화 상태입니다.`:`SafeShield 보호가 이미 비활성화 상태입니다.`),await e(),f([800,2e3,5e3])}catch(e){r({action:null,error:G(e,`SafeShield 작업을 수행하지 못했습니다.`),message:null})}},[u,e,f,d]),h=F(async e=>{u(e?`statistics-enable`:`statistics-disable`);try{let n=await Ri(e);await t(),d(n.changed?n.reconciled?e?`차단 통계 수집을 활성화했습니다.`:`차단 통계 수집을 비활성화했습니다.`:`차단 통계 설정을 저장했습니다.`:e?`차단 통계 수집이 이미 활성화되어 있습니다.`:`차단 통계 수집이 이미 비활성화되어 있습니다.`),n.changed&&p([500,1500])}catch(e){r({action:null,error:G(e,`차단 통계 설정을 변경하지 못했습니다.`),message:null})}},[u,t,p,d]),g=F(async()=>{u(`refresh`);try{await Hi(),r({action:null,error:null,message:null}),f([700,2500,6e3,12e3])}catch(e){r({action:null,error:G(e,`SafeShield 작업을 수행하지 못했습니다.`),message:null})}},[u,f]),_=F(()=>{l(),r(e=>({...e,error:null,message:null}))},[l]);return{...n,dismissFeedback:_,refreshBlocklist:g,setEnabled:m,setStatisticsEnabled:h}}var Yi=500,Xi=45e3;function Zi(e){return new Promise(t=>window.setTimeout(t,e))}function Qi(e){return e===`allow`?`허용`:`차단`}function $i(e,t){if(!t.changed)return e===`add`?t.action===`allow`?`이미 허용 목록에 있는 도메인입니다.`:`이미 차단 목록에 있는 도메인입니다.`:`이미 삭제된 규칙입니다.`;let n=Qi(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 ea(e,t){let n=e===`add`?`추가`:`삭제`;return`${t.domain} ${Qi(t.action)} 규칙 ${n}를 저장했습니다. 로컬 규칙을 DNS에 적용하고 있습니다…`}function ta(e,t){let n=e===`add`?`추가`:`삭제`;return`${t.domain} ${Qi(t.action)} 규칙 ${n}와 DNS 적용이 완료되었습니다.`}async function na(e){let t=Date.now()+Xi,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 Zi(Yi)}return{kind:`timeout`,status:n}}function ra(e){let t=Or({active:e,fallbackError:`사용자 규칙을 불러오지 못했습니다.`,loader:Ui}),[n,r]=M({action:null,error:null,feedback:null}),i=P(0),a=F(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 zi(),i=e===`add`?await Gi(n,a,!0):await Ki(n,a,!0);if(t.replaceData(i.rules),o?.(),!(i.changed&&i.rules.safeshieldEnabled&&i.rules.applyLocalOverrides))return c({action:null,error:null,feedback:$i(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:ea(e,i)});let s;try{s=await na(r)}catch{return c({action:null,error:`규칙은 저장되었지만 DNS 적용 상태를 확인하지 못했습니다. SafeShield 상태를 확인해 주세요.`,feedback:null}),!0}return s.kind===`success`?(c({action:null,error:null,feedback:ta(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:G(e,`사용자 규칙을 변경하지 못했습니다.`),feedback:null}),!1}},[t.replaceData]),o=F((e,t,n)=>a(`add`,e,t,n),[a]),s=F((e,t)=>a(`delete`,e,t),[a]),c=F(async()=>{r(e=>({...e,error:null})),await t.refresh()},[t.refresh]),l=F(()=>{r(e=>({...e,feedback:null}))},[]);return{...t,...n,error:n.error??t.error,addRule:o,deleteRule:s,dismissFeedback:l,refresh:c}}var ia=6e4;function aa(e,t=!0){return Or({active:e,fallbackError:`SafeShield 통계를 불러오지 못했습니다.`,loader:Li,...t?{pollInterval:ia}:{}})}var oa=[{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`]}],sa=new Map;oa.forEach((e,t)=>{e.stages.forEach(e=>sa.set(e,t))});function ca(e,t){return e===`running`||t!==null&&sa.has(t)}function la(e){let t=e===null?0:sa.get(e)??0,n=oa[t];return{description:n.description,label:n.label,number:t+1,total:oa.length}}function ua(e){return(e??``).trim().toLowerCase()}function da(e,t){let n=ua(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=la(t);return r.number===2?`최신 차단 목록 정보를 확인하는 중 문제가 발생했습니다. 인터넷 연결을 확인한 뒤 다시 갱신해 주세요.`:r.number===3?`차단 목록을 다운로드하는 중 문제가 발생했습니다. 인터넷 연결을 확인한 뒤 다시 갱신해 주세요.`:r.number===4?`사용자 규칙을 적용하는 중 문제가 발생했습니다. 사용자 규칙을 확인한 뒤 다시 갱신해 주세요.`:r.number===5?`새 보호 규칙을 적용하는 중 문제가 발생했습니다. 잠시 후 다시 갱신해 주세요.`:r.number===6?`새 차단 목록의 보호 상태를 확인하지 못했습니다. 다시 갱신해 주세요.`:`차단 목록 갱신을 완료하지 못했습니다. 잠시 후 다시 갱신해 주세요.`}var fa=3e3;function pa(e){return e?ca(e.status,e.stage):!1}function ma(e){return Or({active:e,fallbackError:`SafeShield 상태를 불러오지 못했습니다.`,loader:zi,pollInterval:e=>pa(e)?fa:null})}var ha=3e5,ga=1e3,_a=3e3,va=`luci-app-smartsafehub`;function ya(e=!0){let[t,n]=M(null),r=Or({active:e||t===`checking`||t===`installing`,fallbackError:`소프트웨어 업데이트 상태를 불러오지 못했습니다.`,loader:Fn,pollInterval:e=>e?.phase===`checking`?ga:e?.phase===`installing`?_a:ha,refreshOnFocus:!0,refreshOnVisible:t===`checking`||t===`installing`});N(()=>{n(r.data?.phase??null)},[r.data?.phase]);let[i,a]=M(null),[o,s]=M(null),[c,l]=M(null),u=P(!1),d=P(void 0);N(()=>{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===va)?.installedVersion,i=window.__SMARTHUB_BOOTSTRAP__?.assetVersion;n&&i&&n!==i&&(u.current=!0,window.location.reload())},[r.data]);let f=F(e=>{n(e),r.data&&r.replaceData({...r.data,phase:e,lastError:null})},[r]),p=F(async()=>{a(`check`),s(null),l(null);try{if(!(await Hn()).accepted)throw Error(`업데이트 확인 요청이 접수되지 않았습니다.`);f(`checking`),a(null),window.setTimeout(()=>void r.refresh(),500)}catch(e){a(null),s(G(e,`업데이트 확인을 시작하지 못했습니다.`))}},[f,r]),m=F(async()=>{a(`install`),s(null),l(null);try{if(!(await Un()).accepted)throw Error(`업데이트 설치 요청이 접수되지 않았습니다.`);f(`installing`),a(null),window.setTimeout(()=>void r.refresh(),750)}catch(e){a(null),s(G(e,`업데이트 설치를 시작하지 못했습니다.`))}},[f,r]),h=F(async e=>{a(`settings`),s(null),l(null);try{let t=await Wn(e);return r.data&&r.replaceData({...r.data,settings:t}),a(null),l(`자동 업데이트 설정을 저장했습니다.`),!0}catch(e){return a(null),s(G(e,`자동 업데이트 설정을 저장하지 못했습니다.`)),!1}},[r]),g=F(()=>{s(null),l(null)},[]);return{...r,action:i,actionError:o,message:c,check:p,dismissFeedback:g,install:m,saveSettings:h}}var ba=6e4;function xa(e){return Or({active:e,fallbackError:`장치 상태를 불러오지 못했습니다.`,loader:Tn,pollInterval:ba})}function Sa(e){let t=Or({active:e,fallbackError:`예약 재부팅 설정을 불러오지 못했습니다.`,loader:Yn,refreshOnFocus:!0}),[n,r]=M({saving:!1,saveError:null,saveMessage:null}),i=F(async e=>{r({saving:!0,saveError:null,saveMessage:null});try{let n=await Xn(e);return t.replaceData(n),r({saving:!1,saveError:null,saveMessage:e.enabled?`예약 재부팅 일정을 저장했습니다.`:`예약 재부팅을 껐습니다.`}),!0}catch(e){return r({saving:!1,saveError:G(e,`예약 재부팅 설정을 저장하지 못했습니다.`),saveMessage:null}),!1}},[t.replaceData]),a=F(()=>{r(e=>({...e,saveError:null,saveMessage:null}))},[]);return{...t,...n,dismissSaveFeedback:a,saveSettings:i}}function Ca(e){return e.trim().toLowerCase().replace(/[^a-z0-9._-]+/g,`-`)||`openwrt`}function wa(e,t){let n=new Date(t*1e3).toISOString().replace(/[:.]/g,`-`);return`smartsafehub-diagnostics-${Ca(e)}-${n}.json`}function Ta(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 Ea(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 Da(){return{networks:[],totalClients:0,guest:null}}function Oa(){return{enabled:null,status:`unavailable`,lastResult:null,validLineCount:0,blocklistInstalled:!1,dnsmasqOk:!1}}async function ka(e){if(!e)throw Error(`시스템 상태가 준비되지 않았습니다. 새로고침한 뒤 다시 시도해 주세요.`);let[t,n,r]=await Promise.allSettled([dr(),zi(),Mn()]);return{generatedAt:Math.floor(Date.now()/1e3),health:r.status===`fulfilled`?r.value:null,system:e,wifi:t.status===`fulfilled`?t.value:Da(),safeshield:n.status===`fulfilled`?Ea(n.value):Oa()}}function Aa(e){let[t,n]=M({action:null,error:null,message:null,rebootAccepted:!1}),r=F(async()=>{n(e=>({...e,action:`diagnostics`,error:null,message:null}));try{let t=await ka(e);Ta(wa(t.system.device.hostname,t.generatedAt),t),n(e=>({...e,action:null,message:`진단 정보 파일을 만들었습니다.`}))}catch(e){n(t=>({...t,action:null,error:G(e,`시스템 작업을 완료하지 못했습니다.`)}))}},[e]),i=F(async()=>{n(e=>({...e,action:`reboot`,error:null,message:null}));try{let e=await er();n({action:null,error:null,message:e.accepted?`재부팅을 시작했습니다. 잠시 후 공유기에 다시 연결해 주세요.`:`재부팅 요청이 접수되지 않았습니다.`,rebootAccepted:e.accepted})}catch(e){n(t=>({...t,action:null,error:G(e,`시스템 작업을 완료하지 못했습니다.`)}))}},[]),a=F(()=>{n(e=>({...e,error:null,message:null}))},[]);return{...t,dismissFeedback:a,downloadDiagnostics:r,reboot:i}}function ja(e){let t=Or({active:e,fallbackError:`시간대 설정을 불러오지 못했습니다.`,loader:Gn,refreshOnFocus:!0}),[n,r]=M({saving:!1,syncing:!1,saveError:null,saveMessage:null}),i=F(async e=>{r({saving:!0,syncing:!1,saveError:null,saveMessage:null});try{let n=await qn(e);return t.replaceData(n),r({saving:!1,syncing:!1,saveError:null,saveMessage:`시간대를 저장하고 시스템 시간 설정에 적용했습니다.`}),!0}catch(e){return r({saving:!1,syncing:!1,saveError:G(e,`시간대 설정을 저장하지 못했습니다.`),saveMessage:null}),!1}},[t.replaceData]),a=F(async()=>{r({saving:!1,syncing:!0,saveError:null,saveMessage:null});try{await Jn(),await new Promise(e=>{window.setTimeout(e,1500)});let e=await Gn();return t.replaceData(e),r({saving:!1,syncing:!1,saveError:null,saveMessage:`NTP 시간 동기화를 요청하고 장치 시간을 다시 확인했습니다.`}),!0}catch(e){return r({saving:!1,syncing:!1,saveError:G(e,`시간을 동기화하지 못했습니다.`),saveMessage:null}),!1}},[t.replaceData]),o=F(()=>{r(e=>({...e,saveError:null,saveMessage:null}))},[]);return{...t,...n,dismissSaveFeedback:o,saveTimezone:i,syncTime:a}}var Ma=5e3,Na=5e3;function Pa(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 Fa(e){let[t,n]=M(null),[r,i]=M(null),[a,o]=M(null),[s,c]=M(e),[l,u]=M(!1),[d,f]=M(!1),p=P(null),m=P(0),h=P(!0),g=F((e,t)=>!h.current||t!==m.current?!1:(n(e),i(null),c(!1),!0),[]),_=F(()=>{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 An();return g(e,t)&&o(null),e}catch{let e=await kn().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=F(async()=>{let e=++m.current;f(!0);try{let t=await jn();return g(t,e)&&o(null),t}finally{f(!1)}},[g]);N(()=>{if(!e)return;let t=!0;h.current=!0;let n=++m.current;return c(!0),i(null),o(null),kn().then(e=>{t&&(g(e,n)&&c(!1),_())}).catch(()=>{t&&(n===m.current&&(i(`기기 등록 상태를 확인하지 못했습니다.`),c(!1)),_())}),()=>{t=!1,m.current+=1}},[e,_,g]),N(()=>{if(!e)return;let t=!0,n=window.setInterval(()=>{if(document.visibilityState===`hidden`||p.current)return;let e=++m.current;kn().then(n=>{t&&g(n,e)&&o(null)}).catch(()=>{})},Na);return()=>{t=!1,window.clearInterval(n)}},[e,g]),N(()=>()=>{h.current=!1},[]);let y=Qe(()=>Pa(t),[t]);return N(()=>{if(!e||!y)return;let t=!0,n=0,r=async()=>{await _(),t&&(n=window.setTimeout(()=>void r(),Ma))};return n=window.setTimeout(()=>void r(),Ma),()=>{t=!1,window.clearTimeout(n)}},[e,_,y]),{data:t,error:r,loading:s,pairingBusy:d,refresh:_,refreshing:l,requestPairing:v,syncError:a}}var Ia=6e4;function La(e,t){let[n,r]=M(null),i=P(0);return N(()=>{e&&t!==null&&(i.current+=1,r(t))},[e,t]),N(()=>{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(),Ia),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 Ra=[2e3,3e3];function za(e){return new Promise(t=>window.setTimeout(t,e))}function Ba(e){let t=Or({active:e,fallbackError:`Wi-Fi 설정을 처리하지 못했습니다.`,loader:dr}),n=P(!0),r=P(e),[i,a]=M({updatingSection:null,feedback:null});N(()=>(n.current=!0,()=>{n.current=!1}),[]),N(()=>{r.current=e},[e]);let o=F(async()=>{for(let e of Ra){if(await za(e),!n.current||!r.current)return;await t.refresh()}},[t.refresh]),s=F(async e=>{a({updatingSection:e.section,feedback:null});try{let n=await fr(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 L&&[`NETWORK_ERROR`,`RPC_TIMEOUT`].includes(e.code);return a({updatingSection:null,feedback:{kind:t?`warning`:`error`,message:t?`Wi-Fi가 다시 시작되면서 연결이 끊어졌을 수 있습니다. 변경한 SSID로 다시 연결한 뒤 화면을 새로고침해 주세요.`:G(e,`Wi-Fi 설정을 처리하지 못했습니다.`)}}),!1}},[o,t.replaceData,t.data]),c=F(async e=>{a({updatingSection:`ssh_guest`,feedback:null});try{let n=await pr(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 L&&[`NETWORK_ERROR`,`RPC_TIMEOUT`].includes(e.code);return a({updatingSection:null,feedback:{kind:t?`warning`:`error`,message:t?`무선 네트워크가 다시 시작되어 연결이 끊어졌을 수 있습니다. 다시 연결한 뒤 확인해 주세요.`:G(e,`게스트 Wi-Fi 설정을 처리하지 못했습니다.`)}}),!1}},[t.refresh,o]),l=F(()=>{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 z(`div`,{class:`ssh-section-heading mb-4 min-w-0`,children:[z(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.18em] text-teal-700`,children:n}),z(`div`,{class:`mt-2 flex min-w-0 flex-wrap items-center justify-between gap-3`,children:[z(`h2`,{class:`m-0 min-w-0 text-xl font-black tracking-tight text-slate-950`,id:r,children:i}),e?z(`div`,{class:`shrink-0`,children:e}):null]}),z(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500`,children:t})]})}var Va=new Intl.NumberFormat(`ko-KR`),Ha=new Intl.DateTimeFormat(`ko-KR`,{dateStyle:`medium`,timeStyle:`short`});function Ua(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 Wa(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?qa(e):Ja(e,t)}function Xa(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 Za(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 Ha.format(new Date(e*1e3))}}function Qa(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 $a(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 eo(e,t){let n=e.metadata[t];return typeof n==`string`&&n.trim().length>0?n.trim():null}function to(e,t){let n=e.metadata[t];return typeof n==`number`&&Number.isFinite(n)?n:null}function no(e,t){let n=e.metadata[t];return typeof n==`boolean`?n:null}function ro(e,t){let n=eo(e,`error_code`);return n?`${t} (오류 코드: ${n})`:t}function io(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 ao(e){let t=eo(e,`from_version`),n=eo(e,`to_version`);return t&&n?`${t} → ${n}`:n?`${n} 버전이 적용되었습니다.`:null}function oo(e){return e?e.toUpperCase():null}function so(e){switch(e){case`dhcp`:return`자동 IP(DHCP)`;case`pppoe`:return`PPPoE`;case`static`:return`고정 IPv4`;default:return e.toUpperCase()}}function co(e){switch(e.eventType){case`system.booted`:return{title:`공유기 부팅 감지`,description:`공유기가 켜지거나 재시작된 것을 확인했습니다.`};case`network.internet.disconnected`:return{title:`인터넷 연결 끊김 감지`,description:`공유기에서 인터넷 연결을 확인할 수 없었습니다.`};case`network.internet.recovered`:{let t=to(e,`downtime_seconds`);return{title:`인터넷 연결 복구`,description:t!==null&&t>0?`약 ${io(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=to(e,`domain_count`),n=eo(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:ro(e,`차단 목록을 업데이트하지 못했습니다.`)};case`safeshield.statistics.enabled`:return{title:`SafeShield 차단 통계 수집 활성화`,description:`기기별 차단 통계 수집을 활성화했습니다.`};case`safeshield.statistics.disabled`:return{title:`SafeShield 차단 통계 수집 비활성화`,description:`기기별 차단 통계 수집을 비활성화했습니다.`};case`safeshield.rule.added`:return{title:eo(e,`rule_action`)===`allow`?`사용자 허용 규칙 추가`:`사용자 차단 규칙 추가`,description:`SafeShield 사용자 규칙이 변경되었습니다.`};case`safeshield.rule.removed`:return{title:eo(e,`rule_action`)===`allow`?`사용자 허용 규칙 삭제`:`사용자 차단 규칙 삭제`,description:`SafeShield 사용자 규칙이 변경되었습니다.`};case`settings.scheduled_reboot.updated`:{let t=no(e,`enabled`);return{title:`예약 재부팅 설정 변경`,description:t===!0?`예약 재부팅 설정을 저장하고 새 일정에 적용했습니다.`:t===!1?`예약 재부팅을 비활성화했습니다.`:`예약 재부팅 설정을 변경했습니다.`}}case`settings.software_updates.updated`:return{title:`관리 소프트웨어 업데이트 설정 변경`,description:no(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=eo(e,`zonename`);return{title:`시간대 설정 변경`,description:t?`공유기 시간대를 ${t}(으)로 변경했습니다.`:`공유기 시간대를 변경했습니다.`}}case`settings.wifi.updated`:return{title:`Wi-Fi 설정 변경`,description:`Wi-Fi 설정을 저장하고 무선 네트워크에 적용했습니다.`};case`settings.wan.updated`:{let t=eo(e,`protocol`);return{title:`인터넷 설정 변경`,description:t?`${so(t)} 방식으로 WAN 설정을 변경했습니다.`:`WAN 인터넷 연결 설정을 변경했습니다.`}}case`settings.lan.updated`:return{title:`LAN 설정 변경`,description:`내부 네트워크 및 DHCP 설정을 변경했습니다.`};case`settings.iptv.updated`:{let t=no(e,`enabled`),n=eo(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:ao(e)};case`software.update.failed`:return{title:`관리 소프트웨어 업데이트 실패`,description:ro(e,`관리 소프트웨어를 업데이트하지 못했습니다.`)};case`firmware.update.started`:return{title:`펌웨어 업데이트 시작`,description:`펌웨어 설치를 시작했습니다. 완료 과정에서 공유기가 재부팅됩니다.`};case`firmware.update.failed`:return{title:`펌웨어 업데이트 실패`,description:ro(e,`펌웨어 설치 준비 또는 검증에 실패했습니다.`)};case`license.activated`:{let t=oo(eo(e,`to_plan`));return{title:`요금제 활성화`,description:t?`${t} 요금제가 활성화되었습니다.`:`요금제가 활성화되었습니다.`}}case`license.changed`:{let t=oo(eo(e,`from_plan`)),n=oo(eo(e,`to_plan`));return{title:`요금제 변경`,description:t&&n?`${t} → ${n}`:n?`${n} 요금제로 변경되었습니다.`:`요금제 상태가 변경되었습니다.`}}case`license.cleared`:return{title:`계정 권한 해제`,description:eo(e,`reason`)===`manual`?`이 공유기에서 계정 권한 연결을 해제했습니다.`:`서버의 계정 권한 상태가 반영되어 이 공유기의 권한 연결이 해제되었습니다.`};case`health.issue.started`:{let t=to(e,`warning_count`)??0,n=to(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=to(e,`warning_count`)??0,n=to(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 lo(e){return e===`error`?{icon:z(U,{class:`size-4`}),surface:`bg-rose-50 text-rose-700 ring-rose-200`}:e===`warning`?{icon:z(U,{class:`size-4`}),surface:`bg-amber-50 text-amber-800 ring-amber-200`}:e===`success`?{icon:z(H,{class:`size-4`}),surface:`bg-emerald-50 text-emerald-700 ring-emerald-200`}:{icon:z(Ht,{class:`size-4`}),surface:`bg-slate-100 text-slate-600 ring-slate-200`}}function uo(e){let t=new Date(e*1e3);return new Intl.DateTimeFormat(`ko-KR`,{hour:`2-digit`,minute:`2-digit`,hour12:!1}).format(t)}function fo(e){let t=new Date(e*1e3);return`${t.getFullYear()}-${t.getMonth()+1}-${t.getDate()}`}function po(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 mo({compact:e=!1,events:t}){if(t.length===0)return z(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 px-4 py-5 text-center`,children:[z(Ht,{class:`mx-auto size-5 text-slate-400`}),z(`p`,{class:`mt-2 mb-0 text-sm font-extrabold text-slate-700`,children:`아직 기록된 활동이 없습니다.`}),z(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`상태가 실제로 변경되면 이곳에 시간순으로 기록됩니다.`})]});let n=``;return z(`div`,{class:e?`space-y-2`:`space-y-1`,children:t.map(t=>{let r=co(t),i=lo(t.severity),a=fo(t.occurredAt),o=!e&&a!==n;return n=a,z(`div`,{children:[o?z(`p`,{class:`mt-5 mb-2 first:mt-0 text-xs font-black text-slate-500`,children:po(t.occurredAt)}):null,z(`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:[z(`div`,{class:`w-12 shrink-0 pt-1 text-xs font-bold tabular-nums text-slate-400`,children:z(`time`,{dateTime:new Date(t.occurredAt*1e3).toISOString(),title:qa(t.occurredAt),children:uo(t.occurredAt)})}),z(`span`,{class:`mt-0.5 grid size-8 shrink-0 place-items-center rounded-full ring-1 ring-inset ${i.surface}`,children:i.icon}),z(`div`,{class:`min-w-0 flex-1`,children:[z(`p`,{class:`m-0 text-sm font-black leading-5 text-slate-950`,children:r.title}),r.description?z(`p`,{class:`mt-1 mb-0 break-words text-xs font-medium leading-5 text-slate-500`,children:r.description}):null]})]})]},t.eventId)})})}function ho({error:e,loading:t,onRetry:n}){return t?z(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 px-4 py-5 text-center`,children:[z(V,{class:`mx-auto size-5 animate-spin text-teal-700`}),z(`p`,{class:`mt-2 mb-0 text-sm font-bold text-slate-500`,children:`최근 활동을 불러오는 중입니다.`})]}):e?z(`div`,{class:`rounded-xl border border-amber-200 bg-amber-50/70 px-4 py-4`,children:z(`div`,{class:`flex items-start gap-3`,children:[z(U,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),z(`div`,{class:`min-w-0 flex-1`,children:[z(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:`최근 활동을 불러오지 못했습니다.`}),z(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:e}),n?z(`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 go(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 vo({actionError:e,actionMessage:t,cloud:n,onDismissFeedback:r,onSetEnabled:i,saving:a}){let o=go(n),s=n.plan?.toUpperCase()??null,c=n.eligible===!0,l=n.enabled||c;return z(`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:[z(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[z(`div`,{class:`min-w-0 flex-1`,children:[z(`div`,{class:`flex flex-wrap items-center gap-2`,children:z(`span`,{class:`rounded-full px-2.5 py-1 text-xs font-extrabold ring-1 ring-inset ${o.tone}`,children:a?`설정 중`:o.label})}),z(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:o.description})]}),z(`div`,{class:`flex shrink-0 items-center justify-end gap-3 sm:justify-start`,children:[z(`span`,{class:`text-xs font-bold text-slate-600`,children:[`Cloud 전송 `,n.enabled?`켜짐`:`꺼짐`]}),z(`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:z(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${n.enabled?`left-6`:`left-1`}`})})]})]}),(e||t)&&z(`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:[z(`span`,{class:`min-w-0`,children:e||t}),z(`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?z(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`Cloud 전송을 끄면 아직 전송하지 않은 대기 기록이 삭제됩니다. 공유기의 로컬 기록은 유지됩니다.`}):null,z(`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:[z(`span`,{children:[`마지막 동기화: `,n.lastSuccessAt>0?Ja(n.lastSuccessAt):`아직 없음`]}),z(`span`,{children:[`Cloud 보관: `,_o(n)]}),n.pendingEvents>0?z(`span`,{children:[`전송 대기: `,J(n.pendingEvents),`건`]}):null]}),n.enabled&&n.phase===`error`&&n.lastErrorCode?z(`p`,{class:`mt-3 mb-0 text-xs font-semibold text-amber-700`,children:[`오류 코드: `,z(`code`,{children:n.lastErrorCode})]}):null,z(`details`,{class:`mt-3 border-t border-slate-200 pt-3`,children:[z(`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 동기화 상세 정보`}),z(`dl`,{class:`mt-3 grid gap-3 sm:grid-cols-4`,children:[z(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[z(`dt`,{class:`text-xs font-bold text-slate-500`,children:`멤버십`}),z(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:s??(n.eligible===!1?`Free`:`확인 중`)})]}),z(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[z(`dt`,{class:`text-xs font-bold text-slate-500`,children:`전송 대기`}),z(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:[J(n.pendingEvents),`건`]})]}),z(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[z(`dt`,{class:`text-xs font-bold text-slate-500`,children:`마지막 동기화`}),z(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:n.lastSuccessAt>0?Ja(n.lastSuccessAt):`아직 없음`})]}),z(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3 ring-1 ring-inset ring-slate-200`,children:[z(`dt`,{class:`text-xs font-bold text-slate-500`,children:`Cloud 보관`}),z(`dd`,{class:`mt-1 text-sm font-black text-slate-900`,children:_o(n)})]})]}),z(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`SmartSafeHub 계정에 연결된 Free는 최근 7일, Pro와 Ultimate는 최근 90일 동안 Cloud 활동 기록을 보관합니다. 끄면 전송 대기 데이터와 임시 인증 정보를 삭제하며, 다시 켠 뒤 새로 발생한 활동부터 Cloud에 전송합니다. 로컬 최근 활동은 항상 유지됩니다.`})]})]})}function yo({actionError:e,actionMessage:t,data:n,error:r,loading:i,onDismissActionFeedback:a,onRetry:o,onSetCloudSync:s,savingCloudSync:c}){let[l,u]=M(20),d=n?.events??[],f=d.slice(0,l),p=d.length-f.length;return z(`div`,{class:`min-w-0 space-y-6`,children:[z(`section`,{"aria-labelledby":`activity-cloud-title`,class:`min-w-0`,children:[z(q,{id:`activity-cloud-title`,eyebrow:`Cloud history`,title:`Cloud 활동 기록`,description:`활동 기록을 Cloud에 자동으로 전송할지 설정합니다. 공유기 내 최근 활동은 항상 기록됩니다.`}),z(vo,{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})]}),z(`section`,{"aria-labelledby":`activity-history-title`,class:`min-w-0`,children:[z(q,{id:`activity-history-title`,eyebrow:`Local history`,title:`현재 부팅 이후의 최근 활동`,description:`인터넷, SafeShield, 업데이트, 계정 권한과 주요 설정에서 실제 상태가 변경된 시점만 기록합니다.`,action:z(`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,`건`]})}),z(`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?z(ho,{error:r,loading:i,onRetry:o}):z(v,{children:[z(mo,{events:f}),d.length>20?z(`div`,{class:`mt-5 flex flex-wrap items-center justify-between gap-3 border-t border-slate-200 pt-4`,children:[z(`p`,{class:`m-0 text-xs font-semibold text-slate-500`,children:[J(f.length),` / `,J(d.length),`건 표시 중`]}),z(`div`,{class:`flex flex-wrap items-center gap-2`,children:[l>20?z(`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?z(`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]})})]}),z(`aside`,{class:`rounded-2xl border border-slate-200 bg-slate-50 px-5 py-4`,children:[z(`p`,{class:`m-0 text-sm font-extrabold text-slate-800`,children:`로컬 활동 기록 안내`}),z(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`이 기록은 공유기의 메모리 영역에만 보관되어 재부팅하면 초기화됩니다. 직접 설정을 변경한 작업은 성공 시점에 즉시 기록하고, 인터넷 및 장치 진단처럼 외부 상태를 확인해야 하는 항목은 실제 상태 변화가 관찰된 경우에만 추가합니다.`})]})]})}function bo(){return z(`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:z(`div`,{class:`flex items-center gap-4`,children:[z(V,{class:`size-8 shrink-0 animate-spin text-teal-700`}),z(`div`,{class:`min-w-0 text-left`,children:z(`h2`,{class:`m-0 text-lg font-extrabold text-slate-950`,children:`장치 상태를 확인하고 있습니다`})})]})})}function xo({message:e,onRetry:t}){return z(`section`,{"aria-live":`assertive`,class:`rounded-2xl border border-red-200 bg-red-50 p-5 sm:p-6`,children:[z(`p`,{class:`m-0 text-xs font-bold uppercase tracking-[0.16em] text-red-700`,children:`연결 오류`}),z(`h2`,{class:`mt-2 mb-0 text-lg font-extrabold text-red-950`,children:`장치 상태를 불러오지 못했습니다`}),z(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-red-800`,children:e}),z(`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:[z(V,{class:`size-4`}),`다시 시도`]})]})}var So=[{value:`all`,label:`전체`},{value:`online`,label:`현재 연결`},{value:`wifi`,label:`Wi-Fi`},{value:`ethernet`,label:`유선/기타`},{value:`offline`,label:`최근 기기`}],Co=new Intl.Collator(`ko`),wo=new Intl.DateTimeFormat(`ko-KR`,{month:`numeric`,day:`numeric`,hour:`2-digit`,minute:`2-digit`});function To(e){switch(e){case`wifi`:return`Wi-Fi`;case`ethernet`:return`유선/기타`;default:return`연결 방식 미확인`}}function Eo(e){return e.hostname??e.ipv4Address??`이름 없는 기기`}function Do(e){return e===null?null:e>=-50?`매우 좋음`:e>=-60?`좋음`:e>=-70?`보통`:`약함`}function Oo(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 ko(e){return e.leaseActive?e.leaseExpiresAt===null?`DHCP 임대 활성`:`DHCP ${wo.format(new Date(e.leaseExpiresAt*1e3))}까지`:null}function Ao(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 jo({device:e}){return z(`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:[z(`span`,{class:`size-2 rounded-full ${e.online?`bg-emerald-500`:`bg-slate-400`}`}),e.online?`현재 연결`:`최근 임대`]})}function Mo({device:e}){let t=Do(e.signalDbm),n=Oo(e.connectedSeconds),r=ko(e);return z(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:[z(`div`,{class:`flex min-w-0 flex-col items-start gap-4 sm:flex-row sm:justify-between`,children:[z(`div`,{class:`flex min-w-0 items-start gap-3`,children:[z(`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`?z(Lt,{class:`size-6`}):e.connection===`ethernet`?z(zt,{class:`size-6`}):z(Mt,{class:`size-6`})}),z(`div`,{class:`min-w-0`,children:[z(`h3`,{class:`m-0 break-words text-base font-extrabold text-slate-950`,children:Eo(e)}),z(`p`,{class:`mt-1 mb-0 break-all font-mono text-xs leading-5 text-slate-500`,children:e.mac})]})]}),z(`span`,{class:`shrink-0 self-start`,children:z(jo,{device:e})})]}),z(`dl`,{class:`mt-5 grid gap-3 text-sm sm:grid-cols-2`,children:[z(`div`,{class:`rounded-xl bg-slate-50 p-3.5`,children:[z(`dt`,{class:`text-xs font-bold text-slate-500`,children:`IP 주소`}),z(`dd`,{class:`mt-1.5 mb-0 ml-0 font-extrabold text-slate-900`,children:e.ipv4Address??`확인되지 않음`})]}),z(`div`,{class:`rounded-xl bg-slate-50 p-3.5`,children:[z(`dt`,{class:`text-xs font-bold text-slate-500`,children:`연결 방식`}),z(`dd`,{class:`mt-1.5 mb-0 ml-0 font-extrabold text-slate-900`,children:To(e.connection)})]})]}),z(`div`,{class:`mt-4 flex flex-wrap gap-2 text-xs font-bold text-slate-600`,children:[e.ssid?z(`span`,{class:`rounded-full bg-teal-50 px-3 py-1.5 text-teal-800`,children:[e.ssid,e.bandLabel?` · ${e.bandLabel}`:``]}):null,t?z(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:[`신호 `,t,` · `,e.signalDbm,` dBm`]}):null,n?z(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:[`연결 `,n]}):null,r?z(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:r}):null,e.interface?z(`span`,{class:`rounded-full bg-slate-100 px-3 py-1.5`,children:e.interface}):null]})]})}function No({data:e,error:t,loading:n,onRetry:r}){let[i,a]=M(``),[o,s]=M(`all`),c=Qe(()=>{if(!e)return[];let t=i.trim().toLocaleLowerCase();return[...e.devices].filter(e=>Ao(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?Co.compare(Eo(e),Eo(t)):e.connection===`wifi`?-1:1:e.online?-1:1)},[e,o,i]);return n?z(bo,{}):t?z(xo,{message:t,onRetry:r}):e?z(v,{children:[z(q,{eyebrow:`Overview`,title:`연결 기기 현황`,description:`네트워크에 연결되었거나 최근 확인된 기기를 요약합니다.`}),z(`section`,{"aria-label":`연결 기기 요약`,class:`grid gap-4 sm:grid-cols-2 xl:grid-cols-4`,children:[z(Po,{description:`${e.totals.offline}대는 최근 DHCP 임대에서 확인`,icon:z(Mt,{class:`size-6`}),label:`확인된 기기`,value:e.totals.known}),z(Po,{description:`Wi-Fi 또는 ARP에서 현재 확인됨`,icon:z(Ht,{class:`size-6`}),label:`현재 연결`,value:e.totals.online}),z(Po,{description:`무선 AP에 연결된 기기`,icon:z(Lt,{class:`size-6`}),label:`Wi-Fi`,value:e.totals.wireless}),z(Po,{description:`LAN ARP에서 확인된 비무선 기기`,icon:z(zt,{class:`size-6`}),label:`유선/기타`,value:e.totals.ethernet})]}),z(`section`,{class:`mt-7`,"aria-label":`기기 목록 및 필터`,children:[z(q,{eyebrow:`Device inventory`,title:`기기 목록`,description:`연결된 기기를 검색하고 무선·유선 상태별로 확인합니다.`}),z(`div`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:z(`div`,{class:`flex min-w-0 flex-col gap-3 lg:flex-row lg:items-center lg:justify-between`,children:[z(`div`,{class:`ssh-touch-scroll flex min-w-0 gap-2 overflow-x-auto pb-1 lg:flex-1`,role:`group`,"aria-label":`기기 필터`,children:So.map(e=>z(`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))}),z(`label`,{class:`relative block w-full lg:w-72 lg:shrink-0 xl:w-80`,children:[z(`span`,{class:`sr-only`,children:`기기 검색`}),z(`span`,{class:`pointer-events-none absolute inset-y-0 left-0 flex w-11 items-center justify-center text-slate-400`,children:z(en,{class:`size-5`})}),z(`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?z(`section`,{"aria-live":`polite`,class:`mt-5 grid gap-4 lg:grid-cols-2`,children:c.map(e=>z(Mo,{device:e},e.id))}):z(`section`,{class:`mt-5 rounded-2xl border border-dashed border-slate-300 bg-white p-5 text-center sm:p-8`,children:[z(Mt,{class:`mx-auto size-10 text-slate-400`}),z(`h2`,{class:`mt-4 mb-0 text-lg font-extrabold text-slate-950`,children:`조건에 맞는 기기가 없습니다`}),z(`p`,{class:`mt-2 mb-0 text-sm text-slate-600`,children:`검색어 또는 필터를 변경해 주세요.`})]}),z(`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 Po({description:e,icon:t,label:n,value:r}){return z(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,children:[z(`div`,{class:`flex items-start justify-between gap-4`,children:[z(`div`,{children:[z(`p`,{class:`m-0 text-xs font-bold uppercase tracking-[0.14em] text-slate-500`,children:n}),z(`p`,{class:`mt-3 mb-0 text-3xl font-black tracking-tight text-slate-950`,children:r})]}),z(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:t})]}),z(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:e})]})}function Fo(e){return e+.5|0}var Io=(e,t,n)=>Math.max(Math.min(e,n),t);function Lo(e){return Io(Fo(e*2.55),0,255)}function Ro(e){return Io(Fo(e*255),0,255)}function zo(e){return Io(Fo(e/2.55)/100,0,1)}function Bo(e){return Io(Fo(e*100),0,100)}var Vo={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},Ho=[...`0123456789ABCDEF`],Uo=e=>Ho[e&15],Wo=e=>Ho[(e&240)>>4]+Ho[e&15],Go=e=>(e&240)>>4==(e&15),Ko=e=>Go(e.r)&&Go(e.g)&&Go(e.b)&&Go(e.a);function qo(e){var t=e.length,n;return e[0]===`#`&&(t===4||t===5?n={r:255&Vo[e[1]]*17,g:255&Vo[e[2]]*17,b:255&Vo[e[3]]*17,a:t===5?Vo[e[4]]*17:255}:(t===7||t===9)&&(n={r:Vo[e[1]]<<4|Vo[e[2]],g:Vo[e[3]]<<4|Vo[e[4]],b:Vo[e[5]]<<4|Vo[e[6]],a:t===9?Vo[e[7]]<<4|Vo[e[8]]:255})),n}var Jo=(e,t)=>e<255?t(e):``;function Yo(e){var t=Ko(e)?Uo:Wo;return e?`#`+t(e.r)+t(e.g)+t(e.b)+Jo(e.a,t):void 0}var Xo=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Zo(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 Qo(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 $o(e,t,n){let r=Zo(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 es(e,t,n,r,i){return e===i?(t-n)/r+(t.5?l/(2-i-a):l/(i+a),s=es(t,n,r,l,i),s=s*60+.5),[s|0,c||0,o]}function ns(e,t,n,r){return(Array.isArray(t)?e(t[0],t[1],t[2]):e(t,n,r)).map(Ro)}function rs(e,t,n){return ns(Zo,e,t,n)}function is(e,t,n){return ns($o,e,t,n)}function as(e,t,n){return ns(Qo,e,t,n)}function os(e){return(e%360+360)%360}function ss(e){let t=Xo.exec(e),n=255,r;if(!t)return;t[5]!==r&&(n=t[6]?Lo(+t[5]):Ro(+t[5]));let i=os(+t[2]),a=t[3]/100,o=t[4]/100;return r=t[1]===`hwb`?is(i,a,o):t[1]===`hsv`?as(i,a,o):rs(i,a,o),{r:r[0],g:r[1],b:r[2],a:n}}function cs(e,t){var n=ts(e);n[0]=os(n[0]+t),n=rs(n),e.r=n[0],e.g=n[1],e.b=n[2]}function ls(e){if(!e)return;let t=ts(e),n=t[0],r=Bo(t[1]),i=Bo(t[2]);return e.a<255?`hsla(${n}, ${r}%, ${i}%, ${zo(e.a)})`:`hsl(${n}, ${r}%, ${i}%)`}var us={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`},ds={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 fs(){let e={},t=Object.keys(ds),n=Object.keys(us),r,i,a,o,s;for(r=0;r>16&255,a>>8&255,a&255]}return e}var ps;function ms(e){ps||(ps=fs(),ps.transparent=[0,0,0,0]);let t=ps[e.toLowerCase()];return t&&{r:t[0],g:t[1],b:t[2],a:t.length===4?t[3]:255}}var hs=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;function gs(e){let t=hs.exec(e),n=255,r,i,a;if(t){if(t[7]!==r){let e=+t[7];n=t[8]?Lo(e):Io(e*255,0,255)}return r=+t[1],i=+t[3],a=+t[5],r=255&(t[2]?Lo(r):Io(r,0,255)),i=255&(t[4]?Lo(i):Io(i,0,255)),a=255&(t[6]?Lo(a):Io(a,0,255)),{r,g:i,b:a,a:n}}}function _s(e){return e&&(e.a<255?`rgba(${e.r}, ${e.g}, ${e.b}, ${zo(e.a)})`:`rgb(${e.r}, ${e.g}, ${e.b})`)}var vs=e=>e<=.0031308?e*12.92:e**(1/2.4)*1.055-.055,ys=e=>e<=.04045?e/12.92:((e+.055)/1.055)**2.4;function bs(e,t,n){let r=ys(zo(e.r)),i=ys(zo(e.g)),a=ys(zo(e.b));return{r:Ro(vs(r+n*(ys(zo(t.r))-r))),g:Ro(vs(i+n*(ys(zo(t.g))-i))),b:Ro(vs(a+n*(ys(zo(t.b))-a))),a:e.a+n*(t.a-e.a)}}function xs(e,t,n){if(e){let r=ts(e);r[t]=Math.max(0,Math.min(r[t]+r[t]*n,t===0?360:1)),r=rs(r),e.r=r[0],e.g=r[1],e.b=r[2]}}function Ss(e,t){return e&&Object.assign(t||{},e)}function Cs(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=Ro(e[3]))):(t=Ss(e,{r:0,g:0,b:0,a:1}),t.a=Ro(t.a)),t}function ws(e){return e.charAt(0)===`r`?gs(e):ss(e)}var Ts=class e{constructor(t){if(t instanceof e)return t;let n=typeof t,r;n===`object`?r=Cs(t):n===`string`&&(r=qo(t)||ms(t)||ws(t)),this._rgb=r,this._valid=!!r}get valid(){return this._valid}get rgb(){var e=Ss(this._rgb);return e&&(e.a=zo(e.a)),e}set rgb(e){this._rgb=Cs(e)}rgbString(){return this._valid?_s(this._rgb):void 0}hexString(){return this._valid?Yo(this._rgb):void 0}hslString(){return this._valid?ls(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=bs(this._rgb,e._rgb,t)),this}clone(){return new e(this.rgb)}alpha(e){return this._rgb.a=Ro(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=Fo(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 xs(this._rgb,2,e),this}darken(e){return xs(this._rgb,2,-e),this}saturate(e){return xs(this._rgb,1,e),this}desaturate(e){return xs(this._rgb,1,-e),this}rotate(e){return cs(this._rgb,e),this}};function Es(){}var Ds=(()=>{let e=0;return()=>e++})();function Y(e){return e==null}function Os(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 ks(e){return(typeof e==`number`||e instanceof Number)&&isFinite(+e)}function As(e,t){return ks(e)?e:t}function Z(e,t){return e===void 0?t:e}var js=(e,t)=>typeof e==`string`&&e.endsWith(`%`)?parseFloat(e)/100:+e/t,Ms=(e,t)=>typeof e==`string`&&e.endsWith(`%`)?parseFloat(e)/100*t:+e;function Ns(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(Os(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 Hs(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 Us(e){let t=Hs(e);return e=>{for(let n of t){if(n===``)break;e=e&&e[n]}return e}}function Ws(e,t){return(Vs[t]||(Vs[t]=Us(t)))(e)}function Gs(e){return e.charAt(0).toUpperCase()+e.slice(1)}var Ks=e=>e!==void 0,qs=e=>typeof e==`function`,Js=(e,t)=>{if(e.size!==t.size)return!1;for(let n of e)if(!t.has(n))return!1;return!0};function Ys(e){return e.type===`mouseup`||e.type===`click`||e.type===`contextmenu`}var $=Math.PI,Xs=2*$,Zs=Xs+$,Qs=1/0,$s=$/180,ec=$/2,tc=$/4,nc=$*2/3,rc=Math.log10,ic=Math.sign;function ac(e,t,n){return Math.abs(e-t)e-t).pop(),t}function cc(e){return typeof e==`symbol`||typeof e==`object`&&!!e&&!(Symbol.toPrimitive in e||`toString`in e||`valueOf`in e)}function lc(e){return!cc(e)&&!isNaN(parseFloat(e))&&isFinite(e)}function uc(e,t){let n=Math.round(e);return n-t<=e&&n+t>=e}function dc(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 Cc(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 wc=(e,t,n,r)=>Cc(e,n,r?r=>{let i=e[r][t];return ie[r][t]Cc(e,n,r=>e[r][t]>=n);function Ec(e,t,n){let r=0,i=e.length;for(;rr&&e[i-1]>n;)i--;return r>0||i{let n=`_onData`+Gs(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 kc(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)&&(Dc.forEach(t=>{delete e[t]}),delete e._chartjs)}function Ac(e){let t=new Set(e);return t.size===e.length?e:Array.from(t)}var jc=function(){return typeof window>`u`?function(e){return e()}:window.requestAnimationFrame}();function Mc(e,t){let n=[],r=!1;return function(...i){n=i,r||(r=!0,jc.call(window,()=>{r=!1,e.apply(t,n)}))}}function Nc(e,t){let n;return function(...r){return t?(clearTimeout(n),n=setTimeout(e,t,r)):e.apply(this,r),t}}var Pc=e=>e===`start`?`left`:e===`end`?`right`:`center`,Fc=(e,t,n)=>e===`start`?t:e===`end`?n:(t+n)/2;function Ic(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(wc(c,u,d).lo,n?r:wc(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=bc(i,0,r-1)}if(m){let e=Math.max(wc(c,o.axis,f,!0).hi+1,n?0:wc(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=bc(e,i,r)-i}else a=r-i}return{start:i,count:a}}function Lc(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 Rc=e=>e===0||e===1,zc=(e,t,n)=>-(2**(10*--e)*Math.sin((e-t)*Xs/n)),Bc=(e,t,n)=>2**(-10*e)*Math.sin((e-t)*Xs/n)+1,Vc={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*ec)+1,easeOutSine:e=>Math.sin(e*ec),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=>Rc(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=>Rc(e)?e:zc(e,.075,.3),easeOutElastic:e=>Rc(e)?e:Bc(e,.075,.3),easeInOutElastic(e){let t=.1125,n=.45;return Rc(e)?e:e<.5?.5*zc(e*2,t,n):.5+.5*Bc(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-Vc.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?Vc.easeInBounce(e*2)*.5:Vc.easeOutBounce(e*2-1)*.5+.5};function Hc(e){if(e&&typeof e==`object`){let t=e.toString();return t===`[object CanvasPattern]`||t===`[object CanvasGradient]`}return!1}function Uc(e){return Hc(e)?e:new Ts(e)}function Wc(e){return Hc(e)?e:new Ts(e).saturate(.5).darken(.1).hexString()}var Gc=[`x`,`y`,`borderWidth`,`radius`,`tension`],Kc=[`color`,`borderColor`,`backgroundColor`];function qc(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:Kc},numbers:{type:`number`,properties:Gc}}),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 Jc(e){e.set(`layout`,{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})}var Yc=new Map;function Xc(e,t){t=t||{};let n=e+JSON.stringify(t),r=Yc.get(n);return r||(r=new Intl.NumberFormat(e,t),Yc.set(n,r)),r}function Zc(e,t,n){return Xc(t,n).format(e)}var Qc={values(e){return Os(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=$c(e,n)}let o=rc(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),Zc(e,r,c)},logarithmic(e,t,n){if(e===0)return`0`;let r=n[t].significand||e/10**Math.floor(rc(e));return[1,2,3,5,10,15].includes(r)||t>.8*n.length?Qc.numeric.call(this,e,t,n):``}};function $c(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 el={formatters:Qc};function tl(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:el.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 nl=Object.create(null),rl=Object.create(null);function il(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)=>Wc(t.backgroundColor),this.hoverBorderColor=(e,t)=>Wc(t.borderColor),this.hoverColor=(e,t)=>Wc(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 al(this,e,t)}get(e){return il(this,e)}describe(e,t){return al(rl,e,t)}override(e,t){return al(nl,e,t)}route(e,t,n,r){let i=il(this,e),a=il(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}},[qc,Jc,tl]);function sl(e){return!e||Y(e.size)||Y(e.family)?null:(e.style?e.style+` `:``)+(e.weight?e.weight+` `:``)+e.size+`px `+e.family}function cl(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 ll(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 ml(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,yl(e,a),c=0;c+e||0;function Ol(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]=Dl(a(e));return n}function kl(e){return Ol(e,{top:`y`,right:`x`,bottom:`y`,left:`x`})}function Al(e){return Ol(e,[`topLeft`,`topRight`,`bottomLeft`,`bottomRight`])}function jl(e){let t=kl(e);return t.width=t.left+t.right,t.height=t.top+t.bottom,t}function Ml(e,t){e=e||{},t=t||ol.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(Tl)&&(console.warn(`Invalid font style specified: "`+r+`"`),r=void 0);let i={family:Z(e.family,t.family),lineHeight:El(Z(e.lineHeight,t.lineHeight),n),size:n,style:r,weight:Z(e.weight,t.weight),string:``};return i.string=sl(i),i}function Nl(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 Fl(e,t){return Object.assign(Object.create(e),t)}function Il(e,t=[``],n,r,i=()=>e[0]){let a=n||e;return r===void 0&&(r=Ql(`_fallback`,e)),new Proxy({[Symbol.toStringTag]:`Object`,_cacheable:!0,_scopes:e,_rootScopes:a,_fallback:r,_getTarget:i,override:n=>Il([n,...e],t,a,r)},{deleteProperty(t,n){return delete t[n],delete t._keys,delete e[0][n],!0},get(n,r){return Vl(n,r,()=>Zl(r,t,e,n))},getOwnPropertyDescriptor(e,t){return Reflect.getOwnPropertyDescriptor(e._scopes[0],t)},getPrototypeOf(){return Reflect.getPrototypeOf(e[0])},has(e,t){return $l(e).includes(t)},ownKeys(e){return $l(e)},set(e,t,n){let r=e._storage||(e._storage=i());return e[t]=r[t]=n,delete e._keys,!0}})}function Ll(e,t,n,r){let i={_cacheable:!1,_proxy:e,_context:t,_subProxy:n,_stack:new Set,_descriptors:Rl(e,r),setContext:t=>Ll(e,t,n,r),override:i=>Ll(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 Vl(e,t,()=>Hl(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 Rl(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:qs(n)?n:()=>n,isIndexable:qs(r)?r:()=>r}}var zl=(e,t)=>e?e+Gs(t):t,Bl=(e,t)=>X(t)&&e!==`adapters`&&(Object.getPrototypeOf(t)===null||t.constructor===Object);function Vl(e,t,n){if(Object.prototype.hasOwnProperty.call(e,t)||t===`constructor`)return e[t];let r=n();return e[t]=r,r}function Hl(e,t,n){let{_proxy:r,_context:i,_subProxy:a,_descriptors:o}=e,s=r[t];return qs(s)&&o.isScriptable(t)&&(s=Ul(t,s,e,n)),Os(s)&&s.length&&(s=Wl(t,s,e,o.isIndexable)),Bl(t,s)&&(s=Ll(s,i,a&&a[t],o)),s}function Ul(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),Bl(e,c)&&(c=Jl(i._scopes,i,e,c)),c}function Wl(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=Jl(r,i,e,c);t.push(Ll(n,a,o&&o[e],s))}}return t}function Gl(e,t,n){return qs(e)?e(t,n):e}var Kl=(e,t)=>e===!0?t:typeof e==`string`?Ws(t,e):void 0;function ql(e,t,n,r,i){for(let a of t){let t=Kl(n,a);if(t){e.add(t);let a=Gl(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 Jl(e,t,n,r){let i=t._rootScopes,a=Gl(t._fallback,n,r),o=[...e,...i],s=new Set;s.add(r);let c=Yl(s,o,n,a||n,r);return c===null||a!==void 0&&a!==n&&(c=Yl(s,o,a,c,r),c===null)?!1:Il(Array.from(s),[``],i,a,()=>Xl(t,n,r))}function Yl(e,t,n,r,i){for(;n;)n=ql(e,t,n,r,i);return n}function Xl(e,t,n){let r=e._getTarget();t in r||(r[t]={});let i=r[t];return Os(i)&&X(n)?n:i||{}}function Zl(e,t,n,r){let i;for(let a of t)if(i=Ql(zl(a,e),n),i!==void 0)return Bl(e,i)?Jl(n,r,e,i):i}function Ql(e,t){for(let n of t){if(!n)continue;let t=n[e];if(t!==void 0)return t}}function $l(e){let t=e._keys;return t||(t=e._keys=eu(e._scopes)),t}function eu(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 tu(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 au(e,t,n,r){let i=e.skip?t:e,a=t,o=n.skip?t:n,s=gc(a,i),c=gc(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 ou(e,t,n){let r=e.length,i,a,o,s,c,l=ru(e,0);for(let u=0;u!e.skip)),t.cubicInterpolationMode===`monotone`)cu(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 gu(e,t){return hu(e).getPropertyValue(t)}var _u=[`top`,`right`,`bottom`,`left`];function vu(e,t,n){let r={};n=n?`-`+n:``;for(let i=0;i<4;i++){let a=_u[i];r[a]=parseFloat(e[t+`-`+a+n])||0}return r.width=r.left+r.right,r.height=r.top+r.bottom,r}var yu=(e,t,n)=>(e>0||t>0)&&(!n||!n.shadowRoot);function bu(e,t){let n=e.touches,r=n&&n.length?n[0]:e,{offsetX:i,offsetY:a}=r,o=!1,s,c;if(yu(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 xu(e,t){if(`native`in e)return e;let{canvas:n,currentDevicePixelRatio:r}=t,i=hu(n),a=i.boxSizing===`border-box`,o=vu(i,`padding`),s=vu(i,`border`,`width`),{x:c,y:l,box:u}=bu(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 Su(e,t,n){let r,i;if(t===void 0||n===void 0){let a=e&&pu(e);if(!a)t=e.clientWidth,n=e.clientHeight;else{let e=a.getBoundingClientRect(),o=hu(a),s=vu(o,`border`,`width`),c=vu(o,`padding`);t=e.width-c.width-s.width,n=e.height-c.height-s.height,r=mu(o.maxWidth,a,`clientWidth`),i=mu(o.maxHeight,a,`clientHeight`)}}return{width:t,height:n,maxWidth:r||Qs,maxHeight:i||Qs}}var Cu=e=>Math.round(e*10)/10;function wu(e,t,n,r){let i=hu(e),a=vu(i,`margin`),o=mu(i.maxWidth,e,`clientWidth`)||Qs,s=mu(i.maxHeight,e,`clientHeight`)||Qs,c=Su(e,t,n),{width:l,height:u}=c;if(i.boxSizing===`content-box`){let e=vu(i,`border`,`width`),t=vu(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=Cu(Math.min(l,o,c.maxWidth)),u=Cu(Math.min(u,s,c.maxHeight)),l&&!u&&(u=Cu(l/2)),(t!==void 0||n!==void 0)&&r&&c.height&&u>c.height&&(u=c.height,l=Cu(Math.floor(u*r))),{width:l,height:u}}function Tu(e,t,n){let r=t||1,i=Cu(e.height*r),a=Cu(e.width*r);e.height=Cu(e.height),e.width=Cu(e.width);let o=e.canvas;return o.style&&(n||!o.style.height&&!o.style.width)&&(o.style.height=`${e.height}px`,o.style.width=`${e.width}px`),e.currentDevicePixelRatio!==r||o.height!==i||o.width!==a?(e.currentDevicePixelRatio=r,o.height=i,o.width=a,e.ctx.setTransform(r,0,0,r,0,0),!0):!1}var Eu=function(){let e=!1;try{let t={get passive(){return e=!0,!1}};fu()&&(window.addEventListener(`test`,null,t),window.removeEventListener(`test`,null,t))}catch{}return e}();function Du(e,t){let n=gu(e,t),r=n&&n.match(/^(\d+)(\.\d+)?px$/);return r?+r[1]:void 0}function Ou(e,t,n,r){return{x:e.x+n*(t.x-e.x),y:e.y+n*(t.y-e.y)}}function ku(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 Au(e,t,n,r){let i={x:e.cp2x,y:e.cp2y},a={x:t.cp1x,y:t.cp1y},o=Ou(e,i,n),s=Ou(i,a,n),c=Ou(a,t,n);return Ou(Ou(o,s,n),Ou(s,c,n),n)}var ju=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}}},Mu=function(){return{x(e){return e},setWidth(e){},textAlign(e){return e},xPlus(e,t){return e+t},leftForLtr(e,t){return e}}};function Nu(e,t,n){return e?ju(t,n):Mu()}function Pu(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 Fu(e,t){t!==void 0&&(delete e.prevTextDirection,e.canvas.style.setProperty(`direction`,t[0],t[1]))}function Iu(e){return e===`angle`?{between:yc,compare:_c,normalize:vc}:{between:Sc,compare:(e,t)=>e-t,normalize:e=>e}}function Lu({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 Ru(e,t,n){let{property:r,start:i,end:a}=n,{between:o,normalize:s}=Iu(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(Lu({start:g,end:e,loop:f,count:o,style:p})),g=null),n=e,y=_));return g!==null&&m.push(Lu({start:g,end:d,loop:f,count:o,style:p})),m}function Bu(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 Hu(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 Uu(e,t){let n=e.points,r=e.options.spanGaps,i=n.length;if(!i)return[];let a=!!e._loop,{start:o,end:s}=Vu(n,i,a,r);return r===!0?Wu(e,[{start:o,end:s,loop:a}],n,t):Wu(e,Hu(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=jc.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)}},Qu=`transparent`,$u={boolean(e,t,n){return n>.5?t:e},color(e,t,n){let r=Uc(e||Qu),i=r.valid&&Uc(t||Qu);return i&&i.valid?i.mix(r,n).hexString():t},number(e,t,n){return e+(t-e)*n}},ed=class{constructor(e,t,n,r){let i=t[n];r=Nl([e.to,r,i,e.from]);let a=Nl([e.from,i,r]);this._active=!0,this._fn=e.fn||$u[e.type||typeof a],this._easing=Vc[e.easing]||Vc.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=Nl([e.to,t,r,e.from]),this._from=Nl([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];(Os(i.properties)&&i.properties||[r]).forEach(e=>{(e===r||!n.has(e))&&n.set(e,a)})})}_animateOptions(e,t){let n=t.options,r=rd(e,n);if(!r)return[];let i=this._createAnimations(r,n);return n.$shared&&nd(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 ed(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 Zu.add(this._chart,n),!0}};function nd(e,t){let n=[],r=Object.keys(t);for(let t=0;t0||!n&&t<0)return i.index}return null}function hd(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=dd(a,o,r),d=t.length,f;for(let e=0;en[e].axis===t).shift()}function _d(e,t){return Fl(e,{active:!1,dataset:void 0,datasetIndex:t,index:t,mode:`default`,type:`dataset`})}function vd(e,t,n){return Fl(e,{active:!1,dataIndex:t,parsed:void 0,raw:void 0,element:n,index:t,mode:`default`,type:`data`})}function yd(e,t){let n=e.controller.index,r=e.vScale&&e.vScale.axis;if(r){t=t||e._parsed;for(let e of t){let t=e._stacks;if(!t||t[r]===void 0||t[r][n]===void 0)return;delete t[r][n],t[r]._visualValues!==void 0&&t[r]._visualValues[n]!==void 0&&delete t[r]._visualValues[n]}}}var bd=e=>e===`reset`||e===`none`,xd=(e,t)=>t?e:Object.assign({},e),Sd=(e,t,n)=>e&&!t.hidden&&t._stacked&&{keys:sd(n,!0),values:null},Cd=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=ud(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&&yd(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,gd(e,`x`)),a=t.yAxisID=Z(n.yAxisID,gd(e,`y`)),o=t.rAxisID=Z(n.rAxisID,gd(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&&kc(this._data,this),e._stacked&&yd(e)}_dataCheck(){let e=this.getDataset(),t=e.data||(e.data=[]),n=this._data;if(X(t)){let e=this._cachedMeta;this._data=ld(t,e)}else if(n!==t){if(n){kc(n,this);let e=this._cachedMeta;yd(e),e._parsed=[]}t&&Object.isExtensible(t)&&Oc(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=ud(t.vScale,t),t.stack!==n.stack&&(r=!0,yd(t),t.stack=n.stack),this._resyncElements(e),(r||i!==t._stacked)&&(hd(this,t._parsed),t._stacked=ud(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=Os(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(xd(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 td(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||bd(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){bd(r)?Object.assign(e,n):this._resolveAnimations(t,r).update(e,n)}updateSharedOptions(e,t,n){e&&!bd(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 Td(e){let t=e.iScale,n=wd(t,e.type),r=t._length,i,a,o,s,c=()=>{o!==32767&&o!==-32768&&(Ks(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 kd(e,t,n,r){return Os(e)?Od(e,t,n,r):t[n.axis]=n.parse(e,r),t}function Ad(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):ic(e)}function Nd(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;dyc(e,s,c,!0)?1:Math.max(t,t*n,r,r*n),m=(e,t,r)=>yc(e,s,c,!0)?-1:Math.min(t,t*n,r,r*n),h=p(0,l,d),g=p(ec,u,f),_=m($,l,d),v=m($+ec,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 Hd=class extends Cd{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=>+Ws(n[t],e)}let a,o;for(a=e,o=e+t;a0&&!isNaN(e)?Math.abs(e)/t*Xs:0}getLabelAndValue(e){let t=this._cachedMeta,n=this.chart,r=n.data.labels||[],i=Zc(t._parsed[e],n.options.locale);return{label:r[e]||``,value:i}}getMaxBorderWidth(e){let t=0,n=this.chart,r,i,a,o,s;if(!e){for(r=0,i=n.data.datasets.length;re!==`spacing`,_indexable:e=>e!==`spacing`&&!e.startsWith(`borderDash`)&&!e.startsWith(`hoverBorderDash`)}),I(Hd,`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 Ud=class extends Cd{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}=Ic(t,r,a);this._drawStart=o,this._drawCount=s,Lc(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=lc(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()}};I(Ud,`id`,`line`),I(Ud,`defaults`,{datasetElementType:`line`,dataElementType:`point`,showLine:!0,spanGaps:!1}),I(Ud,`overrides`,{scales:{_index_:{type:`category`},_value_:{type:`linear`}}});var Wd=class extends Cd{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=Zc(t._parsed[e].r,n.options.locale);return{label:r[e]||``,value:i}}parseObjectData(e,t,n,r){return tu.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)?fc(this.resolveDataElementOptions(e,t).angle||n):0}};I(Wd,`id`,`polarArea`),I(Wd,`defaults`,{dataElementType:`arc`,animation:{animateRotate:!0,animateScale:!0},animations:{numbers:{type:`number`,properties:[`x`,`y`,`startAngle`,`endAngle`,`innerRadius`,`outerRadius`]}},indexAxis:`r`,startAngle:0}),I(Wd,`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 Gd=class extends Hd{};I(Gd,`id`,`pie`),I(Gd,`defaults`,{cutout:0,rotation:0,circumference:360,radius:`100%`});var Kd=class extends Cd{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 tu.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}};I(qd,`id`,`scatter`),I(qd,`defaults`,{datasetElementType:!1,dataElementType:`point`,showLine:!1,fill:!1}),I(qd,`overrides`,{interaction:{mode:`point`},scales:{x:{type:`linear`},y:{type:`linear`}}});function Jd(){throw Error(`This method is not implemented: Check that a complete date adapter is provided.`)}var Yd={_date:class e{static override(t){Object.assign(e.prototype,t)}constructor(e){I(this,`options`,void 0),this.options=e||{}}init(){}formats(){return Jd()}parse(){return Jd()}format(){return Jd()}add(){return Jd()}diff(){return Jd()}startOf(){return Jd()}endOf(){return Jd()}}};function Xd(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?Tc:wc;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 Zd(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 af={evaluateInteractionItems:Zd,modes:{index(e,t,n,r){let i=xu(t,e),a=n.axis||`x`,o=n.includeInvisible||!1,s=n.intersect?$d(e,i,a,r,o):nf(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=xu(t,e),a=n.axis||`xy`,o=n.includeInvisible||!1,s=n.intersect?$d(e,i,a,r,o):nf(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 cf(e,t){return e.filter(e=>of.indexOf(e.pos)===-1&&e.box.axis===t)}function lf(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 uf(e){let t=[],n,r,i,a,o,s;for(n=0,r=(e||[]).length;ne.box.fullSize),!0),r=lf(sf(t,`left`),!0),i=lf(sf(t,`right`)),a=lf(sf(t,`top`),!0),o=lf(sf(t,`bottom`)),s=cf(t,`x`),c=cf(t,`y`);return{fullSize:n,leftAndTop:r.concat(a),rightAndBottom:i.concat(c).concat(o).concat(s),chartArea:sf(t,`chartArea`),vertical:r.concat(i).concat(c),horizontal:a.concat(o).concat(s)}}function mf(e,t,n,r){return Math.max(e[n],t[n])+Math.max(e[r],t[r])}function hf(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 gf(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&&hf(o,a.getPadding());let s=Math.max(0,t.outerWidth-mf(o,e,`left`,`right`)),c=Math.max(0,t.outerHeight-mf(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 _f(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 vf(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 yf(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);hf(f,jl(r));let p=Object.assign({maxPadding:f,w:a,h:o,x:i.left,y:i.top},i),m=ff(c.concat(l),d);yf(s.fullSize,p,d,m),yf(c,p,d,m),yf(l,p,d,m)&&yf(c,p,d,m),_f(p),xf(s.leftAndTop,p,d,m),p.x+=p.w,p.y+=p.h,xf(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})})}},Cf=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){}},wf=class extends Cf{acquireContext(e){return e&&e.getContext&&e.getContext(`2d`)||null}updateConfig(e){e.options.animation=!1}},Tf=`$chartjs`,Ef={touchstart:`mousedown`,touchmove:`mousemove`,touchend:`mouseup`,pointerenter:`mouseenter`,pointerdown:`mousedown`,pointermove:`mousemove`,pointerup:`mouseup`,pointerleave:`mouseout`,pointerout:`mouseout`},Df=e=>e===null||e===``;function Of(e,t){let n=e.style,r=e.getAttribute(`height`),i=e.getAttribute(`width`);if(e[Tf]={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`,Df(i)){let t=Du(e,`width`);t!==void 0&&(e.width=t)}if(Df(r)){if(e.style.height===``)e.height=e.width/(t||2);else{let t=Du(e,`height`);t!==void 0&&(e.height=t)}}return e}var kf=Eu?{passive:!0}:!1;function Af(e,t,n){e&&e.addEventListener(t,n,kf)}function jf(e,t,n){e&&e.canvas&&e.canvas.removeEventListener(t,n,kf)}function Mf(e,t){let n=Ef[e.type]||e.type,{x:r,y:i}=xu(e,t);return{type:n,chart:t,native:e,x:r===void 0?null:r,y:i===void 0?null:i}}function Nf(e,t){for(let n of e)if(n===t||n.contains(t))return!0}function Pf(e,t,n){let r=e.canvas,i=new MutationObserver(e=>{let t=!1;for(let n of e)t=t||Nf(n.addedNodes,r),t=t&&!Nf(n.removedNodes,r);t&&n()});return i.observe(document,{childList:!0,subtree:!0}),i}function Ff(e,t,n){let r=e.canvas,i=new MutationObserver(e=>{let t=!1;for(let n of e)t=t||Nf(n.removedNodes,r),t=t&&!Nf(n.addedNodes,r);t&&n()});return i.observe(document,{childList:!0,subtree:!0}),i}var If=new Map,Lf=0;function Rf(){let e=window.devicePixelRatio;e!==Lf&&(Lf=e,If.forEach((t,n)=>{n.currentDevicePixelRatio!==e&&t()}))}function zf(e,t){If.size||window.addEventListener(`resize`,Rf),If.set(e,t)}function Bf(e){If.delete(e),If.size||window.removeEventListener(`resize`,Rf)}function Vf(e,t,n){let r=e.canvas,i=r&&pu(r);if(!i)return;let a=Mc((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),zf(e,a),o}function Hf(e,t,n){n&&n.disconnect(),t===`resize`&&Bf(e)}function Uf(e,t,n){let r=e.canvas,i=Mc(t=>{e.ctx!==null&&n(Mf(t,e))},e);return Af(r,t,i),i}var Wf=class extends Cf{acquireContext(e,t){let n=e&&e.getContext&&e.getContext(`2d`);return n&&n.canvas===e?(Of(e,t),n):null}releaseContext(e){let t=e.canvas;if(!t[Tf])return!1;let n=t[Tf].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[Tf],!0}addEventListener(e,t,n){this.removeEventListener(e,t);let r=e.$proxies||(e.$proxies={});r[t]=({attach:Pf,detach:Ff,resize:Vf}[t]||Uf)(e,t,n)}removeEventListener(e,t){let n=e.$proxies||(e.$proxies={}),r=n[t];r&&(({attach:Hf,detach:Hf,resize:Hf}[t]||jf)(e,t,r),n[t]=void 0)}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(e,t,n,r){return wu(e,t,n,r)}isAttached(e){let t=e&&pu(e);return!!(t&&t.isConnected)}};function Gf(e){return!fu()||typeof OffscreenCanvas<`u`&&e instanceof OffscreenCanvas?wf:Wf}var Kf=class{constructor(){I(this,`x`,void 0),I(this,`y`,void 0),I(this,`active`,!1),I(this,`options`,void 0),I(this,`$animations`,void 0)}tooltipPosition(e){let{x:t,y:n}=this.getProps([`x`,`y`],e);return{x:t,y:n}}hasValue(){return lc(this.x)&&lc(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}};I(Kf,`defaults`,{}),I(Kf,`defaultRoutes`,void 0);function qf(e,t){let n=e.options.ticks,r=Jf(e),i=Math.min(n.maxTicksLimit||r,r),a=n.major.enabled?Xf(t):[],o=a.length,s=a[0],c=a[o-1],l=[];if(o>i)return Zf(t,l,a,o/i),l;let u=Yf(a,t,i);if(o>0){let e,n,r=o>1?Math.round((c-s)/(o-1)):null;for(Qf(t,l,u,Y(r)?0:s-r,s),e=0,n=o-1;ei)return t}return Math.max(i,1)}function Xf(e){let t=[],n,r;for(n=0,r=e.length;ne===`left`?`right`:e===`right`?`left`:e,tp=(e,t,n)=>t===`top`||t===`left`?e[t]+n:e[t]-n,np=(e,t)=>Math.min(t||e,e);function rp(e,t){let n=[],r=e.length/t,i=e.length,a=0;for(;ao+s)))return c}function ap(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:As(t,As(n,t)),max:As(n,As(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(){Ns(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=Pl(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=bc(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-op(e.grid)-t.padding-sp(e.title,this.chart.options.font),c=Math.sqrt(u*u+d*d),a=pc(Math.min(Math.asin(bc((l.highest.height+6)/o,-1,1)),Math.asin(bc(s/c,-1,1))-Math.asin(bc(d/c,-1,1)))),a=Math.max(r,Math.min(i,a))),this.labelRotation=a}afterCalculateLabelRotation(){Ns(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){Ns(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=sp(r,t.options.font);if(o?(e.width=this.maxWidth,e.height=op(i)+a):(e.height=this.maxHeight,e.width=op(i)+a),n.display&&this.ticks.length){let{first:t,last:r,widest:i,highest:a}=this._getLabelSizes(),s=n.padding*2,c=fc(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(){Ns(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 xc(this._alignToPixels?ul(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=op(i),d=[],f=o.setContext(this.getContext()),p=f.display?f.width:0,m=p/2,h=function(e){return ul(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(-fc(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(`.`);ol.route(a,i,c,s)})}function gp(e){return`id`in e&&`defaults`in e}var _p=new class{constructor(){this.controllers=new pp(Cd,`datasets`,!0),this.elements=new pp(Kf,`elements`),this.plugins=new pp(Object,`plugins`),this.scales=new pp(fp,`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=Gs(e);Ns(n[`before`+r],[],n),t[e](n),Ns(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 yp(e){let t={},n=[],r=Object.keys(_p.plugins.items);for(let e=0;e1&&Ep(e[0].toLowerCase());if(t)return t}throw Error(`Cannot determine type of '${e}' axis. Please provide 'axis' or 'position' option.`)}function kp(e,t,n){if(n[t+`AxisID`]===e)return{axis:t}}function Ap(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 kp(e,`x`,n[0])||kp(e,`y`,n[0])}return{}}function jp(e,t){let n=nl[e.type]||{scales:{}},r=t.scales||{},i=Cp(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=Op(t,o,Ap(t,e),ol.scales[o.type]),c=Tp(s,i),l=n.scales||{};a[t]=zs(Object.create(null),[{axis:s},o,l[s],l[c]])}),e.data.datasets.forEach(n=>{let i=n.type||e.type,o=n.indexAxis||Cp(i,t),s=(nl[i]||{}).scales||{};Object.keys(s).forEach(e=>{let t=wp(e,o),i=n[t+`AxisID`]||t;a[i]=a[i]||Object.create(null),zs(a[i],[{axis:t},r[i],s[e]])})}),Object.keys(a).forEach(e=>{let t=a[e];zs(t,[ol.scales[t.type],ol.scale])}),a}function Mp(e){let t=e.options||(e.options={});t.plugins=Z(t.plugins,{}),t.scales=jp(e,t)}function Np(e){return e=e||{},e.datasets=e.datasets||[],e.labels=e.labels||[],e}function Pp(e){return e=e||{},e.data=Np(e.data),Mp(e),e}var Fp=new Map,Ip=new Set;function Lp(e,t){let n=Fp.get(e);return n||(n=t(),Fp.set(e,n),Ip.add(n)),n}var Rp=(e,t,n)=>{let r=Ws(t,n);r!==void 0&&e.add(r)},zp=class{constructor(e){this._config=Pp(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=Np(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(),Mp(e)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(e){return Lp(e,()=>[[`datasets.${e}`,``]])}datasetAnimationScopeKeys(e,t){return Lp(`${e}.transition.${t}`,()=>[[`datasets.${e}.transitions.${t}`,`transitions.${t}`],[`datasets.${e}`,``]])}datasetElementScopeKeys(e,t){return Lp(`${e}-${t}`,()=>[[`datasets.${e}.elements.${t}`,`datasets.${e}`,`elements.${t}`,``]])}pluginScopeKeys(e){let t=e.id,n=this.type;return Lp(`${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=>Rp(s,e,t))),t.forEach(e=>Rp(s,r,e)),t.forEach(e=>Rp(s,nl[i]||{},e)),t.forEach(e=>Rp(s,ol,e)),t.forEach(e=>Rp(s,rl,e))});let c=Array.from(s);return c.length===0&&c.push(Object.create(null)),Ip.has(t)&&a.set(t,c),c}chartOptionScopes(){let{options:e,type:t}=this;return[e,nl[t]||{},ol.datasets[t]||{},{type:t},ol,rl]}resolveNamedOptions(e,t,n,r=[``]){let i={$shared:!0},{resolver:a,subPrefixes:o}=Bp(this._resolverCache,e,r),s=a;if(Hp(a,t)){i.$shared=!1,n=qs(n)?n():n;let t=this.createResolver(e,n,o);s=Ll(a,n,t)}for(let e of t)i[e]=s[e];return i}createResolver(e,t,n=[``],r){let{resolver:i}=Bp(this._resolverCache,e,n);return X(t)?Ll(i,t,void 0,r):i}};function Bp(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:Il(t,n),subPrefixes:n.filter(e=>!e.toLowerCase().includes(`hover`))},r.set(i,a)),a}var Vp=e=>X(e)&&Object.getOwnPropertyNames(e).some(t=>qs(e[t]));function Hp(e,t){let{isScriptable:n,isIndexable:r}=Rl(e);for(let i of t){let t=n(i),a=r(i),o=(a||t)&&e[i];if(t&&(qs(o)||Vp(o))||a&&Os(o))return!0}return!1}var Up=`4.5.1`,Wp=[`top`,`bottom`,`left`,`right`,`chartArea`];function Gp(e,t){return e===`top`||e===`bottom`||Wp.indexOf(e)===-1&&t===`x`}function Kp(e,t){return function(n,r){return n[e]===r[e]?n[t]-r[t]:n[e]-r[e]}}function qp(e){let t=e.chart,n=t.options.animation;t.notifyPlugins(`afterRender`),Ns(n&&n.onComplete,[e],t)}function Jp(e){let t=e.chart,n=t.options.animation;Ns(n&&n.onProgress,[e],t)}function Yp(e){return fu()&&typeof e==`string`?e=document.getElementById(e):e&&e.length&&(e=e[0]),e&&e.canvas&&(e=e.canvas),e}var Xp={},Zp=e=>{let t=Yp(e);return Object.values(Xp).filter(e=>e.canvas===t).pop()};function Qp(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 $p(e,t,n,r){return!n||e.type===`mouseout`?null:r?t:e}var em=class{static register(...e){_p.add(...e),tm()}static unregister(...e){_p.remove(...e),tm()}constructor(e,t){let n=this.config=new zp(t),r=Yp(e),i=Zp(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||(Gf(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=Ds(),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 vp,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=Nc(e=>this.update(e),a.resizeDelay||0),this._dataChanges=[],Xp[this.id]=this,!o||!s?console.error(`Failed to create chart: can't acquire context from the given item`):(Zu.listen(this,`complete`,qp),Zu.listen(this,`progress`,Jp),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 _p}_initialize(){return this.notifyPlugins(`beforeInit`),this.options.responsive?this.resize():Tu(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins(`afterInit`),this}clear(){return dl(this.canvas,this.ctx),this}stop(){return Zu.stop(this),this}resize(e,t){Zu.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,Tu(this,o,!0)&&(this.notifyPlugins(`resize`,{size:a}),Ns(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=Op(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=Op(a,i),s=Z(i.type,t.dtype);(i.position===void 0||Gp(i.position,o)!==Gp(t.dposition))&&(i.position=t.dposition),r[a]=!0;let c=null;a in n&&n[a].type===s?c=n[a]:(c=new(_p.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=>{Sf.configure(this,e,e.options),Sf.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(Kp(`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=>{Sf.removeBox(this,e)}),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){let e=this.options;(!Js(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)Qp(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;Sf.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=Xu(this,e);this.notifyPlugins(`beforeDatasetDraw`,n)!==!1&&(r&&hl(t,r),e.controller.draw(),r&&gl(t),n.cancelable=!1,this.notifyPlugins(`afterDatasetDraw`,n))}isPointInArea(e){return ml(e,this.chartArea,this._minPadding)}getElementsAtEventForMode(e,t,n,r){let i=af.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=Fl(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);Ks(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(),Zu.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}});Ps(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=Ys(e),c=$p(e,this._lastEvent,n,s);n&&(this._lastEvent=null,Ns(i.onHover,[e,o,this],this),s&&Ns(i.onClick,[e,o,this],this));let l=!Ps(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)}};I(em,`defaults`,ol),I(em,`instances`,Xp),I(em,`overrides`,nl),I(em,`registry`,_p),I(em,`version`,Up),I(em,`getChart`,Zp);function tm(){return Q(em.instances,e=>e._plugins.invalidate())}function nm(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,vc(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,vc(r-n));e.arc(i,a,s+l/2,n-t/2,r+t/2,!0)}else{let t=Math.min(l/2,o*vc(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 rm(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+ec,r-ec),e.closePath(),e.clip()}function im(e){return Ol(e,[`outerStart`,`outerEnd`,`innerStart`,`innerEnd`])}function am(e,t,n,r){let i=im(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 bc(e,0,Math.min(a,t))};return{outerStart:s(i.outerStart),outerEnd:s(i.outerEnd),innerStart:bc(i.innerStart,0,o),innerEnd:bc(i.innerEnd,0,o)}}function om(e,t,n,r){return{x:n+e*Math.cos(t),y:r+e*Math.sin(t)}}function sm(e,t,n,r,i,a){let{x:o,y:s,startAngle:c,pixelMargin:l,innerRadius:u}=t,d=Math.max(t.outerRadius+r+n-l,0),f=u>0?u+r+n+l:0,p=0,m=i-c;if(r){let e=((u>0?u-r:0)+(d>0?d-r:0))/2;p=(m-(e===0?m:m*e/(e+r)))/2}let h=(m-Math.max(.001,m*d-n/$)/d)/2,g=c+h+p,_=i-h-p,{outerStart:v,outerEnd:y,innerStart:b,innerEnd:x}=am(t,f,d,_-g),S=d-v,C=d-y,w=g+v/S,T=_-y/C,E=f+b,D=f+x,O=g+b/E,k=_-x/D;if(e.beginPath(),a){let t=(w+T)/2;if(e.arc(o,s,d,w,t),e.arc(o,s,d,t,T),y>0){let t=om(C,T,o,s);e.arc(t.x,t.y,y,T,_+ec)}let n=om(D,_,o,s);if(e.lineTo(n.x,n.y),x>0){let t=om(D,k,o,s);e.arc(t.x,t.y,x,_+ec,k+Math.PI)}let r=(_-x/f+(g+b/f))/2;if(e.arc(o,s,f,_-x/f,r,!0),e.arc(o,s,f,r,g+b/f,!0),b>0){let t=om(E,O,o,s);e.arc(t.x,t.y,b,O+Math.PI,g-ec)}let i=om(S,g,o,s);if(e.lineTo(i.x,i.y),v>0){let t=om(S,w,o,s);e.arc(t.x,t.y,v,g-ec,w)}}else{e.moveTo(o,s);let t=Math.cos(w)*d+o,n=Math.sin(w)*d+s;e.lineTo(t,n);let r=Math.cos(T)*d+o,i=Math.sin(T)*d+s;e.lineTo(r,i)}e.closePath()}function cm(e,t,n,r,i){let{fullCircles:a,startAngle:o,circumference:s}=t,c=t.endAngle;if(a){sm(e,t,n,r,c,i);for(let t=0;t=$&&p===0&&u!==`miter`&&nm(e,t,h),a||(sm(e,t,n,r,h,i),e.stroke())}var um=class extends Kf{constructor(e){super(),I(this,`circumference`,void 0),I(this,`endAngle`,void 0),I(this,`fullCircles`,void 0),I(this,`innerRadius`,void 0),I(this,`outerRadius`,void 0),I(this,`pixelMargin`,void 0),I(this,`startAngle`,void 0),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,e&&Object.assign(this,e)}inRange(e,t,n){let{angle:r,distance:i}=hc(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=yc(r,a,o)&&a!==o,p=d>=Xs||f,m=Sc(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>Xs?Math.floor(n/Xs):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,cm(e,this,s,i,a),lm(e,this,s,i,a),e.restore()}};I(um,`id`,`arc`),I(um,`defaults`,{borderAlign:`center`,borderColor:`#fff`,borderDash:[],borderDashOffset:0,borderJoinStyle:void 0,borderRadius:0,borderWidth:2,offset:0,spacing:0,angle:void 0,circular:!0,selfJoin:!1}),I(um,`defaultRoutes`,{backgroundColor:`backgroundColor`}),I(um,`descriptors`,{_scriptable:!0,_indexable:e=>e!==`borderDash`});function dm(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 fm(e,t,n){e.lineTo(n.x,n.y)}function pm(e){return e.stepped?_l:e.tension||e.cubicInterpolationMode===`monotone`?vl:fm}function mm(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 _m(e){let t=e.options,n=t.borderDash&&t.borderDash.length;return!e._decimated&&!e._loop&&!t.tension&&t.cubicInterpolationMode!==`monotone`&&!t.stepped&&!n?gm:hm}function vm(e){return e.stepped?ku:e.tension||e.cubicInterpolationMode===`monotone`?Au:Ou}function ym(e,t,n,r){let i=t._path;i||(i=t._path=new Path2D,t.path(i,n,r)&&i.closePath()),dm(e,t.options),e.stroke(i)}function bm(e,t,n,r){let{segments:i,options:a}=t,o=_m(t);for(let s of i)dm(e,a,s.style),e.beginPath(),o(e,t,s,{start:n,end:n+r-1})&&e.closePath(),e.stroke()}var xm=typeof Path2D==`function`;function Sm(e,t,n,r){xm&&!t.options.segment?ym(e,t,n,r):bm(e,t,n,r)}var Cm=class extends Kf{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;du(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=Uu(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=Bu(this,{property:t,start:r,end:r});if(!a.length)return;let o=[],s=vm(n),c,l;for(c=0,l=a.length;ce!==`borderDash`&&e!==`fill`});function wm(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,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;a-1?e.split(` -`):e}function zm(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 Bm(e,t){let n=e.chart.ctx,{body:r,footer:i,title:a}=e,{boxWidth:o,boxHeight:s}=t,c=Ml(t.bodyFont),l=Ml(t.titleFont),u=Ml(t.footerFont),d=a.length,f=i.length,p=r.length,m=jl(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 Vm(e,t){let{y:n,height:r}=t;return ne.height-r/2?`bottom`:`center`}function Hm(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 Um(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`),Hm(l,e,t,n)&&(l=`center`),l}function Wm(e,t,n){let r=n.yAlign||t.yAlign||Vm(e,n);return{xAlign:n.xAlign||t.xAlign||Um(e,t,n,r),yAlign:r}}function Gm(e,t){let{x:n,width:r}=e;return t===`right`?n-=r:t===`center`&&(n-=r/2),n}function Km(e,t,n){let{y:r,height:i}=e;return t===`top`?r+=n:r-=t===`bottom`?i+n:i/2,r}function qm(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}=Al(o),m=Gm(t,s),h=Km(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:bc(m,0,r.width-t.width),y:bc(h,0,r.height-t.height)}}function Jm(e,t,n){let r=jl(n.padding);return t===`center`?e.x+e.width/2:t===`right`?e.x+e.width-r.right:e.x+r.left}function Ym(e){return Lm([],Rm(e))}function Xm(e,t,n){return Fl(e,{tooltip:t,tooltipItems:n,type:`tooltip`})}function Zm(e,t){let n=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return n?e.override(n):e}var Qm={beforeTitle:Es,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=Zm(n,e);Lm(t.before,Rm($m(i,`beforeLabel`,this,e))),Lm(t.lines,$m(i,`label`,this,e)),Lm(t.after,Rm($m(i,`afterLabel`,this,e))),r.push(t)}),r}getAfterBody(e,t){return Ym($m(t.callbacks,`afterBody`,this,e))}getFooter(e,t){let{callbacks:n}=t,r=$m(n,`beforeFooter`,this,e),i=$m(n,`footer`,this,e),a=$m(n,`afterFooter`,this,e),o=[];return o=Lm(o,Rm(r)),o=Lm(o,Rm(i)),o=Lm(o,Rm(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=Zm(e.callbacks,t);r.push($m(n,`labelColor`,this,t)),i.push($m(n,`labelPointStyle`,this,t)),a.push($m(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=Im[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=Bm(this,n),o=Object.assign({},e,t),s=Wm(this.chart,n,o),c=qm(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}=Al(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=Nu(n.rtl,this.x,this.width);for(e.x=Jm(this,n.titleAlign,n),t.textAlign=c.textAlign(n.titleAlign),t.textBaseline=`middle`,a=Ml(n.titleFont),o=n.titleSpacing,t.fillStyle=n.titleColor,t.font=a.string,s=0;se!==0)?(e.beginPath(),e.fillStyle=i.multiKeyBackground,Cl(e,{x:t,y:p,w:c,h:s,radius:o}),e.fill(),e.stroke(),e.fillStyle=a.backgroundColor,e.beginPath(),Cl(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=Ml(n.bodyFont),d=u.lineHeight,f=0,p=Nu(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=Jm(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=Im[e.position].call(this,this._active,this._eventPosition);if(!n)return;let a=this._size=Bm(this,e),o=Object.assign({},n,this._size),s=Wm(t,e,o),c=qm(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=jl(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),Pu(e,t.textDirection),i.y+=a.top,this.drawTitle(i,e,t),this.drawBody(i,e,t),this.drawFooter(i,e,t),Fu(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=!Ps(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||!Ps(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=Im[i.position].call(this,e,t);return a!==!1&&(n!==a.x||r!==a.y)}};I(eh,`positioners`,Im);var th={id:`tooltip`,_element:eh,positioners:Im,afterInit(e,t,n){n&&(e.tooltip=new eh({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:Qm},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`]},nh=(e,t,n,r)=>(typeof t==`string`?(n=e.push(t)-1,r.unshift({index:n,label:t})):isNaN(t)&&(n=null),n);function rh(e,t,n,r){let i=e.indexOf(t);return i===-1?nh(e,t,n,r):i===e.lastIndexOf(t)?i:n}var ih=(e,t)=>e===null?null:bc(Math.round(e),0,t);function ah(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}};I(oh,`id`,`category`),I(oh,`defaults`,{ticks:{callback:ah}});function sh(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=oc((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=oc(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&&uc((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=ac(w,Math.round(w),b/1e3)?Math.round(w):Math.ceil(w));let T=Math.max(mc(b),mc(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&&ac(n[n.length-1].value,o,ch(o,y,e))?n[n.length-1].value=o:n.push({value:o}):(!_||C===o)&&n.push({value:C}),n}function ch(e,t,{horizontal:n,minRotation:r}){let i=fc(r),a=(n?Math.sin(i):Math.cos(i))||.001,o=.75*t*(``+e).length;return Math.min(t/a,o)}var lh=class extends fp{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=ic(r),t=ic(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=sh({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`&&dc(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 Zc(e,this.chart.options.locale,this.options.ticks.format)}},uh=class extends lh{determineDataLimits(){let{min:e,max:t}=this.getMinMax(!0);this.min=ks(e)?e:0,this.max=ks(t)?t:1,this.handleTickRangeOptions()}computeTickLimit(){let e=this.isHorizontal(),t=e?this.width:this.height,n=fc(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}};I(uh,`id`,`linear`),I(uh,`defaults`,{ticks:{callback:el.formatters.numeric}});var dh=e=>Math.floor(rc(e)),fh=(e,t)=>10**(dh(e)+t);function ph(e){return e/10**dh(e)==1}function mh(e,t,n){let r=10**n,i=Math.floor(e/r);return Math.ceil(t/r)-i}function hh(e,t){let n=dh(t-e);for(;mh(e,t,n)>10;)n++;for(;mh(e,t,n)<10;)n--;return Math.min(n,dh(e))}function gh(e,{min:t,max:n}){t=As(e.min,t);let r=[],i=dh(t),a=hh(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=As(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=As(e.max,f);return r.push({value:p,major:ph(p),significand:d}),r}var _h=class extends fp{constructor(e){super(e),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(e,t){let n=lh.prototype.parse.apply(this,[e,t]);if(n===0)this._zero=!0;else return ks(n)&&n>0?n:null}determineDataLimits(){let{min:e,max:t}=this.getMinMax(!0);this.min=ks(e)?Math.max(0,e):null,this.max=ks(t)?Math.max(0,t):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!ks(this._userMin)&&(this.min=e===fh(this.min,0)?fh(this.min,-1):fh(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(fh(n,-1)),a(fh(r,1)))),n<=0&&i(fh(r,-1)),r<=0&&a(fh(n,1)),this.min=n,this.max=r}buildTicks(){let e=this.options,t=gh({min:this._userMin,max:this._userMax},this);return e.bounds===`ticks`&&dc(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`:Zc(e,this.chart.options.locale,this.options.ticks.format)}configure(){let e=this.min;super.configure(),this._startValue=rc(e),this._valueRange=rc(this.max)-rc(e)}getPixelForValue(e){return(e===void 0||e===0)&&(e=this.min),e===null||isNaN(e)?NaN:this.getPixelForDecimal(e===this.min?0:(rc(e)-this._startValue)/this._valueRange)}getValueForPixel(e){let t=this.getDecimalForPixel(e);return 10**(this._startValue+t*this._valueRange)}};I(_h,`id`,`logarithmic`),I(_h,`defaults`,{ticks:{callback:el.formatters.logarithmic,major:{enabled:!0}}});function vh(e){let t=e.ticks;if(t.display&&e.display){let e=jl(t.backdropPadding);return Z(t.font&&t.font.size,ol.font.size)+e.height}return 0}function yh(e,t,n){return n=Os(n)?n:[n],{w:ll(e,t.string,n),h:n.length*t.lineHeight}}function bh(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 xh(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 Ch(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(pc(vc(c.angle+ec))),u=Oh(c.y,s.h,l),d=Eh(l),f=Dh(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 wh(e,t){if(!t)return!0;let{left:n,top:r,right:i,bottom:a}=e;return!(ml({x:n,y:r},t)||ml({x:n,y:a},t)||ml({x:i,y:r},t)||ml({x:i,y:a},t))}function Th(e,t,n){let r=[],i=e._pointLabels.length,a=e.options,{centerPointLabels:o,display:s}=a.pointLabels,c={extra:vh(a)/2,additionalAngle:o?$/i:0},l;for(let a=0;a270||n<90)&&(e-=t),e}function kh(e,t,n){let{left:r,top:i,right:a,bottom:o}=n,{backdropColor:s}=t;if(!Y(s)){let n=Al(t.borderRadius),c=jl(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(),Cl(e,{x:l,y:u,w:d,h:f,radius:n}),e.fill()):e.fillRect(l,u,d,f)}}function Ah(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));kh(n,a,t);let o=Ml(a.font),{x:s,y:c,textAlign:l}=t;Sl(n,e._pointLabels[i],s,c+o.lineHeight/2,o,{color:a.color,textAlign:l,textBaseline:`middle`})}}function jh(e,t,n,r){let{ctx:i}=e;if(n)i.arc(e.xCenter,e.yCenter,t,0,Xs);else{let n=e.getPointPosition(0,t);i.moveTo(n.x,n.y);for(let a=1;a{let n=Ns(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?xh(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=Xs/(this._pointLabels.length||1),n=this.options.startAngle||0;return vc(e*t+fc(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);Mh(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=Ml(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=jl(s.backdropPadding);e.fillRect(-a/2-t.left,-i-c.size/2-t.top,a+t.width,c.size+t.height)}Sl(e,r.label,0,-i,c,{color:s.color,strokeColor:s.textStrokeColor,strokeWidth:s.textStrokeWidth})}),e.restore()}drawTitle(){}};I(Ph,`id`,`radialLinear`),I(Ph,`defaults`,{display:!0,animate:!0,position:`chartArea`,angleLines:{display:!0,lineWidth:1,borderDash:[],borderDashOffset:0},grid:{circular:!1},startAngle:0,ticks:{showLabelBackdrop:!0,callback:el.formatters.numeric},pointLabels:{backdropColor:void 0,backdropPadding:2,display:!0,font:{size:10},callback(e){return e},padding:5,centerPointLabels:!1}}),I(Ph,`defaultRoutes`,{"angleLines.color":`borderColor`,"pointLabels.color":`color`,"ticks.color":`color`}),I(Ph,`descriptors`,{angleLines:{_fallback:`grid`}});var Fh={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}},Ih=Object.keys(Fh);function Lh(e,t){return e-t}function Rh(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)),ks(o)||(o=typeof r==`string`?n.parse(o,r):n.parse(o)),o===null?null:(i&&(o=i===`week`&&(lc(a)||a===!0)?n.startOf(o,`isoWeek`,a):n.startOf(o,i)),+o)}function zh(e,t,n,r){let i=Ih.length;for(let a=Ih.indexOf(e);a=Ih.indexOf(n);a--){let n=Ih[a];if(Fh[n].common&&e._adapter.diff(i,r,n)>=t-1)return n}return Ih[n?Ih.indexOf(n):0]}function Vh(e){for(let t=Ih.indexOf(e)+1,n=Ih.length;t=t?n[r]:n[i];e[a]=!0}}function Uh(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 Wh(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=bc(t,0,a),n=bc(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||zh(i.minUnit,t,n,this._getLabelCapacity(t)),o=Z(r.ticks.stepSize,1),s=a===`week`&&i.isoWeekday,c=lc(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 Ns(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}=wc(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}=wc(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 qh=class extends Gh{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=Kh(t,this.min),this._tableRange=Kh(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(Kh(this._table,e)-this._minPos)/this._tableRange}getValueForPixel(e){let t=this._offsets,n=this.getDecimalForPixel(e)/t.factor-t.end;return Kh(this._table,n*this._tableRange+this._minPos,!0)}};I(qh,`id`,`timeseries`),I(qh,`defaults`,Gh.defaults),em.register(zd,Fm,oh,uh,th);var Jh=new Intl.DateTimeFormat(`ko-KR`,{hour:`2-digit`,hour12:!1}),Yh=`#94a3b8`,Xh=`rgba(148, 163, 184, 0.18)`,Zh=`#0d9488`,Qh=`#0f766e`;function $h(e){return Jh.format(new Date(e*1e3))}function eg(e){return e.map((t,n)=>n%3==0||n===e.length-1?$h(t.bucketStart):``)}function tg({buckets:e}){let t=P(null),n=P(null),r=P(e);return r.current=e,N(()=>{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?$h(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:Yh,font:{family:a,size:11,weight:700},maxRotation:0,minRotation:0}},y:{beginAtZero:!0,border:{display:!1},grid:{color:Xh},ticks:{color:Yh,font:{family:a,size:11,weight:700},maxTicksLimit:4,precision:0}}}};return n.current=new em(e,{type:`bar`,data:{labels:eg(r.current),datasets:[{data:r.current.map(e=>e.blocked),backgroundColor:Zh,hoverBackgroundColor:Qh,borderRadius:4,borderSkipped:!1,barPercentage:.82,categoryPercentage:.9,maxBarThickness:24}]},options:o}),()=>{n.current?.destroy(),n.current=null}},[]),N(()=>{let t=n.current;t&&(t.data.labels=eg(e),t.data.datasets[0].data=e.map(e=>e.blocked),t.update())},[e]),z(`div`,{class:`mt-5 h-44 sm:h-52`,children:z(`canvas`,{"aria-label":`최근 24시간 시간대별 SafeShield 차단 요청`,ref:t,role:`img`,children:`최근 24시간 시간대별 SafeShield 차단 요청 차트`})})}var ng=3600,rg=24;function ig(e,t){return e<=0?`0.0%`:`${(t/e*100).toFixed(1)}%`}function ag(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/ng)*ng}function og(e){let t=new Map(e.hourly.map(e=>[e.bucketStart,e])),n=ag(e)-23*ng;return Array.from({length:rg},(e,r)=>{let i=n+r*ng;return t.get(i)??{bucketStart:i,queries:0,blocked:0}})}function sg({children:e}){return z(`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:z(`p`,{class:`m-0 max-w-lg text-sm font-semibold leading-6 text-slate-500`,children:e})})}function cg({data:e,error:t,loading:n,nowTimestamp:r,refreshing:i}){let a;if(n&&!e)a=z(sg,{children:`SafeShield 보호 활동을 불러오는 중입니다.`});else if(t&&!e)a=z(sg,{children:`SafeShield 보호 활동을 불러오지 못했습니다. SafeShield 페이지에서 상태를 확인해 주세요.`});else if(!e||!e.available)a=z(sg,{children:`현재 SafeShield에서 차단 통계를 제공하지 않습니다. 통계 기능을 확인해 주세요.`});else if(!e.enabled)a=z(sg,{children:`통계 수집이 꺼져 있습니다. SafeShield 페이지에서 통계를 켜면 최근 24시간 활동이 표시됩니다.`});else{let t=og(e),n=t.reduce((e,t)=>({queries:e.queries+t.queries,blocked:e.blocked+t.blocked}),{queries:0,blocked:0});a=z(v,{children:[z(`dl`,{class:`mt-5 grid gap-3 sm:grid-cols-3`,children:[z(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3`,children:[z(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:`DNS 요청`}),z(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-slate-950`,children:J(n.queries)})]}),z(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3`,children:[z(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:`차단`}),z(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-teal-700`,children:J(n.blocked)})]}),z(`div`,{class:`rounded-xl bg-slate-50 px-4 py-3`,children:[z(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:`차단율`}),z(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-slate-950`,children:ig(n.queries,n.blocked)})]})]}),z(tg,{buckets:t})]})}return z(`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:[z(`div`,{class:`min-w-0`,children:[z(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`SafeShield activity`}),z(`div`,{class:`mt-2 flex min-w-0 items-center justify-between gap-3`,children:[z(`h3`,{class:`m-0 min-w-0 text-xl font-black text-slate-950`,children:`최근 24시간 보호 활동`}),z(`a`,{class:`shrink-0 whitespace-nowrap text-xs font-extrabold text-teal-700 no-underline hover:text-teal-800`,href:`#safeshield`,children:`상세 통계 →`})]}),e?.updatedAt?z(`p`,{class:`mt-2 mb-0 text-[0.68rem] font-semibold text-slate-400`,title:qa(e.updatedAt),children:i?`갱신 중…`:`집계: ${Ja(e.updatedAt,r)}`}):null]}),a,t&&e?z(`p`,{class:`mt-3 mb-0 text-xs font-bold text-amber-700`,children:`최근 통계 갱신에 실패해 마지막 정상 데이터를 표시하고 있습니다.`}):null]})}var lg=7200;function ug(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,lg)}function dg(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 fg({detail:e,eyebrow:t,href:n,icon:r,linkLabel:i=`자세히 보기 →`,meta:a,metaState:o,metaWarning:s=!1,state:c=`neutral`,value:l}){let u=z(v,{children:[z(`div`,{class:`flex items-start justify-between gap-4`,children:[z(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.16em] text-slate-500`,children:t}),z(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl ring-1 ring-inset ${dg(c)}`,children:r})]}),z(`p`,{class:`mt-4 mb-0 break-words text-2xl font-black tracking-tight text-slate-950`,children:l}),z(`p`,{class:`mt-2 mb-0 min-h-10 text-sm font-medium leading-5 text-slate-500`,children:e}),a?z(`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?z(`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?z(`a`,{class:`${d} no-underline hover:border-slate-300 hover:shadow-md`,href:n,children:u}):z(`article`,{class:d,children:u})}var pg=6e4;function mg(e,t){return!e||!Number.isFinite(e)||e<=0?null:Math.max(0,t-e)}function hg(e,t,n,r){return!t||t<=0?`${e}: ${r}`:z(`span`,{title:qa(t),children:[e,`: `,Ja(t,n)]})}function gg(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 _g(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 vg({label:e,value:t}){return z(`div`,{class:`flex min-w-0 items-start justify-between gap-4 border-b border-slate-100 py-3 last:border-b-0`,children:[z(`dt`,{class:`text-sm font-semibold text-slate-500`,children:e}),z(`dd`,{class:`m-0 min-w-0 max-w-[68%] break-words text-right text-sm font-extrabold text-slate-950`,children:t})]})}function yg(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 bg({data:e,error:t,loading:n,nowTimestamp:r}){let i=!!(e&&e.generatedAt>0),a=yg(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 z(`div`,{class:`mt-5 border-t border-slate-200 pt-5`,children:[z(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[z(`div`,{class:`flex items-center gap-2`,children:[z(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:`장치 진단`}),i?z(`span`,{class:`rounded-full px-2.5 py-1 text-[11px] font-extrabold ${a.badge}`,children:a.label}):null]}),z(`a`,{class:`text-xs font-extrabold text-teal-700 no-underline hover:text-teal-900`,href:`#settings`,children:`상세 보기 →`})]}),t&&!i?z(`div`,{class:`mt-3 rounded-xl border border-amber-200 bg-amber-50/70 px-4 py-3`,children:z(`div`,{class:`flex items-start gap-2`,children:[z(U,{class:`mt-0.5 size-4 shrink-0 text-amber-700`}),z(`div`,{class:`min-w-0`,children:[z(`p`,{class:`m-0 text-sm font-extrabold text-slate-900`,children:`진단 상태를 확인하지 못했습니다.`}),z(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`설정에서 장치 진단 상태를 다시 확인해 주세요.`})]})]})}):i&&e?z(`div`,{class:`mt-3 rounded-xl border px-4 py-3 ${a.panel}`,children:[z(`div`,{class:`flex min-w-0 items-start gap-3`,children:[e.overall===`ok`?z(H,{class:`mt-0.5 size-5 shrink-0 ${a.icon}`}):e.overall===`initializing`?z(Ht,{class:`mt-0.5 size-5 shrink-0 ${a.icon}`}):z(U,{class:`mt-0.5 size-5 shrink-0 ${a.icon}`}),z(`div`,{class:`min-w-0 flex-1`,children:[z(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:e.summary.message}),z(`p`,{class:`mt-1 mb-0 text-xs font-semibold leading-5 text-slate-500`,children:[`마지막 진단: `,Ja(e.generatedAt,r),` · 검사 항목:`,` `,e.summary.total,`개`]})]})]}),s.length>0?z(`div`,{class:`mt-3 grid gap-2 sm:grid-cols-2`,children:s.map(e=>z(`div`,{class:`min-w-0 rounded-lg border border-white/80 bg-white/70 px-3 py-2`,children:[z(`p`,{class:`m-0 text-xs font-extrabold text-slate-900`,children:e.label}),z(`p`,{class:`mt-1 mb-0 line-clamp-2 text-xs leading-5 text-slate-500`,children:e.message})]},e.id))}):null,c>0?z(`p`,{class:`mt-2 mb-0 text-xs font-extrabold text-slate-600`,children:[`외 `,c,`건의 확인 항목이 있습니다.`]}):null]}):z(`div`,{class:`mt-3 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3`,children:[z(`p`,{class:`m-0 text-sm font-extrabold text-slate-900`,children:n?`진단 상태를 확인하고 있습니다.`:`아직 생성된 진단 결과가 없습니다.`}),z(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`진단 결과가 생성되면 현재 장치 상태를 이곳에 요약해서 표시합니다.`})]})]})}function xg(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 Sg({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?$a(i.runtime.memory):null,[i]),[j,ee]=M(()=>Math.floor(Date.now()/1e3));if(N(()=>{let e=window.setInterval(()=>{ee(Math.floor(Date.now()/1e3))},pg);return()=>window.clearInterval(e)},[]),v&&!i)return z(bo,{});if(c&&!i)return z(xo,{message:c,onRetry:y});if(!i)return null;let te=A?Math.round(A.percent):0,ne=xg(b,S,x),re=D?.packages.find(e=>e.name===`luci-app-smartsafehub`)??D?.packages[0]??null,ie=!!(D&&D.updateCount>0),ae=D?ie?`${D.updateCount}개 업데이트`:D.lastCheckAt?`최신 상태`:`확인 전`:O?`확인 필요`:`확인 중`,oe=re?ie&&re.availableVersion?`${re.installedVersion} → ${re.availableVersion}`:`설치 버전 ${re.installedVersion}`:O?`업데이트 상태를 불러오지 못했습니다.`:k?`업데이트 상태를 확인하고 있습니다.`:`업데이트 상태를 확인할 수 없습니다.`,se=a?`${J(a.totals.online)}대`:s?`확인 중`:`확인 필요`,ce=a?`Wi-Fi ${J(a.totals.wireless)}대 · 유선/기타 ${J(a.totals.ethernet)}대`:o||`연결 기기 요약을 불러오고 있습니다.`,le=gg(i.network.protocol),ue=i.network.available?i.network.ipv4Address?`WAN ${i.network.ipv4Address}${le?` · ${le}`:``}`:le?`${le} · WAN 주소를 받지 못했습니다.`:`WAN 주소를 받지 못했습니다.`:`WAN 인터페이스를 찾을 수 없습니다.`,de=!!h?.conflict.detected,fe=!i.network.up||de||g!==null&&!h?`warning`:h?`healthy`:`neutral`,pe=de?`네트워크 충돌`:i.network.up?`정상 연결`:`연결 확인`,me=i.network.up?de?`⚠ LAN 대역과 충돌합니다`:_&&!h?`네트워크 대역 확인 중`:g&&!h?`네트워크 대역을 확인하지 못했습니다.`:h?`✓ 네트워크 충돌 없음`:`네트워크 대역 확인 필요`:`WAN 연결 상태를 확인해 주세요.`,he=!i.network.up||de||g!==null&&!h?`warning`:h?`healthy`:`neutral`,ge=i.network.ipv4Address?_g(i.network.ipv4Address)?`${i.network.ipv4Address} · 사설 네트워크`:i.network.ipv4Address:`할당되지 않음`,_e=mg(b?.timestamps.lastSuccess,j),ve=b?Math.max(b.timestamps.refreshIntervalS*2,86400):null,ye=!!(b?.enabled&&b.status!==`running`&&_e!==null&&ve!==null&&_e>ve),be=ug(D,j),xe=!!l?.current.metadataAvailable,Se=d&&!l?`확인 중`:u&&!l?`확인 필요`:xe?l?.current.releaseVersion?`SmartSafeHub ${l.current.releaseVersion}`:`SmartSafeHub 펌웨어`:`${i.software.distribution} ${i.software.version}`,Ce=d&&!l?`확인 중`:u&&!l?`확인 필요`:xe&&l?.current.buildId||i.software.revision,we=xe?`빌드 ID`:`리비전`;return z(`div`,{class:`min-w-0 space-y-6`,children:[z(`section`,{"aria-labelledby":`dashboard-overview-title`,children:[z(q,{description:`인터넷, SafeShield, 연결 기기와 소프트웨어 상태를 한눈에 확인합니다.`,eyebrow:`Overview`,id:`dashboard-overview-title`,title:`시스템 개요`}),z(`div`,{class:`grid min-w-0 gap-4 sm:grid-cols-2 xl:grid-cols-4`,children:[z(fg,{detail:ue,eyebrow:`Internet`,href:`#network`,icon:z(Bt,{class:`size-5`}),linkLabel:de?`해결하기 →`:`자세히 보기 →`,meta:me,metaState:he,state:fe,value:pe}),z(fg,{detail:ne.detail,eyebrow:`SafeShield`,href:`#safeshield`,icon:z(At,{class:`size-5`}),meta:hg(ye?`차단 목록 갱신 지연`:`차단 목록 갱신`,b?.timestamps.lastSuccess,j,S?`확인 중`:`기록 없음`),metaWarning:ye,state:ne.state===`healthy`&&ye?`warning`:ne.state,value:ne.value}),z(fg,{detail:ce,eyebrow:`Connected devices`,href:`#devices`,icon:z(Mt,{class:`size-5`}),meta:hg(`목록 확인`,a?.generatedAt,j,s?`확인 중`:`기록 없음`),state:a?`healthy`:o?`warning`:`neutral`,value:se}),z(fg,{detail:oe,eyebrow:`Software update`,href:`#system`,icon:z(Ft,{class:`size-5`}),meta:D&&!D.settings.checkEnabled?hg(`자동 확인 꺼짐 · 마지막 확인`,D.lastCheckAt,j,`확인 기록 없음`):hg(be?`업데이트 확인 지연`:`마지막 확인`,D?.lastCheckAt,j,k?`확인 중`:`아직 없음`),metaWarning:be,state:O||be||ie?`warning`:D?.lastCheckAt?`healthy`:`neutral`,value:ae})]}),e===!1?z(`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:[z(`div`,{class:`flex min-w-0 items-start gap-3`,children:[z(`span`,{class:`ssh-dashboard-account-banner-icon grid size-10 shrink-0 place-items-center rounded-xl`,"aria-hidden":`true`,children:z(Pt,{class:`size-5`})}),z(`div`,{class:`min-w-0`,children:[z(`div`,{class:`flex flex-wrap items-center gap-2`,children:[z(`p`,{class:`ssh-dashboard-account-banner-title m-0 font-bold`,children:`SmartSafeHub 계정을 연결해 주세요`}),z(`span`,{class:`ssh-dashboard-account-banner-badge rounded-full px-2 py-0.5 text-[0.7rem] font-bold`,children:`연결 필요`})]}),z(`p`,{class:`ssh-dashboard-account-banner-description mt-1 mb-0 text-sm leading-relaxed`,children:`계정을 연결하면 웹사이트에서 기기를 관리하고 구독 정보를 확인할 수 있습니다. DNS 보호는 연결하지 않아도 계속 작동합니다.`})]})]}),z(`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]}),z(`section`,{"aria-labelledby":`dashboard-activity-title`,children:[z(q,{description:`최근 SafeShield 차단 활동과 현재 네트워크 연결 구성을 함께 확인합니다.`,eyebrow:`Activity`,id:`dashboard-activity-title`,title:`네트워크 보호 활동`}),z(`div`,{class:`grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1.55fr)_minmax(18rem,0.65fr)]`,children:[z(cg,{data:C,error:w,loading:T,nowTimestamp:j,refreshing:E}),z(`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:[z(`div`,{class:`flex items-start justify-between gap-4`,children:[z(`div`,{children:[z(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Network`}),z(`h3`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`연결 상태`})]}),z(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl ${i.network.up&&!de?`bg-emerald-50 text-emerald-700`:`bg-amber-50 text-amber-800`}`,children:z(Bt,{class:`size-5`})})]}),z(`dl`,{class:`mt-4 mb-0`,children:[z(vg,{label:`인터넷`,value:de?`네트워크 충돌`:i.network.up?`정상 연결`:`연결 확인`}),z(vg,{label:`WAN IP`,value:ge}),z(vg,{label:`프로토콜`,value:le||`확인되지 않음`}),z(vg,{label:`상위 네트워크`,value:h?.wan.subnet||(_?`확인 중`:g?`확인 필요`:`확인되지 않음`)}),z(vg,{label:`LAN 네트워크`,value:h?.lan.subnet||(_?`확인 중`:g?`확인 필요`:`확인되지 않음`)}),z(vg,{label:`대역 충돌`,value:h?z(`span`,{class:h.conflict.detected?`text-amber-700`:`text-emerald-700`,children:h.conflict.detected?`충돌 감지`:`없음`}):_?`확인 중`:`확인 필요`})]}),z(`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:`네트워크 보기`})]})]})]}),z(`section`,{"aria-labelledby":`dashboard-recent-activity-title`,children:[z(q,{action:z(`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:`최근 활동`}),z(`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?z(ho,{error:n,loading:r}):z(mo,{compact:!0,events:(t?.events??[]).slice(0,3)})})]}),z(`section`,{"aria-labelledby":`dashboard-system-title`,children:[z(q,{description:`메모리, 시스템 부하와 장치 정보를 빠르게 점검합니다.`,eyebrow:`System`,id:`dashboard-system-title`,title:`시스템 상태`}),z(`div`,{class:`grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1.45fr)_minmax(20rem,0.75fr)]`,children:[z(`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:[z(`div`,{class:`flex flex-wrap items-start justify-between gap-4`,children:[z(`div`,{children:[z(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Runtime`}),z(`h3`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`리소스 사용량`})]}),z(`span`,{class:`inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-black ring-1 ring-inset ${te>=90?`bg-amber-50 text-amber-800 ring-amber-200`:`bg-emerald-50 text-emerald-700 ring-emerald-200`}`,children:[z(`span`,{class:`size-2 rounded-full ${te>=90?`bg-amber-500`:`bg-emerald-500`}`}),te>=90?`메모리 확인`:`정상 실행`]})]}),z(`div`,{class:`mt-5 grid gap-3 sm:grid-cols-3`,children:[z(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[z(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[z(Ut,{class:`size-4`}),z(`span`,{class:`text-xs font-extrabold`,children:`메모리`})]}),z(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:[te,`%`]}),z(`p`,{class:`mt-1 mb-0 text-xs font-semibold text-slate-500`,children:[A?Wa(A.used):`-`,` /`,` `,Wa(i.runtime.memory.total)]}),z(`div`,{"aria-label":`메모리 사용률 ${te}%`,class:`mt-3 h-1.5 overflow-hidden rounded-full bg-slate-200`,role:`progressbar`,"aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":te,children:z(`div`,{class:`h-full rounded-full transition-[width] duration-300 ${te>=90?`bg-amber-500`:`bg-teal-600`}`,style:{width:`${Math.min(100,te)}%`}})})]}),z(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[z(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[z(jt,{class:`size-4`}),z(`span`,{class:`text-xs font-extrabold`,children:`시스템 부하`})]}),z(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:Ga(i.runtime.load[0])}),z(`p`,{class:`mt-1 mb-0 text-xs font-semibold text-slate-500`,children:[`5분 `,Ga(i.runtime.load[1]),` · 15분`,` `,Ga(i.runtime.load[2])]})]}),z(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[z(`div`,{class:`flex items-center gap-2 text-slate-500`,children:[z(Ht,{class:`size-4`}),z(`span`,{class:`text-xs font-extrabold`,children:`실행 시간`})]}),z(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:Ua(i.runtime.uptime)}),z(`p`,{class:`mt-1 mb-0 text-xs font-semibold text-slate-500`,children:[`부팅 `,Ka(i.runtime.localtime,i.runtime.uptime)]})]})]}),z(bg,{data:f,error:p,loading:m,nowTimestamp:j})]}),z(`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:[z(`div`,{class:`flex items-start justify-between gap-4`,children:[z(`div`,{children:[z(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Device`}),z(`h3`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:`장치 정보`})]}),z(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:z(jt,{class:`size-5`})})]}),z(`dl`,{class:`mt-4 mb-0`,children:[z(vg,{label:`모델`,value:i.device.model}),z(vg,{label:`호스트 이름`,value:i.device.hostname}),z(vg,{label:`펌웨어`,value:z(`span`,{title:xe?l?.current.releaseVersion?`SmartSafeHub 펌웨어 ${l.current.releaseVersion}`:`SmartSafeHub 커스텀 펌웨어`:u||void 0,children:Se})}),z(vg,{label:we,value:Ce}),z(vg,{label:`커널`,value:i.software.kernel}),z(vg,{label:`WAN IP`,value:i.network.ipv4Address||`할당되지 않음`})]})]})]})]})]})}var Cg=Object.assign,wg=/^(-|f[lo].*[^se]$|g.{5,}[^ps]$|z|o[pr]|(W.{5})?[lL]i.*(t|mp)$|an|(bo|s).{4}Im|sca|m.{6}[ds]|ta|c.*[st]$|wido|ini)/;function Tg(e,t){var n=le(e,t);return n.containerInfo=t,n}var Eg=Symbol.for(`react.element`),Dg=/^(?: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]/,Og=/[A-Z0-9]/g,kg=typeof document<`u`,Ag=function(e){return/fil|che|rad/.test(e)};y.prototype.isReactComponent=!0,[`componentWillMount`,`componentWillReceiveProps`,`componentWillUpdate`].forEach(function(e){Object.defineProperty(y.prototype,e,{configurable:!0,get:function(){return this[`UNSAFE_`+e]},set:function(t){Object.defineProperty(this,e,{configurable:!0,writable:!0,value:t})}})});var jg=e.event;e.event=function(e){return jg&&(e=jg(e)),e.persist=function(){},e.isPropagationStopped=function(){return this.cancelBubble},e.isDefaultPrevented=function(){return this.defaultPrevented},e.nativeEvent=e};var Mg={configurable:!0,get:function(){return this.class}},Ng=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||kg&&a==`children`&&n==`noscript`||a==`class`||a==`className`)){if(a==`style`&&typeof o==`object`){var s=void 0;for(var c in o)typeof o[c]!=`number`||wg.test(c)||(s||(s=o=Cg({},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`||Ag(t.type)?l==`onfocus`?a=`onfocusin`:l==`onblur`&&(a=`onfocusout`):l=a=`oninput`,l==`oninput`&&r[a=l]&&(a=`oninputCapture`)}else i&&Dg.test(a)?a=a.replace(Og,`-$&`).toLowerCase():o===null&&(o=void 0);r[a]=o}}n==`select`&&(r.multiple&&Array.isArray(r.value)&&(r.value=D(t.children).forEach(function(e){e.props.selected=r.value.indexOf(e.props.value)!=-1})),r.defaultValue!=null&&(r.value=D(t.children).forEach(function(e){e.props.selected=r.multiple?r.defaultValue.indexOf(e.props.value)!=-1:r.defaultValue==e.props.value}))),t.class&&!t.className?(r.class=t.class,Object.defineProperty(r,"className",Mg)):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=Cg({},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=Eg,Ng&&Ng(e)};var Pg=8,Fg=256,Ig=8;function Lg(e){return e.findIndex(e=>!e.disabled)}function Rg(e){for(let t=e.length-1;t>=0;--t)if(!e[t]?.disabled)return t;return-1}function zg(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:Lg(e)}function Vg({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=P(null),f=P(null),p=P(null),m=P([]),[h,g]=M(!1),[_,v]=M(()=>Bg(a,o)),[y,b]=M(`bottom`),[x,S]=M(Fg),[C,w]=M({}),[T,E]=M(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(Fg,Math.max(44,a.length*42+12)),r=window.innerHeight-t.bottom-Ig,i=t.top-Ig,o=rr?`top`:`bottom`,s=Math.min(t.width,Math.max(0,window.innerWidth-16)),c=Math.max(Ig,Math.min(t.left,window.innerWidth-s-Ig)),l=o===`bottom`?r:i;b(o),S(Math.max(44,Math.min(Fg,Math.floor(l)))),w({left:`${c}px`,width:`${s}px`,...o===`bottom`?{top:`${t.bottom+Pg}px`}:{bottom:`${window.innerHeight-t.top+Pg}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()))},ee=e=>{n||a.length===0||(O(),g(!0),A(e??Bg(a,o)))};N(()=>{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)},[]),N(()=>{v(Bg(a,o)),m.current=m.current.slice(0,a.length)},[a,o]),N(()=>{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]),N(()=>{n&&h&&k()},[n,h]);let te=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`,ne=h?z(`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:z(`div`,{class:`overflow-y-auto overscroll-contain`,style:{maxHeight:`${x}px`},children:a.map((e,t)=>{let n=e.value===o;return z(`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(zg(a,t,1));break;case`ArrowUp`:e.preventDefault(),A(zg(a,t,-1));break;case`Home`:e.preventDefault(),A(Lg(a));break;case`End`:e.preventDefault(),A(Rg(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:[z(`span`,{class:`min-w-0 truncate`,children:e.label}),n?z(`span`,{"aria-hidden":`true`,class:`size-2 shrink-0 rounded-full bg-teal-500`}):null]},e.value)})})}):null;return z(`div`,{class:`relative ${t}`,ref:d,children:[z(`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 ${te}`,disabled:n,id:l,onClick:()=>{h?k():(O(),g(!0))},onKeyDown:e=>{switch(e.key){case`ArrowDown`:e.preventDefault(),ee(h?zg(a,_,1):Bg(a,o));break;case`ArrowUp`:e.preventDefault(),ee(h?zg(a,_,-1):Bg(a,o));break;case`Home`:e.preventDefault(),ee(Lg(a));break;case`End`:e.preventDefault(),ee(Rg(a));break;case`Enter`:case` `:e.preventDefault(),h?k():ee();break;case`Escape`:h&&(e.preventDefault(),k())}},ref:f,type:`button`,children:[z(`span`,{class:`min-w-0 truncate`,children:D?.label??`선택하세요`}),z(an,{"aria-hidden":`true`,class:`size-4 shrink-0 text-slate-400 transition-transform ${h?`rotate-180`:``}`})]}),ne&&T?Tg(ne,T):null,z(`span`,{class:`sr-only`,"aria-live":`polite`,children:y===`top`?`선택 목록이 위쪽에 열렸습니다.`:``})]})}var Hg=[{value:`skb`,label:`SK Broadband`},{value:`lgu`,label:`LG U+`}];function Ug(e){return e===`lgu`?`LG U+`:`SK Broadband`}function Wg({active:e,label:t,value:n}){return z(`div`,{class:`rounded-xl border border-slate-200 bg-white p-4`,children:[z(`div`,{class:`flex items-center justify-between gap-3`,children:[z(`p`,{class:`m-0 text-xs font-extrabold text-slate-500`,children:t}),z(`span`,{class:`size-2.5 shrink-0 rounded-full ${e?`bg-emerald-500`:`bg-slate-300`}`,"aria-hidden":`true`})]}),z(`p`,{class:`mt-2 mb-0 text-sm font-black text-slate-950`,children:n})]})}function Gg({data:e,error:t,feedback:n,loading:r,onDismissFeedback:i,onRetry:a,onSave:o,saving:s}){let[c,l]=M(!1),[u,d]=M(`skb`);if(N(()=>{e&&(l(e.enabled),d(e.provider))},[e]),r&&!e)return z(bo,{});if(t&&!e)return z(xo,{message:t,onRetry:a});if(!e)return z(xo,{message:`IPTV 설정을 확인할 수 없습니다.`,onRetry:a});let f=c!==e.enabled||u!==e.provider,p=!e.enabled||e.running&&e.igmpSnooping;return z(`div`,{class:`space-y-5`,children:[z(`section`,{class:`rounded-2xl border border-amber-200 bg-amber-50 p-5 sm:p-6`,children:z(`div`,{class:`flex items-start gap-4`,children:[z(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-amber-100 text-amber-800`,children:z(U,{class:`size-5`})}),z(`div`,{class:`min-w-0 flex-1`,children:[z(`div`,{class:`flex flex-wrap items-center gap-2`,children:[z(`h2`,{class:`m-0 text-base font-black text-amber-950`,children:`IPTV 실험 기능`}),z(`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`})]}),z(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-amber-900/80`,children:`현재 SK Broadband와 LG U+를 지원하며 두 통신사에는 동일한 멀티캐스트 IPTV 프로파일을 적용합니다. KT IPTV는 별도 네트워크 방식이 필요해 아직 지원하지 않습니다.`})]})]})}),n?z(`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:z(`div`,{class:`flex items-start justify-between gap-3`,children:[z(`p`,{class:`m-0 text-sm font-bold leading-6`,children:n.message}),z(`button`,{class:`shrink-0 text-xs font-extrabold underline underline-offset-2`,onClick:i,type:`button`,children:`닫기`})]})}):null,z(q,{eyebrow:`IPTV`,title:`IPTV 설정`,description:`통신사 환경에 맞는 멀티캐스트 중계 기능을 관리합니다.`}),z(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:[z(`div`,{class:`flex min-w-0 items-start gap-3`,children:[z(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:z(Vt,{class:`size-5`})}),z(`div`,{class:`min-w-0 flex-1`,children:[z(`div`,{class:`flex min-w-0 flex-wrap items-center justify-between gap-2`,children:[z(`h2`,{class:`m-0 text-lg font-black text-slate-950`,children:`IPTV`}),z(`div`,{class:`flex shrink-0 items-center gap-2`,children:[f?z(`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:[z(U,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}):null,z(`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:z(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${c?`left-6`:`left-1`}`})})]})]}),z(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`WAN의 멀티캐스트 방송을 LAN 셋톱박스로 전달합니다.`})]})]}),z(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]`,children:[z(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[z(`div`,{class:`flex flex-wrap items-start justify-between gap-2`,children:[z(`div`,{children:[z(`label`,{class:`block text-xs font-extrabold text-slate-600`,htmlFor:`iptv-provider`,children:`통신사`}),z(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`IPTV를 제공하는 회선 사업자를 선택하세요.`})]}),z(`span`,{class:`rounded-full border border-teal-200 bg-teal-50 px-2.5 py-1 text-[10px] font-black text-teal-700`,children:`멀티캐스트 방식`})]}),z(Vg,{ariaLabel:`IPTV 통신사`,className:`mt-3`,disabled:s||!c,id:`iptv-provider`,onChange:e=>d(e),options:Hg,value:u}),z(`div`,{class:`mt-3 flex items-start gap-2 rounded-lg border border-slate-200 bg-white p-3`,children:[z(H,{"aria-hidden":`true`,class:`mt-0.5 size-4 shrink-0 text-teal-700`}),z(`p`,{class:`m-0 text-xs leading-5 text-slate-600`,children:[Ug(u),`에는 현재 SK Broadband/LG U+ 공통 IGMP Proxy 프로파일을 적용합니다. KT는 추후 별도 IPTV 프로파일로 지원할 예정입니다.`]})]})]}),z(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[z(`div`,{class:`flex items-center justify-between gap-3`,children:[z(`p`,{class:`m-0 text-xs font-extrabold text-slate-500`,children:`현재 지원 프로파일`}),z(`span`,{class:`text-xs font-extrabold text-slate-500`,children:`SKB · LG U+`})]}),z(`p`,{class:`mt-2 mb-0 text-sm font-black text-slate-950`,children:`WAN → IGMP Proxy → LAN`}),z(`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:z(`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 패키지가 포함되어 있는지 확인해 주세요.`}),z(`div`,{class:`mt-6 flex flex-wrap items-center justify-between gap-3 border-t border-slate-200 pt-5`,children:[z(`p`,{class:`m-0 text-xs leading-5 text-slate-500`,children:`설정을 적용하면 네트워크 서비스가 잠시 다시 로드될 수 있습니다.`}),z(`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?`변경사항 적용`:`설정 다시 적용`})]})]}),z(q,{eyebrow:`Runtime`,title:`IPTV 동작 상태`,description:`마지막으로 적용한 IPTV 설정과 중계 서비스의 실행 상태를 확인합니다.`}),z(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:[z(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[z(`p`,{class:`m-0 text-xs leading-5 text-slate-500`,children:`적용된 IPTV 중계 서비스와 멀티캐스트 전달 상태입니다.`}),z(`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:[z(H,{class:`size-4`}),p?e.enabled?`정상`:`꺼짐`:`확인 필요`]})]}),f?z(`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:[z(U,{"aria-hidden":`true`,class:`mt-0.5 size-4 shrink-0`}),`아래 동작 상태는 마지막으로 적용된 설정 기준입니다. 변경사항을 적용하면 새 설정으로 갱신됩니다.`]}):null,z(`div`,{class:`mt-5 grid gap-3 sm:grid-cols-2 lg:grid-cols-4`,children:[z(Wg,{active:e.enabled&&e.running,label:`IGMP Proxy`,value:e.enabled?e.running?`동작 중`:`중지됨`:`꺼짐`}),z(Wg,{active:e.enabled&&e.igmpSnooping,label:`IGMP Snooping`,value:e.igmpSnooping?`켜짐`:`꺼짐`}),z(Wg,{active:e.enabled,label:`Upstream`,value:e.upstreamNetwork.toUpperCase()}),z(Wg,{active:e.enabled,label:`Downstream`,value:e.lanBridge??e.downstreamNetwork.toUpperCase()})]})]})]})}function Kg(e){let t=e.split(`.`);return[t[0]??``,t[1]??``,t[2]??``,t[3]??``]}function qg(e){return e.replace(/\D/g,``).slice(0,3)}function Jg(e){return e.every(e=>e===``)?``:e.join(`.`)}function Yg({disabled:e=!1,label:t,value:n,onChange:r}){let i=Kg(n),a=(e,t)=>{let n=[...i];n[e]=qg(t),r(Jg(n))};return z(`div`,{class:`ssh-ipv4-segments mt-2 min-h-11`,"aria-label":`${t} 입력`,children:i.map((n,r)=>z(`div`,{class:`ssh-ipv4-segment`,children:[z(`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 $g(e){let t=Kg(e);return t.slice(0,3).some(e=>e===``||Number(e)>255)?null:t.slice(0,3).join(`.`)}function e_(e){return Kg(e)[3]}function t_(e){let t=$g(e.lan.address),n=e_(e.dhcp.start??``),r=e_(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 n_({disabled:e,label:t,lanAddress:n,value:r,onChange:i}){let a=$g(n),o=a?.split(`.`)??[``,``,``],s=e_(r),c=e=>{let t=qg(e);i(a&&t!==``?`${a}.${t}`:t)};return z(`label`,{class:`block`,children:[z(`span`,{class:`text-sm font-extrabold text-slate-800`,children:t}),z(`div`,{class:`ssh-ipv4-segments mt-2 min-h-11`,"aria-label":`${t} 입력`,children:[o.map((e,t)=>z(`div`,{class:`ssh-ipv4-segment`,children:[z(`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||`—`}),z(`span`,{"aria-hidden":`true`,class:`ssh-ipv4-separator text-sm font-black text-slate-400`,children:`.`})]},t)),z(`div`,{class:`ssh-ipv4-segment`,children:z(`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})})]}),z(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`앞 3개 주소는 공유기 IP 주소와 동일하게 유지됩니다.`})]})}function r_(e){return`${window.location.protocol}//${e}${window.location.pathname}${window.location.search}${window.location.hash}`}function i_({feedback:e,onDismiss:t}){return z(`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:[z(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[z(`p`,{class:`m-0 font-bold leading-6`,children:e.message}),z(`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?z(`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:r_(e.newAddress),children:[`새 공유기 주소 `,e.newAddress,`로 연결`]}):null]})}function a_({data:e}){let t=e.conflict.detected;return z(`section`,{class:`grid gap-4 lg:grid-cols-3`,"aria-label":`내부 네트워크 상태`,children:[z(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[z(`span`,{class:`grid size-10 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:z(jt,{class:`size-5`})}),z(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`SmartSafeHub LAN`}),z(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:e.lan.subnet}),z(`p`,{class:`mt-1 mb-0 text-sm font-semibold text-slate-600`,children:[`공유기 주소 `,e.lan.address]})]}),z(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[z(`span`,{class:`grid size-10 place-items-center rounded-xl bg-slate-100 text-slate-700`,children:z(Bt,{class:`size-5`})}),z(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`상위 네트워크`}),z(`p`,{class:`mt-2 mb-0 text-xl font-black text-slate-950`,children:e.wan.subnet??`확인 전`}),z(`p`,{class:`mt-1 mb-0 text-sm font-semibold text-slate-600`,children:e.wan.connected?`WAN ${e.wan.address??`주소 확인 중`}`:`WAN 연결 후 자동으로 확인합니다.`})]}),z(`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:[z(`span`,{class:`grid size-10 place-items-center rounded-xl ${t?`bg-red-100 text-red-700`:`bg-emerald-100 text-emerald-700`}`,children:z(t?U:H,{class:`size-5`})}),z(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] opacity-70`,children:`대역 충돌`}),z(`p`,{class:`mt-2 mb-0 text-lg font-black`,children:t?`주소 대역이 겹칩니다`:`충돌이 감지되지 않았습니다`}),z(`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 o_({action:e,data:t,error:n,feedback:r,loading:i,onApplyRecommendation:a,onDismissFeedback:o,onRetry:s,onSave:c}){let[l,u]=M(``),[d,f]=M(24),[p,m]=M(!0),[h,g]=M(``),[_,y]=M(``),[b,x]=M(`12h`),[S,C]=M(!1),[w,T]=M(null),E=(e={})=>{if(!t)return!1;let n=t_(t);return(e.ipAddress??l).trim()!==n.ipAddress||(e.prefixLength??d)!==n.prefixLength||(e.dhcpEnabled??p)!==n.dhcpEnabled||(e.dhcpStart??h).trim()!==n.dhcpStart||(e.dhcpEnd??_).trim()!==n.dhcpEnd||(e.leaseTime??b)!==n.leaseTime},D=e=>{let t=$g(e);if(u(e),!t){C(E({ipAddress:e}));return}let n=e_(h),r=e_(_),i=n?`${t}.${n}`:``,a=r?`${t}.${r}`:``;g(i),y(a),C(E({ipAddress:e,dhcpStart:i,dhcpEnd:a}))},O=e=>{g(e),C(E({dhcpStart:e}))},k=e=>{y(e),C(E({dhcpEnd:e}))},A=e=>{f(e),C(E({prefixLength:e}))},j=e=>{x(e),C(E({leaseTime:e}))},ee=e=>{m(e),C(E({dhcpEnabled:e}))};N(()=>{if(!t||S)return;let e=t_(t);u(e.ipAddress),f(e.prefixLength),m(e.dhcpEnabled),g(e.dhcpStart),y(e.dhcpEnd),x(e.leaseTime),T(null)},[t,S]);let te=Qe(()=>{let e=[...Xg];return e.includes(d)||(e.push(d),e.sort((e,t)=>e-t)),e},[d]),ne=Qe(()=>{let e=[...Zg];return e.some(([e])=>e===b)||e.push([b,`현재 설정 (${b})`]),e},[b]);if(i)return z(bo,{});if(n)return z(xo,{message:n,onRetry:s});if(!t)return null;let re=e!==null,ie=async e=>{e.preventDefault(),Qg(l)?!Qg(h)||!Qg(_)?T(`DHCP 시작/종료 주소를 올바른 IPv4 형식으로 입력해 주세요.`):window.confirm(`내부 네트워크 설정을 적용하면 연결된 기기가 새 DHCP 정보를 받는 동안 잠시 연결이 끊길 수 있습니다. 계속하시겠습니까?`)&&(T(null),await c({ipAddress:l.trim(),prefixLength:d,dhcpEnabled:p,dhcpStart:h.trim(),dhcpEnd:_.trim(),leaseTime:b})&&C(!1)):T(`공유기 주소를 올바른 IPv4 형식으로 입력해 주세요.`)},ae=async()=>{t.recommendation&&window.confirm(`상위 네트워크와 겹치지 않는 ${t.recommendation.subnet} 대역으로 자동 변경합니다. 현재 연결이 잠시 끊길 수 있습니다. 계속하시겠습니까?`)&&(T(null),await a()&&C(!1))};return z(v,{children:[r?z(i_,{feedback:r,onDismiss:o}):null,z(q,{eyebrow:`Overview`,title:`내부 네트워크 상태`,description:`LAN 주소와 DHCP 할당 상태를 확인합니다.`}),z(a_,{data:t}),t.conflict.detected?z(`section`,{class:`mt-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-amber-950 sm:p-6`,children:z(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between`,children:[z(`div`,{class:`min-w-0`,children:[z(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-amber-700`,children:`자동 충돌 해결`}),z(`h2`,{class:`mt-2 mb-0 text-lg font-black`,children:`안전한 LAN 대역으로 변경할 수 있습니다`}),z(`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?z(`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:re,onClick:()=>void ae(),type:`button`,children:[z(H,{class:`size-5 ${e===`auto`?`animate-pulse`:``}`}),e===`auto`?`변경 중`:`추천 대역으로 자동 변경`]}):null]})}):null,z(`div`,{class:`mt-7`,children:z(q,{eyebrow:`Local network`,title:`LAN 및 DHCP 설정`,description:`상위 공유기와 다른 주소 대역을 사용해야 안정적으로 라우팅할 수 있습니다.`})}),z(`form`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,onSubmit:e=>void ie(e),children:[S?z(`div`,{class:`mb-5 flex justify-end`,children:z(`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:[z(U,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]})}):null,z(`div`,{class:`grid gap-5 lg:grid-cols-2`,children:[z(`div`,{class:`block`,children:[z(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`공유기 IP 주소`}),z(Yg,{disabled:re,label:`공유기 IP 주소`,onChange:D,value:l})]}),z(`div`,{class:`block`,children:[z(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`현재 LAN 대역`}),z(`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}),z(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`서브넷 마스크는 아래 고급 설정에서 변경할 수 있습니다.`})]}),z(n_,{disabled:re,label:`DHCP 시작 주소`,lanAddress:l,onChange:O,value:h}),z(n_,{disabled:re,label:`DHCP 종료 주소`,lanAddress:l,onChange:k,value:_})]}),z(`details`,{class:`mt-6 rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[z(`summary`,{class:`cursor-pointer text-sm font-black text-slate-900`,children:`고급 DHCP 설정`}),z(`div`,{class:`mt-5 grid gap-5 lg:grid-cols-2`,children:[z(`div`,{class:`block`,children:[z(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`서브넷 마스크`}),z(Vg,{ariaLabel:`서브넷 마스크`,className:`mt-2`,disabled:re,id:`lan-prefix-length`,onChange:e=>A(Number(e)),options:te.map(e=>({value:String(e),label:`/${e} · ${e===t.lan.prefixLength?t.lan.netmask:`CIDR /${e}`}`})),value:String(d),variant:`emphasized`}),z(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`일반 가정용 네트워크는 /24 사용을 권장합니다.`})]}),z(`div`,{class:`block`,children:[z(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`DHCP 임대 시간`}),z(Vg,{ariaLabel:`DHCP 임대 시간`,className:`mt-2`,disabled:re,id:`lan-dhcp-lease-time`,onChange:j,options:ne.map(([e,t])=>({value:e,label:t})),value:b,variant:`emphasized`})]}),z(`label`,{class:`inline-flex min-h-11 items-center gap-3 text-sm font-extrabold text-slate-800 lg:col-span-2`,children:[z(`input`,{checked:p,class:`size-5 accent-teal-700`,disabled:re,onChange:e=>ee(e.currentTarget.checked),type:`checkbox`}),`SmartSafeHub DHCP 서버 사용`]}),z(`p`,{class:`m-0 text-xs font-semibold leading-5 text-slate-500 lg:col-span-2`,children:`DHCP를 끄면 SmartSafeHub에 연결되는 장치에 IP 주소를 자동으로 할당하지 않습니다. 별도의 DHCP 서버를 사용하는 고급 구성에서만 비활성화하세요.`})]})]}),w?z(`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,z(`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:[z(`p`,{class:`m-0 text-xs font-semibold leading-5 text-slate-500`,children:`주소가 바뀌면 현재 관리 페이지 연결이 종료되며 새 IP를 받아 다시 접속해야 합니다.`}),z(`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:re||!S,type:`submit`,children:[z(H,{class:`size-5 ${e===`saving`?`animate-pulse`:``}`}),e===`saving`?`저장 중`:`설정 저장`]})]})]})]})}var s_=[`dhcp`,`pppoe`,`static`],c_=`1.1.1.1`,l_=Array.from({length:33},(e,t)=>t);function u_(e){return s_.includes(e)}function d_(e){switch(e){case`dhcp`:return`자동 IP (DHCP)`;case`pppoe`:return`PPPoE`;case`static`:return`고정 IPv4`;default:return e?e.toUpperCase():`확인 전`}}function f_(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 p_(e){return e.trim()}function m_(e,t){let n=f_(t);return e.protocol===n.protocol?e.protocol===`dhcp`||!u_(e.protocol)?!1:e.protocol===`pppoe`?p_(e.pppoeUsername)!==n.pppoeUsername||e.pppoePassword.length>0:p_(e.staticAddress)!==n.staticAddress||e.staticPrefixLength!==n.staticPrefixLength||p_(e.staticGateway)!==n.staticGateway||p_(e.dnsPrimary)!==n.dnsPrimary||p_(e.dnsSecondary)!==n.dnsSecondary:!0}function h_(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 g_(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 __(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 v_({feedback:e,onDismiss:t}){return z(`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:z(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[z(`p`,{class:`m-0 font-bold leading-6`,children:e.message}),z(`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 y_({data:e,busy:t,onReconnect:n}){let r=e.status,i=r.connected;return z(`section`,{class:`grid gap-4 lg:grid-cols-3`,"aria-label":`인터넷 연결 상태`,children:[z(`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:[z(`div`,{class:`flex items-start justify-between gap-3`,children:[z(`span`,{class:`grid size-10 place-items-center rounded-xl ${i?`bg-emerald-100 text-emerald-700`:`bg-amber-100 text-amber-700`}`,children:z(i?H:U,{class:`size-5`})}),z(`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:[z(V,{class:`size-4 ${t?`animate-spin`:``}`}),`재연결`]})]}),z(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] opacity-70`,children:`인터넷 상태`}),z(`p`,{class:`mt-2 mb-0 text-xl font-black`,children:r.pending?`연결 중`:i?`연결됨`:`연결 안 됨`}),z(`p`,{class:`mt-1 mb-0 text-sm font-semibold leading-6 opacity-80`,children:[d_(r.protocol??e.configuration.protocol),` · `,__(r.uptimeSeconds)]})]}),z(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[z(`span`,{class:`grid size-10 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:z(Bt,{class:`size-5`})}),z(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`WAN IPv4`}),z(`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}`}`:`주소 없음`}),z(`p`,{class:`mt-1 mb-0 text-sm font-semibold text-slate-600`,children:r.device?`인터페이스 ${r.device}`:`인터페이스 확인 전`})]}),z(`article`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5`,children:[z(`span`,{class:`grid size-10 place-items-center rounded-xl bg-slate-100 text-slate-700`,children:z(jt,{class:`size-5`})}),z(`p`,{class:`mt-4 mb-0 text-xs font-extrabold uppercase tracking-[0.14em] text-slate-500`,children:`Gateway · DNS`}),z(`p`,{class:`mt-2 mb-0 break-all text-base font-black text-slate-950`,children:r.gateway??`Gateway 확인 전`}),z(`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 b_({action:e,data:t,error:n,feedback:r,loading:i,onDismissFeedback:a,onReconnect:o,onRetry:s,onSave:c}){let[l,u]=M({protocol:`dhcp`,pppoeUsername:``,pppoePassword:``,staticAddress:``,staticPrefixLength:24,staticGateway:``,dnsPrimary:``,dnsSecondary:``}),[d,f]=M(!1),[p,m]=M(!1),[h,g]=M(null);N(()=>{t&&!d&&(u(f_(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 z(bo,{});if(n)return z(xo,{message:n,onRetry:s});if(!t)return null;let y=e!==null,b=e=>{u(n=>{let r={...n,...e};return f(m_(r,t)),r}),g(null)},x=async e=>{if(e.preventDefault(),!u_(l.protocol))g(`DHCP, PPPoE 또는 고정 IPv4 중 하나를 선택해 주세요.`);else{if(l.protocol===`pppoe`){if(!p_(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(!h_(l.staticAddress)){g(`고정 IPv4 주소를 올바르게 입력해 주세요.`);return}if(!h_(l.staticGateway)){g(`기본 게이트웨이를 올바르게 입력해 주세요.`);return}if(!h_(l.dnsPrimary)){g(`기본 DNS 서버를 올바르게 입력해 주세요.`);return}if(p_(l.dnsSecondary)&&!h_(l.dnsSecondary)){g(`보조 DNS 서버를 올바르게 입력해 주세요.`);return}}window.confirm(`인터넷 연결 설정을 적용하면 WAN 연결이 다시 시작되어 인터넷이 잠시 끊길 수 있습니다. 계속하시겠습니까?`)&&await c({protocol:l.protocol,pppoeUsername:p_(l.pppoeUsername),pppoePassword:l.pppoePassword,pppoePasswordChanged:l.pppoePassword.length>0,staticAddress:p_(l.staticAddress),staticPrefixLength:l.staticPrefixLength,staticGateway:p_(l.staticGateway),dnsPrimary:p_(l.dnsPrimary),dnsSecondary:p_(l.dnsSecondary)})&&(u(e=>({...e,pppoePassword:``})),f(!1))}};return z(v,{children:[r?z(v_,{feedback:r,onDismiss:a}):null,z(q,{eyebrow:`Overview`,title:`인터넷 연결 상태`,description:`WAN 연결 상태와 네트워크 정보를 확인합니다.`}),z(y_,{data:t,busy:y,onReconnect:()=>{window.confirm(`WAN 연결을 다시 시작합니다. 인터넷이 잠시 끊길 수 있습니다. 계속하시겠습니까?`)&&o()}}),t.configuration.supported?null:z(`section`,{class:`mt-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-amber-950 sm:p-6`,children:z(`div`,{class:`flex items-start gap-3`,children:[z(U,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),z(`div`,{children:[z(`h2`,{class:`m-0 text-base font-black`,children:`현재 WAN 방식은 직접 관리하지 않습니다`}),z(`p`,{class:`mt-2 mb-0 text-sm font-semibold leading-6 text-amber-900/80`,children:[`현재 설정은 `,t.configuration.protocol,`입니다. 아래에서 DHCP, PPPoE 또는 고정 IPv4를 선택해 저장하면 해당 방식으로 전환합니다.`]})]})]})}),z(`div`,{class:`mt-7`,children:z(q,{eyebrow:`Internet`,title:`인터넷 연결 설정`,description:`통신사 또는 상위 네트워크 환경에 맞는 WAN 연결 방식을 선택합니다.`})}),z(`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?z(`div`,{class:`mb-5 flex justify-end`,children:z(`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:[z(U,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]})}):null,z(`div`,{children:[z(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`연결 방식`}),z(Vg,{ariaLabel:`WAN 연결 방식`,className:`mt-2 max-w-xl`,disabled:y,id:`wan-protocol`,onChange:e=>b(e===`static`&&!p_(l.dnsSecondary)?{protocol:e,dnsSecondary:c_}:{protocol:e}),options:_,value:l.protocol,variant:`emphasized`}),z(`span`,{class:`mt-2 block text-xs font-semibold leading-5 text-slate-500`,children:`대부분의 가정용 인터넷과 상위 공유기 연결은 자동 IP(DHCP)를 사용합니다.`})]}),l.protocol===`pppoe`?z(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[z(`label`,{class:`block`,children:[z(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`PPPoE 사용자명`}),z(`input`,{autocomplete:`username`,class:`mt-2 min-h-11 w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-4 py-2.5 text-sm font-semibold text-slate-950 shadow-inner outline-none transition focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:cursor-not-allowed disabled:bg-slate-100`,disabled:y,onInput:e=>b({pppoeUsername:e.currentTarget.value}),spellcheck:!1,value:l.pppoeUsername})]}),z(`label`,{class:`block`,children:[z(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`PPPoE 비밀번호`}),z(`span`,{class:`relative mt-2 block`,children:[z(`input`,{autocomplete:`new-password`,class:`min-h-11 w-full rounded-xl border-2 border-slate-300 bg-slate-50 px-4 py-2.5 pr-12 text-sm font-semibold text-slate-950 shadow-inner outline-none transition focus:border-teal-500 focus:bg-white focus:ring-4 focus:ring-teal-100 disabled:cursor-not-allowed disabled:bg-slate-100`,disabled:y,onInput:e=>b({pppoePassword:e.currentTarget.value}),placeholder:t.configuration.protocol===`pppoe`&&t.configuration.pppoe.passwordConfigured?`변경할 때만 입력`:`PPPoE 비밀번호`,...p?{type:`text`}:{type:`password`},value:l.pppoePassword}),z(`button`,{"aria-label":p?`비밀번호 숨기기`:`비밀번호 표시`,class:`absolute inset-y-0 right-1 grid w-10 place-items-center rounded-lg border-0 bg-transparent text-slate-500`,disabled:y,onClick:()=>m(e=>!e),type:`button`,children:z(p?Xt:Yt,{class:`size-5`})})]}),z(`span`,{class:`mt-2 block text-xs text-slate-500`,children:t.configuration.protocol===`pppoe`&&t.configuration.pppoe.passwordConfigured?`비워 두면 현재 저장된 비밀번호를 그대로 사용합니다.`:`통신사에서 제공한 PPPoE 비밀번호를 입력해 주세요.`})]})]}):null,l.protocol===`static`?z(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[z(`label`,{class:`block`,children:[z(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`IPv4 주소`}),z(Yg,{disabled:y,label:`IPv4 주소`,onChange:e=>b({staticAddress:e}),value:l.staticAddress})]}),z(`div`,{class:`block`,children:[z(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`서브넷 마스크`}),z(Vg,{ariaLabel:`WAN 서브넷 마스크`,className:`mt-2`,disabled:y,id:`wan-static-prefix-length`,onChange:e=>b({staticPrefixLength:Number(e)}),options:l_.map(e=>({value:String(e),label:`/${e} · ${g_(e)}`})),value:String(l.staticPrefixLength),variant:`emphasized`})]}),z(`label`,{class:`block`,children:[z(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`기본 게이트웨이`}),z(Yg,{disabled:y,label:`기본 게이트웨이`,onChange:e=>b({staticGateway:e}),value:l.staticGateway})]}),z(`label`,{class:`block`,children:[z(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`기본 DNS`}),z(Yg,{disabled:y,label:`기본 DNS`,onChange:e=>b({dnsPrimary:e}),value:l.dnsPrimary})]}),z(`label`,{class:`block lg:col-start-2`,children:[z(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`보조 DNS (선택)`}),z(Yg,{disabled:y,label:`보조 DNS`,onChange:e=>b({dnsSecondary:e}),value:l.dnsSecondary})]})]}):null,l.protocol===`dhcp`?z(`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?z(`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,z(`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:[z(`p`,{class:`m-0 text-xs font-semibold leading-5 text-slate-500`,children:`설정 저장 후 WAN만 다시 연결하며 LAN 관리 페이지와 내부 네트워크 설정은 유지됩니다.`}),z(`button`,{class:`inline-flex min-h-11 w-full shrink-0 items-center justify-center gap-2 rounded-xl border-0 bg-teal-700 px-5 py-2.5 text-sm font-extrabold text-white shadow-sm transition hover:bg-teal-800 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-200 disabled:cursor-wait disabled:opacity-60 sm:w-auto`,disabled:y||!d,type:`submit`,children:[z(H,{class:`size-5 ${e===`saving`?`animate-pulse`:``}`}),e===`saving`?`저장 중`:`설정 저장`]})]})]})]})}var x_=3,S_=10;function C_(e,t){return e<=0?`0.0%`:`${(t/e*100).toFixed(1)}%`}function w_(e){return e.id===`other`?`기타 기기`:e.hostname?e.hostname:e.identified?`이름 없는 기기`:`미식별 기기`}function T_(e){return[...e].sort((e,t)=>e.id===`other`?1:t.id===`other`?-1:t.blocked-e.blocked||t.queries-e.queries||w_(e).localeCompare(w_(t),`ko`))}function E_({devices:e,deviceLimit:t,truncated:n}){let r=T_(e),[i,a]=M(!1),[o,s]=M(1),c=Math.max(1,Math.ceil(r.length/S_)),l=Math.min(o,c),u=(l-1)*S_,d=Math.min(u+S_,r.length),f=i?r.slice(u,d):r.slice(0,x_),p=r.length>x_;return N(()=>{s(e=>Math.min(e,c))},[c]),z(`div`,{class:`mt-6 rounded-2xl border border-slate-100 bg-slate-50/70 p-4 sm:p-5`,children:[z(`div`,{class:`flex flex-wrap items-start justify-between gap-3`,children:[z(`div`,{children:[z(`div`,{class:`flex flex-wrap items-center gap-2`,children:[z(`p`,{class:`m-0 text-xs font-bold text-slate-500`,children:`기기별 통계`}),p&&!i?z(`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]}),z(`h3`,{class:`mt-1 mb-0 text-base font-extrabold text-slate-950`,children:`어떤 기기에서 차단되었는지 확인`}),z(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-500`,children:`DHCP 정보가 있는 기기는 MAC 주소를 기준으로 식별하며, 기기별 숫자만 현재 부팅 세션 동안 메모리에 집계합니다.`})]}),z(`div`,{class:`flex flex-wrap items-center justify-end gap-2`,children:[z(`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:[z(Mt,{class:`size-4 text-teal-700`}),J(r.length),`개 항목`]}),p?z(`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?z(`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 요청이 발생하면 다음 통계 갱신 시 표시됩니다.`}):z(`div`,{class:`mt-4 grid gap-3`,id:`safeshield-device-statistics-list`,children:f.map(e=>z(`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:[z(`div`,{class:`min-w-0`,children:z(`div`,{class:`flex min-w-0 flex-wrap items-center gap-2`,children:[z(`span`,{class:`grid size-9 shrink-0 place-items-center rounded-lg bg-teal-50 text-teal-700`,children:z(Mt,{class:`size-4`})}),z(`div`,{class:`min-w-0`,children:[z(`h4`,{class:`m-0 truncate text-sm font-extrabold text-slate-950`,children:w_(e)}),z(`p`,{class:`mt-1 mb-0 break-all text-xs text-slate-500`,children:[e.ip||`IP 미확인`,e.mac?` · ${e.mac}`:``]})]}),z(`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 임시 식별`})]})}),z(`dl`,{class:`m-0 grid grid-cols-3 gap-2 text-right`,children:[z(`div`,{class:`min-w-20`,children:[z(`dt`,{class:`text-[11px] font-bold text-slate-400`,children:`DNS 요청`}),z(`dd`,{class:`mt-1 mb-0 ml-0 text-sm font-extrabold text-slate-900`,children:J(e.queries)})]}),z(`div`,{class:`min-w-20`,children:[z(`dt`,{class:`text-[11px] font-bold text-slate-400`,children:`차단`}),z(`dd`,{class:`mt-1 mb-0 ml-0 text-sm font-extrabold text-teal-700`,children:J(e.blocked)})]}),z(`div`,{class:`min-w-20`,children:[z(`dt`,{class:`text-[11px] font-bold text-slate-400`,children:`차단율`}),z(`dd`,{class:`mt-1 mb-0 ml-0 text-sm font-extrabold text-slate-900`,children:C_(e.queries,e.blocked)})]})]})]},e.id))}),p&&!i?z(`p`,{class:`mt-3 mb-0 text-xs font-semibold leading-5 text-slate-500`,children:[`전체 `,J(r.length),`개 기기 중 차단 수 기준 상위 `,x_,`개를 표시하고 있습니다.`]}):null,i&&r.length>S_?z(`nav`,{"aria-label":`기기별 통계 페이지`,class:`mt-4 flex flex-wrap items-center justify-between gap-3`,children:[z(`p`,{class:`m-0 text-xs font-semibold text-slate-500`,children:[J(u+1),`–`,J(d),` /`,` `,J(r.length),`개`]}),z(`div`,{class:`flex items-center gap-2`,children:[z(`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:`이전`}),z(`span`,{"aria-current":`page`,class:`min-w-20 text-center text-xs font-extrabold text-slate-600`,children:[J(l),` / `,J(c)]}),z(`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?z(`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 D_=3600,O_=24;function k_(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/D_)*D_}function j_(e){let t=new Map(e.hourly.map(e=>[e.bucketStart,e])),n=A_(e)-23*D_;return Array.from({length:O_},(e,r)=>{let i=n+r*D_;return t.get(i)??{bucketStart:i,queries:0,blocked:0}})}function M_({emphasized:e=!1,label:t,value:n}){return z(`div`,{class:`rounded-xl bg-slate-50 p-4 sm:p-5`,children:[z(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.12em] text-slate-400`,children:t}),z(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight ${e?`text-teal-700`:`text-slate-950`}`,children:n})]})}function N_({action:e,data:t,error:n,loading:r,onRetry:i,onSetEnabled:a,refreshing:o}){if(r&&t===null)return z(v,{children:[z(q,{eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청과 차단 현황을 확인합니다.`}),z(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:z(`p`,{class:`m-0 text-sm font-bold text-slate-500`,children:`차단 통계를 불러오는 중입니다…`})})]});if(n&&t===null)return z(v,{children:[z(q,{eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청과 차단 현황을 확인합니다.`}),z(`section`,{class:`rounded-2xl border border-amber-200 bg-amber-50 p-5 sm:p-6`,children:[z(`p`,{class:`m-0 text-sm font-extrabold text-amber-950`,children:`차단 통계를 불러오지 못했습니다.`}),z(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-amber-800`,children:n}),z(`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 z(v,{children:[z(q,{eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청과 차단 현황을 확인합니다.`}),z(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 shadow-sm shadow-slate-900/5 sm:p-6`,children:z(`p`,{class:`m-0 text-sm leading-6 text-slate-600`,children:`현재 SafeShield에서 통계 데이터를 제공하지 않습니다. SafeShield 0.3.15 버전 이상이 설치되어 있고, 통계 수집이 활성화되어 있는지 확인해 주세요.`})})]});let s=j_(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 z(v,{children:[z(q,{action:z(`div`,{class:`flex items-center gap-3 rounded-xl border border-slate-100 bg-slate-50 px-3 py-2.5`,children:[z(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-500`,children:z(Kt,{class:`size-5`})}),z(`div`,{class:`text-right`,children:[z(`p`,{class:`m-0 text-[0.68rem] font-bold text-slate-400`,children:`통계 수집`}),z(`p`,{class:`mt-1 mb-0 text-xs font-extrabold ${u||d?`text-emerald-700`:`text-slate-500`}`,role:`status`,children:f})]}),z(`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:z(`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?z(V,{"aria-hidden":`true`,class:`size-3 animate-spin text-teal-600`}):null})})]}),eyebrow:`Activity`,title:`최근 24시간 보호 활동`,description:`DNS 요청 원본은 저장하지 않고 숫자만 로컬 메모리에 집계합니다.`}),z(`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:z(`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 요청 원본은 저장하지 않고 요청 수와 차단 수만 로컬 메모리에 집계합니다.`}),z(`dl`,{class:`mt-2 grid gap-3 sm:grid-cols-2 xl:grid-cols-4`,children:[z(M_,{label:`최근 24시간 DNS 요청`,value:J(l.queries)}),z(M_,{emphasized:!0,label:`최근 24시간 차단`,value:J(l.blocked)}),z(M_,{label:`최근 24시간 차단율`,value:k_(l.queries,l.blocked)}),z(M_,{label:`현재 시간 차단`,value:J(c?.blocked??0)})]}),z(`div`,{class:`mt-5 rounded-xl bg-slate-50/70 p-4 sm:p-5`,children:[z(`div`,{class:`flex flex-wrap items-end justify-between gap-3`,children:[z(`div`,{children:[z(`p`,{class:`m-0 text-xs font-bold text-slate-500`,children:`최근 24시간`}),z(`h3`,{class:`mt-1 mb-0 text-base font-extrabold text-slate-950`,children:`시간대별 차단 요청`})]}),z(`span`,{class:`text-xs font-bold text-slate-500`,children:o?`갱신 중…`:`마지막 집계 ${qa(t.updatedAt)}`})]}),z(tg,{buckets:s})]}),z(E_,{deviceLimit:t.deviceLimit,devices:t.devices,truncated:t.devicesTruncated}),z(`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:[z(`span`,{children:[`수집 누적 DNS 요청 `,J(t.totals.queries)]}),z(`span`,{children:[`수집 누적 차단 `,J(t.totals.blocked)]}),z(`span`,{children:[`수집 시작 `,qa(t.startedAt)]}),z(`span`,{children:[`보존 `,J(t.retentionHours),`시간`]}),t.volatile?z(`span`,{children:`재부팅 시 초기화되는 메모리 통계`}):null]}),n?z(`p`,{class:`mt-3 mb-0 text-xs font-bold text-amber-700`,children:`최근 자동 갱신에 실패했습니다. 현재 표시된 마지막 정상 통계를 유지합니다.`}):null]})]})}function P_({timestamp:e,fallback:t=`기록 없음`}){let[n,r]=M(()=>Date.now()/1e3),i=typeof e==`number`&&Number.isFinite(e)&&e>0&&e<=864e10;if(N(()=>{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 z(`span`,{children:t});let a=qa(e);return z(`span`,{class:`inline-flex max-w-full flex-wrap items-center gap-1`,children:[z(`time`,{class:`min-w-0`,dateTime:new Date(e*1e3).toISOString(),title:a,children:Ya(e,n)}),z(`details`,{class:`group inline-flex max-w-full flex-wrap items-center gap-1 align-middle`,children:[z(`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:z(`svg`,{"aria-hidden":`true`,class:`size-3.5`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.8`,viewBox:`0 0 24 24`,children:[z(`circle`,{cx:`12`,cy:`12`,r:`9`}),z(`path`,{d:`M12 11v5m0-8h.01`,strokeLinecap:`round`})]})}),z(`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 F_=`https://www.smartsafehub.com/pricing/`;function I_({value:e,trueLabel:t=`정상`,falseLabel:n=`확인 필요`}){return z(`span`,{class:`inline-flex items-center gap-2 text-sm font-extrabold ${e?`text-emerald-700`:`text-slate-500`}`,children:[z(`span`,{class:`size-2 rounded-full ${e?`bg-emerald-500`:`bg-slate-300`}`}),e?t:n]})}function L_(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 R_(e){return!e.enabled||e.status===`disabled`?`비활성화`:e.active&&e.runtime.dnsmasqRunning&&e.runtime.dnsRuntimeOk?`보호 중`:e.status===`paused`?`일시 중지`:`확인 필요`}function z_(e){let t=L_(e);return t===`error`&&R_(e)===`보호 중`&&e.runtime.lastErrorCode?`갱신 실패`:{disabled:`비활성화`,refreshing:`갱신 중`,protecting:`보호 중`,paused:`일시 중지`,error:`오류`,attention:`확인 필요`}[t]}function B_(e){return e?`자동 최적화`:`확인되지 않음`}function V_(e){let t=Xa(e);return t===null?`확인되지 않음`:z(P_,{timestamp:t})}function H_(e){let t=L_(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`?R_(e)===`보호 중`?`기존 DNS 보호는 유지되고 있지만 최근 차단 목록 갱신을 완료하지 못했습니다.`:`SafeShield 보호 상태에 문제가 발생했습니다. 아래 오류 내용을 확인해 주세요.`:e.active?e.runtime.dnsmasqRunning?e.runtime.dnsRuntimeOk?e.summary.message:`SafeShield DNS 런타임 상태를 확인해야 합니다.`:`dnsmasq가 실행 중이 아니어서 DNS 보호 상태를 확인해야 합니다.`:`SafeShield가 활성화되어 있지만 서비스가 실행 중이 아닙니다.`}function U_(e){return{ok:`정상`,warning:`경고`,error:`오류`,unavailable:`사용 불가`}[e]??e}function W_({data:e}){let t=L_(e);return z(`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:z_(e)})}function G_({failed:e,step:t,total:n}){let r=2*Math.PI*17,i=r*.22,a=r-i;return z(`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:[z(`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:[z(`circle`,{class:`ssh-safeshield-refresh-loader-track`,cx:`24`,cy:`24`,fill:`none`,r:17,stroke:`currentColor`,strokeWidth:2.5}),e?null:z(`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})]}),z(`span`,{class:`ssh-safeshield-refresh-donut-label`,children:e?`!`:`${t}/${n}`})]})}function K_({data:e}){let t=ca(e.status,e.stage),n=L_(e)===`error`;if(!t&&!n)return null;let r=la(e.stage),i=n?da(e.runtime.lastErrorCode,e.stage):null;return z(`div`,{"aria-live":n?`assertive`:`polite`,class:`ssh-safeshield-refresh`,"data-state":n?`error`:`active`,role:n?`alert`:`status`,children:[z(G_,{failed:n,step:r.number,total:r.total}),z(`div`,{class:`ssh-safeshield-refresh-copy`,children:[z(`p`,{class:`ssh-safeshield-refresh-kicker`,children:n?`갱신 실패 · ${r.number}/${r.total}`:`갱신 ${r.number}/${r.total}`}),z(`p`,{class:`ssh-safeshield-refresh-title`,children:n?`${r.label} 단계에서 문제가 발생했습니다.`:r.label}),z(`p`,{class:`ssh-safeshield-refresh-description`,children:i??r.description}),n&&e.runtime.lastErrorCode?z(`p`,{class:`ssh-safeshield-refresh-error-code`,children:[`오류 코드: `,z(`code`,{children:e.runtime.lastErrorCode})]}):null]})]})}function q_({label:e,value:t}){return z(`div`,{class:`bg-white px-5 py-4 sm:px-6`,children:[z(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.14em] text-slate-400`,children:e}),z(`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 J_(e){return e?.trim().toUpperCase()||`FREE`}function Y_(e){return e===`FREE`?`free`:e===`PRO`?`pro`:e===`ULTIMATE`?`ultimate`:`paid`}function X_({compact:e=!1,plan:t}){let n=Y_(t),r=n!==`free`;return z(`span`,{class:`ssh-safeshield-plan-badge`,"data-compact":e?`true`:`false`,"data-tier":n,title:r?`${t} 멤버십`:`무료 플랜`,children:[z(`span`,{"aria-hidden":`true`,class:`ssh-safeshield-plan-badge-mark`,children:r?`✦`:`•`}),z(`span`,{children:t})]})}function Z_({plan:e}){let t=Y_(e)===`free`;return z(`div`,{class:`bg-white px-5 py-4 sm:px-6`,children:[z(`dt`,{class:`text-[0.68rem] font-black uppercase tracking-[0.14em] text-slate-400`,children:`Plan`}),z(`dd`,{class:`mt-2 mb-0 ml-0 flex min-w-0 flex-wrap items-center gap-2`,children:[z(X_,{plan:e}),t?z(`span`,{class:`ssh-safeshield-plan-caption`,children:`기본 플랜`}):null]})]})}function Q_(){return z(`div`,{class:`ssh-safeshield-upgrade-card`,children:[z(`span`,{"aria-hidden":`true`,class:`ssh-safeshield-upgrade-mark`,children:`✦`}),z(`div`,{class:`ssh-safeshield-upgrade-copy`,children:[z(`div`,{class:`ssh-safeshield-upgrade-meta`,children:[z(`span`,{children:`SmartSafeHub Membership`}),z(`span`,{class:`ssh-safeshield-upgrade-soon`,children:`Coming soon`})]}),z(`strong`,{children:`더 강력한 보호 기능을 준비하고 있습니다.`}),z(`span`,{children:`프리미엄 보호와 확장된 관리 기능을 미리 확인해 보세요.`})]}),z(`a`,{class:`ssh-safeshield-upgrade-action`,href:F_,rel:`noopener noreferrer`,target:`_blank`,children:[`요금제 보기`,z(`span`,{"aria-hidden":`true`,children:`→`})]})]})}function $_({children:e,eyebrow:t,icon:n,title:r}){return z(`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:[z(`div`,{class:`flex min-w-0 items-start justify-between gap-4 xl:gap-2`,children:[z(`div`,{children:[z(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.16em] text-slate-400`,children:t}),z(`h3`,{class:`mt-2 mb-0 text-lg font-black tracking-tight text-slate-950 xl:text-base 2xl:text-lg`,children:r})]}),z(`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})]}),z(`dl`,{class:`mt-5 mb-0 grid gap-3`,children:e})]})}function ev({label:e,value:t}){return z(`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:[z(`dt`,{class:`text-sm font-semibold text-slate-500`,children:e}),z(`dd`,{class:`m-0 min-w-0 break-words text-right text-sm font-extrabold text-slate-950`,children:t})]})}function tv({error:e,message:t,onDismiss:n}){if(!e&&!t)return null;let r=e!==null;return z(`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:[z(`div`,{class:`flex min-w-0 items-start gap-3`,children:[z(r?U:H,{class:`mt-0.5 size-5 shrink-0`}),z(`p`,{class:`m-0 font-semibold leading-6`,children:e??t})]}),z(`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 nv({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,_]=M(null);if(N(()=>{r?.available&&r.blocklist.validLineCount>0&&!ca(r.status,r.stage)&&_(r.blocklist.validLineCount)},[r?.available,r?.blocklist.validLineCount,r?.stage,r?.status]),a)return z(bo,{});if(i)return z(xo,{message:i,onRetry:f});if(!r)return z(bo,{});if(!r.available)return z(`section`,{class:`rounded-2xl border border-amber-200 bg-amber-50 p-5 sm:p-6`,children:[z(`span`,{class:`grid size-11 place-items-center rounded-xl bg-amber-100 text-amber-800`,children:z(U,{class:`size-6`})}),z(`h2`,{class:`mt-5 mb-0 text-xl font-extrabold text-amber-950`,children:`SafeShield 상태 API를 찾을 수 없습니다`}),z(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-amber-900`,children:`safeshield 패키지와 rpcd ucode 플러그인이 설치되어 있는지 확인해 주세요.`})]});let y=r.enabled,b=J_(r.entitlement.plan),x=r.status===`running`,S=e!==null,C=(ca(r.status,r.stage)||L_(r)===`error`)&&r.blocklist.validLineCount===0&&g!==null?g:r.blocklist.validLineCount,w=y?e===`disable`?`끄는 중…`:`보호 끄기`:e===`enable`?`켜는 중…`:`보호 켜기`;function T(){(!y||window.confirm(`보호를 끄면, 현재 차단 목록이 제거되어 위험에 노출될 수 있습니다. 그래도 계속하시겠습니까?`))&&m(!y)}return z(v,{children:[z(q,{eyebrow:`Protection`,title:`SafeShield 보호 상태`,description:`DNS 보호 상태와 적용 중인 차단 목록을 확인합니다.`}),z(`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:[z(`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:[z(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between xl:flex-col 2xl:flex-row`,children:[z(`div`,{class:`flex min-w-0 items-start gap-3`,children:[z(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:z(At,{class:`size-5`})}),z(`div`,{class:`min-w-0`,children:[z(`div`,{class:`flex flex-wrap items-center gap-2`,children:[z(`h3`,{class:`m-0 text-base font-black text-slate-950`,children:`현재 보호 상태`}),z(W_,{data:r})]}),z(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:H_(r)})]})]}),z(`div`,{class:`flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap xl:justify-end`,children:[z(`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:[z(Wt,{class:`size-4 ${e===`enable`||e===`disable`?`animate-pulse`:``}`}),w]}),z(`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:[z(Gt,{class:`size-4 ${e===`refresh`||x?`animate-bounce`:``}`}),e===`refresh`?`시작 중…`:x?`갱신 중…`:r.status===`error`?`다시 갱신`:`지금 갱신`]})]})]}),z(K_,{data:r}),z(`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:[z(q_,{label:`SafeShield`,value:r.version??`확인되지 않음`}),z(q_,{label:`Blocklist`,value:`${J(C)}개 도메인`}),z(q_,{label:`Last refresh`,value:z(P_,{timestamp:r.timestamps.lastSuccess})}),z(Z_,{plan:b})]}),b===`FREE`?z(Q_,{}):null]}),z(`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:[z(`div`,{class:`flex flex-wrap items-center justify-between gap-3`,children:[z(`div`,{class:`flex min-w-0 items-center gap-3`,children:[z(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-500`,children:z(Kt,{class:`size-5`})}),z(`div`,{children:[z(`p`,{class:`m-0 text-[0.65rem] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Protection data`}),z(`h3`,{class:`mt-1 mb-0 text-sm font-black text-slate-950`,children:`보호 데이터`})]})]}),z(`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?z(H,{class:`size-3.5 shrink-0`}):z(U,{class:`size-3.5 shrink-0`}),r.artifact.resolved&&r.blocklist.installed&&r.blocklist.verificationOk?`최신`:`확인 필요`]})]}),z(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-500`,children:`SafeShield가 기기 성능에 맞춰 사용하는 보호 목록의 구성과 최신 상태를 확인합니다.`}),z(`dl`,{class:`mt-3 mb-0 grid grid-cols-1 gap-2 sm:grid-cols-2`,children:[z(`div`,{class:`min-w-0 rounded-lg border border-slate-200 bg-white px-3 py-2.5`,children:[z(`dt`,{class:`text-xs font-bold text-slate-500`,children:`기기 최적화`}),z(`dd`,{class:`mt-1 mb-0 text-sm font-extrabold text-slate-950`,children:B_(r.artifact.tier)})]}),z(`div`,{class:`min-w-0 rounded-lg border border-slate-200 bg-white px-3 py-2.5`,children:[z(`dt`,{class:`text-xs font-bold text-slate-500`,children:`마지막 업데이트`}),z(`dd`,{class:`mt-1 mb-0 text-sm font-extrabold leading-5 text-slate-950`,children:V_(r.artifact.version)})]})]}),z(`section`,{"aria-label":`보호 데이터 세부 정보`,class:`mt-3 border-t border-slate-200 pt-3`,children:[z(`h4`,{class:`m-0 text-xs font-extrabold text-slate-600`,children:`세부 정보`}),z(`dl`,{class:`mt-3 mb-0 grid gap-2`,children:[z(ev,{label:`데이터 프로필`,value:r.artifact.tier||`확인되지 않음`}),z(ev,{label:`데이터 버전`,value:r.artifact.version||`확인되지 않음`})]})]})]})]}),z(tv,{error:t,message:n,onDismiss:u}),z(N_,{action:e,data:o,error:s,loading:c,onRetry:p,onSetEnabled:h,refreshing:l}),z(`section`,{class:`mt-7`,children:[z(q,{description:`DNS 보호 런타임, 차단 목록, 갱신 일정과 상태 점검 결과를 한곳에서 확인합니다.`,eyebrow:`Protection details`,title:`보호 구성`}),z(`div`,{class:`grid min-w-0 grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4`,children:[z($_,{eyebrow:`Runtime`,icon:z(H,{class:`size-5`}),title:`DNS 보호 런타임`,children:[z(ev,{label:`SafeShield 서비스`,value:z(I_,{falseLabel:`중지됨`,trueLabel:`동작 중`,value:r.active})}),z(ev,{label:`DNS 서비스`,value:z(I_,{falseLabel:`중지됨`,value:r.runtime.dnsmasqRunning})}),z(ev,{label:`SafeShield DNS 연동`,value:z(I_,{value:r.runtime.dnsRuntimeOk})}),z(ev,{label:`차단 목록 갱신`,value:z(I_,{falseLabel:`중지됨`,value:r.runtime.refreshdRunning})})]}),z($_,{eyebrow:`Blocklist`,icon:z(Kt,{class:`size-5`}),title:`차단 목록`,children:[z(ev,{label:`적용 규칙`,value:`${J(r.blocklist.validLineCount)}개 도메인`}),z(ev,{label:`파일 크기`,value:Wa(r.blocklist.fileSizeKb*1024)}),z(ev,{label:`설치 상태`,value:z(I_,{falseLabel:`미설치`,trueLabel:`설치됨`,value:r.blocklist.installed})}),z(ev,{label:`검증 상태`,value:z(I_,{falseLabel:`확인 필요`,value:r.blocklist.verificationOk})})]}),z($_,{eyebrow:`Refresh schedule`,icon:z(Zt,{class:`size-5`}),title:`차단 목록 갱신`,children:[z(ev,{label:`마지막 성공`,value:z(P_,{timestamp:r.timestamps.lastSuccess})}),z(ev,{label:`다음 갱신`,value:qa(r.timestamps.nextRefreshAt)}),z(ev,{label:`갱신 주기`,value:Qa(r.timestamps.refreshIntervalS)}),z(ev,{label:`최근 결과`,value:r.runtime.lastResult||(x?`갱신 중`:`확인되지 않음`)})]}),z($_,{eyebrow:`Health`,icon:z(At,{class:`size-5`}),title:`상태 점검`,children:[z(ev,{label:`전체 상태`,value:U_(r.health.overall)}),z(ev,{label:`경고`,value:z(`span`,{class:`text-amber-700`,children:J(r.issueCounts.warnings)})}),z(ev,{label:`오류`,value:z(`span`,{class:`text-red-700`,children:J(r.issueCounts.errors)})}),z(ev,{label:`최근 오류 코드`,value:r.runtime.lastErrorCode||`없음`})]})]})]})]})}function rv(e){return e.trim().toLowerCase().replace(/\.+$/,``)}function iv(e){return e.length===0||e.length>253||!e.includes(`.`)||!/^[a-z0-9.-]+$/.test(e)?!1:e.split(`.`).every(e=>e.length>0&&e.length<=63&&/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(e))}function av({action:e,kind:t,onAddRule:n,onDeleteRule:r,rules:i,recommendedDomains:a,recommendedKnown:o,recommendedRules:s,onRequestRevoke:c,revokeBusy:l}){let[u,d]=M(``),[f,p]=M(``),[m,h]=M(null),[g,_]=M(!0),[v,y]=M(null),b=t===`allow`,x=e!==null,S=Qe(()=>{let e=f.trim().toLowerCase();return e?i.filter(t=>t.includes(e)):i},[i,f]),C=f.trim().length>0,w=b&&o?S.filter(e=>a.includes(e)):[],T=b&&o?S.filter(e=>!a.includes(e)):S,E=b?s.filter(e=>!C||e.title.toLowerCase().includes(f.trim().toLowerCase())||e.domains.some(e=>e.includes(f.trim().toLowerCase()))):[],D=C||(v??(s.length||w.length)<=3),O=C||g;async function k(e){e.preventDefault();let r=rv(u);iv(r)?(h(null),await n(t,r,()=>{d(e=>rv(e)===r?``:e)})):h(`example.com 형식의 도메인을 입력해 주세요. URL, IP 주소, 와일드카드는 사용할 수 없습니다.`)}let A=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 j(n){let i=b&&o&&a.includes(n),s=!b||o?i?`추천 규칙`:`직접 추가`:null,c=e?.operation===`delete`&&e.action===t&&e.domain===n;return z(`li`,{class:`flex min-w-0 items-center justify-between gap-3 px-3.5 py-3`,children:[z(`div`,{class:`flex min-w-0 flex-1 flex-wrap items-center gap-2`,children:z(`code`,{class:`min-w-0 break-all font-mono text-sm font-semibold text-slate-800`,children:n})}),i?z(`span`,{class:`shrink-0 rounded-full bg-teal-50 px-2.5 py-1 text-[11px] font-bold text-teal-800`,title:`추천 규칙의 적용과 해제는 웹사이트에서 관리합니다.`,children:`웹사이트 관리`}):z(`button`,{"aria-label":`${n} ${A.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:z($t,{class:`size-4 ${c?`animate-pulse`:``}`})})]},n)}return z(`article`,{class:`min-w-0 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm shadow-slate-900/5`,children:[z(`div`,{class:`border-b border-slate-200 p-4 sm:p-6`,children:[z(`div`,{class:`flex min-w-0 items-start gap-3`,children:[z(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl ${A.icon}`,children:z(b?H:At,{class:`size-6`})}),z(`div`,{class:`min-w-0`,children:[z(`div`,{class:`flex flex-wrap items-center gap-2`,children:[z(`h2`,{class:`m-0 text-xl font-extrabold tracking-tight text-slate-950`,children:A.label}),z(`span`,{class:`rounded-full px-2.5 py-1 text-xs font-extrabold ${A.badge}`,children:i.length})]}),z(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:A.description})]})]}),z(`form`,{class:`mt-5`,onSubmit:k,children:[z(`label`,{class:`block text-xs font-extrabold text-slate-600`,for:`${t}-domain`,children:`새 도메인`}),z(`div`,{class:`mt-2 flex flex-col gap-2 sm:flex-row`,children:[z(`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}),z(`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 ${A.button}`,disabled:x||u.trim().length===0,type:`submit`,children:[z(Qt,{class:`size-4`}),e?.operation===`add`&&e.action===t?`추가 중…`:`추가`]})]}),m?z(`p`,{class:`mt-2 mb-0 text-xs font-semibold leading-5 text-red-700`,id:`${t}-domain-error`,children:m}):null]})]}),z(`div`,{class:`p-4 sm:p-6`,children:[z(`label`,{class:`relative block`,for:`${t}-search`,children:[z(`span`,{class:`sr-only`,children:[A.label,` 검색`]}),z(`span`,{class:`pointer-events-none absolute inset-y-0 left-0 flex w-11 items-center justify-center text-slate-400`,children:z(en,{class:`size-4`})}),z(`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:`${A.label} 검색`,type:`search`,value:f})]}),b&&o?z(`div`,{class:`mt-4 overflow-hidden rounded-xl border border-slate-200`,children:[z(`section`,{"aria-label":`직접 추가한 허용 규칙`,children:[z(`button`,{"aria-controls":`allow-manual-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:()=>_(!O),type:`button`,children:[z(`span`,{class:`min-w-0 text-sm font-extrabold text-slate-800`,children:[`직접 추가한 규칙 `,z(`span`,{class:`ml-1 rounded-full bg-white px-2 py-0.5 text-xs font-bold text-slate-600`,children:[T.length,C?`개 검색됨`:`개`]})]}),z(`span`,{"aria-hidden":`true`,class:`shrink-0 text-sm font-bold text-slate-500`,children:O?`접기 −`:`펼치기 +`})]}),z(`div`,{hidden:!O,id:`allow-manual-domains`,children:T.length?z(`ul`,{class:`m-0 max-h-[20rem] list-none divide-y divide-slate-200 overflow-y-auto p-0`,children:T.map(j)}):z(`p`,{class:`m-0 px-4 py-5 text-sm text-slate-500`,children:C?`검색 결과가 없습니다.`:`직접 추가한 허용 도메인이 없습니다.`})})]}),z(`section`,{"aria-label":`웹사이트 추천 규칙`,class:`border-t border-slate-200`,children:[z(`button`,{"aria-controls":`allow-recommended-domains`,"aria-expanded":D,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(!D),type:`button`,children:[z(`span`,{class:`min-w-0 text-sm font-extrabold text-slate-800`,children:[`추천 규칙 `,z(`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:w.length,C?`개 검색됨`:`개`]})]}),z(`span`,{"aria-hidden":`true`,class:`shrink-0 text-sm font-bold text-slate-500`,children:D?`접기 −`:`펼치기 +`})]}),z(`div`,{hidden:!D,id:`allow-recommended-domains`,children:[E.length?z(`ul`,{class:`m-0 max-h-[22rem] list-none divide-y divide-slate-200 overflow-y-auto p-0`,children:E.map(e=>z(`li`,{class:`flex min-w-0 flex-wrap items-center justify-between gap-3 px-3.5 py-3`,children:[z(`div`,{class:`min-w-0 flex-1`,children:[z(`strong`,{class:`block break-words text-sm text-slate-900`,children:e.title}),z(`div`,{class:`mt-1 flex flex-wrap gap-1.5`,children:e.domains.map(e=>z(`code`,{class:`break-all text-xs text-slate-600`,children:e},e))})]}),c?z(`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:`적용 해제`}):z(`span`,{class:`text-xs text-slate-500`,children:`웹사이트 관리`})]},e.id))}):w.length?z(`ul`,{class:`m-0 max-h-[20rem] list-none divide-y divide-slate-200 overflow-y-auto p-0`,children:w.map(j)}):z(`p`,{class:`m-0 px-4 py-5 text-sm text-slate-500`,children:C?`검색 결과가 없습니다.`:`추천으로 추가된 허용 도메인이 없습니다.`}),C?null:z(`p`,{class:`m-0 border-t border-slate-100 px-4 py-3 text-xs leading-5 text-slate-500`,children:`추천 규칙은 이 공유기에서 적용 해제할 수 있습니다. 새 규칙의 적용은 SmartSafeHub 웹사이트에서 관리해 주세요.`})]})]})]}):z(`div`,{class:`mt-4 max-h-[28rem] overflow-y-auto rounded-xl border border-slate-200`,children:S.length?z(`ul`,{class:`m-0 list-none divide-y divide-slate-200 p-0`,children:S.map(j)}):z(`div`,{class:`px-5 py-10 text-center`,children:[z(tn,{class:`mx-auto size-8 text-slate-300`}),z(`p`,{class:`mt-3 mb-0 text-sm font-bold text-slate-600`,children:C?`검색 결과가 없습니다.`:A.empty})]})})]})]})}function ov({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]=M(null),[m,h]=M(!1),[g,_]=M(!1),[y,b]=M(null),[x,S]=M(!1),[C,w]=M(!1),[T,E]=M(!1),D=P(null),O=P(null),k=P(c);k.current=c;let[A,j]=M(null),[ee,te]=M(null),ne=f?.domains??[],re=f?.rules??[],ie=F(e=>{p(e),h(!0);let t=JSON.stringify([e.revision,e.domains,e.rules.map(e=>e.id),e.syncStatus]);O.current!==null&&O.current!==t&&k.current(),O.current=t,e.syncStatus===`confirmed`&&D.current===null&&te(null),D.current!==null&&e.syncStatus===`confirmed`&&!e.rules.some(e=>e.id===D.current)&&(D.current=null,w(!1),E(!1),te(null),j(`추천 규칙 적용 해제를 확인했습니다.`))},[]);N(()=>{if(!l)return;let e=l,t=!0,n=!1;function r(){n||g||x||(n=!0,e().then(e=>{t&&ie(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)}},[t?.allow.join(` -`),l,g,x,ie]),N(()=>{if(!C)return;let e=window.setTimeout(()=>E(!0),6e4);return()=>window.clearTimeout(e)},[C]);async function ae(){if(!(!u||g||x)){_(!0),te(null),j(null);try{let e=await u();ie(e),j(e.syncStatus===`confirmed`?`서버에서 규칙을 가져와 SafeShield 허용 목록을 확인했습니다.`:`추천 규칙 상태를 확인 중입니다. 진행 결과는 자동으로 갱신됩니다.`),c()}catch{te(`추천 규칙을 동기화하지 못했습니다. 서버 연결 및 기기 등록 상태를 확인해 주세요.`)}finally{_(!1)}}}async function oe(){if(!y||x||!d)return;let e=y.id;D.current=e,S(!0),w(!0),E(!1),te(null),j(null);try{let t=await d(e);ie(t),b(null),D.current!==null&&j(`적용 해제 요청을 처리했습니다. 공유기의 허용 목록을 확인하고 있습니다.`),k.current()}catch{b(null),te(`적용 해제 결과를 아직 확인하지 못했습니다. 공유기의 상태를 자동으로 다시 확인합니다.`);try{let e=await l?.();e&&ie(e)}catch{}}finally{S(!1)}}let se=f?.syncStage,ce=f?.syncStatus===`pending`,le=f?.syncStatus===`delayed`,ue=f?.syncStatus===`failed`,de=se===`server_sync`?`서버 동기화 중`:se===`safeshield_refreshing`?`SafeShield 갱신 중`:se===`applying`?`추천 규칙 적용 중`:se===`verifying`?`허용 목록 확인 중`:le?`재확인 지연`:ue?`동기화 실패`:ce?`동기화 대기`:`허용 목록 확인`,fe=se===`safeshield_refreshing`&&ce?`SafeShield가 보호 목록을 갱신하고 있습니다. 갱신이 끝나면 추천 규칙을 자동으로 다시 확인합니다.`:se===`applying`&&ce?`추천 규칙을 SafeShield 허용 목록에 반영하고 있습니다.`:se===`verifying`&&ce?`SafeShield 허용 목록에서 추천 규칙 반영 결과를 확인하고 있습니다.`:se===`server_sync`&&ce?`SmartSafeHub 서버에서 최신 추천 규칙을 확인하고 있습니다.`:le?`서버 응답이 지연되고 있습니다. 이미 확인한 허용 규칙은 유지되며 자동으로 다시 시도합니다.`:ue?f?.syncReason===`auth_rejected`?`SmartSafeHub 기기 인증이 거부되었습니다. 공유기 계정 연결 상태를 확인해 주세요.`:f?.syncReason===`api_rejected`?`서버가 추천 규칙 요청을 거부했습니다. Hub API 배포 상태를 확인해 주세요.`:f?.syncReason===`invalid_response`?`서버 응답을 확인할 수 없습니다. 규칙은 유지되며 다음 동기화에서 다시 확인합니다.`:`추천 규칙 반영을 완료하지 못했습니다. 다음 자동 동기화에서 다시 시도합니다.`:ce?`추천 규칙의 적용 결과를 기다리고 있습니다.`:null,pe=C&&D.current!==null?{tone:`amber`,label:T?`적용 해제 처리 지연`:`적용 해제 확인 중`,description:T?`적용 해제 확인이 지연되고 있습니다. SafeShield 갱신 중이라면 완료 후 자동으로 재확인합니다.`:fe??`적용 해제 상태를 확인하고 있습니다. 확인되면 목록과 상태가 자동 갱신됩니다.`}:ce||le||ue?{tone:ue?`red`:`amber`,label:de,description:fe??``}:ee?{tone:`red`,label:`상태 확인 필요`,description:ee}:f?.syncWarning?{tone:`amber`,label:`서버 재확인 지연`,description:`이미 확인한 허용 규칙은 유지되며 자동으로 다시 시도합니다.`}:A?{tone:`green`,label:`허용 목록 확인`,description:A}:null;return i&&!t?z(bo,{}):n&&!t?z(xo,{message:n,onRetry:c}):t?z(v,{children:[y?z(`div`,{class:`fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70 p-4`,role:`presentation`,onClick:()=>{x||b(null)},children:z(`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:[z(`h2`,{class:`m-0 text-lg font-extrabold text-slate-950`,id:`recommended-revoke-title`,children:`추천 규칙 적용 해제`}),z(`p`,{class:`mt-3 text-sm text-slate-700`,children:[z(`strong`,{children:y.title}),` 규칙을 이 공유기에서 해제할까요?`]}),z(`p`,{class:`mt-2 text-xs leading-5 text-slate-600`,children:[`포함된 도메인 `,y.domains.length,`개를 다시 확인합니다. 직접 추가한 허용 규칙과 다른 추천 규칙에 필요한 도메인은 유지됩니다.`]}),z(`div`,{class:`mt-5 flex justify-end gap-2`,children:[z(`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:()=>b(null),type:`button`,children:`취소`}),z(`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 oe(),type:`button`,children:[x?z(`svg`,{class:`size-4 animate-spin`,"aria-hidden":`true`,viewBox:`0 0 24 24`,fill:`none`,children:[z(`circle`,{cx:`12`,cy:`12`,r:`9`,stroke:`currentColor`,"stroke-width":`3`,opacity:`0.25`}),z(`path`,{d:`M21 12a9 9 0 0 0-9-9`,stroke:`currentColor`,"stroke-width":`3`,"stroke-linecap":`round`})]}):null,z(`span`,{role:`status`,children:x?`해제 중…`:`적용 해제`})]})]})]})}):null,z(q,{eyebrow:`Local overrides`,title:`사용자 도메인 규칙`,description:`허용 목록은 오탐을 해제하고, 차단 목록은 원하는 도메인을 SafeShield 보호 목록에 추가합니다.`}),z(`section`,{"aria-label":`규칙 개수 요약`,class:`min-w-0`,children:z(`dl`,{class:`m-0 grid grid-cols-1 gap-3 sm:grid-cols-3`,children:[z(`div`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white px-5 py-4 shadow-sm shadow-slate-900/5`,children:[z(`dt`,{class:`text-sm font-extrabold text-emerald-700`,children:`허용 도메인`}),z(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight text-slate-950`,children:[t.counts.allow,z(`span`,{class:`ml-1 text-sm font-bold text-slate-500`,children:`개`})]}),z(`p`,{class:`mt-1 mb-0 text-xs text-slate-500`,children:`차단하지 않도록 등록한 도메인`})]}),z(`div`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white px-5 py-4 shadow-sm shadow-slate-900/5`,children:[z(`dt`,{class:`text-sm font-extrabold text-red-700`,children:`차단 도메인`}),z(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight text-slate-950`,children:[t.counts.block,z(`span`,{class:`ml-1 text-sm font-bold text-slate-500`,children:`개`})]}),z(`p`,{class:`mt-1 mb-0 text-xs text-slate-500`,children:`추가로 차단하도록 등록한 도메인`})]}),z(`div`,{class:`min-w-0 rounded-2xl border border-slate-200 bg-white px-5 py-4 shadow-sm shadow-slate-900/5`,children:[z(`dt`,{class:`text-sm font-extrabold text-slate-600`,children:`전체 등록 규칙`}),z(`dd`,{class:`mt-2 mb-0 ml-0 text-2xl font-black tracking-tight text-slate-950`,children:[t.counts.total,z(`span`,{class:`ml-1 text-sm font-bold text-slate-500`,children:`개`})]}),z(`p`,{class:`mt-1 mb-0 text-xs text-slate-500`,children:`허용 및 차단 규칙의 합계`})]})]})}),z(`div`,{class:`mt-7`,children:z(q,{eyebrow:`Recommendations`,title:`추천 사용자 규칙`,description:`웹사이트에서 선택한 권장 허용 규칙의 동기화 상태를 확인하고 관리합니다.`})}),z(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,"aria-label":`추천 사용자 규칙`,children:[z(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between`,children:[z(`div`,{class:`min-w-0 flex-1`,children:[z(`div`,{class:`flex flex-wrap items-center gap-2`,children:[z(`span`,{class:`grid size-8 place-items-center rounded-lg bg-teal-50 text-teal-700`,children:z(H,{class:`size-4`})}),z(`h3`,{class:`m-0 text-base font-extrabold text-slate-950`,children:`동기화 상태`}),l?z(`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`?`허용 목록 확인`:de}):null]}),z(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:`웹사이트에서 선택한 허용 규칙을 약 30초마다 확인합니다. 적용 해제는 이 공유기에서도 할 수 있습니다.`}),z(`div`,{class:`mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs font-semibold text-slate-600`,children:[z(`span`,{children:[`추천으로 추가 `,z(`strong`,{class:`text-teal-800`,children:[m?ne.length:`—`,`개 도메인`]})]}),f?.syncStatus===`confirmed`?z(`span`,{children:[`허용 확인 `,z(`strong`,{class:`text-emerald-800`,children:[f.confirmedDomains.length,`개`]})]}):null,f?.lastCheckedAt?z(`span`,{children:[f.syncWarning?`최근 재확인 시도`:`최근 확인`,`: `,z(P_,{timestamp:f.lastCheckedAt})]}):null]})]}),z(`div`,{class:`flex w-full flex-wrap gap-2 sm:w-auto lg:shrink-0`,children:[u?z(`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 ae(),type:`button`,children:g?`동기화 중…`:`지금 동기화`}):null,z(`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:`추천 규칙 보기 ↗`})]})]}),pe?z(`div`,{"aria-live":`polite`,class:`mt-3 flex items-start gap-2 rounded-xl px-3 py-2.5 text-sm ${pe.tone===`red`?`bg-red-50 text-red-800`:pe.tone===`green`?`bg-emerald-50 text-emerald-800`:`bg-amber-50 text-amber-800`}`,role:`status`,children:[z(`span`,{"aria-hidden":`true`,class:`mt-1 size-2 shrink-0 rounded-full ${pe.tone===`red`?`bg-red-500`:pe.tone===`green`?`bg-emerald-500`:`bg-amber-500`}`}),z(`div`,{class:`min-w-0`,children:[z(`strong`,{children:pe.label}),z(`p`,{class:`mt-1 mb-0 leading-5`,children:pe.description})]})]}):null,z(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`추천 규칙 동기화는 SafeShield 차단 목록 갱신과 별개입니다. 허용 목록 확인은 DNS 처리나 서비스 재생 성공을 보장하지 않습니다.`})]}),t.applyLocalOverrides?null:z(`section`,{class:`mt-4 flex items-start gap-3 rounded-2xl border border-amber-200 bg-amber-50 p-4`,role:`alert`,children:[z(U,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),z(`div`,{children:[z(`p`,{class:`m-0 text-sm font-extrabold text-amber-950`,children:`로컬 규칙 적용이 꺼져 있습니다`}),z(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:`규칙은 저장되지만 SafeShield의 apply_local_overrides 설정을 켜기 전까지 DNS에 반영되지 않습니다.`})]})]}),t.safeshieldEnabled?null:z(`section`,{class:`mt-4 flex items-start gap-3 rounded-2xl border border-slate-200 bg-slate-100 p-4`,role:`status`,children:[z(At,{class:`mt-0.5 size-5 shrink-0 text-slate-600`}),z(`div`,{children:[z(`p`,{class:`m-0 text-sm font-extrabold text-slate-950`,children:`SafeShield가 비활성화되어 있습니다`}),z(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`규칙을 미리 저장할 수 있으며, SafeShield를 켜고 차단 목록을 갱신하면 적용됩니다.`})]})]}),r?z(`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:[z(`div`,{class:`flex min-w-0 items-start gap-3`,children:[z(H,{class:`mt-0.5 size-5 shrink-0 text-teal-700`}),z(`p`,{class:`m-0 text-sm font-semibold leading-6 text-teal-950`,children:r})]}),z(`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?z(`section`,{"aria-live":`assertive`,class:`mt-4 flex items-start gap-3 rounded-2xl border border-red-200 bg-red-50 p-4`,children:[z(U,{class:`mt-0.5 size-5 shrink-0 text-red-700`}),z(`p`,{class:`m-0 text-sm font-semibold leading-6 text-red-900`,children:n})]}):null,z(`section`,{class:`mt-5 grid gap-5 xl:grid-cols-2`,children:[z(av,{action:e,kind:`allow`,onAddRule:a,onDeleteRule:o,rules:t.allow,recommendedDomains:ne,recommendedKnown:m,recommendedRules:re,...d?{onRequestRevoke:b}:{},revokeBusy:x}),z(av,{action:e,kind:`block`,onAddRule:a,onDeleteRule:o,rules:t.block,recommendedDomains:ne,recommendedKnown:m,recommendedRules:re,revokeBusy:x})]})]}):z(xo,{message:`사용자 규칙 응답이 비어 있습니다.`,onRetry:c})}function sv(e,t){return R(`smartsafehub`,`system_root_password_change`,{current_password:e,new_password:t})}var cv=Array.from({length:24},(e,t)=>{let n=String(t).padStart(2,`0`);return{value:n,label:`${n}시`}}),lv=Array.from({length:60},(e,t)=>{let n=String(t).padStart(2,`0`);return{value:n,label:`${n}분`}});function uv(e){let t=/^([01]\d|2[0-3]):([0-5]\d)$/.exec(e);return{hour:t?.[1]??`03`,minute:t?.[2]??`00`}}function dv({ariaLabel:e,className:t=``,disabled:n=!1,id:r,onChange:i,value:a,variant:o=`default`}){let{hour:s,minute:c}=uv(a),l=(e,t)=>{i(`${e}:${t}`)};return z(`div`,{"aria-label":e,class:`ssh-time-select ${t}`.trim(),"data-time-select":r,role:`group`,children:[z(Vg,{ariaLabel:`${e} 시`,disabled:n,id:`${r}-hour`,onChange:e=>l(e,c),options:cv,value:s,variant:o}),z(Vg,{ariaLabel:`${e} 분`,disabled:n,id:`${r}-minute`,onChange:e=>l(s,e),options:lv,value:c,variant:o})]})}function fv(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone||null}catch{return null}}function pv(e){return z(`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:[z(`div`,{class:`flex min-w-0 items-start justify-between gap-3`,children:[z(`div`,{class:`flex min-w-0 items-start gap-4`,children:[z(`div`,{class:`flex size-11 shrink-0 items-center justify-center rounded-xl bg-slate-100 text-slate-600`,children:e.icon}),z(`div`,{class:`min-w-0`,children:[z(`div`,{class:`flex flex-wrap items-center gap-2`,children:[z(`h2`,{class:`m-0 text-lg font-black text-slate-950`,children:e.title}),e.titleActions]}),z(`p`,{class:`mt-1.5 mb-0 text-sm leading-6 text-slate-500`,children:e.description})]})]}),e.headerActions?z(`div`,{class:`shrink-0`,children:e.headerActions}):null]}),z(`div`,{class:`mt-5`,children:e.children})]})}function mv(e){let[t,n]=M(``),[r,i]=M(e.data?.localtime??0),a=fv();N(()=>{e.data?.zonename&&n(e.data.zonename)},[e.data?.zonename]),N(()=>{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=Za(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 z(v,{children:[z(pv,{description:`공유기의 기준 시간대를 설정합니다. 로그, 통계, 자동 설치와 예약 재부팅 시각도 이 시간대를 기준으로 동작합니다.`,icon:z(Ht,{class:`size-5`}),title:`시간 및 시간대`,children:[(e.saveError||e.saveMessage)&&z(`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:[z(`span`,{class:`min-w-0`,children:e.saveError||e.saveMessage}),z(`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?z(`div`,{class:`rounded-xl border border-rose-200 bg-rose-50 p-4`,children:[z(`p`,{class:`m-0 text-sm font-bold text-rose-800`,children:e.error}),z(`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:`다시 불러오기`})]}):z(v,{children:[z(`div`,{class:`grid min-w-0 grid-cols-1 gap-3 sm:grid-cols-2`,children:[z(`div`,{class:`rounded-xl bg-slate-50 p-4`,children:[z(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.12em] text-slate-500`,children:`현재 시간`}),z(`p`,{class:`mt-2 mb-0 text-sm font-extrabold leading-6 text-slate-900`,children:s}),z(`p`,{class:`mt-1 mb-0 break-words text-xs leading-5 text-slate-500`,children:e.data?.zonename??`시간대 확인 중`})]}),z(`div`,{class:`rounded-xl bg-slate-50 p-4`,children:[z(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.12em] text-slate-500`,children:`시간 동기화`}),z(`p`,{class:`mt-2 mb-0 text-sm font-extrabold ${e.data?.ntpEnabled?`text-emerald-700`:`text-amber-700`}`,children:e.data?.ntpEnabled?`자동 동기화 설정됨`:`자동 동기화 꺼짐`}),z(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`장치의 기본 NTP 설정 상태입니다.`}),z(`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?`동기화 요청 중`:`지금 동기화`})]})]}),z(`div`,{class:`mt-5`,children:[z(`label`,{class:`mb-2 block text-sm font-extrabold text-slate-800`,for:`smartsafehub-timezone`,children:`시간대`}),z(Vg,{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}),z(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-500`,children:`시간대를 변경하면 시스템에 즉시 적용되며 자동 설치와 예약 재부팅 일정도 새 기준 시간으로 다시 계산합니다.`})]}),z(`div`,{class:`mt-4 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:justify-end`,children:[c&&a!==t&&z(`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:`브라우저 시간대 사용`}),z(`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?`시간대 저장 중`:`시간대 저장`})]})]})]}),z(`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:z(Ev,{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 hv(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 gv(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 _v(e){return e.lastReportAt>0?Ja(e.lastReportAt):e.enabled?`첫 보고 대기 중`:`보고 기록 없음`}function vv(e){let t=e?.plan?.trim().toUpperCase(),n=e?.entitlementStatus?.trim().toLowerCase();return t&&(n===`trial`||n===`trialing`)?`${t} · 체험`:t||`유료 · Trial`}function yv(e){let t=e.data,n=hv(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 z(v,{children:[z(pv,{description:`SmartSafeHub가 장치 상태를 직접 점검하고 이상 항목을 알려줍니다.`,icon:z(Gt,{class:`size-5`}),title:`장치 진단`,children:[(e.actionError||e.actionMessage)&&z(`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:[z(`span`,{children:e.actionError||e.actionMessage}),z(`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?z(`div`,{class:`rounded-xl border border-rose-200 bg-rose-50 p-4`,children:[z(`p`,{class:`m-0 text-sm font-bold text-rose-800`,children:e.error}),z(`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:`다시 불러오기`})]}):z(v,{children:[z(`div`,{class:`rounded-xl border p-4 ${n.panel}`,children:z(`div`,{class:`flex flex-wrap items-start justify-between gap-3`,children:[z(`div`,{class:`flex min-w-0 items-start gap-3`,children:[z(`span`,{class:`mt-0.5 shrink-0`,children:t?.overall===`ok`?z(H,{class:`size-5 text-emerald-700`}):t?.overall===`initializing`?z(Ht,{class:`size-5 text-teal-700`}):z(U,{class:`size-5 text-amber-700`})}),z(`div`,{class:`min-w-0`,children:[z(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:t?.summary.message??`장치 상태를 확인하고 있습니다.`}),z(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-600`,children:[`마지막 진단 : `,t?Ja(t.generatedAt):`확인 중`]})]})]}),z(`span`,{class:`rounded-full px-2.5 py-1 text-xs font-extrabold ${n.badge}`,children:n.label})]})}),t&&z(`div`,{class:`mt-4 grid grid-cols-3 gap-2`,children:[z(`div`,{class:`rounded-xl bg-slate-50 p-3`,children:[z(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`진단 항목`}),z(`p`,{class:`mt-1 mb-0 text-lg font-black text-slate-950`,children:t.summary.total})]}),z(`div`,{class:`rounded-xl bg-slate-50 p-3`,children:[z(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`주의`}),z(`p`,{class:`mt-1 mb-0 text-lg font-black text-amber-700`,children:t.summary.warning})]}),z(`div`,{class:`rounded-xl bg-slate-50 p-3`,children:[z(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`이상`}),z(`p`,{class:`mt-1 mb-0 text-lg font-black text-rose-700`,children:t.summary.critical})]})]}),i.length>0&&z(`div`,{class:`mt-4 space-y-2`,children:i.map(e=>{let t=hv(e.status);return z(`div`,{class:`flex min-w-0 items-start justify-between gap-3 rounded-xl border border-slate-200 px-3 py-2.5`,children:[z(`div`,{class:`min-w-0`,children:[z(`p`,{class:`m-0 text-xs font-extrabold text-slate-900`,children:e.label}),z(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:e.message})]}),z(`span`,{class:`shrink-0 rounded-full px-2 py-1 text-[11px] font-extrabold ${t.badge}`,children:t.label})]},e.id)})}),z(`div`,{class:`mt-4 flex flex-col gap-2 sm:flex-row`,children:[z(`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?`진단 중`:`지금 진단`}),z(`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?`진단 정보 생성 중`:`진단 정보 다운로드`})]}),z(`p`,{class:`mt-3 mb-0 text-xs leading-5 text-slate-500`,children:`다운로드 파일에는 호스트명, WAN IP와 Wi-Fi SSID 같은 네트워크 식별 정보가 포함될 수 있으므로 외부 전달 전에 내용을 확인해 주세요.`})]})]}),z(`div`,{class:`grid min-w-0 content-start gap-4`,children:[z(pv,{description:`장치의 최소 상태 정보와 이상 코드만 서버로 전송합니다. 직접 켜고 언제든지 끌 수 있습니다.`,icon:z(At,{class:`size-5`}),title:`원격 상태 보고`,titleActions:z(`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:vv(a)}),headerActions:z(`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:z(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${a?.enabled?`left-6`:`left-1`}`})}),children:z(`div`,{class:`min-w-0`,children:a?.eligible?z(`div`,{class:`rounded-xl bg-slate-50 p-4`,children:[z(`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:[z(`span`,{class:`mt-0.5 shrink-0`,children:o?z(H,{class:`size-5 text-emerald-700`}):z(Wt,{class:`size-5 text-slate-500`})}),z(`div`,{class:`min-w-0 flex-1`,children:[z(`div`,{class:`flex flex-wrap items-center gap-2`,children:[z(`p`,{class:`m-0 text-sm font-black text-slate-950`,children:c}),z(`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?`켜짐`:`꺼짐`})]}),z(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-600`,children:l})]})]}),z(`div`,{class:`grid grid-cols-1 gap-3 sm:grid-cols-2`,children:[z(`div`,{children:[z(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`최근 전송 상태`}),z(`p`,{class:`mt-1 mb-0 text-sm font-black text-slate-900`,children:e.savingReporter?`설정 반영 중`:gv(a)})]}),z(`div`,{children:[z(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`마지막 서버 보고`}),z(`p`,{class:`mt-1 mb-0 text-sm font-black text-slate-900`,children:_v(a)})]})]}),a.enabled&&a.lastErrorCode&&z(`p`,{class:`mt-3 mb-0 text-xs font-bold text-rose-700`,children:[`최근 보고 오류 : `,a.lastErrorCode]})]}):z(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[z(`p`,{class:`m-0 text-sm font-extrabold text-slate-900`,children:`원격 상태 보고는 유료 멤버십 또는 체험 기간에 사용할 수 있습니다.`}),z(`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:`멤버십 알아보기`})]})})}),z(pv,{description:`진단에 필요한 최소한의 상태 정보만 전송하며, 네트워크 식별 정보와 이용 내용은 보내지 않습니다.`,icon:z(At,{class:`size-5`}),title:`개인정보 보호`,children:z(`div`,{class:`grid grid-cols-1 gap-3 sm:grid-cols-2`,children:[z(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-3.5`,children:[z(`div`,{class:`flex items-center gap-2`,children:[z(`span`,{class:`flex size-7 shrink-0 items-center justify-center rounded-lg bg-white text-slate-600`,children:z(Kt,{class:`size-4`})}),z(`p`,{class:`m-0 text-sm font-black text-slate-900`,children:`전송되는 정보`})]}),z(`div`,{class:`mt-3 space-y-3`,children:[z(`div`,{children:[z(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`기기 상태 수치`}),z(`p`,{class:`mt-1 mb-0 text-xs font-semibold leading-5 text-slate-700`,children:`메모리 · 부하 · 저장 공간`})]}),z(`div`,{children:[z(`p`,{class:`m-0 text-[11px] font-extrabold text-slate-500`,children:`진단 정보`}),z(`p`,{class:`mt-1 mb-0 text-xs font-semibold leading-5 text-slate-700`,children:`진단 상태 · 이상 코드 · 보고 시각`})]})]})]}),z(`div`,{class:`rounded-xl border border-teal-200 bg-teal-50 p-3.5`,children:[z(`div`,{class:`flex items-center gap-2`,children:[z(`span`,{class:`flex size-7 shrink-0 items-center justify-center rounded-lg bg-white text-teal-700`,children:z(At,{class:`size-4`})}),z(`p`,{class:`m-0 text-sm font-black text-slate-900`,children:`전송하지 않는 정보`})]}),z(`div`,{class:`mt-3 space-y-3`,children:[z(`div`,{children:[z(`p`,{class:`m-0 text-[11px] font-extrabold text-teal-800`,children:`개인 식별 · 네트워크 정보`}),z(`p`,{class:`mt-1 mb-0 text-xs font-semibold leading-5 text-slate-700`,children:`호스트명 · WAN IP · Wi-Fi SSID/MAC`})]}),z(`div`,{children:[z(`p`,{class:`m-0 text-[11px] font-extrabold text-teal-800`,children:`이용 내용 및 원문 데이터`}),z(`p`,{class:`mt-1 mb-0 text-xs font-semibold leading-5 text-slate-700`,children:`DNS 요청 내용 · 시스템 로그 원문`})]})]})]})]})})]})]})}function bv(e){let[t,n]=M(!1);return z(`label`,{class:`block min-w-0`,for:e.id,children:[z(`span`,{class:`mb-2 block text-sm font-extrabold text-slate-800`,children:e.label}),z(`span`,{class:`relative block min-w-0`,children:[z(`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}),z(`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:z(t?Xt:Yt,{class:`size-4`})})]})]})}function xv(e){return z(`li`,{class:`flex min-w-0 items-center gap-1.5 text-xs font-bold ${e.met?`text-emerald-700`:`text-slate-500`}`,children:[z(H,{class:`size-3.5 shrink-0`,"aria-hidden":`true`}),z(`span`,{children:e.label})]})}function Sv(e){let[t,n]=M(``),[r,i]=M(``),[a,o]=M(``),[s,c]=M(!1),[l,u]=M(null),d=hn(r),f=gn(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 sv(t,r),n(``),i(``),o(``);try{await Oe()}catch{}c(!1),e.onChanged()}catch(e){u(G(e,`관리자 비밀번호를 변경하지 못했습니다. 잠시 후 다시 시도해 주세요.`)),c(!1)}}};return z(pv,{description:`현재 비밀번호를 확인한 뒤 공유기 관리자 비밀번호를 변경합니다. 변경이 완료되면 새 비밀번호로 다시 로그인해야 합니다.`,icon:z(qt,{class:`size-5`}),title:`관리자 비밀번호`,children:z(`form`,{class:`space-y-4`,noValidate:!0,onSubmit:e=>{e.preventDefault(),g()},children:[l&&z(`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}),z(bv,{autoComplete:`current-password`,disabled:s,id:`smartsafehub-current-admin-password`,label:`현재 비밀번호`,name:`current-password`,onInput:n,placeholder:`현재 관리자 비밀번호`,value:t}),z(bv,{autoComplete:`new-password`,disabled:s,id:`smartsafehub-new-admin-password`,label:`새 비밀번호`,name:`new-password`,onInput:i,placeholder:`새 관리자 비밀번호`,value:r}),z(`ul`,{"aria-label":`새 비밀번호 요구 사항`,class:`m-0 grid list-none grid-cols-1 gap-2 p-0 sm:grid-cols-3`,children:[z(xv,{label:`8자 이상`,met:d.length}),z(xv,{label:`영문자 포함`,met:d.letter}),z(xv,{label:`숫자 포함`,met:d.number})]}),z(`div`,{class:`pt-2`,children:z(bv,{autoComplete:`new-password`,disabled:s,id:`smartsafehub-confirm-admin-password`,label:`새 비밀번호 확인`,name:`confirm-password`,onInput:o,placeholder:`새 관리자 비밀번호 다시 입력`,value:a})}),a.length>0&&z(`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&&z(`p`,{class:`m-0 text-xs font-bold text-amber-700`,children:`새 비밀번호는 현재 비밀번호와 다르게 설정해 주세요.`}),z(`div`,{class:`pt-2`,children:z(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-3 text-xs leading-5 text-slate-600`,children:`비밀번호는 현재 공유기에 직접 적용되며 외부 서버로 전송되지 않습니다. 변경 후 현재 로그인 세션은 종료됩니다.`})}),z(`div`,{class:`rounded-xl border border-amber-200 bg-amber-50 p-3 text-xs leading-5 text-amber-900`,children:[z(`strong`,{class:`block font-extrabold`,children:`비밀번호를 잊었을 때`}),`전원이 켜진 상태에서 Reset 버튼을 5~9초 누른 뒤 놓으면 관리자 비밀번호만 복구할 수 있습니다. 네트워크, Wi-Fi와 SafeShield 설정은 유지됩니다. 10초 이상 누르면 모든 사용자 설정을 초기화하는 기기 초기화가 실행됩니다.`]}),z(`div`,{class:`flex justify-end`,children:z(`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 Cv(e){let t=P(null),[n,r]=M(null),[i,a]=M(!1),o=e.action!==null;N(()=>{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 z(pv,{description:`펌웨어 업데이트나 복구 전에 현재 OpenWrt 설정을 표준 백업 파일로 저장하고, 필요할 때 검증 후 복원합니다.`,icon:z(Kt,{class:`size-5`}),title:`설정 백업 및 복원`,children:[(e.error||e.message)&&z(`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:[z(`span`,{class:`min-w-0`,children:e.error||e.message}),!e.restoreAccepted&&z(`button`,{class:`shrink-0 rounded-lg px-2 py-1 text-xs font-extrabold hover:bg-black/5`,onClick:e.onDismissFeedback,type:`button`,children:`닫기`})]}),z(`div`,{class:`grid min-w-0 grid-cols-1 gap-3`,children:[z(`div`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4 sm:p-5`,children:[z(`p`,{class:`m-0 text-sm font-black text-slate-900`,children:`현재 설정 백업`}),z(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-600`,children:`네트워크, Wi-Fi, SmartSafeHub와 SafeShield 등 OpenWrt가 관리하는 설정을 표준 백업 파일로 저장합니다.`}),z(`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:[z(Gt,{class:`size-4`}),e.action===`download`?`백업 생성 중`:`설정 백업 다운로드`]}),z(`p`,{class:`mt-3 mb-0 text-xs font-bold leading-5 text-amber-700`,children:`백업에는 Wi-Fi 비밀번호, 관리자 설정, VPN 키나 기기 인증 정보 같은 민감한 값이 포함될 수 있으므로 안전한 위치에 보관해 주세요.`})]}),z(`div`,{class:`rounded-xl border border-slate-200 bg-white p-4 sm:p-5`,children:[z(`p`,{class:`m-0 text-sm font-black text-slate-900`,children:`설정 복원`}),z(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-slate-600`,children:`SmartSafeHub 또는 OpenWrt 백업을 업로드하고 검증한 뒤 복원합니다. 최대 16MB까지 사용할 수 있습니다.`}),e.validated?z(`div`,{class:`mt-4 rounded-xl border border-emerald-200 bg-emerald-50 p-4`,children:[z(`div`,{class:`flex min-w-0 flex-col gap-1 sm:flex-row sm:items-center sm:justify-between`,children:z(`div`,{class:`min-w-0`,children:[z(`p`,{class:`m-0 break-words text-sm font-extrabold text-emerald-900`,children:e.validated.filename}),z(`p`,{class:`mt-1 mb-0 text-xs font-bold text-emerald-700`,children:[Wa(e.validated.sizeBytes),` · 검증 완료`]})]})}),i?z(`div`,{class:`mt-4 rounded-xl border border-rose-200 bg-white p-4`,children:[z(`p`,{class:`m-0 text-sm font-black text-rose-900`,children:`현재 설정을 백업 파일의 내용으로 덮어쓰시겠습니까?`}),z(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-rose-700`,children:`LAN 주소, Wi-Fi, 관리자 접속 정보가 바뀌어 현재 연결이 끊길 수 있습니다. 복원 직후 공유기가 자동으로 재부팅됩니다. 같은 장치와 호환되는 백업만 사용해 주세요.`}),z(`div`,{class:`mt-4 flex flex-col gap-3 sm:flex-row sm:flex-wrap`,children:[z(`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:`취소`}),z(`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`?`설정 복원 중`:`설정 복원 및 재부팅`})]})]}):z(`div`,{class:`mt-4 flex flex-col gap-3 sm:flex-row sm:flex-wrap`,children:[z(`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`?`파일 삭제 중`:`파일 삭제`}),z(`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:`복원 준비`})]})]}):z(v,{children:[z(`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`}),z(`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:[z(`span`,{class:`min-w-0 truncate`,children:n?.name??`복원할 백업 파일 선택`}),z(`span`,{class:`shrink-0 text-xs font-extrabold text-teal-700`,children:`찾아보기`})]}),n&&z(`div`,{class:`mt-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[z(`span`,{class:`text-xs font-bold text-slate-500`,children:Wa(n.size)}),z(`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 wv(e){return z(pv,{description:`공유기 재부팅과 SmartSafeHub에서 제공하지 않는 고급 관리 도구를 한곳에서 제공합니다.`,...e.className===void 0?{}:{className:e.className},icon:z(It,{class:`size-5`}),title:`시스템 도구`,children:z(`div`,{class:`space-y-3`,children:[z(`section`,{"aria-labelledby":`router-reboot-heading`,class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[z(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[z(`div`,{class:`flex min-w-0 items-start gap-3`,children:[z(`span`,{class:`flex size-9 shrink-0 items-center justify-center rounded-lg bg-rose-50 text-rose-700`,children:z(Wt,{class:`size-4`})}),z(`div`,{class:`min-w-0`,children:[z(`p`,{class:`m-0 text-sm font-black text-rose-950`,id:`router-reboot-heading`,children:`공유기 재부팅`}),z(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`재부팅 중에는 인터넷과 Wi-Fi 연결이 잠시 중단됩니다.`})]})]}),!e.confirmingReboot&&z(`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&&z(`div`,{class:`mt-3 rounded-xl border border-rose-200 bg-rose-50 p-3`,children:[z(`p`,{class:`m-0 text-xs font-extrabold leading-5 text-rose-900`,children:`지금 공유기를 재부팅하시겠습니까? 저장되지 않은 LuCI 설정이 있다면 먼저 저장해 주세요.`}),z(`div`,{class:`mt-3 flex flex-col gap-2 sm:flex-row sm:flex-wrap`,children:[z(`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:`취소`}),z(`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`?`재부팅 요청 중`:`지금 재부팅`})]})]})]}),z(`section`,{"aria-labelledby":`advanced-tools-heading`,class:`rounded-xl border border-slate-200 bg-slate-50 p-4`,children:[z(`div`,{class:`min-w-0`,children:[z(`p`,{class:`m-0 text-sm font-black text-slate-900`,id:`advanced-tools-heading`,children:`고급 도구`}),z(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`SmartSafeHub에서 아직 제공하지 않는 상세 시스템 설정이나 원본 로그가 필요할 때 사용합니다.`})]}),z(`div`,{class:`mt-3 flex flex-col gap-2 sm:flex-row sm:flex-wrap`,children:[z(`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 고급 설정 열기`}),z(`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 Tv=[{value:`mon`,label:`월요일`},{value:`tue`,label:`화요일`},{value:`wed`,label:`수요일`},{value:`thu`,label:`목요일`},{value:`fri`,label:`금요일`},{value:`sat`,label:`토요일`},{value:`sun`,label:`일요일`}];function Ev(e){let[t,n]=M(!1),[r,i]=M(`weekly`),[a,o]=M(`sun`),[s,c]=M(`04:00`);N(()=>{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=Tv.find(e=>e.value===a),d=t?r===`daily`?`매일 ${s}`:`매주 ${u?.label??`일요일`} ${s}`:`예약 재부팅 꺼짐`;return z(`section`,{"aria-labelledby":`scheduled-reboot-heading`,class:`min-w-0`,children:[z(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[z(`div`,{class:`flex min-w-0 items-start gap-3`,children:[z(`span`,{class:`flex size-11 shrink-0 items-center justify-center rounded-xl bg-slate-100 text-slate-600`,children:z(Zt,{class:`size-5`})}),z(`div`,{class:`min-w-0`,children:[z(`h2`,{class:`m-0 text-lg font-black text-slate-950`,id:`scheduled-reboot-heading`,children:`예약 재부팅`}),z(`p`,{class:`mt-1.5 mb-0 text-sm leading-6 text-slate-500`,children:`공유기를 정해진 시간에 자동으로 재부팅합니다. 기본값은 꺼짐이며 필요한 장치에서만 사용하세요.`})]})]}),z(`div`,{class:`flex shrink-0 flex-wrap items-center justify-between gap-3 sm:justify-end`,children:[l&&z(`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:[z(U,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}),z(`span`,{class:`text-xs font-extrabold text-slate-500`,children:t?`사용 중`:`꺼짐`}),z(`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:z(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${t?`left-6`:`left-1`}`})})]})]}),(e.saveError||e.saveMessage)&&z(`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:[z(`span`,{class:`min-w-0`,children:e.saveError||e.saveMessage}),z(`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?z(`div`,{class:`mt-4 rounded-xl border border-rose-200 bg-rose-50 p-4`,children:[z(`p`,{class:`m-0 text-sm font-bold text-rose-800`,children:e.error}),z(`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:`다시 불러오기`})]}):z(v,{children:[z(`div`,{class:`mt-4 grid min-w-0 grid-cols-1 gap-4 md:grid-cols-3`,children:[z(`div`,{children:[z(`label`,{class:`mb-2 block text-sm font-extrabold text-slate-800`,for:`scheduled-reboot-frequency`,children:`주기`}),z(Vg,{ariaLabel:`예약 재부팅 주기`,disabled:!t||e.saving||!e.data,id:`scheduled-reboot-frequency`,onChange:e=>i(e),options:[{value:`weekly`,label:`매주`},{value:`daily`,label:`매일`}],value:r})]}),z(`div`,{children:[z(`label`,{class:`mb-2 block text-sm font-extrabold text-slate-800`,for:`scheduled-reboot-day`,children:`요일`}),z(Vg,{ariaLabel:`예약 재부팅 요일`,disabled:!t||r!==`weekly`||e.saving||!e.data,id:`scheduled-reboot-day`,onChange:e=>o(e),options:Tv.map(e=>({value:e.value,label:e.label})),value:a})]}),z(`div`,{children:[z(`label`,{class:`mb-2 block text-sm font-extrabold text-slate-800`,for:`scheduled-reboot-time-hour`,children:`재부팅 시간`}),z(dv,{ariaLabel:`예약 재부팅 시간`,disabled:!t||e.saving||!e.data,id:`scheduled-reboot-time`,onChange:c,value:s})]})]}),z(`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:[z(`div`,{class:`min-w-0`,children:[z(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.12em] text-slate-500`,children:`예약 일정`}),z(`p`,{class:`mt-1.5 mb-0 text-sm font-extrabold text-slate-900`,children:d}),z(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:[e.data?.timezone??`장치 시간대`,` 기준 · 업데이트 작업 중이면 15분 단위로 최대 2시간 연기합니다.`]})]}),z(`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 Dv({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:ee,timeSyncing:te,onBackupDiscard:ne,onBackupDownload:re,onBackupRestore:ie,onBackupUpload:ae,onDismissBackupFeedback:oe,onDismissFeedback:se,onDismissHealthFeedback:ce,onDismissTimeFeedback:le,onDownloadDiagnostics:ue,onRunHealth:de,onSetHealthReporter:fe,onReboot:pe,onRetry:me,onDismissScheduledRebootFeedback:he,onSaveScheduledReboot:ge,onSaveTimezone:_e,onSyncTime:ve,onAdministratorPasswordChanged:ye}){let[xe,Se]=M(!1),Ce=be(`/admin/system`),we=be(`/admin/status/logs`);return z(`section`,{class:`min-w-0 space-y-7`,children:[(u||d)&&z(`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:[z(`span`,{class:`min-w-0`,children:u||d}),!b&&z(`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:se,type:`button`,children:`닫기`})]}),z(Dr,{active:e,label:`설정 메뉴`,tabs:[{id:`diagnostics`,label:`진단 및 지원`,href:`#settings`},{id:`time`,label:`시간 및 예약`,href:`#settings/time`},{id:`management`,label:`관리 및 복구`,href:`#settings/management`}]}),z(`section`,{"aria-labelledby":`diagnostics-tab`,class:`min-w-0`,hidden:e!==`diagnostics`,id:`diagnostics-panel`,role:`tabpanel`,children:[l&&!c?z(`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:[z(`p`,{class:`m-0`,children:[`시스템 정보를 불러오지 못해 진단 파일 다운로드가 제한될 수 있습니다. `,l]}),z(`button`,{class:`mt-2 rounded-lg border border-current px-3 py-1.5 text-xs font-extrabold`,onClick:me,type:`button`,children:`다시 시도`})]}):y&&!c?z(`p`,{class:`mb-4 text-sm font-semibold text-slate-500`,role:`status`,children:`진단 파일에 필요한 시스템 정보를 확인하고 있습니다.`}):null,z(q,{eyebrow:`Device health`,title:`진단 및 지원`,description:`장치 진단과 원격 상태 보고를 확인하고 개인정보 보호 정책을 관리합니다.`}),z(`div`,{class:`grid min-w-0 grid-cols-1 gap-4 lg:grid-cols-2`,children:z(yv,{actionError:p,actionMessage:m,data:f,downloadBusy:t===`diagnostics`,error:h,loading:g,onDismissFeedback:ce,onDownload:ue,onRetry:me,onRun:de,onSetReporter:fe,running:_,savingReporter:v})})]}),z(`section`,{"aria-labelledby":`time-tab`,class:`min-w-0`,hidden:e!==`time`,id:`time-panel`,role:`tabpanel`,children:[z(q,{eyebrow:`Device settings`,title:`시간 및 예약`,description:`시간 기준과 예약 재부팅을 SmartSafeHub에서 직접 관리합니다.`}),z(`div`,{class:`grid min-w-0 grid-cols-1 gap-4 lg:grid-cols-2`,children:z(mv,{data:D,error:O,loading:k,onDismissFeedback:le,onDismissScheduledRebootFeedback:he,onRetry:me,onSave:_e,onSaveScheduledReboot:ge,onSync:ve,saveError:A,saveMessage:j,saving:ee,scheduledRebootData:x,scheduledRebootError:S,scheduledRebootLoading:C,scheduledRebootSaveError:w,scheduledRebootSaveMessage:T,scheduledRebootSaving:E,syncing:te})})]}),z(`section`,{"aria-labelledby":`management-tab`,class:`min-w-0`,hidden:e!==`management`,id:`management-panel`,role:`tabpanel`,children:[z(q,{eyebrow:`System management`,title:`시스템 관리`,description:`관리자 비밀번호, 설정 백업·복원과 공유기 시스템 도구를 한곳에서 관리합니다.`}),z(`div`,{class:`grid min-w-0 grid-cols-1 gap-4 lg:grid-cols-2`,children:[z(Sv,{onChanged:ye}),z(Cv,{action:n,error:r,message:i,onDiscard:ne,onDismissFeedback:oe,onDownload:re,onRestore:ie,onUpload:ae,restoreAccepted:a,uploadProgress:o,validated:s}),z(wv,{action:t,className:`lg:col-span-2`,advancedSystemUrl:Ce,confirmingReboot:xe,logsUrl:we,onCancelReboot:()=>Se(!1),onConfirmReboot:()=>Se(!0),onReboot:pe,rebootAccepted:b})]})]})]})}var Ov=`https://www.smartsafehub.com/dashboard/devices/`;async function kv(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 Av(e){return(e||`free`).toUpperCase()}function jv(e){return e===`FREE`?`free`:e===`PRO`?`pro`:e===`ULTIMATE`?`ultimate`:`paid`}function Mv({plan:e}){let t=jv(e),n=t!==`free`;return z(`span`,{class:`ssh-safeshield-plan-badge`,"data-compact":`false`,"data-tier":t,title:n?`${e} 멤버십`:`무료 플랜`,children:[z(`span`,{"aria-hidden":`true`,class:`ssh-safeshield-plan-badge-mark`,children:n?`✦`:`•`}),z(`span`,{children:e})]})}function Nv(e){if(!e.pairingCode||!e.pairingExpiresAt)return!1;let t=Date.parse(e.pairingExpiresAt);return Number.isFinite(t)&&t<=Date.now()}function Pv({data:e,error:t,loading:n,pairingBusy:r,refreshing:i,syncError:a,onRequestPairing:o,onRetry:s}){let[c,l]=M(null),[u,d]=M(!1);if(N(()=>{if(!u)return;let e=window.setTimeout(()=>d(!1),1800);return()=>window.clearTimeout(e)},[u]),n&&!e)return z(bo,{});if(t&&!e)return z(xo,{message:t,onRetry:s});let f=e,p=f?.accountRegistered===!0,m=Av(f?.plan??null),h=f?Nv(f):!1,g=!p&&!h?f?.pairingCode:null;async function _(){l(null),d(!1);try{await o()}catch{l(`계정 연결 코드를 발급하지 못했습니다. 잠시 후 다시 시도해 주세요.`)}}async function v(){if(g)try{await kv(g),l(null),d(!0)}catch{d(!1),l(`연결 코드를 복사하지 못했습니다. 코드를 직접 선택해 복사해 주세요.`)}}return z(`div`,{class:`min-w-0`,children:[z(q,{eyebrow:`Account connection`,title:`기기 계정 연결`,description:`계정에 연결하면 웹사이트에서 기기를 관리하고 Cloud 기능과 구독 권한을 사용할 수 있습니다.`}),z(`div`,{class:`grid items-start gap-5 xl:items-stretch xl:grid-cols-[minmax(0,1.2fr)_minmax(18rem,0.8fr)]`,children:[z(`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:p?z(`div`,{children:[z(`div`,{class:`flex items-center gap-3`,children:[z(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-full bg-emerald-100 text-emerald-700`,children:z(H,{class:`size-5`})}),z(`div`,{children:[z(`p`,{class:`m-0 text-base font-black text-slate-950`,children:`SmartSafeHub 계정에 연결됨`}),z(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`이 기기가 계정과 정상적으로 연결되어 있습니다. 연결 코드는 더 이상 필요하지 않습니다.`})]})]}),z(`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:Ov,rel:`noopener noreferrer`,target:`_blank`,children:`웹사이트에서 기기 관리`}),z(`p`,{class:`mt-4 mb-0 border-t border-slate-100 pt-4 text-xs font-semibold leading-5 text-slate-500`,children:`웹사이트에서 기기 등록을 해제하면 공유기도 다음 계정 상태 동기화에서 자동으로 연결 해제를 반영합니다.`})]}):z(`div`,{children:[z(`div`,{children:[z(`div`,{class:`flex flex-wrap items-center justify-between gap-2`,children:[z(`p`,{class:`m-0 text-sm font-black text-slate-900`,children:`계정 연결 코드`}),z(`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:[z(Ht,{class:`size-3.5`}),`10분 동안 유효`]})]}),z(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-500`,children:`코드를 발급한 뒤 smartsafehub.com에 로그인하여 기기를 연결하세요.`})]}),g?z(`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:[z(`div`,{class:`min-w-0`,children:[z(`span`,{class:`block text-[0.7rem] font-black text-teal-700`,children:`연결 코드`}),z(`code`,{class:`mt-1 block select-all break-all font-mono text-2xl font-black tracking-[0.14em] text-slate-950`,children:g})]}),z(`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 v(),type:`button`,children:[z(u?H:Jt,{class:`size-4`}),z(`span`,{"aria-live":`polite`,children:u?`복사됨`:`복사`})]})]}):h?z(`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,z(`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:r,onClick:()=>void _(),type:`button`,children:[r?z(V,{class:`size-4 animate-spin`}):null,r?`발급 중…`:g?`새 코드 발급`:`연결 코드 발급`]}),c?z(`p`,{class:`mt-3 mb-0 text-sm font-semibold text-red-700`,children:c}):null]})}),z(`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:[z(`p`,{class:`m-0 text-[0.68rem] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Device status`}),z(`h2`,{class:`mt-2 mb-0 text-lg font-black tracking-tight text-slate-950`,children:`연결 상태`}),z(`dl`,{class:`mt-5 mb-0 grid gap-4`,children:[z(`div`,{class:`flex items-center justify-between gap-4 border-b border-slate-100 pb-4`,children:[z(`dt`,{class:`text-sm font-bold text-slate-500`,children:`계정`}),z(`dd`,{class:`m-0 text-sm font-black ${p?`text-emerald-700`:`text-slate-700`}`,children:p?`연결됨`:`연결 필요`})]}),z(`div`,{class:`flex items-center justify-between gap-4 border-b border-slate-100 pb-4`,children:[z(`dt`,{class:`text-sm font-bold text-slate-500`,children:`요금제`}),z(`dd`,{class:`m-0 flex min-w-0 flex-wrap items-center justify-end gap-2`,children:[z(Mv,{plan:m}),m===`FREE`?z(`span`,{class:`ssh-safeshield-plan-caption`,children:`기본 플랜`}):null]})]}),z(`div`,{class:`flex items-center justify-between gap-4`,children:[z(`dt`,{class:`text-sm font-bold text-slate-500`,children:`마지막 확인`}),z(`dd`,{class:`m-0 text-sm font-black text-slate-700`,children:Ja(f?.lastSuccessAt??0)})]})]}),i?z(`p`,{class:`mt-5 mb-0 text-xs font-bold text-teal-700`,children:`최신 상태를 확인하고 있습니다…`}):null,a?p?z(`p`,{class:`mt-5 mb-0 text-xs font-semibold leading-5 text-slate-500`,children:`최신 상태 확인이 지연되고 있습니다. 마지막으로 확인된 연결 상태를 표시합니다.`}):z(`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 Fv(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 Lv(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 Rv(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 zv(e){return e.phase===`checking`||e.phase===`downloading`||e.phase===`verifying`||e.phase===`flashing`?z(V,{class:`size-3.5 animate-spin`}):e.phase===`error`?z(U,{class:`size-3.5`}):e.phase===`ready`||e.updateAvailable?z(Gt,{class:`size-3.5`}):e.lastCheckAt?z(H,{class:`size-3.5`}):z(Ht,{class:`size-3.5`})}function Bv({checked:e,disabled:t,onChange:n}){return z(`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:z(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${e?`left-6`:`left-1`}`})})}function Vv({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=P(null),[_,y]=M(null),[b,x]=M(!0),[S,C]=M(!1);N(()=>{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=Fv(k,A),ee=async()=>{_&&await h(_)&&(y(null),g.current&&(g.current.value=``))};return z(`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:[z(`div`,{class:`p-5 sm:p-6`,children:[z(`div`,{class:`flex flex-col gap-5 lg:flex-row lg:items-start lg:justify-between`,children:[z(`div`,{class:`flex min-w-0 gap-4`,children:[z(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-sky-50 text-sky-700`,children:z(jt,{class:`size-6`})}),z(`div`,{class:`min-w-0`,children:[z(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-sky-700`,children:`Firmware update`}),z(`h2`,{class:`mt-2 mb-0 text-xl font-black text-slate-950 sm:text-2xl`,children:`펌웨어 업데이트`}),z(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500 lg:whitespace-nowrap`,children:`시스템 펌웨어를 확인하고 설치합니다. 온라인·수동 설치는 안전성 검증 후 진행됩니다.`})]})]}),n?z(`div`,{class:`flex shrink-0 flex-col gap-2 sm:flex-row lg:justify-end`,children:[z(`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 ${Rv(n)}`,children:[zv(n),Lv(n)]}),z(`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:[z(V,{class:`size-4 ${E?`animate-spin`:``}`}),E?`확인 중...`:`펌웨어 확인`]}),n.updateAvailable&&!T?z(`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?z(V,{class:`size-4 animate-spin`}):z(Gt,{class:`size-4`}),D?`준비 중...`:`다운로드 및 검증`]}):null]}):null]}),t||o?z(`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:[z(`span`,{children:t||o}),z(`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`?z(`div`,{class:`mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4 sm:p-5`,role:`status`,children:z(`div`,{class:`flex gap-3`,children:[z(`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:z(V,{class:`size-5 animate-spin`})}),z(`div`,{children:[z(`strong`,{class:`block text-sm font-black text-amber-950`,children:`펌웨어를 설치하고 재부팅하고 있습니다.`}),z(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:`전원을 끄지 마세요. 현재 주소에서 공유기가 다시 응답하면 SmartSafeHub가 자동으로 새로고침됩니다.`})]})]})}):null,i&&!n?z(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-800`,children:[z(`p`,{class:`m-0 font-bold`,children:i}),z(`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?z(`div`,{class:`mt-5 flex items-center gap-3 rounded-xl bg-slate-50 p-4 text-sm font-bold text-slate-500`,children:[z(V,{class:`size-4 animate-spin`}),`펌웨어 상태를 확인하고 있습니다.`]}):n?z(v,{children:[n.lastError?z(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 sm:p-5`,children:z(`div`,{class:`flex gap-3`,children:[z(`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:z(U,{class:`size-5`})}),z(`div`,{class:`min-w-0 flex-1`,children:[z(`strong`,{class:`block text-sm font-black text-rose-950`,children:`펌웨어 작업을 완료하지 못했습니다.`}),z(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-rose-800`,children:n.lastError.message}),z(`details`,{class:`mt-3 rounded-lg border border-rose-200 bg-white p-3`,children:[z(`summary`,{class:`cursor-pointer text-xs font-extrabold text-rose-800`,children:`상세 정보 보기`}),z(`p`,{class:`mt-2 mb-0 break-all font-mono text-[11px] text-slate-600`,children:n.lastError.code})]})]})]})}):null,n.current.metadataAvailable?null:z(`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:[z(`strong`,{class:`block font-extrabold`,children:`현재 펌웨어의 버전 정보를 완전히 확인할 수 없습니다.`}),z(`span`,{class:`mt-1 block text-amber-800`,children:`새 펌웨어를 설치하면 이후부터 정확한 설치 버전을 자동으로 확인할 수 있습니다.`}),z(`details`,{class:`mt-2`,children:[z(`summary`,{class:`cursor-pointer font-extrabold text-amber-800`,children:`기술 상세 보기`}),z(`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.`})]})]}),z(`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:[z(`div`,{class:`bg-white p-4`,children:[z(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Device`}),z(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black text-slate-950`,children:n.current.deviceCode||n.current.boardName||`미확인`})]}),z(`div`,{class:`bg-white p-4`,children:[z(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Current version`}),z(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black text-slate-950`,children:n.current.releaseVersion||`미확인`}),n.current.buildId?z(`span`,{class:`mt-1 block break-all text-[11px] leading-5 text-slate-400`,children:[`빌드 ID `,n.current.buildId]}):n.current.openwrtVersion?z(`span`,{class:`mt-1 block text-[11px] leading-5 text-slate-400`,children:[`OpenWrt `,n.current.openwrtVersion]}):null]}),z(`div`,{class:`bg-white p-4`,children:[z(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Available version`}),z(`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?z(`span`,{class:`mt-1 block break-all text-[11px] leading-5 text-slate-400`,children:[`빌드 ID `,n.release.buildId]}):null]}),z(`div`,{class:`bg-white p-4`,children:[z(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Last check`}),z(`dd`,{class:`mt-2 mb-0 ml-0 text-sm font-black text-slate-950`,children:z(P_,{fallback:`아직 없음`,timestamp:n.lastCheckAt})})]})]})]}):null,n?.updateAvailable&&n.release?z(`section`,{class:`mt-5 rounded-xl border border-sky-200 bg-sky-50/60 p-4 sm:p-5`,"data-section":`available-firmware`,children:[z(`div`,{class:`flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4`,children:[z(`div`,{children:[z(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-sky-700`,children:`Available firmware`}),z(`h3`,{class:`mt-2 mb-0 text-lg font-black text-slate-950`,children:`새 펌웨어가 있습니다.`}),z(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:[`펌웨어 `,n.release.version||`미확인`,` · `,Iv(n.release.sysupgrade.sizeBytes),` · `,(n.release.channel||n.settings.channel)===`beta`?`Beta`:`Stable`]})]}),z(`div`,{class:`flex shrink-0 flex-wrap items-center justify-start gap-2 sm:justify-end`,children:[z(`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?z(`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:[z(`span`,{class:`text-[11px] uppercase tracking-[0.12em] text-slate-500`,children:`관리 SW`}),z(`span`,{class:`text-slate-950`,children:k})]}):null]})]}),k?z(`div`,{class:`mt-4 rounded-xl border border-sky-100 bg-white p-4`,"data-section":`firmware-bundled-software`,children:[z(`div`,{class:`flex flex-wrap items-center justify-between gap-2`,children:[z(`span`,{class:`text-xs font-extrabold text-slate-800`,children:`포함된 관리 소프트웨어`}),z(`div`,{class:`flex flex-wrap items-center gap-2`,children:[z(`span`,{class:`text-sm font-black text-slate-950`,children:k}),j===`same`?z(`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]})]}),z(`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?z(`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)=>z(`li`,{children:e},`${t}-${e}`))}):null]}):null,T?z(`section`,{class:`mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4 sm:p-5`,"data-section":`prepared-firmware`,children:z(`div`,{class:`flex gap-4`,children:[z(`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:z(H,{class:`size-5`})}),z(`div`,{class:`min-w-0 flex-1`,children:[z(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-amber-700`,children:`Verified image`}),z(`h3`,{class:`mt-2 mb-0 text-lg font-black text-amber-950`,children:`설치할 펌웨어가 준비되었습니다.`}),z(`p`,{class:`mt-2 mb-0 break-all text-sm leading-6 text-amber-900`,children:[T.filename||`firmware.bin`,` · `,Iv(T.sizeBytes)]}),T.sha256?z(`p`,{class:`mt-2 mb-0 break-all font-mono text-[11px] leading-5 text-amber-800`,children:[`SHA-256 `,T.sha256]}):null,z(`div`,{class:`mt-5 flex items-start justify-between gap-4 rounded-xl border border-amber-200 bg-white p-4`,children:[z(`div`,{children:[z(`strong`,{class:`block text-sm font-black text-slate-950`,children:`현재 설정 유지`}),z(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`Wi-Fi, SafeShield, SmartSafeHub 설정을 유지합니다.`}),T.allowBackup?null:z(`span`,{class:`mt-2 block text-xs font-bold text-rose-700`,children:`이 이미지는 설정 유지 업그레이드를 지원하지 않습니다.`})]}),z(Bv,{checked:b,disabled:!T.allowBackup,onChange:x})]}),S?z(`div`,{class:`mt-4 rounded-xl border border-rose-200 bg-rose-50 p-4`,children:[z(`strong`,{class:`block text-sm font-black text-rose-950`,children:`펌웨어 설치를 시작할까요?`}),z(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-rose-800`,children:`설치 중 전원을 끄면 장치가 복구 불가능한 상태가 될 수 있습니다. 이미지 검증은 설치 직전에 다시 수행됩니다.`}),z(`div`,{class:`mt-4 flex flex-wrap justify-end gap-2`,children:[z(`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:`취소`}),z(`button`,{class:`min-h-10 rounded-lg bg-rose-700 px-3 py-2 text-xs font-extrabold text-white`,onClick:()=>{C(!1),f(b)},type:`button`,children:`설치 시작`})]})]}):z(`div`,{class:`mt-4 flex flex-wrap justify-end gap-2`,children:[z(`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:`파일 삭제`}),z(`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:[z(Ft,{class:`size-4`}),`펌웨어 설치`]})]})]})]})}):null]}),z(`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:[z(`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:[z(`div`,{class:`flex min-w-0 items-center gap-3`,children:[z(`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:z(Gt,{class:`size-5 rotate-180`})}),z(`div`,{class:`min-w-0`,children:[z(`strong`,{class:`block text-sm font-black text-slate-950`,children:`수동 펌웨어 설치`}),z(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`펌웨어 파일(.bin)을 직접 업로드하여 검증한 뒤 설치합니다.`})]})]}),z(`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:`열기`}),z(`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:`닫기`})]}),z(`div`,{class:`border-t border-slate-200 px-5 pb-5 pt-3 sm:px-6`,children:[z(`p`,{class:`mt-0 mb-4 text-xs leading-5 text-slate-500 lg:whitespace-nowrap`,children:"이 장치에 맞는 Sysupgrade 이미지(.bin)만 사용하세요. 업로드한 파일은 이미지 검증과 `sysupgrade --test`를 모두 통과해야 설치할 수 있으며 강제 설치는 제공하지 않습니다."}),z(`div`,{class:`rounded-xl border border-slate-200 bg-white p-4 sm:p-5`,"data-layout":`firmware-file-picker`,children:[z(`input`,{accept:`.bin,application/octet-stream`,"aria-label":`펌웨어 파일 선택`,class:`sr-only`,disabled:w||T!==null,onChange:e=>y(e.currentTarget.files?.[0]??null),ref:g,type:`file`}),z(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between`,children:[z(`div`,{class:`flex min-w-0 items-center gap-3`,children:[z(`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:z(Gt,{class:`size-5 rotate-180`})}),z(`div`,{class:`min-w-0`,children:[z(`strong`,{class:`block truncate text-sm font-black text-slate-900`,children:_?_.name:`펌웨어 파일을 선택하세요`}),z(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:_?`${Iv(_.size)} · Sysupgrade 이미지 (.bin)`:`이 장치에 맞는 Sysupgrade 이미지 (.bin)를 선택합니다.`})]})]}),z(`div`,{class:`flex shrink-0 flex-wrap gap-2 sm:justify-end`,children:[z(`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:_?`다른 파일 선택`:`파일 선택`}),z(`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 ee(),type:`button`,children:[O?z(V,{class:`size-4 animate-spin`}):z(Gt,{class:`size-4 rotate-180`}),O?`처리 중...`:`업로드 및 검증`]})]})]}),c===null?null:z(`div`,{class:`mt-4 border-t border-slate-100 pt-4`,children:[z(`div`,{class:`mb-2 flex items-center justify-between gap-3 text-xs font-bold text-slate-600`,children:[z(`span`,{children:`업로드 중`}),z(`span`,{children:[c,`%`]})]}),z(`div`,{class:`h-2 overflow-hidden rounded-full bg-slate-200`,children:z(`div`,{class:`h-full rounded-full bg-sky-600 transition-[width]`,style:{width:`${Math.max(0,Math.min(100,c))}%`}})})]})]})]})]})]})}var Hv=[{value:3600,label:`1시간마다`},{value:21600,label:`6시간마다`},{value:43200,label:`12시간마다`},{value:86400,label:`24시간마다`}],Uv=`luci-app-smartsafehub`;function Wv(e){return e?new Date(e*1e3).toLocaleString(`ko-KR`,{dateStyle:`medium`,timeStyle:`short`}):`아직 없음`}function Gv(e){return e===`stable`?`Stable`:e===`beta`?`Beta`:`미확인`}function Kv(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 qv(e){return e.code===`UPDATES_INDEX_REFRESH_FAILED`?{title:`패키지 저장소를 확인하지 못했습니다.`,description:`OpenWrt 패키지 저장소 동기화 또는 네트워크 상태가 일시적으로 불안정할 수 있습니다. 잠시 후 업데이트 확인을 다시 실행해 주세요.`}:e.code===`UPDATES_INSTALL_FAILED`?{title:`관리 소프트웨어 업데이트 설치에 실패했습니다.`,description:`패키지 설치가 완료되지 않았습니다. 네트워크 연결과 저장 공간을 확인한 뒤 다시 시도해 주세요.`}:{title:`최근 업데이트 작업을 완료하지 못했습니다.`,description:`잠시 후 다시 시도해 주세요. 문제가 반복되면 상세 정보를 확인해 주세요.`}}function Jv(e,t){return e.phase===`checking`?`업데이트 확인 중`:e.phase===`installing`?`업데이트 설치 중`:e.phase===`error`?`확인 필요`:e.updateCount>0?`업데이트 가능`:t?`업데이트 확인 지연`:e.lastCheckAt?`최신 상태`:`확인 전`}function Yv(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 Xv(e,t){return e.phase===`checking`||e.phase===`installing`?z(V,{class:`size-3.5 shrink-0 animate-spin`}):e.phase===`error`?z(U,{class:`size-3.5 shrink-0`}):e.updateCount>0?z(Gt,{class:`size-3.5 shrink-0`}):t?z(U,{class:`size-3.5 shrink-0`}):e.lastCheckAt?z(H,{class:`size-3.5 shrink-0`}):z(Ht,{class:`size-3.5 shrink-0`})}function Zv({checked:e,disabled:t=!1,label:n,onChange:r}){return z(`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:z(`span`,{"aria-hidden":`true`,class:`ssh-switch-thumb absolute top-1 shadow-sm transition-[left] ${e?`left-6`:`left-1`}`})})}function Qv({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]=M(!0),[m,h]=M(21600),[g,_]=M(!1),[y,b]=M(`03:00`),[x,S]=M(!1),[C,w]=M(!1);N(()=>{n&&!x&&(p(n.settings.checkEnabled),h(n.settings.checkIntervalSeconds),_(n.settings.autoInstall),b(n.settings.autoInstallTime))},[n,x]);let T=e===`check`||n?.phase===`checking`,E=e===`install`||n?.phase===`installing`,D=T||E,O=ug(n),k=n?.phase===`error`&&n.lastError?.code===`UPDATES_INDEX_REFRESH_FAILED`,A=n?.lastError?qv(n.lastError):null,j=n?.packages.find(e=>e.name===Uv)??n?.packages[0]??null,ee=r?.updateAvailable?r.release?.bundledSoftware?.smartsafehub:null,te=!(!n?.lastCheckAt||n.updateCount!==0||O||j?.name!==Uv)&&Fv(ee,j?.installedVersion)===`same`,ne=(e={})=>n?(e.checkEnabled??f)!==n.settings.checkEnabled||(e.checkIntervalSeconds??m)!==n.settings.checkIntervalSeconds||(e.autoInstall??g)!==n.settings.autoInstall||(e.autoInstallTime??y)!==n.settings.autoInstallTime:!1,re=async()=>{await d({checkEnabled:f,checkIntervalSeconds:m,autoInstall:g,autoInstallTime:y})&&S(!1)};return z(`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:[z(`section`,{class:`min-w-0 xl:col-start-1 xl:row-start-1`,"data-section":`software-update-status`,children:[z(`div`,{class:`p-5 sm:p-6`,children:[z(`div`,{class:`flex flex-col gap-5 lg:flex-row lg:items-start lg:justify-between`,children:[z(`div`,{class:`flex min-w-0 gap-4`,children:[z(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-teal-50 text-teal-700`,children:z(Ft,{class:`size-6`})}),z(`div`,{class:`min-w-0`,children:[z(`div`,{class:`flex flex-wrap items-center gap-2`,children:[z(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700`,children:`Management software`}),n?z(`span`,{class:`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-extrabold ring-1 ring-inset ${Yv(n,O)}`,children:[z(`span`,{"aria-hidden":`true`,class:`flex items-center justify-center`,children:Xv(n,O)}),Jv(n,O)]}):null]}),z(`h2`,{class:`mt-2 mb-0 text-xl font-black text-slate-950 sm:text-2xl`,children:`관리 소프트웨어 업데이트`}),z(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500 xl:whitespace-nowrap`,children:`새 버전을 확인하고 안전하게 설치합니다.`})]})]}),n?z(`div`,{class:`flex shrink-0 flex-col gap-2 sm:flex-row lg:justify-end`,children:[z(`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:[z(V,{class:`size-4 ${T?`animate-spin`:``}`}),T?`확인 중...`:`업데이트 확인`]}),n.updateCount>0&&!C?z(`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?z(V,{class:`size-4 animate-spin`}):z(Gt,{class:`size-4`}),E?`설치 중...`:`업데이트 설치`]}):null]}):null]}),(t||o)&&z(`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:[z(`span`,{children:t||o}),z(`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?z(`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:[z(`div`,{class:`flex items-start gap-3`,children:[z(`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:z(V,{"aria-hidden":`true`,class:`size-5 animate-spin`})}),z(`div`,{class:`min-w-0`,children:[z(`h3`,{class:`m-0 text-sm font-black text-sky-950`,children:`관리 소프트웨어를 업데이트하고 있습니다.`}),z(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-sky-800`,children:`설치가 완료되면 이 화면이 자동으로 갱신됩니다. 작업 중에는 웹 연결이 잠시 끊길 수 있습니다.`})]})]}),z(`div`,{"aria-hidden":`true`,class:`ssh-update-progress-track mt-4`,children:z(`span`,{class:`ssh-update-progress-bar`})})]}):null,i&&!n?z(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-800`,children:[z(`p`,{class:`m-0 font-bold`,children:i}),z(`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?z(`div`,{class:`mt-5 flex items-center gap-3 rounded-xl bg-slate-50 p-4 text-sm font-bold text-slate-500`,children:[z(`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:z(V,{"aria-hidden":`true`,class:`size-4 animate-spin`})}),z(`span`,{children:`관리 소프트웨어 업데이트 상태를 확인하고 있습니다.`})]}):n?z(v,{children:[n.lastError&&A?z(`div`,{class:`mt-5 rounded-xl border border-rose-200 bg-rose-50 p-4 sm:p-5`,children:z(`div`,{class:`flex items-start gap-3`,children:[z(`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:z(U,{class:`size-5`})}),z(`div`,{class:`min-w-0 flex-1`,children:[z(`p`,{class:`m-0 text-sm font-extrabold text-rose-950`,children:A.title}),z(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-rose-800`,children:A.description}),n.lastError.at?z(`p`,{class:`mt-2 mb-0 text-xs font-bold text-rose-700`,children:[`발생 시각 `,Wv(n.lastError.at)]}):null,z(`details`,{class:`mt-3 rounded-lg border border-rose-200 bg-white p-3`,children:[z(`summary`,{class:`cursor-pointer text-xs font-extrabold text-rose-800`,children:`상세 정보 보기`}),z(`div`,{class:`mt-3 grid gap-2 text-xs leading-5 text-slate-600`,children:[z(`p`,{class:`m-0`,children:[`오류 코드 `,z(`strong`,{class:`font-black text-slate-900`,children:n.lastError.code})]}),z(`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?z(`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:[z(`div`,{class:`min-w-0`,children:[z(`strong`,{class:`block text-sm font-black text-amber-900`,children:`관리 소프트웨어를 지금 업데이트할까요?`}),z(`span`,{class:`mt-1 block text-xs leading-5 text-amber-800`,children:`설치 중 SmartSafeHub 웹 화면 연결이 잠시 끊길 수 있습니다.`})]}),z(`div`,{class:`flex shrink-0 gap-2`,children:[z(`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:`취소`}),z(`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?z(`div`,{class:`px-5 pb-5 sm:px-6 sm:pb-6`,children:[z(`h3`,{class:`mb-3 text-sm font-black text-slate-950`,children:`현재 상태`}),z(`dl`,{class:`grid overflow-hidden rounded-xl border border-slate-200 bg-slate-200 sm:grid-cols-2`,children:[z(`div`,{class:`bg-white p-4`,children:[z(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Installed`}),z(`dd`,{class:`mt-2 mb-0 ml-0 break-all text-sm font-black text-slate-950`,children:j?.installedVersion||`미확인`})]}),z(`div`,{class:`bg-white p-4`,children:[z(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Available`}),z(`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?`확인 필요`:`최신 버전`})]}),z(`div`,{class:`bg-white p-4`,children:[z(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Last check`}),z(`dd`,{class:`mt-2 mb-0 ml-0 text-sm font-black text-slate-950`,children:z(P_,{fallback:`아직 없음`,timestamp:n.lastCheckAt})})]}),z(`div`,{class:`bg-white p-4`,children:[z(`dt`,{class:`text-[11px] font-black uppercase tracking-[0.16em] text-slate-400`,children:`Auto install`}),z(`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?z(`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:z(`div`,{class:`flex gap-3`,children:[z(`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:z(U,{class:`size-5`})}),z(`div`,{children:[z(`h3`,{class:`m-0 text-base font-black text-amber-950`,children:`업데이트 확인이 지연되고 있습니다.`}),z(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:[`마지막 업데이트 확인: `,z(P_,{timestamp:n.lastCheckAt}),`. 자동 확인 상태를 점검하거나 업데이트 확인을 실행해 주세요.`]})]})]})}):z(`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:z(`div`,{class:`flex gap-3`,children:[z(`span`,{class:`grid size-10 shrink-0 place-items-center rounded-xl bg-white text-emerald-700`,children:z(H,{class:`size-5`})}),z(`div`,{children:[z(`h3`,{class:`m-0 text-base font-black text-emerald-950`,children:`최신 버전을 사용 중입니다.`}),z(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-emerald-800`,children:[`마지막 업데이트 확인: `,z(P_,{timestamp:n.lastCheckAt}),`.`]}),te?z(`p`,{class:`mt-2 mb-0 text-xs leading-5 text-emerald-800`,"data-section":`software-firmware-match`,children:[`새 펌웨어에도 동일한 관리 소프트웨어(`,ee,`)가 포함되어 있어 별도로 업데이트할 필요가 없습니다.`]}):null]})]})}):null,n&&n.updateCount===0&&!n.lastCheckAt?z(`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:z(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between`,children:[z(`div`,{class:`flex min-w-0 gap-3`,children:[z(`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:z(V,{class:`size-5`})}),z(`div`,{class:`min-w-0`,children:[z(`h3`,{class:`m-0 text-base font-black text-slate-900`,children:`업데이트 상태를 아직 확인하지 않았습니다.`}),z(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`업데이트 확인을 실행하면 현재 설치 버전과 사용 가능한 최신 버전을 확인합니다.`})]})]}),z(`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:[z(V,{class:`size-4`}),`지금 확인`]})]})}):null]}),n?z(v,{children:[z(`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:[z(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[z(`div`,{class:`flex min-w-0 gap-4`,children:[z(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-600`,children:z(It,{class:`size-5`})}),z(`div`,{class:`min-w-0`,children:[z(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`Settings`}),z(`h3`,{class:`mt-2 mb-0 text-lg font-black text-slate-950`,children:`자동 업데이트`}),z(`p`,{class:`mt-2 mb-0 max-w-3xl text-sm leading-6 text-slate-500`,children:`관리 소프트웨어의 자동 확인과 자동 설치를 설정합니다.`})]})]}),x?z(`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:[z(U,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}):null]}),z(`div`,{class:`ssh-software-settings-grid mt-4`,children:[z(`div`,{class:`ssh-software-settings-channel rounded-xl border border-slate-200 p-4 sm:p-5`,children:z(`div`,{class:`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`,children:[z(`div`,{class:`min-w-0`,children:[z(`strong`,{class:`block text-sm font-black text-slate-950`,children:`업데이트 채널`}),z(`span`,{class:`mt-1 block text-xs leading-5 text-slate-500`,children:`현재 관리 소프트웨어 업데이트를 확인하는 배포 채널입니다.`})]}),z(`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 ${Kv(n.settings.channel)}`,title:`현재 업데이트 확인 채널`,children:Gv(n.settings.channel)})]})}),z(`div`,{class:`min-w-0 overflow-hidden rounded-xl border border-slate-200 p-4 sm:p-5`,children:[z(`div`,{class:`flex min-w-0 items-start justify-between gap-4`,children:[z(`div`,{class:`min-w-0`,children:z(`strong`,{class:`block text-sm font-black text-slate-950`,children:`자동으로 업데이트 확인`})}),z(Zv,{checked:f,label:`자동 업데이트 확인`,onChange:e=>{p(e),S(ne({checkEnabled:e}))}})]}),z(`div`,{class:`mt-4 block`,children:[z(`span`,{class:`text-xs font-extrabold text-slate-700`,children:`확인 주기`}),z(Vg,{ariaLabel:`업데이트 확인 주기`,className:`mt-2`,disabled:!f,id:`software-update-check-interval`,onChange:e=>{let t=Number(e);h(t),S(ne({checkIntervalSeconds:t}))},options:Hv.map(e=>({value:String(e.value),label:e.label})),value:String(m),variant:`emphasized`})]})]}),z(`div`,{class:`min-w-0 overflow-hidden rounded-xl border border-slate-200 p-4 sm:p-5`,children:[z(`div`,{class:`flex min-w-0 items-start justify-between gap-4`,children:[z(`div`,{class:`min-w-0`,children:z(`strong`,{class:`block text-sm font-black text-slate-950`,children:`자동으로 업데이트 설치`})}),z(Zv,{checked:g,label:`자동 업데이트 설치`,onChange:e=>{_(e),S(ne({autoInstall:e}))}})]}),z(`div`,{class:`mt-4 min-w-0`,children:[z(`span`,{class:`text-xs font-extrabold text-slate-700`,children:`설치 시각`}),z(dv,{ariaLabel:`자동 업데이트 설치 시각`,className:`mt-2`,disabled:!g,id:`software-update-auto-install-time`,onChange:e=>{b(e),S(ne({autoInstallTime:e}))},value:y,variant:`emphasized`})]})]})]}),g?z(`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,z(`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:[z(`p`,{class:`m-0 text-xs leading-5 text-slate-500`,children:[`마지막 설치: `,Wv(n.lastInstallAt)]}),z(`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 re(),type:`button`,children:e===`settings`?`저장 중`:`자동 업데이트 설정 저장`})]})]}),n.updateCount>0?z(`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:[z(`div`,{class:`flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4`,children:[z(`div`,{children:[z(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700`,children:`Release notes`}),z(`h3`,{class:`mt-2 mb-0 text-lg font-black text-slate-950`,children:`업데이트 내용`}),z(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-500`,children:`현재 설치된 버전 이후 포함되는 주요 변경 사항입니다.`})]}),n.releaseNotes.length?z(`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?z(`div`,{class:`mt-5 grid gap-3`,children:[n.releaseNotes.map(e=>z(`article`,{class:`rounded-xl border border-slate-200 bg-slate-50 p-4 sm:p-5`,children:[z(`div`,{class:`flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-4`,children:[z(`strong`,{class:`break-all text-sm font-black text-slate-950`,children:e.version}),e.date?z(`span`,{class:`text-xs font-bold text-slate-500`,children:e.date}):null]}),e.summary?z(`p`,{class:`mt-3 mb-0 text-sm leading-6 text-slate-700`,children:e.summary}):null,e.sections.length?z(`details`,{class:`mt-4 rounded-lg border border-slate-200 bg-white p-3`,open:n.releaseNotes.length===1,children:[z(`summary`,{class:`cursor-pointer text-xs font-extrabold text-slate-800`,children:`자세한 변경 사항`}),z(`div`,{class:`mt-3 grid gap-4`,children:e.sections.map(t=>z(`section`,{children:[z(`h4`,{class:`m-0 text-xs font-extrabold text-slate-700`,children:t.title}),z(`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)=>z(`li`,{children:n},`${e.version}-${t.title}-${r}`))})]},`${e.version}-${t.title}`))})]}):null]},e.version)),n.releaseNotesComplete?null:z(`p`,{class:`m-0 rounded-xl bg-amber-50 px-4 py-3 text-xs font-bold leading-5 text-amber-800`,children:`일부 릴리즈 노트를 불러오지 못했습니다. 업데이트 확인과 설치는 계속 사용할 수 있습니다.`})]}):z(`p`,{class:`mt-4 mb-0 rounded-xl bg-slate-50 px-4 py-3 text-sm text-slate-500`,children:`릴리즈 노트를 불러오지 못했습니다. 업데이트 확인과 설치는 계속 사용할 수 있습니다.`})]}):null]}):null]})}function $v({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 z(`section`,{class:`min-w-0 space-y-5`,children:[z(`div`,{children:z(q,{eyebrow:`Firmware`,title:`시스템 펌웨어`,description:`공유기 운영체제의 새 버전을 확인하고 설치합니다.`})}),z(Vv,{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}),z(`div`,{class:`pt-2`,children:z(q,{eyebrow:`Software`,title:`관리 소프트웨어`,description:`SmartSafeHub 관리 화면 및 서비스의 업데이트를 확인합니다.`})}),z(Qv,{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 ey={};ey.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},ey.QRErrorCorrectLevel=(e,t)=>{e.exports={L:1,M:0,Q:3,H:2}},ey.QRMaskPattern=(e,t)=>{e.exports={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7}},ey.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}},ey.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(ny(`index`))(0,ny(`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 iy(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 ay(e){return`${(e.replace(/[\\/:*?"<>|\x00-\x1f\x7f]/g,`_`).replace(/[. ]+$/g,``).trim()||`Wi-Fi`).slice(0,64)}-QR.png`}function oy(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 sy({load:e,onClose:t}){let[n,r]=M(null),[i,a]=M(null),[o,s]=M(null);N(()=>{let t=!0;return e().then(e=>{if(t)try{r({ssid:e.ssid,matrix:ry(iy(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 z(`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:z(`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:[z(`h2`,{class:`m-0 text-xl font-extrabold`,children:`Wi-Fi QR 코드`}),z(`p`,{class:`mt-2 break-all text-sm font-semibold text-slate-600`,children:n?.ssid??`Wi-Fi 정보를 확인하는 중`}),i?z(`p`,{role:`alert`,class:`my-6 text-sm font-bold text-red-700`,children:i}):null,!n&&!i?z(`p`,{class:`my-6 text-sm`,children:`QR 코드를 만드는 중...`}):null,n?z(`div`,{class:`mx-auto my-5 w-full max-w-[288px] rounded-xl border border-slate-200 bg-white p-3`,children:z(`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:[z(`rect`,{width:c+8,height:c+8,fill:`white`}),n.matrix.map((e,t)=>e.map((e,n)=>e?z(`rect`,{x:n+4,y:t+4,width:`1`,height:`1`,fill:`black`},`${t}-${n}`):null))]})}):null,z(`p`,{class:`text-xs leading-5 text-slate-500`,children:[z(`span`,{class:`block`,children:`카메라로 스캔해 Wi-Fi에 연결하세요.`}),z(`span`,{class:`block`,children:`QR 코드에는 비밀번호가 포함됩니다.`})]}),o?z(`p`,{role:`alert`,class:`mt-3 text-sm font-bold text-red-700`,children:o}):null,n?z(`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`);oy(n.matrix,r,8);let i=document.createElement(`a`);i.href=t.toDataURL(`image/png`),i.download=ay(n.ssid),i.click()}catch{s(`QR 코드 이미지를 저장하지 못했습니다. 다시 시도해 주세요.`)}}},type:`button`,children:[z(Gt,{class:`size-5`}),`QR 코드 다운로드`]}):null,z(`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 cy({guest:e,busy:t,saving:n,onUpdate:r,onLoadQr:i}){let[a,o]=M(!1),[s,c]=M(!1),[l,u]=M(e.ssid),[d,f]=M(``),[p,m]=M(e.enabled),[h,g]=M(null);if(N(()=>{u(e.ssid),m(e.enabled),f(``),g(null)},[e]),!e.supported)return z(`section`,{class:`mt-5 rounded-2xl border border-slate-200 bg-white p-5 sm:p-6`,children:[z(`h2`,{class:`m-0 text-lg font-extrabold text-slate-900`,children:`게스트 Wi-Fi`}),z(`p`,{class:`mt-2 mb-0 text-sm text-slate-600`,children:`2.4GHz 기본 Wi-Fi 장치를 찾지 못해 게스트 Wi-Fi를 사용할 수 없습니다.`})]});let _=l.trim()!==e.ssid||p!==e.enabled||d.length>0,y=t||_||!e.enabled||!e.passwordConfigured,b=async t=>{t.preventDefault();let n=l.trim(),i=new TextEncoder().encode(n).length;i<1||i>32?g(`Wi-Fi 이름은 UTF-8 기준 1~32바이트로 입력해 주세요.`):d&&!(d.length>=8&&d.length<=63||/^[0-9a-fA-F]{64}$/.test(d))?g(`비밀번호는 8~63자 또는 64자리 16진수여야 합니다.`):p&&!e.passwordConfigured&&!d?g(`게스트 전용 비밀번호를 입력해 주세요.`):window.confirm(`게스트 Wi-Fi 설정을 적용하면 무선 연결이 잠시 끊어질 수 있습니다. 계속하시겠습니까?`)&&(g(null),await r({ssid:n,enabled:p,password:d})&&f(``))};return z(v,{children:[z(`form`,{"aria-label":`게스트 Wi-Fi 설정`,class:`mt-5 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-6`,onSubmit:e=>void b(e),children:[z(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[z(`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:[z(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:z(jt,{class:`size-6`})}),z(`span`,{class:`min-w-0 flex-1`,children:[z(`span`,{class:`block text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:`2.4 GHz · 분리 네트워크`}),z(`span`,{class:`mt-2 block text-xl font-extrabold tracking-tight text-slate-950`,children:`게스트 Wi-Fi`}),z(`span`,{class:`mt-2 block break-all text-sm text-slate-600`,children:e.ssid})]}),z(an,{class:`mt-3 size-5 shrink-0 text-slate-500 transition-transform ${a?`rotate-180`:``}`})]}),z(`div`,{class:`flex flex-wrap items-center gap-2 sm:justify-end`,children:[_&&z(`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:[z(U,{class:`size-3.5`}),`저장되지 않음`]}),z(`button`,{"aria-label":`게스트 Wi-Fi QR 코드 보기`,class:`inline-flex min-h-10 items-center gap-2 rounded-xl border border-slate-300 bg-white px-3.5 py-2 text-sm font-extrabold text-slate-800 transition hover:border-teal-300 hover:text-teal-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-100 disabled:cursor-not-allowed disabled:opacity-50`,disabled:y,onClick:()=>c(!0),title:e.enabled?void 0:`게스트 Wi-Fi를 켜고 저장하면 QR 코드를 사용할 수 있습니다.`,type:`button`,children:[z(Rt,{class:`size-4 shrink-0`}),`QR 코드 보기`]}),z(`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?`사용 중`:`꺼짐`})]})]}),z(`div`,{class:a?``:`hidden`,id:`ssh-guest-settings`,children:[z(`p`,{class:`mt-5 mb-0 text-sm leading-6 text-slate-600`,children:`방문자는 인터넷만 이용하며 내부 네트워크와 공유기 관리 화면에는 접근할 수 없습니다.`}),z(`div`,{class:`mt-5 grid gap-5 lg:grid-cols-2`,children:[z(`label`,{class:`block`,children:[z(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`게스트 Wi-Fi 이름 (SSID)`}),z(`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})]}),z(`label`,{class:`block`,children:[z(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`게스트 Wi-Fi 비밀번호`}),z(`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}),z(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`WPA2/WPA3 혼합 · 기본 Wi-Fi와 다른 비밀번호를 권장합니다.`})]})]}),z(`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&&z(`span`,{class:`block text-xs text-slate-500`,children:[`게스트 게이트웨이: `,e.subnet]})]}),z(`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:[z(`label`,{class:`inline-flex min-h-11 items-center gap-3 text-sm font-extrabold text-slate-800`,children:[z(`input`,{checked:p,class:`size-5 accent-teal-700`,disabled:t,onChange:e=>m(e.currentTarget.checked),type:`checkbox`}),`게스트 Wi-Fi 사용`]}),z(`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:[z(H,{class:`size-5 ${n?`animate-pulse`:``}`}),n?`적용 중`:`설정 저장`]})]}),h&&z(`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&&z(sy,{load:i,onClose:()=>c(!1)})]})}var ly=[{value:`sae-mixed`,label:`WPA2/WPA3 혼합 (권장)`},{value:`sae`,label:`WPA3-Personal`},{value:`psk2`,label:`WPA2-Personal`},{value:`none`,label:`암호화 없음`}];function uy(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 dy(e){return e.length>=8&&e.length<=63||/^[0-9a-fA-F]{64}$/.test(e)}function fy({network:e,busy:t,saving:n,onUpdate:r,onLoadQr:i}){let[a,o]=M(e.ssid),[s,c]=M(e.enabled),[l,u]=M(e.security===`custom`?`keep`:e.security),[d,f]=M(``),[p,m]=M(!1),h=F(()=>i(e.section),[i,e.section]),[g,_]=M(null);N(()=>{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})`}]:[],...ly],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&&!dy(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 z(`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:[z(`div`,{class:`flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between`,children:[z(`div`,{class:`flex min-w-0 items-start gap-3`,children:[z(`span`,{class:`grid size-11 shrink-0 place-items-center rounded-xl bg-teal-50 text-teal-700`,children:z(jt,{class:`size-6`})}),z(`div`,{class:`min-w-0`,children:[z(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500`,children:e.bandLabel}),z(`h2`,{class:`mt-2 mb-0 break-words text-xl font-extrabold tracking-tight text-slate-950`,children:e.ssid||`이름 없는 Wi-Fi`}),z(`p`,{class:`mt-2 mb-0 text-sm text-slate-600`,children:[`채널 `,e.channel??`자동`,` · 연결 기기 `,e.clientCount,`대`]})]})]}),z(`div`,{class:`flex flex-wrap items-center gap-2 sm:justify-end`,children:[x?z(`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:[z(U,{"aria-hidden":`true`,class:`size-3.5 shrink-0`}),`저장되지 않음`]}):null,z(`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:[z(Rt,{class:`size-4 shrink-0`}),`QR 코드 보기`]}),z(`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:[z(`span`,{class:`size-2 rounded-full ${e.runtimeUp?`bg-emerald-500`:`bg-slate-400`}`}),e.runtimeUp?`동작 중`:e.enabled?`시작 대기`:`꺼짐`]})]})]}),z(`div`,{class:`mt-6 grid gap-5 lg:grid-cols-2`,children:[z(`label`,{class:`block`,children:[z(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`Wi-Fi 이름 (SSID)`}),z(`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})]}),z(`div`,{class:`block`,children:[z(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`보안 방식`}),z(Vg,{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`}),z(`span`,{class:`mt-2 block text-xs text-slate-500`,children:[`현재: `,uy(e)]})]}),z(`label`,{class:`block lg:col-span-2`,children:[z(`span`,{class:`text-sm font-extrabold text-slate-800`,children:`새 비밀번호`}),z(`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}),z(`span`,{class:`mt-2 block text-xs text-slate-500`,children:`저장된 비밀번호는 일반 설정 조회에 노출되지 않습니다. QR 코드에는 비밀번호가 포함됩니다.`})]})]}),z(`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:[z(`label`,{class:`inline-flex min-h-11 items-center gap-3 text-sm font-extrabold text-slate-800`,children:[z(`input`,{checked:s,class:`size-5 accent-teal-700`,disabled:t,onChange:e=>c(e.currentTarget.checked),type:`checkbox`}),`이 Wi-Fi 사용`]}),z(`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:[z(H,{class:`size-5 ${n?`animate-pulse`:``}`}),n?`적용 중`:`설정 저장`]})]}),p?z(sy,{load:h,onClose:()=>m(!1)}):null,g?z(`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 py({feedback:e,onDismiss:t}){return z(`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:[z(`p`,{class:`m-0 font-bold leading-6`,children:e.message}),z(`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 my({data:e,error:t,feedback:n,loading:r,updatingSection:i,onDismissFeedback:a,onRetry:o,onUpdate:s,onUpdateGuest:c,onLoadQr:l,onLoadGuestQr:u}){return r?z(bo,{}):t?z(xo,{message:t,onRetry:o}):e?z(v,{children:[n?z(py,{feedback:n,onDismiss:a}):null,z(`section`,{class:`mb-5 rounded-2xl border border-amber-200 bg-amber-50 p-5 text-amber-950 sm:p-6`,children:[z(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-amber-700`,children:`연결 주의`}),z(`h2`,{class:`mt-2 mb-0 text-lg font-extrabold`,children:`Wi-Fi 이름이나 비밀번호를 바꾸면 현재 연결이 끊어집니다`}),z(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-amber-900/80`,children:`변경한 Wi-Fi 이름과 비밀번호로 다시 연결하면 SmartSafeHub를 계속 사용할 수 있습니다. VLAN 등 고급 무선 옵션은 기존 LuCI에서 관리합니다.`})]}),z(q,{eyebrow:`Wireless`,title:`기본 Wi-Fi`,description:`무선 네트워크의 이름, 비밀번호, 보안 방식과 QR 코드를 관리합니다.`}),e.networks.length?z(`section`,{"aria-label":`기본 Wi-Fi 설정`,class:`grid gap-5 xl:grid-cols-2`,children:e.networks.map(e=>z(fy,{busy:i!==null,network:e,onUpdate:s,onLoadQr:l,saving:i===e.section},e.section))}):z(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-5 text-center shadow-sm shadow-slate-900/5 sm:p-8`,children:[z(jt,{class:`mx-auto size-10 text-slate-400`}),z(`h2`,{class:`mt-4 mb-0 text-lg font-extrabold text-slate-950`,children:`관리할 기본 Wi-Fi를 찾지 못했습니다`}),z(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:`기존 LuCI에서 AP 모드의 무선 네트워크를 먼저 구성해 주세요.`})]}),e.guest&&z(v,{children:[z(`div`,{role:`separator`,"aria-label":`기본 Wi-Fi와 게스트 Wi-Fi 구분`,class:`mt-5 w-full border-t border-slate-200`}),z(`div`,{class:`mt-5`,children:z(q,{eyebrow:`Guest network`,title:`방문자용 네트워크`,description:`기본 네트워크와 분리된 게스트 Wi-Fi를 관리합니다.`})}),z(cy,{guest:e.guest,busy:i!==null,saving:i===`ssh_guest`,onUpdate:c,onLoadQr:u})]}),e.guestError&&z(`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 정보를 불러오지 못했습니다. 패키지를 설치한 직후라면 로그아웃 후 다시 로그인해 주세요.`,z(`button`,{class:`ml-3 underline font-bold`,onClick:o,type:`button`,children:`다시 시도`})]})]}):null}function hy({onAdministratorPasswordChanged:e}){let t=ii(),n=ci(),r=Br(),i=Ar(t===`activity`),a=xa(t===`home`||t===`settings`),o=ya(!0),s=ti(!0),c=yi(t===`network`),l=_i(t===`home`||t===`network`),u=hi(t===`iptv`),d=Ba(t===`wifi`),f=Hr(t===`home`,!1),p=Hr(t===`devices`),m=ma(t===`home`||t===`safeshield`),h=aa(t===`home`||t===`safeshield`,t===`safeshield`),g=m,_=h,v=ra(t===`rules`),y=Fa(t===`account`),b=La(t===`account`,y.data?.accountRegistered??null),x=Aa(a.data),S=mi(t===`home`||t===`settings`),C=Sa(t===`settings`),w=ja(t===`settings`);N(()=>{let e=fv();e&&Kn(e).then(e=>{e.applied&&t===`settings`&&w.refresh()}).catch(()=>{})},[]);let T=Ji(m.refresh,h.refresh),E=t===`activity`?i:t===`network`?c:t===`wifi`?d:t===`iptv`?u:t===`devices`?p:t===`safeshield`?m:t===`rules`?v:t===`account`?y:t===`system`?o:a,D;switch(t){case`network`:D=z(`div`,{class:`min-w-0`,children:[z(Dr,{active:n.network===`internet`?`internet`:`lan`,label:`네트워크 설정 메뉴`,tabs:[{id:`internet`,label:`인터넷 연결`,href:`#network`},{id:`lan`,label:`LAN 및 DHCP`,href:`#network/lan`}]}),z(`div`,{"aria-label":`인터넷 연결 설정`,"aria-labelledby":`internet-tab`,class:`min-w-0`,hidden:n.network!==`internet`,id:`internet-panel`,role:`tabpanel`,children:z(b_,{action:c.action,data:c.data,error:c.error,feedback:c.feedback,loading:c.loading,onDismissFeedback:c.dismissFeedback,onReconnect:()=>void c.reconnect(),onRetry:()=>void c.refresh(),onSave:c.save})}),z(`div`,{"aria-label":`내부 네트워크 설정`,"aria-labelledby":`lan-tab`,class:`min-w-0`,hidden:n.network!==`lan`,id:`lan-panel`,role:`tabpanel`,children:z(o_,{action:l.action,data:l.data,error:l.error,feedback:l.feedback,loading:l.loading,onApplyRecommendation:l.applyRecommendation,onDismissFeedback:l.dismissFeedback,onRetry:()=>void l.refresh(),onSave:l.save})})]});break;case`activity`:D=z(yo,{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`:D=z(my,{data:d.data,error:d.error,feedback:d.feedback,loading:d.loading,onDismissFeedback:d.dismissFeedback,onRetry:()=>void d.refresh(),onUpdate:d.update,onUpdateGuest:d.updateGuest,onLoadQr:lr,onLoadGuestQr:ur,updatingSection:d.updatingSection});break;case`iptv`:D=z(Gg,{data:u.data,error:u.error,feedback:u.feedback,loading:u.loading,onDismissFeedback:u.dismissFeedback,onRetry:()=>void u.refresh(),onSave:u.save,saving:u.saving});break;case`devices`:D=z(No,{data:p.data,error:p.error,loading:p.loading,onRetry:()=>void p.refresh()});break;case`account`:D=z(Pv,{data:y.data,error:y.error,loading:y.loading,pairingBusy:y.pairingBusy,refreshing:y.refreshing,syncError:y.syncError,onRequestPairing:y.requestPairing,onRetry:()=>void y.refresh()});break;case`system`:D=z($v,{action:o.action,actionError:o.actionError,data:o.data,error:o.error,firmware:s,loading:o.loading,message:o.message,onCheck:()=>void o.check(),onDismissFeedback:o.dismissFeedback,onInstall:()=>void o.install(),onRetry:()=>void o.refresh(),onSaveSettings:o.saveSettings});break;case`settings`:D=z(Dv,{activeTab:n.settings,action:x.action,backupAction:r.action,backupError:r.error,backupMessage:r.message,backupRestoreAccepted:r.restoreAccepted,backupUploadProgress:r.uploadProgress,backupValidated:r.validated,data:a.data,error:a.error,feedbackError:x.error,feedbackMessage:x.message,health:S.data,healthActionError:S.actionError,healthActionMessage:S.actionMessage,healthError:S.error,healthLoading:S.loading,healthRunning:S.running,healthSavingReporter:S.savingReporter,loading:a.loading,onBackupDiscard:r.discard,onBackupDownload:()=>void r.download(),onBackupRestore:r.restore,onBackupUpload:r.upload,onDismissBackupFeedback:r.dismissFeedback,onDismissFeedback:x.dismissFeedback,onDismissHealthFeedback:S.dismissActionFeedback,onDismissTimeFeedback:w.dismissSaveFeedback,onDownloadDiagnostics:()=>void x.downloadDiagnostics(),onRunHealth:()=>void S.runDiagnostic(),onSetHealthReporter:e=>void S.setReporterEnabled(e),onReboot:()=>void x.reboot(),onRetry:()=>void Promise.all([a.refresh(),s.refresh(),w.refresh(),C.refresh(),S.refresh()]),onDismissScheduledRebootFeedback:C.dismissSaveFeedback,onSaveScheduledReboot:C.saveSettings,onSaveTimezone:w.saveTimezone,onSyncTime:w.syncTime,onAdministratorPasswordChanged:e,rebootAccepted:x.rebootAccepted,scheduledRebootData:C.data,scheduledRebootError:C.error,scheduledRebootLoading:C.loading,scheduledRebootSaveError:C.saveError,scheduledRebootSaveMessage:C.saveMessage,scheduledRebootSaving:C.saving,timeData:w.data,timeError:w.error,timeLoading:w.loading,timeSaveError:w.saveError,timeSaveMessage:w.saveMessage,timeSaving:w.saving,timeSyncing:w.syncing});break;case`safeshield`:D=z(nv,{action:T.action,actionError:T.error,actionMessage:T.message,data:m.data,error:m.error,loading:m.loading,statistics:h.data,statisticsError:h.error,statisticsLoading:h.loading,statisticsRefreshing:h.refreshing,onDismissFeedback:T.dismissFeedback,onRefreshBlocklist:()=>void T.refreshBlocklist(),onRetry:()=>void m.refresh(),onRetryStatistics:()=>void h.refresh(),onSetEnabled:e=>void T.setEnabled(e),onSetStatisticsEnabled:e=>void T.setStatisticsEnabled(e)});break;case`rules`:D=z(ov,{action:v.action,data:v.data,error:v.error,feedback:v.feedback,loading:v.loading,onAddRule:v.addRule,onDeleteRule:v.deleteRule,onLoadRecommendedRulesStatus:En,onSyncRecommendedRules:Dn,onRevokeRecommendedRule:On,onDismissFeedback:v.dismissFeedback,onRetry:()=>void v.refresh()});break;default:D=z(Sg,{accountRegistered:b,activity:a.data?.activityHistory??null,activityError:a.error,activityLoading:a.loading,data:a.data,devices:f.data,devicesError:f.error,devicesLoading:f.loading,error:a.error,firmware:s.data,firmwareError:s.error,firmwareLoading:s.loading,health:S.data,healthError:S.error,healthLoading:S.loading,lan:l.data,lanError:l.error,lanLoading:l.loading,loading:a.loading,onRetry:()=>void Promise.all([a.refresh(),f.refresh(),m.refresh(),h.refresh(),o.refresh(),s.refresh(),S.refresh(),l.refresh()]),safeshield:g.data,safeshieldError:g.error,safeshieldLoading:g.loading,statistics:_.data,statisticsError:_.error,statisticsLoading:_.loading,statisticsRefreshing:_.refreshing,updates:o.data,updatesError:o.error,updatesLoading:o.loading})}let O=()=>{t===`home`?Promise.all([a.refresh(),f.refresh(),g.refresh(),_.refresh(),o.refresh(),s.refresh(),S.refresh(),l.refresh()]):t===`network`?Promise.all([c.refresh(),l.refresh()]):t===`system`?Promise.all([o.refresh(),s.refresh()]):t===`settings`?Promise.all([a.refresh(),s.refresh(),w.refresh(),C.refresh(),S.refresh()]):t===`safeshield`?Promise.all([m.refresh(),h.refresh()]):E.refresh()},k=+((o.data?.updateCount??0)>0)+ +!!s.data?.updateAvailable;return z(Er,{accountRegistered:b,loading:t===`network`?c.loading||l.loading:E.loading,onRefresh:O,refreshing:E.refreshing||t===`network`&&l.refreshing||t===`home`&&(i.refreshing||f.refreshing||g.refreshing||_.refreshing||o.refreshing||s.refreshing||S.refreshing||l.refreshing)||t===`system`&&s.refreshing||t===`settings`&&(s.refreshing||w.refreshing||C.refreshing||S.refreshing)||t===`safeshield`&&h.refreshing,route:t,updateCount:k,children:D})}function gy({onAdministratorPasswordChanged:e,onPasswordConfigured:t}){let[n,r]=M(`checking`),[i,a]=M(!1),o=F(async()=>{r(`checking`);try{let e=await Dt();a(e.recovery),r(e.configured?`ready`:`required`)}catch{r(`error`)}},[]);return N(()=>{o()},[o]),n===`ready`?z(hy,{onAdministratorPasswordChanged:e}):n===`required`?z(yn,{onCompleted:()=>t(i),recovery:i}):z(`main`,{class:`ssh-initial-setup-probe`,children:z(`div`,{class:`ssh-initial-setup-probe-card`,role:n===`error`?`alert`:`status`,children:[n===`checking`?z(V,{class:`ssh-login-probe-spinner`,"aria-hidden":`true`}):z(At,{"aria-hidden":`true`}),z(`strong`,{children:n===`checking`?`관리자 보안 상태 확인 중`:`관리자 보안 상태를 확인할 수 없습니다`}),z(`span`,{children:n===`checking`?`root 비밀번호 설정 여부를 확인하고 있습니다.`:`공유기 연결을 확인한 뒤 다시 시도해 주세요.`}),n===`error`?z(`button`,{onClick:()=>void o(),type:`button`,children:`다시 시도`}):null]})})}function _y({icon:e,title:t,description:n}){return z(`div`,{class:`ssh-login-feature`,children:[z(`span`,{class:`ssh-login-feature-icon`,"aria-hidden":`true`,children:e}),z(`span`,{class:`ssh-login-feature-copy`,children:[z(`strong`,{children:t}),z(`span`,{children:n})]})]})}function vy({theme:e}){return z(e===`dark`?un:ln,{})}function yy({notice:e,onAuthenticated:t,probing:n=!1}){let r=Qe(Ce,[]),i=P(null),a=P(null),[o,s]=M(`root`),[c,l]=M(``),[u,d]=M(`ready`),[f,p]=M(null),[m,h]=M(e??null),[g,_]=M(!1),[v,y]=M(!1),[b,x]=M(!1),[S,C]=M(fn),w=u===`submitting`,T=S===`dark`?`라이트 모드로 전환`:`다크 모드로 전환`;N(()=>{pn(S),mn(S)},[S]),N(()=>{if(n)return;let e=window.requestAnimationFrame(()=>a.current?.focus());return()=>window.cancelAnimationFrame(e)},[n]),N(()=>{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 z(`main`,{class:`ssh-login-page`,"data-theme":S,children:[m?z(`div`,{"aria-atomic":`true`,"aria-live":`assertive`,class:`ssh-login-toast`,role:`alert`,children:[z(At,{"aria-hidden":`true`}),z(`span`,{children:m})]}):null,z(`section`,{class:`ssh-login-brand`,"aria-labelledby":`ssh-login-brand-title`,children:[z(`div`,{class:`ssh-login-brand-orb ssh-login-brand-orb-one`,"aria-hidden":`true`}),z(`div`,{class:`ssh-login-brand-orb ssh-login-brand-orb-two`,"aria-hidden":`true`}),z(`div`,{class:`ssh-login-brand-content`,children:[z(`span`,{class:`ssh-login-brand-mark`,"aria-hidden":`true`,children:z(At,{})}),z(`p`,{class:`ssh-login-eyebrow`,children:`SMART NETWORK PROTECTION`}),z(`h1`,{id:`ssh-login-brand-title`,children:`SmartSafeHub`}),z(`p`,{class:`ssh-login-brand-description`,children:`네트워크와 보안을 하나의 허브에서 편리하게 관리하세요.`}),z(`div`,{class:`ssh-login-features`,"aria-label":`SmartSafeHub 주요 기능`,children:[z(_y,{description:`DNS 기반 광고·피싱·추적 차단과 사용자 보호 정책을 관리합니다.`,icon:z(At,{}),title:`SafeShield 보호`}),z(_y,{description:`Wi-Fi 설정과 현재 연결된 기기의 상태를 한눈에 확인합니다.`,icon:z(Lt,{}),title:`네트워크 관리`}),z(_y,{description:`관리 기능과 인증은 현재 SmartSafeHub 장치에서 직접 처리됩니다.`,icon:z(qt,{}),title:`로컬 우선 관리`})]})]})]}),z(`section`,{class:`ssh-login-panel`,"aria-labelledby":`ssh-login-title`,children:[z(`button`,{"aria-label":T,class:`ssh-login-theme-toggle`,onClick:()=>C(e=>e===`dark`?`light`:`dark`),title:T,type:`button`,children:z(vy,{theme:S})}),z(`div`,{class:`ssh-login-panel-inner`,children:[n?z(`div`,{class:`ssh-login-probe`,role:`status`,"aria-live":`polite`,children:[z(V,{class:`ssh-login-probe-spinner`,"aria-hidden":`true`}),z(`strong`,{children:`SmartSafeHub 연결 확인 중`}),z(`span`,{children:`로그인 세션을 확인하고 있습니다.`})]}):z(`div`,{class:`ssh-login-card`,children:[z(`div`,{class:`ssh-login-card-heading`,children:[z(`span`,{class:`ssh-login-mobile-mark`,"aria-hidden":`true`,children:z(At,{})}),z(`div`,{children:[z(`p`,{class:`ssh-login-kicker`,children:`SMARTSAFEHUB ACCESS`}),z(`h2`,{id:`ssh-login-title`,children:`로그인`}),z(`p`,{class:`ssh-login-subtitle`,children:`공유기 관리자 계정의 사용자 이름과 비밀번호를 입력해 주세요.`})]})]}),f?z(`div`,{class:`ssh-login-alert`,id:`ssh-login-error`,role:`alert`,"aria-live":`polite`,children:f}):null,z(`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:[z(`label`,{class:`ssh-login-field`,for:`smartsafehub-username`,children:[z(`span`,{class:`ssh-login-label`,children:`사용자 이름`}),z(`span`,{class:`ssh-login-input-wrap`,children:[z(`span`,{class:`ssh-login-field-icon`,"aria-hidden":`true`,children:z(Pt,{})}),z(`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})]})]}),z(`label`,{class:`ssh-login-field`,for:`smartsafehub-password`,children:[z(`span`,{class:`ssh-login-label`,children:`비밀번호`}),z(`span`,{class:`ssh-login-input-wrap`,children:[z(`span`,{class:`ssh-login-field-icon`,"aria-hidden":`true`,children:z(qt,{})}),z(`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}),z(`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:z(v?Xt:Yt,{})})]})]}),o.trim()===`root`&&c.length===0?z(`p`,{class:`ssh-login-password-hint`,children:`초기 설정 전 root 계정은 비밀번호가 비어 있을 수 있습니다.`}):null,b?z(`p`,{class:`ssh-login-caps`,role:`status`,children:`Caps Lock이 켜져 있습니다.`}):null,z(`button`,{class:`ssh-login-submit`,disabled:w,type:`submit`,children:[w?z(V,{class:`ssh-login-submit-spinner`,"aria-hidden":`true`}):null,z(`span`,{children:w?`로그인 중…`:`로그인`})]})]}),z(`div`,{class:`ssh-login-security-note`,children:[z(qt,{"aria-hidden":`true`}),z(`span`,{children:`관리자 비밀번호를 잊었다면 전원이 켜진 상태에서 Reset 버튼을 5~9초 누른 뒤 놓아 비밀번호만 복구할 수 있습니다. 10초 이상 누르면 모든 사용자 설정을 초기화하는 기기 초기화가 실행됩니다.`})]}),g?z(`a`,{class:`ssh-login-fallback`,href:r,children:`기본 LuCI 로그인으로 계속`}):null]}),z(`footer`,{class:`ssh-login-footer`,children:[z(`span`,{children:`SmartSafeHub`}),z(`span`,{"aria-hidden":`true`,children:`·`}),z(`span`,{children:window.location.host})]})]})]})]})}var by=`smartsafehub-entry-root`,xy=`data-smartsafehub-mount`,Sy=`data-smartsafehub-shadow-style`;function Cy(){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 wy(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.30-r1`,locale:document.documentElement.lang||`ko`})}function Ty(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 Ey(e,t){let n=e.shadowRoot??e.attachShadow({mode:`open`}),r=n.querySelector(`link[${Sy}]`),i=Ty(`app.css`,e);r?r.getAttribute(`href`)!==i&&(r.href=i):(r=document.createElement(`link`),r.rel=`stylesheet`,r.href=i,r.setAttribute(Sy,``),n.appendChild(r));let a=n.querySelector(`[${xy}]`);return a?a.className!==t&&(a.className=t):(a=document.createElement(`div`),a.className=t,a.setAttribute(xy,``),n.appendChild(a)),a}function Dy(e,t,n){wy(n,e),t.className=`smartsafehub-shadow-root`,se(z(gy,{onAdministratorPasswordChanged:()=>{Oy(e,t,`관리자 비밀번호가 변경되었습니다. 새 비밀번호로 다시 로그인해 주세요.`)},onPasswordConfigured:n=>{Oy(e,t,n?`관리자 비밀번호 복구가 완료되었습니다. 새 비밀번호로 다시 로그인해 주세요.`:`관리자 비밀번호가 설정되었습니다. 새 비밀번호로 다시 로그인해 주세요.`)}}),t)}function Oy(e,t,n){delete window.__SMARTHUB_BOOTSTRAP__,t.className=`smartsafehub-login-shadow-root`,se(z(yy,{notice:n,onAuthenticated:n=>{Dy(e,t,n)}}),t)}async function ky(){mn(fn());let e=document.getElementById(by);if(!e)return;Cy(),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=Ey(e,`smartsafehub-login-shadow-root`);window.addEventListener(Ae,()=>{Oy(e,t,je)}),se(z(yy,{onAuthenticated:n=>{Dy(e,t,n)},probing:!0}),t);try{let n=await pe();if(n){Dy(e,t,n);return}}catch{}try{let n=await De();if(n){Dy(e,t,n);return}}catch{}Oy(e,t)}ky(); \ No newline at end of file +`):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 diff --git a/luci-app-smartsafehub/spec/contracts_spec.sh b/luci-app-smartsafehub/spec/contracts_spec.sh index 60d7ef45f..97e403353 100755 --- a/luci-app-smartsafehub/spec/contracts_spec.sh +++ b/luci-app-smartsafehub/spec/contracts_spec.sh @@ -22,6 +22,13 @@ Describe 'SmartSafeHub 셸 계약 테스트' The error should be blank End + It 'OpenWrt 25.12 BusyBox 기본 명령과 의존성 호환성을 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-openwrt-commands.sh" + The status should be success + The output should start with 'PASS:' + The error should be blank + End + It '셸 파이프라인의 Broken pipe 회귀를 방지한다' When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-shell-pipeline-safety.sh" The status should be success @@ -70,6 +77,13 @@ Describe 'SmartSafeHub 셸 계약 테스트' The error should be blank End + It '사용자 규칙 렌더링 최적화와 RPC 진단 개인정보 보호 계약을 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-rules-performance.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-rules.sh" The status should be success @@ -112,6 +126,20 @@ Describe 'SmartSafeHub 셸 계약 테스트' The error should be blank End + It 'Apps 설치 성공 이벤트 전송과 재시도·복원 제외 계약을 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-apps-metrics.sh" + The status should be success + The output should start with 'PASS:' + The error should be blank + End + + It '펌웨어 업데이트 후 검증된 Apps 자동 복원 계약을 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-apps-restore.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-navigation-contract.sh" The status should be success @@ -133,6 +161,13 @@ Describe 'SmartSafeHub 셸 계약 테스트' The error should be blank End + It 'CHANGELOG 릴리즈 사이 빈 줄 두 개를 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-changelog-spacing.sh" + The status should be success + The output should start with 'PASS:' + The error should be blank + End + It 'README와 상세 문서 분리 계약을 검증한다' When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-documentation-contract.sh" The status should be success @@ -273,6 +308,41 @@ Describe 'SmartSafeHub 셸 계약 테스트' The error should be blank End + It 'RPC 외부 작업 분리와 캐시/작업 중복 방지를 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-rpc-jobs.sh" + The status should be success + The output should start with 'PASS: detached RPC worker' + The error should be blank + End + + It 'SmartSafeHub Apps 설치와 제거 안전성 계약을 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-apps-manager.sh" + The status should be success + The output should start with 'PASS: Apps install/remove' + The error should be blank + End + + It 'Apps 설치/제거 색상과 제거 확인 팝업 및 진행 상태를 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-apps-ui.sh" + The status should be success + The output should start with 'PASS: Apps removal confirmation' + 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 + The output should start with 'PASS: Apps secure new-tab launch' + The error should be blank + End + + It 'SmartSafeHub Apps 메뉴와 승인된 앱 카탈로그를 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-apps-contract.sh" + The status should be success + The output should start with 'PASS:' + 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 @@ -335,4 +405,111 @@ Describe 'SmartSafeHub 셸 계약 테스트' The output should start with 'PASS:' The error should be blank End + + # 지속적으로 보호해야 하는 기능 계약(과거 CI에서 누락된 테스트 포함) + It '최근 활동 동기화 데몬의 유휴 폴링 제한을 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-activity-idle-polling.sh" + The status should be success + The output should start with 'PASS:' + The error should be blank + End + + It '최근 활동의 Cloud 설정과 기록 표시 및 접근성을 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-activity-layout.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-admin-password-change.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-admin-password-recovery.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-daemon-runtime.sh" + The status should be success + The output should start with 'PASS:' + The error should be blank + End + + It '대시보드의 데이터 중복 호출과 병렬 RPC를 방지한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-dashboard-performance-contract.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-device-registration.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-firmware-bundling.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-firmware-identity.sh" + The status should be success + The output should start with 'PASS:' + The error should be blank + End + + It 'WAN/LAN 통합 메뉴 및 과거 링크 호환성을 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-network-settings.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-page-tabs.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-runtime-path-contract.sh" + The status should be success + The output should start with 'PASS:' + The error should be blank + End + + It '테스트 파일이 CI 실행 목록에서 누락되지 않는지 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-suite-integrity.sh" + The status should be success + The output should start with 'PASS:' + The error should be blank + End + + It '기본 UCI 설정값과 초기화를 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-system-defaults.sh" + The status should be success + The output should start with 'PASS:' + The error should be blank + End + + It 'Wi-Fi QR 인증과 비밀번호 보호를 검증한다' + When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-wifi-qr-contract.sh" + The status should be success + The output should start with 'PASS:' + The error should be blank + End + End diff --git a/luci-app-smartsafehub/tests/apps-storage-test.uc b/luci-app-smartsafehub/tests/apps-storage-test.uc new file mode 100644 index 000000000..d005bf7c3 --- /dev/null +++ b/luci-app-smartsafehub/tests/apps-storage-test.uc @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +'use strict'; +import { from_system_root, from_statvfs } from '../root/usr/share/rpcd/ucode/smartsafehub/apps-storage.uc'; +function check(ok, message) { + if (!ok) { + warn(`FAIL: ${message}\n`); + exit(1); + } +}; +let s = from_system_root({ total: 65536, avail: 40960 }); +check(s?.mount == '/' && s?.totalBytes == 67108864 && s?.availableBytes == 41943040, + 'procd root stats must convert KiB to bytes'); +check(from_system_root({ total: 500, avail: 501 }) == null, 'invalid root counters'); +check(from_system_root({ total: '65536', avail: '40960' }) == null, 'reject string counters'); +check(from_system_root({ total: 9007199254740991, avail: 1 }) == null, 'reject integer overflow'); +check(from_system_root(null) == null, 'missing procd statistics must be optional'); +check(from_system_root({ total: 42, avail: 0 })?.availableBytes == 0, 'allow a full filesystem'); +let t = from_statvfs({ frsize: 4096, blocks: 20000, bavail: 13000 }, '/overlay'); +check(t?.totalBytes == 81920000 && t?.availableBytes == 53248000, 'statvfs basic units'); +check(from_statvfs({ frsize: 0, bsize: 4096, blocks: 20000, bavail: 0 }, '/')?.availableBytes == 0, + 'fall back to bsize if frsize is zero'); +check(from_statvfs({ frsize: 4096, blocks: 20000, bavail: 20001 }, '/') == null, + 'reject invalid available blocks'); +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'); diff --git a/luci-app-smartsafehub/tests/runtime/apps-operation-notice.test.mjs b/luci-app-smartsafehub/tests/runtime/apps-operation-notice.test.mjs new file mode 100644 index 000000000..564575977 --- /dev/null +++ b/luci-app-smartsafehub/tests/runtime/apps-operation-notice.test.mjs @@ -0,0 +1,50 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import { + appsJobNoticeKey, + readDismissedAppsJob, + saveDismissedAppsJob, +} from '../../frontend/src/utils/appsJobNotice.ts'; + +test('Apps 알림: 같은 작업은 새로고침해도 닫힘 상태를 유지한다', () => { + const entries = new Map(); + const oldWindow = globalThis.window; + globalThis.window = { + localStorage: { + getItem: (key) => entries.get(key) ?? null, + setItem: (key, value) => entries.set(key, value), + }, + }; + try { + const failed = { phase: 'failed', action: 'install', appId: 'network-usage-monitor', code: 'E22', operationId: '1800000000-1234' }; + const key = appsJobNoticeKey(failed); + assert.equal(readDismissedAppsJob(), null); + saveDismissedAppsJob(key); + assert.equal(readDismissedAppsJob(), key); + assert.equal(appsJobNoticeKey({ ...failed }), key); + assert.notEqual(appsJobNoticeKey({ ...failed, operationId: '1800000000-1235' }), key, + 'a new operation with the same error must show a fresh notice'); + assert.equal(appsJobNoticeKey({ ...failed, phase: 'idle' }), ''); + } finally { + if (oldWindow === undefined) delete globalThis.window; + else globalThis.window = oldWindow; + } +}); + +test('Apps 알림: 저장소 사용을 막아도 UI가 동작하고 구형 작업 상태도 지원한다', () => { + const oldWindow = globalThis.window; + globalThis.window = { + get localStorage() { throw new Error('SecurityError'); }, + }; + try { + assert.equal(readDismissedAppsJob(), null); + assert.doesNotThrow(() => saveDismissedAppsJob('operation:1800000000-1234')); + assert.equal(appsJobNoticeKey({ phase: 'failed', action: 'install', appId: 'retro-arcade', code: 'E29' }), + 'legacy:failed:install:retro-arcade:E29'); + assert.equal(appsJobNoticeKey(null), ''); + } finally { + if (oldWindow === undefined) delete globalThis.window; + else globalThis.window = oldWindow; + } +}); diff --git a/luci-app-smartsafehub/tests/runtime/apps-storage.test.mjs b/luci-app-smartsafehub/tests/runtime/apps-storage.test.mjs new file mode 100644 index 000000000..c60b2368e --- /dev/null +++ b/luci-app-smartsafehub/tests/runtime/apps-storage.test.mjs @@ -0,0 +1,61 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import { formatStorageBytes, summarizeAppsStorage } from '../../frontend/src/utils/appsStorage.ts'; + +const MB = 1024 ** 2; + +test('앱 공간: /overlay 기준 실제 설치 가능 공간과 백분율을 표시한다', () => { + const summary = summarizeAppsStorage({ mount: '/overlay', totalBytes: 80 * MB, availableBytes: 68 * MB }); + assert.equal(summary.availablePercent, 85); + assert.equal(summary.availableText, '68.0 MB'); + assert.equal(summary.usedText, '12.0 MB'); + assert.equal(summary.totalText, '80.0 MB'); + assert.ok(Math.abs(summary.availableRatio - 0.85) < 1e-10); + assert.equal(summary.level, 'normal'); +}); + +test('앱 공간: 극단값, 경계값과 ext4 루트 파일시스템을 처리한다', () => { + const empty = summarizeAppsStorage({ mount: '/', totalBytes: 100 * MB, availableBytes: 0 }); + assert.equal(empty.availablePercent, 0); + assert.equal(empty.usedText, '100.0 MB'); + const full = summarizeAppsStorage({ mount: '/', totalBytes: 100 * MB, availableBytes: 100 * MB }); + assert.equal(full.availableRatio, 1); + assert.equal(full.usedText, '0 B'); + assert.equal(summarizeAppsStorage({ mount: '/overlay', totalBytes: 100 * MB, availableBytes: 20 * MB }).level, 'warning'); + assert.equal(summarizeAppsStorage({ mount: '/overlay', totalBytes: 100 * MB, availableBytes: 10 * MB }).level, 'critical'); +}); + +test('앱 공간: 실제 시스템 용량은 MiB(표시 MB)로 계산하고 반올림한다', () => { + const summary = summarizeAppsStorage({ mount: '/', totalBytes: 81031168, availableBytes: 75153408 }); + assert.equal(summary.availableText, '71.7 MB'); + assert.equal(summary.usedText, '5.6 MB'); + assert.equal(summary.totalText, '77.3 MB'); +}); + +test('앱 공간: 누락·유효하지 않은 RPC 응답은 임의의 용량으로 채우지 않는다', () => { + for (const storage of [null, undefined, {}, { mount: '/tmp', totalBytes: 100, availableBytes: 20 }, + { mount: '/overlay', totalBytes: 0, availableBytes: 0 }, + { mount: '/overlay', totalBytes: 100, availableBytes: -1 }, + { mount: '/overlay', totalBytes: 100, availableBytes: 101 }, + { mount: '/overlay', totalBytes: Infinity, availableBytes: 20 }, + { mount: '/overlay', totalBytes: NaN, availableBytes: 20 }]) { + assert.equal(summarizeAppsStorage(storage), null); + } +}); + +test('앱 공간: 작은 용량과 큰 용량을 올바른 단위로 표시한다', () => { + assert.equal(formatStorageBytes(512), '512 B'); + assert.equal(formatStorageBytes(512 * 1024), '512.0 KB'); + assert.equal(formatStorageBytes(77.3 * MB), '77.3 MB'); + assert.equal(formatStorageBytes(3 * 1024 ** 3), '3.0 GB'); + assert.equal(formatStorageBytes(-3), '—'); +}); + +test('대시보드 디스크 사용률은 Apps 설치 가능 여유 공간과 정확히 대응한다', () => { + const summary = summarizeAppsStorage({ mount: '/', totalBytes: 100 * MB, availableBytes: 89 * MB }); + assert.equal(100 - summary.availablePercent, 11); + assert.equal(summary.usedText, '11.0 MB'); + assert.equal(summary.totalText, '100.0 MB'); + assert.equal(summarizeAppsStorage(null), null); // older RPC: never show a false 0% bar +}); diff --git a/luci-app-smartsafehub/tests/runtime/recommended-sync-stages.test.mjs b/luci-app-smartsafehub/tests/runtime/recommended-sync-stages.test.mjs index 1023c4aab..3be9b4d27 100644 --- a/luci-app-smartsafehub/tests/runtime/recommended-sync-stages.test.mjs +++ b/luci-app-smartsafehub/tests/runtime/recommended-sync-stages.test.mjs @@ -23,7 +23,7 @@ test('추천 규칙 상태를 단계별로 보여주고 UI의 오류를 중복 const rpc = source('root/usr/share/rpcd/ucode/smartsafehub/device-registration.uc'); const page = source('frontend/src/pages/SafeShieldRulesPage.tsx'); const types = source('frontend/src/types/rules.ts'); - assert.match(rpc, /syncStage: sync\?\.stage/); + assert.match(rpc, /syncStage: operationRunning \? 'server_sync' : \(sync\?\.stage/); assert.match(rpc, /syncReason: sync\?\.reason/); assert.match(types, /syncStage: string \| null/); for (const stage of ['server_sync', 'safeshield_refreshing', 'applying', 'verifying']) { diff --git a/luci-app-smartsafehub/tests/runtime/router-demo.test.mjs b/luci-app-smartsafehub/tests/runtime/router-demo.test.mjs index a5081cfe8..5ab1fdf70 100644 --- a/luci-app-smartsafehub/tests/runtime/router-demo.test.mjs +++ b/luci-app-smartsafehub/tests/runtime/router-demo.test.mjs @@ -79,3 +79,100 @@ test('추천 규칙 API 조회는 실제 공유기 앱에서만 주입된다', ( assert.match(app, /onLoadRecommendedRulesStatus=\{fetchRecommendedRulesStatus\}/); assert.doesNotMatch(demo, /onLoadRecommendedRulesStatus=/); }); + + +test('SmartSafeHub Account and Apps use fictional fixtures, not live APIs', () => { + const routes = readFileSync(resolve(root, 'app/routes.ts'), 'utf8'); + const app = readFileSync(resolve(root, 'demo/main.tsx'), 'utf8'); + const fixtures = readFileSync(resolve(root, 'demo/data.ts'), 'utf8'); + assert.match(routes, /'rules', 'account', 'apps', 'system'/); + for (const [route, component] of [['account', 'SmartSafeHubAccountPage'], ['apps', 'AppsPage']]) { + assert.match(app, new RegExp(`case '${route}':`)); + assert.ok(app.includes(`<${component}`), `${component} not reused by demo`); + assert.match(app, new RegExp(`<${component}[^;]*?demo`, 's')); + } + for (const fixture of ['demoAccount', 'demoApps', 'demoAppsStatus', 'demoAppsMetrics']) { + assert.ok(fixtures.includes(`export const ${fixture}:`), `missing ${fixture}`); + assert.ok(app.includes(fixture), `unused ${fixture}`); + } + assert.match(fixtures, /accountRegistered: true/); + assert.match(fixtures, /pairingCode: null/); + assert.match(fixtures, /id: 'network-usage-monitor'/); + assert.match(fixtures, /id: 'retro-arcade'/); + assert.match(fixtures, /id: 'wake-on-lan'/); + assert.match(fixtures, /category: 'entertainment'/); + const newFixtures = fixtures.slice(fixtures.indexOf('export const demoAccount:'), fixtures.indexOf('export const demoDevices:')); + assert.doesNotMatch(newFixtures, /https?:\/\/|\/cgi-bin\/|password|token|api_key/i); + assert.match(app, /accountRegistered=\{demoAccount\.accountRegistered\}/); +}); + +test('demo showcases verified Apps versions, upgrade badge, and installed sizes without changing router state', () => { + const fixtures = readFileSync(resolve(root, 'demo/data.ts'), 'utf8'); + const catalog = fixtures.split('export const demoApps: AppsCatalog = {')[1].split('export const demoAppsStatus:')[0]; + const status = fixtures.split('export const demoAppsStatus: AppsInstallStatus = {')[1].split('export const demoAppsMetrics:')[0]; + const metrics = fixtures.split('export const demoAppsMetrics: AppsMetrics = {')[1].split('export const demoDevices:')[0]; + + 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: 'network-usage-monitor',[\s\S]*?version: '2025\.06\.02~29236be6-r1'/); + 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/); + + assert.match(status, /'retro-arcade': \{\s*installed: true,\s*partial: false,\s*managed: true,\s*updateAvailable: true,\s*installedVersion: '1\.0\.0-r1'/); + assert.match(status, /'network-usage-monitor': \{\s*installed: true,\s*partial: false,\s*managed: true,\s*updateAvailable: false,\s*installedVersion: '2025\.06\.02~29236be6-r1'/); + assert.match(metrics, /'retro-arcade': 1394688/); + assert.match(metrics, /'network-usage-monitor': 117760/); + assert.match(fixtures, /storage: demoAppsMetrics\.storage/, + 'dashboard disk and Apps donut must share the same fictional storage'); + + const demo = readFileSync(resolve(root, 'demo/main.tsx'), 'utf8'); + assert.match(demo, /installStatus=\{demoAppsStatus\} metrics=\{demoAppsMetrics\}/); + assert.match(demo, /onUpdate=\{async \(\) => \{ readonlyNotice\(\); \}\}/); +}); + +test('Apps 카테고리 아이콘은 공유기와 데모에서 동일하고 도구와 기타가 구분된다', () => { + const page = readFileSync(resolve(root, 'pages/AppsPage.tsx'), 'utf8'); + const category = readFileSync(resolve(root, 'components/AppsCategoryIcon.tsx'), 'utf8'); + const icons = readFileSync(resolve(root, 'components/Icons.tsx'), 'utf8'); + const demo = readFileSync(resolve(root, 'demo/main.tsx'), 'utf8'); + assert.match(demo, / { + const demo = readFileSync(resolve(root, 'demo/main.tsx'), 'utf8'); + const apps = readFileSync(resolve(root, 'pages/AppsPage.tsx'), 'utf8'); + const account = readFileSync(resolve(root, 'pages/SmartSafeHubAccountPage.tsx'), 'utf8'); + assert.match(demo, /onDemoAction=\{readonlyNotice\}/); + assert.match(demo, /onRequestPairing=\{async \(\) => \{ readonlyNotice\(\); return null; \}\}/); + for (const action of ['onInstall', 'onRemove', 'onUpdate']) { + assert.ok(demo.includes(`${action}={async () => { readonlyNotice(); }}`), `${action} must be a no-op`); + } + assert.match(apps, /if \(demo\) \{/); + assert.match(apps, /if \(action === 'remove'\) closeRemoveDialog\(\)/); + assert.match(apps, /onDemoAction\?\.\(\);/); + assert.match(apps, /launchPath \? \(demo \?/); + assert.match(apps, /onClick=\{onDemoAction\} type="button">열기/); + assert.match(account, /demo \? \(/); + assert.match(account, /onClick=\{onDemoAction\}/); + assert.match(account, /href=\{DEVICE_CONSOLE_URL\}/); + assert.doesNotMatch(demo, /import .*from ['"]\.\.\/api\//); +}); + +test('README links to the public router demo', () => { + 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/rpc-scheduler.test.mjs b/luci-app-smartsafehub/tests/runtime/rpc-scheduler.test.mjs new file mode 100644 index 000000000..5570bdea2 --- /dev/null +++ b/luci-app-smartsafehub/tests/runtime/rpc-scheduler.test.mjs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { acquireRpcSlot } from '../../frontend/src/api/rpcScheduler.ts'; + +test('RPC queue reserves a slot for interactive actions and releases cancelled waiters', async () => { + const background = await Promise.all(Array.from({ length: 3 }, () => acquireRpcSlot('low'))); + const pending = acquireRpcSlot('low'); + const interactive = await acquireRpcSlot('high'); + // With four slots taken, a queued request must be cancellable without a leak. + const controller = new AbortController(); + const cancelled = acquireRpcSlot('normal', controller.signal); + controller.abort(); + await assert.rejects(cancelled, (error) => error.name === 'AbortError'); + interactive(); + let settled = false; + void pending.then(() => { settled = true; }); + await Promise.resolve(); + assert.equal(settled, false, 'fourth low-priority RPC must not consume the reserved slot'); + background[0](); + const fourth = await pending; + fourth(); + background[1](); + background[2](); +}); + +test('pre-aborted signal never starts work', async () => { + const controller = new AbortController(); + controller.abort(); + await assert.rejects(acquireRpcSlot('high', controller.signal), (error) => error.name === 'AbortError'); +}); + +test('queued RPC starts after an earlier queued request is aborted', async () => { + const active = await Promise.all(Array.from({ length: 4 }, () => acquireRpcSlot('normal'))); + const controller = new AbortController(); + const cancelled = acquireRpcSlot('normal', controller.signal); + const successor = acquireRpcSlot('high'); + + controller.abort(); + await assert.rejects(cancelled, (error) => error.name === 'AbortError'); + + active[0](); + const releaseSuccessor = await successor; + releaseSuccessor(); + active.slice(1).forEach((release) => release()); + + // All accounting must be restored after cancelling, draining and releasing. + const again = await Promise.all(Array.from({ length: 4 }, () => acquireRpcSlot('normal'))); + again.forEach((release) => release()); +}); diff --git a/luci-app-smartsafehub/tests/runtime/rule-origin.test.mjs b/luci-app-smartsafehub/tests/runtime/rule-origin.test.mjs index 3d1f5d495..20cd43d27 100644 --- a/luci-app-smartsafehub/tests/runtime/rule-origin.test.mjs +++ b/luci-app-smartsafehub/tests/runtime/rule-origin.test.mjs @@ -19,7 +19,7 @@ test('허용 목록의 수동 규칙과 추천 규칙을 로컬 소유 정보로 assert.match(rpc, /recommended-rules\.state/); assert.match(rpc, /domains: domains/); assert.match(helper, /grep -Fxq "\$domain" "\$OWNED"/); - assert.match(page, /const cloudManaged = isAllow && recommendedKnown && recommendedDomains\.includes\(domain\);/); + assert.match(page, /const cloudManaged = isAllow && recommendedKnown && recommendedDomainSet\.has\(domain\);/); assert.match(page, /cloudManaged \? '추천 규칙' : '직접 추가'/); assert.match(page, /disabled=\{busy \|\| cloudManaged\}/); }); @@ -50,7 +50,7 @@ test('추천 규칙 수동 동기화와 적용 확인 상태는 데모 API를 assert.match(acl, /"device_recommended_rules_refresh"/); assert.match(device, /write_recommended_sync_status failed/); assert.match(device, /write_recommended_sync_status confirmed/); - assert.match(rpc, /const syncStatus = sync\?\.status == 'pending'/); + assert.match(rpc, /const syncStatus = operationRunning \|\| sync\?\.status == 'pending'/); assert.match(rpc, /const syncWarning = confirmed &&/); assert.match(rpc, /const confirmed = revision != null && receipt\?\.status == 'confirmed'/); assert.doesNotMatch(page, /from ['"]\.\.\/api\//); @@ -78,9 +78,9 @@ test('추천 규칙 상태는 화면을 새로고침하지 않아도 주기적 test('갱신 실패 후에도 추천 규칙의 소유권을 유지하고 확인서가 없으면 재조회한다', () => { const device = readFileSync(resolve(root, 'root/usr/libexec/smartsafehub-device'), 'utf8'); - assert.match(helper, /Keep the ownership claim until the final SafeShield read confirms/); - assert.match(helper, /Only after successful reconciliation may we forget ownership/); - assert.match(helper, /ownership intent before creating the allow entry/); + assert.match(helper, /Retire ownership only after final verification/); + assert.match(helper, /persist "\$revision"/); + assert.match(helper, /Record intent before rule_add/); assert.match(device, /A state revision alone is not proof that SafeShield applied the rules/); assert.match(device, /json_value "\$RECOMMENDED_RECEIPT" '@\.revision'/); }); @@ -102,7 +102,7 @@ test('해제 요청 시간 초과 후에도 로컬 적용 확인을 반복하고 }); test('백그라운드 규칙 변경을 감지하면 SafeShield 목록을 다시 읽는다', () => { - assert.match(page, /previousStatusSignature\.current !== signature/); + assert.match(page, /previousStatusSignature\.current !== null/); assert.match(page, /onRetryRef\.current\(\)/); assert.match(page, /window\.setInterval\(updateStatus, 15_000\)/); assert.match(page, /window\.addEventListener\('focus', onFocus\)/); diff --git a/luci-app-smartsafehub/tests/runtime/rules-performance.test.mjs b/luci-app-smartsafehub/tests/runtime/rules-performance.test.mjs new file mode 100644 index 000000000..9e2d02180 --- /dev/null +++ b/luci-app-smartsafehub/tests/runtime/rules-performance.test.mjs @@ -0,0 +1,79 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + filterRuleGroups, + filterRecommendedAssignments, + sameRecommendationContent, +} from '../../frontend/src/utils/ruleLists.ts'; +import { + recordRpcTiming, + resetRpcDiagnostics, + snapshotRpcDiagnostics, +} from '../../frontend/src/api/rpcDiagnostics.ts'; + +test('규칙 검색은 한 번만 분류하고 웹사이트 관리 규칙을 직접 삭제 가능한 그룹에서 분리한다', () => { + const rules = ['ads.example.com', 'manual.example.org', 'tv.example.net']; + const ownership = new Set(['ads.example.com', 'tv.example.net']); + const result = filterRuleGroups(rules, '', true, ownership); + assert.equal(result.filteredRules, rules, 'no search preserves source array'); + assert.deepEqual(result.managedRules, ['ads.example.com', 'tv.example.net']); + assert.deepEqual(result.manualRules, ['manual.example.org']); + assert.deepEqual(filterRuleGroups(rules, 'example.org', true, ownership).manualRules, ['manual.example.org']); + assert.deepEqual(filterRuleGroups(rules, 'no-match', true, ownership).managedRules, []); + assert.equal(filterRuleGroups(rules, '', false, ownership).manualRules, rules); +}); + +test('추천 규칙 검색은 제목 또는 도메인과 일치하고 빈 검색어에서 원래 목록을 유지한다', () => { + const rules = [ + { id: 1, title: 'Samsung TV', domains: ['ads.its-newid.net'] }, + { id: 2, title: 'Other', domains: ['example.org'] }, + ]; + assert.equal(filterRecommendedAssignments(rules, ''), rules); + assert.deepEqual(filterRecommendedAssignments(rules, 'samsung'), [rules[0]]); + assert.deepEqual(filterRecommendedAssignments(rules, 'its-newid'), [rules[0]]); +}); + +test('백그라운드 상태 갱신에서는 동일한 목록 참조를 재사용하되 실제 변경은 반영한다', () => { + const previous = { + revision: 'r1', domains: ['ads.its-newid.net'], + rules: [{ id: 2, title: 'TV', domains: ['ads.its-newid.net'] }], + lastCheckedAt: 1, syncStatus: 'pending', + }; + const progress = { + ...previous, + domains: ['ads.its-newid.net'], + rules: [{ id: 2, title: 'TV', domains: ['ads.its-newid.net'] }], + lastCheckedAt: 2, + syncStatus: 'confirmed', + }; + const stable = sameRecommendationContent(previous, progress); + assert.equal(stable.domains, previous.domains); + assert.equal(stable.rules, previous.rules); + assert.equal(progress.syncStatus, 'confirmed', 'status transitions still flow to the parent'); + const changed = sameRecommendationContent(previous, { + ...progress, domains: ['new.example.org'], + rules: [{ id: 2, title: 'TV updated', domains: ['new.example.org'] }], + }); + assert.notEqual(changed.domains, previous.domains); + assert.notEqual(changed.rules, previous.rules); +}); + +test('운영 환경 RPC 진단은 느린/실패 요청을 제한된 메모리에만 기록한다', () => { + resetRpcDiagnostics(); + recordRpcTiming({ object: 'safeshield', method: 'rules_list', elapsedMs: 12, responseMs: 10, decodeMs: 2, result: 'ok' }); + assert.equal(snapshotRpcDiagnostics().recent.length, 0); + assert.equal(recordRpcTiming({ object: 'safeshield', method: 'rules_list', elapsedMs: 1501, responseMs: 1490, decodeMs: 11, result: 'ok' }), true); + recordRpcTiming({ object: 'smartsafehub', method: 'device_recommended_rules_status', elapsedMs: 28, responseMs: 24, decodeMs: 4, result: 'RPC_TIMEOUT' }); + const snapshot = snapshotRpcDiagnostics(); + assert.deepEqual(snapshot.summary[0], { method: 'safeshield.rules_list', calls: 2, slow: 1, errors: 0, lastMs: 1501, maxMs: 1501, averageMs: 757 }); + assert.equal(snapshot.recent.length, 2); + assert.equal(snapshot.recent[1].result, 'RPC_TIMEOUT'); + assert.equal('params' in snapshot.recent[0], false); + assert.equal('sessionId' in snapshot.recent[0], false); + for (let i = 0; i < 70; i++) { + recordRpcTiming({ object: 'safeshield', method: 'rules_list', elapsedMs: 2000, responseMs: null, decodeMs: null, result: 'RPC_TIMEOUT' }); + } + assert.equal(snapshotRpcDiagnostics().recent.length, 60); + resetRpcDiagnostics(); + assert.deepEqual(snapshotRpcDiagnostics().summary, []); +}); diff --git a/luci-app-smartsafehub/tests/test-account-ui-contract.sh b/luci-app-smartsafehub/tests/test-account-ui-contract.sh index b96c11a67..eae2e8727 100755 --- a/luci-app-smartsafehub/tests/test-account-ui-contract.sh +++ b/luci-app-smartsafehub/tests/test-account-ui-contract.sh @@ -32,8 +32,8 @@ grep -Fq 'void refresh();' "$HOOK" || fail 'page entry must refresh Hub state in grep -Fq 'if (loading && !data) return ;' "$PAGE" || fail 'account page must render cached local state without waiting for Hub' grep -Fq '최신 상태 확인이 지연되고 있습니다. 마지막으로 확인된 연결 상태를 표시합니다.' "$PAGE" || fail 'connected devices must present cloud delay as freshness information, not a connection warning' grep -Fq '>마지막 확인' "$PAGE" || fail 'account status must label the timestamp as the last confirmed state' -grep -Fq "timeoutMs: 30000" "$API" || fail 'background account synchronization must allow the router helper enough time to complete' -grep -Fq "status-sync' ], 25000" "$RPC" || fail 'rpcd must allow status-sync to finish before the frontend timeout' +grep -Fq "timeoutMs: 6000" "$API" || fail 'RPC account sync must promptly return an accepted job' +grep -Fq "ensure_job('device-sync')" "$RPC" || fail 'RPC account sync must use detached worker' grep -Fq 'const connected = status?.accountRegistered === true;' "$PAGE" || fail 'page must derive explicit account connection state' grep -Fq 'const pairingCode = !connected && !expired ? status?.pairingCode : null;' "$PAGE" || fail 'connected devices must never display a stale pairing code' diff --git a/luci-app-smartsafehub/tests/test-activity-idle-polling.sh b/luci-app-smartsafehub/tests/test-activity-idle-polling.sh index a82ebc391..c4f4baa64 100755 --- a/luci-app-smartsafehub/tests/test-activity-idle-polling.sh +++ b/luci-app-smartsafehub/tests/test-activity-idle-polling.sh @@ -17,5 +17,5 @@ assert loop.index('cloud_sync_enabled') < loop.index("previous_state_number '@.n assert 'retry_backoff_active' in loop assert 'consume_wake_marker || true' in loop assert 'ssh_runtime_execute sync-once sync_once' in loop -print('activity idle polling contract: OK') +print('PASS: activity idle polling contract') PY diff --git a/luci-app-smartsafehub/tests/test-apps-contract.sh b/luci-app-smartsafehub/tests/test-apps-contract.sh new file mode 100755 index 000000000..f3178efea --- /dev/null +++ b/luci-app-smartsafehub/tests/test-apps-contract.sh @@ -0,0 +1,152 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +set -eu +ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +ROUTES="$ROOT/frontend/src/app/routes.ts" +NAV="$ROOT/frontend/src/components/ProductNavigation.tsx" +UCODE="$ROOT/root/usr/share/rpcd/ucode/smartsafehub/apps.uc" +RPC="$ROOT/root/usr/share/rpcd/ucode/smartsafehub.uc" +HELPER="$ROOT/root/usr/libexec/smartsafehub-apps" +ACL="$ROOT/root/usr/share/rpcd/acl.d/luci-app-smartsafehub.json" + +fail() { echo "FAIL: $*" >&2; exit 1; } +for file in "$ROUTES" "$NAV" "$UCODE" "$RPC" "$HELPER" "$ACL" \ + "$ROOT/frontend/src/pages/AppsPage.tsx" "$ROOT/frontend/src/types/apps.ts"; do + [ -f "$file" ] || fail "missing ${file##*/}" +done +[ -x "$HELPER" ] || fail 'helper not executable' +sh -n "$HELPER" || fail 'invalid helper shell syntax' +python3 - "$ROUTES" "$NAV" "$ROOT/frontend/src/hooks/useHashRoute.ts" <<'PY' || exit 1 +from pathlib import Path +import sys +routes, nav, hash_route = map(lambda p: Path(p).read_text(), sys.argv[1:]) +assert "route: 'apps'" in routes and "hash: '#apps'" in routes +assert routes.index("route: 'account'") < routes.index("route: 'apps'") < routes.index("route: 'system'") +assert "{ label: 'Explore', routes: ['apps'] }" in nav +assert "{ label: 'System', routes: ['account', 'system', 'settings'] }" in nav +assert "case 'apps':" in nav and '' in activity +assert '
' in apps +heading = apps.split('', 1)[0] +assert 'action=' not in heading, 'refresh action changes heading height' +card = apps.split('/>', 1)[1].split('{error ?', 1)[0] +assert 'p-4 shadow-sm shadow-slate-900/5 sm:p-5' in card, 'card padding differs from Activity' +assert '목록 새로고침' in card, 'refresh action must be within card' +assert 'disabled={loading || refreshing}' in card, 'refresh loading guard missing' +PY +grep -Fq "item.runtime != 'router' || item.installer != 'apk'" "$UCODE" || fail 'unknown runtimes must stay hidden' +# Separate the privileged APK adapter from display-only Hub catalog parsing. +MANAGER="$ROOT/root/usr/libexec/smartsafehub-apps-manager" +MODULE="$ROOT/root/usr/share/rpcd/ucode/smartsafehub/apps-manager.uc" +[ -x "$MANAGER" ] || fail 'APK manager missing or not executable' +[ -f "$MODULE" ] || fail 'APK RPC adapter missing' +sh -n "$MANAGER" || fail 'APK manager shell syntax invalid' +grep -Fq 'apps_install: {' "$RPC" || fail 'Apps install RPC missing' +grep -Fq 'apps_update: {' "$RPC" || fail 'Apps update RPC missing' +grep -Fq 'apps_remove: {' "$RPC" || fail 'Apps remove RPC missing' +grep -Fq 'apps_status: {' "$RPC" || fail 'Apps status RPC missing' +grep -Fq '"apps_install"' "$ACL" || fail 'Apps install ACL missing' +grep -Fq '"apps_update"' "$ACL" || fail 'Apps update ACL missing' +grep -Fq '"apps_remove"' "$ACL" || fail 'Apps remove ACL missing' +grep -Fq '"apps_status"' "$ACL" || fail 'Apps status ACL missing' +grep -Fq '"apps_metrics"' "$ACL" || fail 'Apps metrics read ACL missing' +grep -Fq 'updateVerifiedApp' "$ROOT/frontend/src/api/smartsafehub.ts" || fail 'Apps frontend update call missing' +grep -Fq 'installVerifiedApp' "$ROOT/frontend/src/api/smartsafehub.ts" || fail 'Apps frontend installation call missing' +# 다크 모드에서 앱 작업 상태 텍스트가 어둡게 남지 않도록 색상 매핑을 확인한다. +python3 - "$ROOT/frontend/src/pages/AppsPage.tsx" "$ROOT/frontend/src/styles/app.css" <<'PY_APPS_THEME' || fail 'Apps 작업 상태 다크 모드 계약이 올바르지 않습니다' +from pathlib import Path +import sys + +page, styles = (Path(file).read_text() for file in sys.argv[1:]) +assert "'border-teal-200 bg-teal-50 text-teal-900'" in page +assert "'border-rose-200 bg-rose-50 text-rose-800'" in page +assert "[class~='text-teal-900']" in styles +assert "[class~='text-rose-800']" in styles +PY_APPS_THEME +# Apps RPC 인자가 rpcd의 request.args 아래로 전달되는지 검사한다. +# request.app_id는 항상 빈 값이어서 패키지 작업 시작 전에 APPS_INVALID_ID가 발생한다. +python3 - "$RPC" "$MODULE" "$ROOT/frontend/src/api/smartsafehub.ts" <<'PY_RPC' || fail 'Apps RPC 인자 전달 계약이 올바르지 않습니다' +from pathlib import Path +import re +import sys + +rpc, adapter, frontend = (Path(path).read_text() for path in sys.argv[1:]) +for action in ('install', 'remove'): + method = f'apps_{action}' + assert re.search(rf'{method}:\s*\{{\s*args:\s*\{{\s*app_id:\s*', rpc), method + assert f"'{method}', {{ app_id: appId }}" in frontend, method +assert 'const app_id = request?.args?.app_id;' in adapter +assert 'const app_id = request?.app_id;' not in adapter +assert 'match(app_id, /^[a-z0-9][a-z0-9-]{0,79}$/)' in adapter +assert "run_command([ MANAGER, 'start', action, app_id ], 5000)" in adapter +PY_RPC + +# Fake only the HTTP/JSON filter utilities: no external calls or OpenWrt runtime +# are needed to verify identity pinning and mismatch/failure handling. +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT HUP INT TERM +mkdir -p "$TMP/bin" "$TMP/runtime" +cat > "$TMP/firmware.json" <<'EOF' +{"device_code":"iptime-ax3000sm","build_id":"2026.10.20-123","channel":"stable"} +EOF +cat > "$TMP/response.json" <<'EOF' +{"schema":1,"device_code":"iptime-ax3000sm","firmware_build_id":"2026.10.20-123","channel":"stable","apps":[]} +EOF +cat > "$TMP/bin/jsonfilter" <<'PY' +#!/usr/bin/env python3 +import json,sys +args=sys.argv +obj=json.load(open(args[args.index('-i')+1],encoding='utf-8')) +for key in args[args.index('-e')+1].removeprefix('@.').split('.'): + obj=obj[key] +print(obj if not isinstance(obj,bool) else str(obj).lower()) +PY +cat > "$TMP/bin/uclient-fetch" <<'SH2' +#!/bin/sh +[ "${APPS_TEST_FAIL:-0}" = 0 ] || exit 1 +while [ "$#" -gt 0 ]; do + case "$1" in -O) dest="$2"; shift 2;; *) url="$1"; shift;; esac +done +printf '%s\n' "$url" > "$APPS_TEST_URL_FILE" +cp "$APPS_TEST_RESPONSE" "$dest" +SH2 +chmod +x "$TMP/bin/jsonfilter" "$TMP/bin/uclient-fetch" +export SMARTSAFEHUB_APPS_ROM_METADATA="$TMP/firmware.json" +export SMARTSAFEHUB_APPS_METADATA="$TMP/firmware.json" +export SMARTSAFEHUB_APPS_RUNTIME_DIR="$TMP/runtime" +export SMARTSAFEHUB_APPS_JSONFILTER_BIN="$TMP/bin/jsonfilter" +export SMARTSAFEHUB_APPS_UCLIENT_BIN="$TMP/bin/uclient-fetch" +export APPS_TEST_RESPONSE="$TMP/response.json" +export APPS_TEST_URL_FILE="$TMP/url" +"$HELPER" || fail 'valid pinned catalog fetch failed' +grep -Fq 'firmware_build_id=2026.10.20-123&channel=stable' "$TMP/url" || fail 'local identity missing in request' +grep -Fq '"apps":[]' "$TMP/runtime/apps-catalog.json" || fail 'catalog not stored' + +# Wrong firmware builds must never be treated as approved. +sed 's/2026.10.20-123/2026.10.20-999/' "$TMP/response.json" > "$TMP/mismatch.json" +export APPS_TEST_RESPONSE="$TMP/mismatch.json" +if "$HELPER"; then fail 'mismatched firmware catalog accepted'; fi +export APPS_TEST_FAIL=1 +if "$HELPER"; then fail 'offline catalog fetch accepted'; fi +unset APPS_TEST_FAIL +cat > "$TMP/firmware.json" <<'EOF' +{"device_code":"iptime-ax3000sm","build_id":"../../bad","channel":"stable"} +EOF +if "$HELPER"; then fail 'invalid firmware identity accepted'; fi +printf 'PASS: Apps navigation, catalog pinning and APK RPC contracts\n' diff --git a/luci-app-smartsafehub/tests/test-apps-launch.sh b/luci-app-smartsafehub/tests/test-apps-launch.sh new file mode 100755 index 000000000..240496618 --- /dev/null +++ b/luci-app-smartsafehub/tests/test-apps-launch.sh @@ -0,0 +1,72 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +set -eu +ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +python3 - "$ROOT" <<'PY' +from pathlib import Path +import re +import sys +root = Path(sys.argv[1]) +ui = (root/'frontend/src/pages/AppsPage.tsx').read_text() +types = (root/'frontend/src/types/apps.ts').read_text() +route = (root/'frontend/src/utils/appsWebUi.ts').read_text() +catalog = (root/'root/usr/share/rpcd/ucode/smartsafehub/apps.uc').read_text() +manager = (root/'root/usr/libexec/smartsafehub-apps-manager').read_text() +status = (root/'root/usr/share/rpcd/ucode/smartsafehub/apps-manager.uc').read_text() +assert 'target="_blank"' in ui and 'rel="noopener noreferrer"' in ui +assert 'approvedAppPath(app.webUi)' in ui and 'state?.webUiReady' in ui +assert '웹 UI 미설치' in ui and '설치 마무리' in ui +assert 'onClick={() => dismissJob(jobKey)}' in ui +assert 'useState(readDismissedAppsJob)' in ui +assert 'window.setTimeout(() => dismissJob(jobKey), 7000)' in ui +assert 'webUiReady?: boolean' in types and 'webUi?:' in types +assert 'operationId?: string' in types +assert 'updateAvailable?: boolean' in types +assert "updateAvailable: length(fields) >= 6 && fields[5] == '1'" in status +# rpcd must accept both existing timestamp-pid jobs and new receipt IDs. +assert r'^[0-9]{1,12}-[0-9]{1,12}(-[0-9a-f]{16})?$' in status +assert 'od -An' not in manager +assert "if (webUi.type !== 'luci')" in route +assert "'/cgi-bin/luci/admin/services/nlbw/display'" in catalog +assert 'luci_web_ui_path(item.web_ui.path)' in catalog +assert 'luci-app-nlbwmon' in manager +# Hub-approved Wake-on-LAN must route only to its matching LuCI app, never +# another administrator view or to the backend etherwake executable. +assert "id == 'wake-on-lan'" in catalog +assert "item?.web_ui?.package == 'luci-app-wol'" in catalog +assert "item?.web_ui?.path == '/cgi-bin/luci/admin/services/wol'" in catalog +assert '[ "$id" = wake-on-lan ]' in manager +assert 'web_ui.package")" = luci-app-wol' in manager +assert 'web_ui.path")" = /cgi-bin/luci/admin/services/wol' in manager +assert '[ "$app" = wake-on-lan ]' in manager +assert 'for required in etherwake luci-app-wol' in manager +assert 'primary_package=luci-app-wol' in manager +assert 'rm -f "$LUCI_INDEX_CACHE"' in manager +category = (root/'frontend/src/components/AppsCategoryIcon.tsx').read_text() +page = (root/'frontend/src/pages/AppsPage.tsx').read_text() +assert "case 'utilities': return 'utilities'" in category +assert "case 'utilities': return = 5 && fields[4] == '1'" in status +allowed = re.compile(r'^/cgi-bin/luci/admin/[a-z0-9_-]+(?:/[a-z0-9_-]+)*$') +assert allowed.fullmatch('/cgi-bin/luci/admin/services/nlbw/display') +for bad in ('https://evil.example/', '//evil.example/', '/cgi-bin/luci/admin/../logout', + '/cgi-bin/luci/admin/%2e%2e', '/cgi-bin/luci/admin/services/x?url=http', + '/cgi-bin/luci/admin/services/x#fragment', '/cgi-bin/luci/admin//services/x'): + assert not allowed.fullmatch(bad), bad +PY +# Check TypeScript helper without producing frontend build files. +if command -v tsc >/dev/null 2>&1; then + tsc --noEmit --skipLibCheck --target es2020 "$ROOT/frontend/src/utils/appsWebUi.ts" +fi +echo 'PASS: Apps secure new-tab launch and notification contracts' diff --git a/luci-app-smartsafehub/tests/test-apps-manager.sh b/luci-app-smartsafehub/tests/test-apps-manager.sh new file mode 100755 index 000000000..7f2397953 --- /dev/null +++ b/luci-app-smartsafehub/tests/test-apps-manager.sh @@ -0,0 +1,728 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +set -eu +ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +MANAGER="$ROOT/root/usr/libexec/smartsafehub-apps-manager" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT HUP INT TERM +fail() { echo "FAIL: $*" >&2; exit 1; } +mkdir -p "$TMP/bin" "$TMP/runtime" "$TMP/owned" "$TMP/repo" "$TMP/overlay" +# A default OpenWrt BusyBox image does not provide od. Simulate that +# environment even if the developer workstation has GNU/coreutils od. +cat > "$TMP/bin/od" <<'SH_OD' +#!/bin/sh +exit 127 +SH_OD +chmod +x "$TMP/bin/od" +PATH="$TMP/bin:$PATH" +export PATH +# Linux /proc is unavailable on macOS. A fixture keeps the production check enabled. +printf 'MemTotal: 262144 kB\n' > "$TMP/meminfo" +printf 'all\naarch64_cortex-a53\n' > "$TMP/apk-arch" +printf 'signed immutable package-index test fixture\n' > "$TMP/repo/packages.adb" +printf 'signed mock package payload\n' > "$TMP/repo/nlbwmon-2025.06.02~29236be6-r1.apk" +printf 'approved LuCI mock package payload\n' > "$TMP/repo/luci-app-nlbwmon-26.288.56989~75a306d.apk" +printf 'approved chart mock package payload\n' > "$TMP/repo/luci-lib-chartjs-26.288.56989~75a306d.apk" +printf 'approved Retro Arcade mock package payload\n' > "$TMP/repo/smartsafehub-app-retro-arcade-1.0.0-r1.apk" +printf 'approved etherwake mock package payload\n' > "$TMP/repo/etherwake-1.09-r5.apk" +printf 'approved WOL LuCI mock package payload\n' > "$TMP/repo/luci-app-wol-26.288.56989~75a306d.apk" +INDEX_SHA="$(sha256sum "$TMP/repo/packages.adb" | cut -d ' ' -f 1)" +ARCHIVE_SHA="$(sha256sum "$TMP/repo/nlbwmon-2025.06.02~29236be6-r1.apk" | cut -d ' ' -f 1)" +LUCISH="$(sha256sum "$TMP/repo/luci-app-nlbwmon-26.288.56989~75a306d.apk" | cut -d ' ' -f 1)" +CHARTSHA="$(sha256sum "$TMP/repo/luci-lib-chartjs-26.288.56989~75a306d.apk" | cut -d ' ' -f 1)" +RETROSHA="$(sha256sum "$TMP/repo/smartsafehub-app-retro-arcade-1.0.0-r1.apk" | cut -d ' ' -f 1)" +ETHERSHA="$(sha256sum "$TMP/repo/etherwake-1.09-r5.apk" | cut -d ' ' -f 1)" +WOLSHA="$(sha256sum "$TMP/repo/luci-app-wol-26.288.56989~75a306d.apk" | cut -d ' ' -f 1)" +cat > "$TMP/catalog.json" < "$TMP/bin/jsonfilter" <<'SH2' +#!/bin/sh +if command -v jq >/dev/null 2>&1; then + file=''; expr='' + while [ "$#" -gt 0 ]; do + case "$1" in + -i) file="$2"; shift 2;; + -e) expr="$2"; shift 2;; + *) exit 2;; + esac + done + case "$expr" in + '@.apps[*].id') jq_expr='.apps[].id';; + @.*) jq_expr="${expr#@}";; + *) exit 2;; + esac + jq -r "($jq_expr) | select(. != null)" "$file" 2>/dev/null +else + exec python3 "$0.py" "$@" +fi +SH2 +cat > "$TMP/bin/jsonfilter.py" <<'PY' +import json, re, sys +args=sys.argv +obj=json.load(open(args[args.index('-i')+1])) +expression=args[args.index('-e')+1] +if expression == '@.apps[*].id': + for app in obj.get('apps',[]): print(app['id']) +else: + try: + for part in re.findall(r'[^.\[\]]+|\[\d+\]', expression[2:]): + obj = obj[int(part[1:-1])] if part.startswith('[') else obj[part] + if isinstance(obj,(str,int,float)): print(obj) + except (KeyError,IndexError,TypeError): pass +PY +cat > "$TMP/bin/fetch-catalog" <<'SH2' +#!/bin/sh +cp "$APPS_TEST_CATALOG" "$SMARTSAFEHUB_APPS_RUNTIME_DIR/apps-catalog.json" +SH2 +cat > "$TMP/bin/uclient-fetch" <<'SH2' +#!/bin/sh +while [ "$#" -gt 0 ]; do + case "$1" in + -O) dest="$2"; shift 2;; + -q|-T) [ "$1" != -T ] || shift; shift;; + *) url="$1"; shift;; + esac +done +case "$url" in + https://repo.smartsafehub.com/stable/packages/aarch64_cortex-a53/smartsafehub/20261027T123000/packages.adb) + cp "$APPS_TEST_REPO/packages.adb" "$dest";; + https://repo.smartsafehub.com/stable/packages/aarch64_cortex-a53/smartsafehub/20261027T123000/nlbwmon-2025.06.02~29236be6-r1.apk) + cp "$APPS_TEST_REPO/nlbwmon-2025.06.02~29236be6-r1.apk" "$dest";; + https://repo.smartsafehub.com/stable/packages/aarch64_cortex-a53/smartsafehub/20261027T123000/luci-app-nlbwmon-26.288.56989~75a306d.apk) + cp "$APPS_TEST_REPO/luci-app-nlbwmon-26.288.56989~75a306d.apk" "$dest";; + https://repo.smartsafehub.com/stable/packages/aarch64_cortex-a53/smartsafehub/20261027T123000/luci-lib-chartjs-26.288.56989~75a306d.apk) + cp "$APPS_TEST_REPO/luci-lib-chartjs-26.288.56989~75a306d.apk" "$dest";; + https://repo.smartsafehub.com/stable/packages/aarch64_cortex-a53/smartsafehub/20261027T123000/smartsafehub-app-retro-arcade-1.0.0-r1.apk) + cp "$APPS_TEST_REPO/smartsafehub-app-retro-arcade-1.0.0-r1.apk" "$dest";; + https://repo.smartsafehub.com/stable/packages/aarch64_cortex-a53/smartsafehub/20261027T123000/smartsafehub-app-retro-arcade-1.1.0-r1.apk) + cp "$APPS_TEST_REPO/smartsafehub-app-retro-arcade-1.1.0-r1.apk" "$dest";; + https://repo.smartsafehub.com/stable/packages/aarch64_cortex-a53/smartsafehub/20261027T123000/etherwake-1.09-r5.apk) + cp "$APPS_TEST_REPO/etherwake-1.09-r5.apk" "$dest";; + https://repo.smartsafehub.com/stable/packages/aarch64_cortex-a53/smartsafehub/20261027T123000/luci-app-wol-26.288.56989~75a306d.apk) + cp "$APPS_TEST_REPO/luci-app-wol-26.288.56989~75a306d.apk" "$dest";; + *) exit 2;; +esac +printf '%s\n' "$url" >> "$APPS_TEST_URL_LOG" +SH2 +cat > "$TMP/bin/apk" <<'SH2' +#!/bin/sh +printf '%s\n' "$*" >> "$APPS_TEST_APK_LOG" +case "$1" in + --print-arch) [ "${APPS_TEST_ARCH_CLI_FAIL:-0}" = 0 ] || exit 1; echo "${APPS_TEST_ARCH_CLI:-aarch64_cortex-a53}"; exit 0;; + version) + [ "$2" = -t ] || exit 2 + case "$3:$4" in + 1.0.0-r1:1.1.0-r1) echo '<';; + 1.1.0-r1:1.0.0-r1) echo '>';; + *) [ "$3" = "$4" ] && echo '=' || exit 2;; + esac + exit 0;; + info) + if [ "$2" = -s ]; then + shift 2 + [ "${APPS_TEST_SIZE_MISSING:-0}" = 0 ] || exit 2 + [ "$#" -eq 1 ] || exit 2 + [ "${APPS_TEST_SIZE_FULL_INFO_ONLY:-0}" = 0 ] || exit 2 + for name in "$@"; do + grep -qxF "$name" "$APPS_TEST_INSTALLED" 2>/dev/null || exit 2 + [ "${APPS_TEST_SIZE_MISSING_PACKAGE:-}" != "$name" ] || exit 2 + # Device capture (OpenWrt 25.12 APK): repeating the same installed + # size twice or three times is valid, but conflicting values are not. + # During an update the APK index can expose old and new versions. + # Only the installed version should determine the displayed size. + if [ "${APPS_TEST_SIZE_MIXED_RELEASES:-0}" != 0 ] && + [ "$name" = smartsafehub-app-retro-arcade ]; then + printf '%s-1.0.0-r1 installed size:\n1362 KiB\n\n' "$name" + printf '%s-1.1.0-r1 installed size:\n2048 KiB\n\n' "$name" + if [ "$APPS_TEST_SIZE_MIXED_RELEASES" = conflicting ]; then + printf '%s-1.1.0-r1 installed size:\n2096 KiB\n\n' "$name" + fi + continue + fi + if [ "${APPS_TEST_SIZE_DEVICE_REPEAT:-0}" = 1 ]; then + case "$name" in + nlbwmon) version=2025.06.02~29236be6-r1; kib=46; copies=2;; + luci-app-nlbwmon) version=26.288.56989~75a306d; kib=36; copies=3;; + luci-lib-chartjs) version=26.288.56989~75a306d; kib=33; copies=3;; + *) exit 2;; + esac + i=0 + while [ "$i" -lt "$copies" ]; do + value="$kib" + if [ "${APPS_TEST_SIZE_CONFLICTING_REPEAT:-0}" = 1 ] && + [ "$name" = nlbwmon ] && [ "$i" -eq 1 ]; then + value=47 + fi + printf '%s-%s installed size:\n%s KiB\n\n' "$name" "$version" "$value" + i=$((i + 1)) + done + continue + fi + if [ "${APPS_TEST_SIZE_INLINE:-0}" = 1 ]; then + case "$name" in + nlbwmon) printf '%s-1.0.0-r1 installed size: 1.5 MiB\n' "$name";; + luci-app-nlbwmon) printf '%s-1.0.0-r1 installed size: 512KiB\n' "$name";; + luci-lib-chartjs) printf '%s-1.0.0-r1 installed size: 256 KiB\n' "$name";; + smartsafehub-app-retro-arcade) printf '%s-1.0.0-r1 installed size: 12.5 MiB\n' "$name";; + esac + continue + fi + printf '%s-1.0.0-r1 installed size:\n' "$name" + if [ "${APPS_TEST_SIZE_INVALID:-0}" = 1 ]; then + echo 'not-a-size'; continue + fi + case "$name" in + nlbwmon) printf '1.5 MiB\n';; + luci-app-nlbwmon) printf '512 KiB\n';; + luci-lib-chartjs) printf '256 KiB\n';; + smartsafehub-app-retro-arcade) printf '12.5 MiB\n';; + *) printf '0 B\n';; + esac + done + exit 0 + fi + if [ "$#" -eq 2 ] && [ "${APPS_TEST_SIZE_FULL_INFO_ONLY:-0}" = 1 ]; then + name="$2" + grep -qxF "$name" "$APPS_TEST_INSTALLED" 2>/dev/null || exit 2 + printf '%s-1.0.0-r1 description:\nMock metadata\n\n%s-1.0.0-r1 installed size:\n' "$name" "$name" + case "$name" in + nlbwmon) printf '1.5 MiB\n';; + luci-app-nlbwmon) printf '512 KiB\n';; + luci-lib-chartjs) printf '256 KiB\n';; + *) exit 2;; + esac + exit 0 + fi + [ "$2" = -e ] || exit 2 + grep -qxF "$3" "$APPS_TEST_INSTALLED" 2>/dev/null; exit $?;; + list) + [ "$2" = -I ] || exit 2 + grep -qxF "$3" "$APPS_TEST_INSTALLED" 2>/dev/null || exit 0 + if [ -s "$APPS_TEST_VERSION_DB" ]; then + version="$(awk -v n="$3" '$1 == n {print $2; exit}' "$APPS_TEST_VERSION_DB")" + [ -z "$version" ] || { echo "$3-$version aarch64_cortex-a53 [installed]"; exit 0; } + fi + if [ "$3" = etherwake ]; then + echo 'etherwake-1.09-r5 aarch64_cortex-a53 [installed]' + else + echo "$3-${APPS_TEST_SHARED_VERSION:-26.288.56989~75a306d} aarch64_cortex-a53 [installed]" + fi + exit 0;; + --repositories-file) + [ "$2" = /dev/null ] || exit 2 + [ "$3" = -X ] || exit 2 + case "$4" in + file://*/apps-work.*/packages/packages.adb) repo=hub; local_index="${4#file://}";; + *) exit 2;; + esac + [ -s "$local_index" ] || exit 2 + cmp -s "$local_index" "$APPS_TEST_REPO/packages.adb" || exit 2 + if [ -s "${local_index%/*}/smartsafehub-app-retro-arcade-1.0.0-r1.apk" ] || + [ -s "${local_index%/*}/smartsafehub-app-retro-arcade-1.1.0-r1.apk" ]; then + [ "$(find "${local_index%/*}" -name '*.apk' | wc -l | tr -d ' ')" -eq 1 ] || exit 2 + elif [ -s "${local_index%/*}/etherwake-1.09-r5.apk" ] || + [ -s "${local_index%/*}/luci-app-wol-26.288.56989~75a306d.apk" ]; then + [ "$(find "${local_index%/*}" -name '*.apk' | wc -l | tr -d ' ')" -eq 2 ] || exit 2 + for name in etherwake-1.09-r5 luci-app-wol-26.288.56989~75a306d; do + [ -s "${local_index%/*}/$name.apk" ] || exit 2 + done + else + for name in nlbwmon-2025.06.02~29236be6-r1 \ + luci-app-nlbwmon-26.288.56989~75a306d \ + luci-lib-chartjs-26.288.56989~75a306d; do + [ -s "${local_index%/*}/$name.apk" ] || exit 2 + done + fi + if [ "$5" = update ]; then + if [ "${APPS_TEST_REPO_UPDATE_FAIL:-0}" = 1 ]; then + echo 'ERROR: repository index update rejected' >&2 + exit 1 + fi + touch "$APPS_TEST_INDEX_READY" + exit 0 + fi + [ "$5" = add ] || exit 2 + [ -f "$APPS_TEST_INDEX_READY" ] || { + echo 'WARNING: opening from cache: No such file or directory' >&2 + exit 1 + } + shift 5;; + del) + if [ "$2" = --simulate ]; then + [ "${APPS_TEST_DEL_CORE:-0}" = 1 ] && echo 'Purging kmod-nf-conntrack-netlink (mock)' + [ "${APPS_TEST_DEL_SHARED:-0}" = 1 ] && echo 'Purging luci-lib-chartjs (mock)' + exit 0 + fi + shift + for item in "$@"; do grep -vxF "$item" "$APPS_TEST_INSTALLED" > "$APPS_TEST_INSTALLED.new" || :; mv "$APPS_TEST_INSTALLED.new" "$APPS_TEST_INSTALLED"; done + exit 0;; + *) exit 2;; +esac +upgrade=0 +if [ "${1:-}" = --upgrade ]; then upgrade=1; shift; fi +if [ "${1:-}" = --simulate ]; then + [ "${APPS_TEST_MISSING_DEP:-0}" = 1 ] && { echo 'ERROR: unable to select packages: missing dependency' >&2; exit 1; } + shift + for pin in "$@"; do + name="${pin%%=*}" + case "$repo:$name" in + hub:nlbwmon|hub:luci-app-nlbwmon|hub:luci-lib-chartjs|hub:smartsafehub-app-retro-arcade|hub:etherwake|hub:luci-app-wol) ;; + *) exit 2;; + esac + if [ "${APPS_TEST_UPGRADE:-0}" = 1 ]; then + echo '(1/3) Upgrading luci-base (mock)' + elif [ "$upgrade" = 1 ]; then + echo "(1/1) Upgrading $name (mock)" + else + echo "(1/2) Installing $name (mock)" + fi + done + exit 0 +fi +for pin in "$@"; do + name="${pin%%=*}" + case "$repo:$name" in + hub:nlbwmon|hub:luci-app-nlbwmon|hub:luci-lib-chartjs|hub:smartsafehub-app-retro-arcade|hub:etherwake|hub:luci-app-wol) ;; + *) exit 2;; + esac + grep -qxF "$name" "$APPS_TEST_INSTALLED" 2>/dev/null || printf '%s\n' "$name" >> "$APPS_TEST_INSTALLED" + if [ "$upgrade" = 1 ]; then + awk -v n="$name" '$1 != n' "$APPS_TEST_VERSION_DB" > "$APPS_TEST_VERSION_DB.tmp" + printf '%s %s\n' "$name" "${pin#*=}" >> "$APPS_TEST_VERSION_DB.tmp" + mv -f "$APPS_TEST_VERSION_DB.tmp" "$APPS_TEST_VERSION_DB" + fi +done +SH2 +cat > "$TMP/bin/start-stop-daemon" <<'SH2' +#!/bin/sh +[ "$1" = -S ] && [ "$2" = -b ] && [ "$3" = -x ] && [ "$5" = -- ] || exit 2 +# Starting and final states must carry the same operation ID. +awk -F '\t' '$1 == "operation_id" { print $2; exit }' \ + "$SMARTSAFEHUB_APPS_RUNTIME_DIR/apps-operation.state" >> "$APPS_TEST_OPERATION_START_LOG" +"$4" "$6" "$7" "$8" +SH2 +chmod +x "$TMP/bin/"* +cat > "$TMP/bin/metrics-hook" <<'METRICS' +#!/bin/sh +printf '%s +' "$*" >> "$APPS_TEST_METRICS_LOG" +METRICS +chmod +x "$TMP/bin/metrics-hook" +export APPS_TEST_METRICS_LOG="$TMP/install-events" +export SMARTSAFEHUB_APPS_METRICS_BIN="$TMP/bin/metrics-hook" +: > "$APPS_TEST_METRICS_LOG" +export SMARTSAFEHUB_APPS_MANAGER_BIN="$MANAGER" +export SMARTSAFEHUB_APPS_HELPER_BIN="$TMP/bin/fetch-catalog" +export SMARTSAFEHUB_APPS_RUNTIME_DIR="$TMP/runtime" +export SMARTSAFEHUB_APPS_OWNED_DIR="$TMP/owned" +export SMARTSAFEHUB_APPS_UCLIENT_BIN="$TMP/bin/uclient-fetch" +export APPS_TEST_URL_LOG="$TMP/fetched-urls" +export SMARTSAFEHUB_APPS_OVERLAY_PATH="$TMP/overlay" +export SMARTSAFEHUB_APPS_MEMINFO_PATH="$TMP/meminfo" +export SMARTSAFEHUB_APPS_JSONFILTER_BIN="$TMP/bin/jsonfilter" +export SMARTSAFEHUB_APPS_APK_BIN="$TMP/bin/apk" +export SMARTSAFEHUB_APPS_ARCH_FILE="$TMP/apk-arch" +export SMARTSAFEHUB_APPS_DAEMON_BIN="$TMP/bin/start-stop-daemon" +export SMARTSAFEHUB_APPS_LUCI_INDEX_CACHE="$TMP/luci-indexcache" +export APPS_TEST_CATALOG="$TMP/catalog.json" +export APPS_TEST_REPO="$TMP/repo" +export APPS_TEST_INSTALLED="$TMP/installed" +export APPS_TEST_APK_LOG="$TMP/apk-commands" +export APPS_TEST_INDEX_READY="$TMP/index-ready" +export APPS_TEST_VERSION_DB="$TMP/installed-versions" +: > "$APPS_TEST_VERSION_DB" +export APPS_TEST_OPERATION_START_LOG="$TMP/operation-start-ids" +: > "$APPS_TEST_INSTALLED" +# A validated fixed release installs all three APKs from a single signed feed. +"$MANAGER" start install '../../../../etc/passwd' >/dev/null 2>&1 && fail 'invalid app ID accepted' +"$MANAGER" start install 'not-approved' || fail 'unknown app should queue' +grep -Fqx "$(printf 'code\tE22')" "$TMP/runtime/apps-operation.state" || fail 'unknown app was installed' +first_operation_id="$(awk -F '\t' '$1 == "operation_id" { print $2; exit }' "$TMP/runtime/apps-operation.state")" +[ -n "$first_operation_id" ] || fail 'first failed operation has no identity' +printf '%s\n' "$first_operation_id" | grep -Eq '^[0-9]{10}-[0-9]+-[0-9a-f]{16}$' || fail 'operation ID must include a valid random nonce without od' +[ "$(sed -n '1p' "$APPS_TEST_OPERATION_START_LOG")" = "$first_operation_id" ] || fail 'operation identity changed during failure' +"$MANAGER" start install not-approved || fail 'repeated failed install did not queue' +repeat_operation_id="$(awk -F '\t' '$1 == "operation_id" { print $2; exit }' "$TMP/runtime/apps-operation.state")" +[ "$repeat_operation_id" != "$first_operation_id" ] || fail 'repeated E22 failure reused dismissed operation ID' +[ "$(sed -n '2p' "$APPS_TEST_OPERATION_START_LOG")" = "$repeat_operation_id" ] || fail 'repeated failure changed identity between phases' +"$MANAGER" start install network-usage-monitor || fail 'install did not queue' +grep -Fqx "$(printf 'phase\tsuccess')" "$TMP/runtime/apps-operation.state" || fail 'three APK installation failed' +second_operation_id="$(awk -F '\t' '$1 == "operation_id" { print $2; exit }' "$TMP/runtime/apps-operation.state")" +[ -n "$second_operation_id" ] && [ "$first_operation_id" != "$second_operation_id" ] || fail 'new operation reused old identity' +[ "$(sed -n '3p' "$APPS_TEST_OPERATION_START_LOG")" = "$second_operation_id" ] || fail 'operation identity changed during success' +[ "$(wc -l < "$APPS_TEST_INSTALLED")" -eq 3 ] || fail 'all three APKs were not installed' +[ "$(wc -l < "$APPS_TEST_METRICS_LOG" | tr -d ' ')" -eq 1 ] || fail 'successful installation not recorded exactly once' +grep -Eq '^enqueue network-usage-monitor 2025\.06\.02-r1 [0-9]+-[0-9]+-[a-f0-9]{16} install$' "$APPS_TEST_METRICS_LOG" || fail 'invalid install receipt fields' + +grep -Eq '^--repositories-file /dev/null -X file://.*/packages/packages.adb update$' "$APPS_TEST_APK_LOG" || fail 'approved local index not loaded before simulation' +grep -Eq '^--repositories-file /dev/null -X file://.*/packages/packages.adb add --simulate nlbwmon=2025\.06\.02~29236be6-r1 luci-app-nlbwmon=26\.288\.56989~75a306d luci-lib-chartjs=26\.288\.56989~75a306d$' "$APPS_TEST_APK_LOG" || fail 'isolated signed repository simulation not used' +grep -Eq '^--repositories-file /dev/null -X file://.*/packages/packages.adb add nlbwmon=' "$APPS_TEST_APK_LOG" || fail 'isolated signed repository install not used' +# A failed random-source read must never launch a removal or leave a lock. +# This also guards against accidentally generating a fixed nonce from empty data. +cat > "$TMP/bin/dd" <<'SH_DD' +#!/bin/sh +exit 1 +SH_DD +chmod +x "$TMP/bin/dd" +if "$MANAGER" start remove network-usage-monitor >/dev/null 2>&1; then + fail 'failed entropy read started a removal' +fi +rm -f "$TMP/bin/dd" +[ ! -e "$TMP/runtime/apps-operation.lock" ] || fail 'failed entropy read left a lock' +grep -Fqx "$(printf 'phase\tsuccess')" "$TMP/runtime/apps-operation.state" || fail 'failed launch overwrote existing operation' +[ "$(wc -l < "$APPS_TEST_INSTALLED" | tr -d ' ')" -eq 3 ] || fail 'failed entropy read touched installed APKs' +"$MANAGER" status +grep -Fq "$(printf 'network-usage-monitor\t1\t1\t1\t1')" "$TMP/runtime/apps-status.tsv" || fail 'ready state missing' +grep -Fq "$(printf 'network-usage-monitor\t1\t1\t1\t1\t0\t')" "$TMP/runtime/apps-status.tsv" || fail 'installed state changed' +# Optional size lookup must never run on the critical installation-status path. +[ "$(grep -c '^info -s ' "$APPS_TEST_APK_LOG" || :)" -eq 0 ] || fail 'status unexpectedly queried APK sizes' +"$MANAGER" sizes +grep -Fqx "$(printf 'network-usage-monitor\t2359296')" "$TMP/runtime/apps-size.tsv" || fail 'multi-package installed size not summed' +[ "$(grep -c '^info -s ' "$APPS_TEST_APK_LOG" || :)" -eq 3 ] || fail 'each network APK size must be queried separately' +for package in nlbwmon luci-app-nlbwmon luci-lib-chartjs; do + grep -Fqx "info -s $package" "$APPS_TEST_APK_LOG" || fail "missing individual size lookup: $package" +done +# Standard apk info must work when this apk v3 variant does not expose -s. +export APPS_TEST_SIZE_FULL_INFO_ONLY=1 +"$MANAGER" sizes +grep -Fqx "$(printf 'network-usage-monitor\t2359296')" "$TMP/runtime/apps-size.tsv" || fail 'full APK info fallback failed' +unset APPS_TEST_SIZE_FULL_INFO_ONLY +# OpenWrt APK versions may render the size on the header line or with no +# whitespace between the number and KiB. The sum must be identical. +export APPS_TEST_SIZE_INLINE=1 +"$MANAGER" sizes +grep -Fqx "$(printf 'network-usage-monitor\t2359296')" "$TMP/runtime/apps-size.tsv" || fail 'inline/compact installed size not summed' +[ ! -e "$TMP/runtime/apps-size-diagnostics.tsv" ] || fail 'retired size diagnostic file was created' +unset APPS_TEST_SIZE_INLINE +# Real OpenWrt 25.12 device output repeats the *same* installed-size header: +# nlbwmon twice, luci-app-nlbwmon / luci-lib-chartjs three times each. +export APPS_TEST_SIZE_DEVICE_REPEAT=1 +"$MANAGER" sizes +grep -Fqx "$(printf 'network-usage-monitor\t117760')" "$TMP/runtime/apps-size.tsv" || fail 'real duplicated APK info output did not sum to 115 KiB' +[ ! -e "$TMP/runtime/apps-size-diagnostics.tsv" ] || fail 'repeated APK size metadata wrote a diagnostic file' +# A repeated heading with a *different* size is ambiguous: refuse to guess. +export APPS_TEST_SIZE_CONFLICTING_REPEAT=1 +"$MANAGER" sizes +[ ! -s "$TMP/runtime/apps-size.tsv" ] || fail 'conflicting duplicated size was accepted' +[ ! -e "$TMP/runtime/apps-size-diagnostics.tsv" ] || fail 'conflicting size metadata wrote a diagnostic file' +unset APPS_TEST_SIZE_CONFLICTING_REPEAT APPS_TEST_SIZE_DEVICE_REPEAT +# Do not show a misleading partial sum if one package cannot be measured. +export APPS_TEST_SIZE_MISSING_PACKAGE=luci-lib-chartjs +"$MANAGER" sizes +[ ! -s "$TMP/runtime/apps-size.tsv" ] || fail 'partial APK metadata reported as complete installed size' +[ ! -e "$TMP/runtime/apps-size-diagnostics.tsv" ] || fail 'missing package size wrote a diagnostic file' +unset APPS_TEST_SIZE_MISSING_PACKAGE +# Malformed and missing metadata must not alter the actual installed state. +export APPS_TEST_SIZE_INVALID=1 +"$MANAGER" sizes +[ ! -s "$TMP/runtime/apps-size.tsv" ] || fail 'invalid APK size reported as bytes' +"$MANAGER" status +grep -Fq "$(printf 'network-usage-monitor\t1\t1\t1\t1\t0\t')" "$TMP/runtime/apps-status.tsv" || fail 'invalid size changed app state' +unset APPS_TEST_SIZE_INVALID +export APPS_TEST_SIZE_MISSING=1 +"$MANAGER" sizes +[ ! -s "$TMP/runtime/apps-size.tsv" ] || fail 'missing APK size must be omitted' +[ ! -e "$TMP/runtime/apps-size-diagnostics.tsv" ] || fail 'invalid/missing APK size created retired diagnostics' +unset APPS_TEST_SIZE_MISSING +"$MANAGER" start remove network-usage-monitor || fail 'remove did not queue' +[ ! -s "$APPS_TEST_INSTALLED" ] || fail 'new Apps uninstall did not remove owned packages' +[ "$(wc -l < "$APPS_TEST_METRICS_LOG" | tr -d ' ')" -eq 1 ] || fail 'removal counted as an installation' + +cp "$TMP/catalog.json" "$TMP/catalog-good.json" +python3 - "$TMP/catalog.json" <<'PY_BAD' +import json,sys +path=sys.argv[1]; obj=json.load(open(path)); obj['apps'][0].pop('repository_release_id') +open(path,'w').write(json.dumps(obj)) +PY_BAD +"$MANAGER" start install network-usage-monitor || fail 'missing release ID did not queue' +grep -Fqx "$(printf 'code\tE25')" "$TMP/runtime/apps-operation.state" || fail 'missing release ID accepted' +cp "$TMP/catalog-good.json" "$TMP/catalog.json" +python3 - "$TMP/catalog.json" <<'PY_BAD' +import json,sys +path=sys.argv[1]; obj=json.load(open(path)); obj['apps'][0]['packages'].pop() +open(path,'w').write(json.dumps(obj)) +PY_BAD +"$MANAGER" start install network-usage-monitor || fail 'partial package set did not queue' +grep -Fqx "$(printf 'code\tE27')" "$TMP/runtime/apps-operation.state" || fail 'partial package set accepted' +cp "$TMP/catalog-good.json" "$TMP/catalog.json" +# A repository signature/index failure must fail closed before any install. +export APPS_TEST_REPO_UPDATE_FAIL=1 +rm -f "$APPS_TEST_INDEX_READY" +"$MANAGER" start install network-usage-monitor || fail 'failed repository update did not queue' +grep -Fqx "$(printf 'code\tE34')" "$TMP/runtime/apps-operation.state" || fail 'failed repository update was accepted' +grep -Fq 'repository index update rejected' "$TMP/runtime/apps-last-error.log" || fail 'repository update error was not preserved' +[ ! -s "$APPS_TEST_INSTALLED" ] || fail 'failed repository update installed packages' +unset APPS_TEST_REPO_UPDATE_FAIL +cp "$TMP/catalog-good.json" "$TMP/catalog.json" +printf 'tampered APK\n' > "$TMP/repo/luci-lib-chartjs-26.288.56989~75a306d.apk" +"$MANAGER" start install network-usage-monitor || fail 'tampered APK did not queue' +grep -Fqx "$(printf 'code\tE33')" "$TMP/runtime/apps-operation.state" || fail 'modified APK accepted' +[ "$(wc -l < "$APPS_TEST_METRICS_LOG" | tr -d ' ')" -eq 1 ] || fail 'failed installation counted' + +printf 'PASS: Apps install/remove signed immutable release, three APKs, ownership and hash checks\n' + +# Self-built App publications must install from immutable signed releases. +if ! grep -Fq 'repo_url="$REPO_BASE/$channel/packages/$arch/smartsafehub/$publication"' "$MANAGER"; then + echo 'FAIL: signed immutable Apps package publication is missing' >&2 + exit 1 +fi + +# Retro Arcade is a single APK from the same signed feed. Verify readiness of +# its static entry point independently of the package-manager install state. +: > "$APPS_TEST_INSTALLED" +export SMARTSAFEHUB_APPS_WEB_ROOT="$TMP/www" +cat > "$TMP/catalog.json" < "$APPS_TEST_VERSION_DB" + +"$MANAGER" status +grep -Fq "$(printf 'retro-arcade\t1\t1\t1\t0')" "$TMP/runtime/apps-status.tsv" || fail 'Retro Arcade should not launch before its HTML exists' +mkdir -p "$TMP/www/retro-arcade" +printf 'retro entry\n' > "$TMP/www/retro-arcade/index.html" +"$MANAGER" status +grep -Fq "$(printf 'retro-arcade\t1\t1\t1\t1')" "$TMP/runtime/apps-status.tsv" || fail 'Retro Arcade entry point is not ready' +grep -Fqx "$(printf 'retro-arcade\t1\t1\t1\t1\t0\t1.0.0-r1')" "$TMP/runtime/apps-status.tsv" || fail 'installed Retro version missing' +"$MANAGER" sizes +grep -Fqx "$(printf 'retro-arcade\t13107200')" "$TMP/runtime/apps-size.tsv" || fail 'Retro Arcade size missing' +rm "$TMP/www/retro-arcade/index.html" +"$MANAGER" status +grep -Fq "$(printf 'retro-arcade\t1\t1\t1\t0')" "$TMP/runtime/apps-status.tsv" || fail 'removed Retro Arcade entry point still launches' +# A malformed catalog must not allow more than one approved Retro APK. +python3 - "$TMP/catalog.json" <<'PY_RETRO_BAD' +import json,sys +p=sys.argv[1]; data=json.load(open(p)); data['apps'][0]['packages'].append(data['apps'][0]['packages'][0]); open(p,'w').write(json.dumps(data)) +PY_RETRO_BAD +"$MANAGER" start install retro-arcade || fail 'invalid multi-APK Retro install should queue' +grep -Fqx "$(printf 'code\tE27')" "$TMP/runtime/apps-operation.state" || fail 'multiple Retro packages not rejected' +# Restore the catalog for safe removal of the original installed APK. +python3 - "$TMP/catalog.json" <<'PY_RETRO_GOOD' +import json,sys +p=sys.argv[1]; data=json.load(open(p)); data['apps'][0]['packages'].pop(); open(p,'w').write(json.dumps(data)) +PY_RETRO_GOOD +"$MANAGER" start remove retro-arcade || fail 'Retro Arcade removal did not queue' +[ ! -s "$APPS_TEST_INSTALLED" ] || fail 'Retro Arcade was not uninstalled' +printf 'PASS: Retro Arcade single APK, static entry point readiness and safe removal\n' + +# Restore the mock file deliberately corrupted by the earlier hash test. +printf 'approved chart mock package payload\n' > "$TMP/repo/luci-lib-chartjs-26.288.56989~75a306d.apk" +# A catalog may contain multiple apps. Each app must be visible to the +# installer and status reporter regardless of its position in the list. +: > "$APPS_TEST_INSTALLED" +rm -f "$TMP/owned/retro-arcade" "$TMP/owned/network-usage-monitor" +printf '%s\n' smartsafehub-app-retro-arcade > "$APPS_TEST_INSTALLED" +cat > "$TMP/catalog.json" < "$APPS_TEST_INSTALLED" +rm -f "$TMP/owned/network-usage-monitor" "$TMP/owned/retro-arcade" +: > "$APPS_TEST_VERSION_DB" +cat > "$TMP/catalog.json" < "$APPS_TEST_VERSION_DB" +receipt_before="$(wc -l < "$APPS_TEST_METRICS_LOG" | tr -d ' ')" +printf 'new signed Retro payload\n' > "$TMP/repo/smartsafehub-app-retro-arcade-1.1.0-r1.apk" +NEWRETROSHA="$(sha256sum "$TMP/repo/smartsafehub-app-retro-arcade-1.1.0-r1.apk" | cut -d ' ' -f 1)" +python3 - "$TMP/catalog.json" "$NEWRETROSHA" <<'PY_UPDATE' +import json,sys +p=sys.argv[1]; data=json.load(open(p)); app=data['apps'][0] +app['version']='1.1.0-r1'; app['packages'][0]['version']='1.1.0-r1' +app['packages'][0]['sha256']=sys.argv[2] +open(p,'w').write(json.dumps(data)) +PY_UPDATE +"$TMP/bin/fetch-catalog" +"$MANAGER" status +grep -Fqx "$(printf 'retro-arcade\t1\t1\t1\t0\t1\t1.0.0-r1')" "$TMP/runtime/apps-status.tsv" || fail 'new approved release hid installed 1.0.0-r1' +# Prevent core upgrades even if the APK solver proposes them. +export APPS_TEST_UPGRADE=1 +"$MANAGER" start update retro-arcade || fail 'unrelated package simulation did not queue' +grep -Fqx "$(printf 'code\tE53')" "$TMP/runtime/apps-operation.state" || fail 'unrelated system upgrade was not blocked' +grep -Fq "$(printf 'smartsafehub-app-retro-arcade\t1.0.0-r1\t')" "$TMP/owned/retro-arcade" || fail 'failed simulation changed ownership' +unset APPS_TEST_UPGRADE +# Reject altered signed payloads before mutation. +printf 'tampered\n' > "$TMP/repo/smartsafehub-app-retro-arcade-1.1.0-r1.apk" +"$MANAGER" start update retro-arcade || fail 'tampered update did not queue' +grep -Fqx "$(printf 'code\tE33')" "$TMP/runtime/apps-operation.state" || fail 'tampered update was not rejected' +printf 'new signed Retro payload\n' > "$TMP/repo/smartsafehub-app-retro-arcade-1.1.0-r1.apk" +"$MANAGER" start update retro-arcade || fail 'approved update did not queue' +grep -Fqx "$(printf 'phase\tsuccess')" "$TMP/runtime/apps-operation.state" || fail 'approved update did not succeed' +grep -Fqx "$(printf 'smartsafehub-app-retro-arcade\t1.1.0-r1\t%s' "$NEWRETROSHA")" "$TMP/owned/retro-arcade" || fail 'new version manifest not committed' +[ "$(awk '$1 == "smartsafehub-app-retro-arcade" {print $2}' "$APPS_TEST_VERSION_DB")" = 1.1.0-r1 ] || fail 'mock apk version did not update' +[ "$(wc -l < "$APPS_TEST_METRICS_LOG" | tr -d ' ')" -eq "$((receipt_before + 1))" ] || fail 'successful upgrade not recorded exactly once' +grep -Eq '^enqueue retro-arcade 1\.1\.0-r1 [0-9]+-[0-9]+-[a-f0-9]{16} update$' "$APPS_TEST_METRICS_LOG" || fail 'update receipt kind/version incorrect' +[ "$(awk '$1 == "enqueue" && $5 == "update" { n++ } END {print n+0}' "$APPS_TEST_METRICS_LOG")" -eq 1 ] || fail 'upgrade counted as a new install or multiple updates' +"$MANAGER" status +grep -Fqx "$(printf 'retro-arcade\t1\t1\t1\t0\t0\t1.1.0-r1')" "$TMP/runtime/apps-status.tsv" || fail 'installed version not updated after APK upgrade' +# Update releases may appear alongside older APK index metadata: the size +# must match the installed version, even if the older version appears first. +export APPS_TEST_SIZE_MIXED_RELEASES=1 +"$MANAGER" sizes +grep -Fqx "$(printf 'retro-arcade\t2097152')" "$TMP/runtime/apps-size.tsv" || fail 'updated Retro size lost when APK info listed multiple releases' +# Contradictory records for that same installed version are still rejected. +export APPS_TEST_SIZE_MIXED_RELEASES=conflicting +"$MANAGER" sizes +[ ! -s "$TMP/runtime/apps-size.tsv" ] || fail 'conflicting current APK release size must be omitted' +unset APPS_TEST_SIZE_MIXED_RELEASES +"$MANAGER" start update retro-arcade || fail 'repeated update should report a no-op' +grep -Fqx "$(printf 'code\tE52')" "$TMP/runtime/apps-operation.state" || fail 'repeated update was not rejected' +[ "$(wc -l < "$APPS_TEST_METRICS_LOG" | tr -d ' ')" -eq "$((receipt_before + 1))" ] || fail 'no-op upgrade produced an event' +# Any attempt to downgrade a package must fail closed. +python3 - "$TMP/catalog.json" "$RETROSHA" <<'PY_DOWNGRADE' +import json,sys +p=sys.argv[1]; data=json.load(open(p)); app=data['apps'][0] +app['version']='1.0.0-r1'; app['packages'][0]['version']='1.0.0-r1' +app['packages'][0]['sha256']=sys.argv[2] +open(p,'w').write(json.dumps(data)) +PY_DOWNGRADE +"$TMP/bin/fetch-catalog" +"$MANAGER" status +grep -Fqx "$(printf 'retro-arcade\t1\t1\t1\t0\t0\t1.1.0-r1')" "$TMP/runtime/apps-status.tsv" || fail 'downgrade must not be advertised' +"$MANAGER" start update retro-arcade || fail 'downgrade did not queue' +grep -Fqx "$(printf 'code\tE52')" "$TMP/runtime/apps-operation.state" || fail 'downgrade was not refused' +# Installed APK changed outside Apps ownership: never perform another upgrade. +python3 - "$TMP/catalog.json" "$NEWRETROSHA" <<'PY_NEW' +import json,sys +p=sys.argv[1]; data=json.load(open(p)); app=data['apps'][0] +app['version']='1.1.0-r1'; app['packages'][0]['version']='1.1.0-r1' +app['packages'][0]['sha256']=sys.argv[2] +open(p,'w').write(json.dumps(data)) +PY_NEW +printf 'smartsafehub-app-retro-arcade 1.0.0-r1\n' > "$APPS_TEST_VERSION_DB" +"$MANAGER" start update retro-arcade || fail 'manual version change did not queue' +grep -Fqx "$(printf 'code\tE52')" "$TMP/runtime/apps-operation.state" || fail 'manual version drift not rejected' +printf 'PASS: selected-app signed updates, version checks, no core upgrades and no duplicate receipts\n' + +# Approved catalog and installed status are fetched independently. A mismatch +# across requests is not an orphan: only an Apps-owned manifest for a package +# that remains installed can mark an approval as retired. +python3 - "$TMP/catalog.json" <<'PY_RETIRED' +import json, sys +p = sys.argv[1] +data = json.load(open(p)) +data['apps'] = [] +with open(p, 'w') as fp: + json.dump(data, fp) +PY_RETIRED +"$TMP/bin/fetch-catalog" +"$MANAGER" status +grep -Fqx "$(printf 'retro-arcade\t0\t1\t1\t0\t0\t\t1')" "$TMP/runtime/apps-status.tsv" || fail 'a genuinely Apps-owned removed approval was not marked retired' +# Manual APK installations have no ownership manifest and must not be promoted +# to managed/retired Apps. Likewise, forged/corrupt ownership is ignored. +rm -f "$TMP/owned/retro-arcade" +"$MANAGER" status +[ ! -s "$TMP/runtime/apps-status.tsv" ] || fail 'manually installed APK falsely marked retired' +printf 'invalid manifest contents\n' > "$TMP/owned/retro-arcade" +"$MANAGER" status +[ ! -s "$TMP/runtime/apps-status.tsv" ] || fail 'invalid manifest falsely marked retired' +rm -f "$TMP/owned/retro-arcade" +printf 'PASS: catalog/status refresh races do not mistake manual APK installs for retired Apps\n' + +# Wake-on-LAN integrates with the same signed APK flow, but requires exactly +# etherwake + luci-app-wol. Its UI is NOT the etherwake helper executable. +printf 'signed index cache\n' > "$SMARTSAFEHUB_APPS_LUCI_INDEX_CACHE" +: > "$APPS_TEST_INSTALLED" +: > "$APPS_TEST_VERSION_DB" +cat > "$TMP/catalog.json" < "$SMARTSAFEHUB_APPS_LUCI_INDEX_CACHE" +"$MANAGER" start remove wake-on-lan || fail 'WOL removal not queued' +grep -Fqx "$(printf 'phase\tsuccess')" "$TMP/runtime/apps-operation.state" || fail 'WOL removal failed' +[ ! -e "$SMARTSAFEHUB_APPS_LUCI_INDEX_CACHE" ] || fail 'LuCI menu cache not invalidated after WOL removal' +[ ! -e "$TMP/owned/wake-on-lan" ] || fail 'WOL ownership not cleared' +[ ! -s "$APPS_TEST_INSTALLED" ] || fail 'WOL left installed APKs behind' +# An extra/omitted package in a bad catalog must be rejected before any APK +# download/install, even if the index and SHA values look valid. +python3 - "$TMP/catalog.json" <<'PY_WOL_EXTRA' +import json,sys +p=sys.argv[1]; data=json.load(open(p)) +data['apps'][0]['packages'].append({'name':'luci-app-nlbwmon','version':'26.288.56989~75a306d','sha256': 'b'*64}) +open(p,'w').write(json.dumps(data)) +PY_WOL_EXTRA +"$MANAGER" start install wake-on-lan || fail 'bad WOL catalog not queued' +grep -Fqx "$(printf 'code\tE27')" "$TMP/runtime/apps-operation.state" || fail 'WOL extra APK was not rejected' +[ ! -s "$APPS_TEST_INSTALLED" ] || fail 'invalid WOL feed changed package state' +[ ! -e "$TMP/owned/wake-on-lan" ] || fail 'invalid WOL feed wrote ownership' +python3 - "$TMP/catalog.json" <<'PY_WOL_MISSING' +import json,sys +p=sys.argv[1]; data=json.load(open(p)) +data['apps'][0]['packages']=data['apps'][0]['packages'][:1] +open(p,'w').write(json.dumps(data)) +PY_WOL_MISSING +"$MANAGER" start install wake-on-lan || fail 'incomplete WOL catalog not queued' +grep -Fqx "$(printf 'code\tE27')" "$TMP/runtime/apps-operation.state" || fail 'missing WOL LuCI APK was not rejected' +printf 'PASS: Wake-on-LAN signed dual-APK lifecycle, LuCI route and corrupt approvals\n' diff --git a/luci-app-smartsafehub/tests/test-apps-metrics.sh b/luci-app-smartsafehub/tests/test-apps-metrics.sh new file mode 100755 index 000000000..7a57a4a00 --- /dev/null +++ b/luci-app-smartsafehub/tests/test-apps-metrics.sh @@ -0,0 +1,91 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +set -eu +ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +HELPER="$ROOT/root/usr/libexec/smartsafehub-apps-metrics" +MANAGER="$ROOT/root/usr/libexec/smartsafehub-apps-manager" +DEVICE="$ROOT/root/usr/libexec/smartsafehub-device" +RESTORE="$ROOT/root/usr/libexec/smartsafehub-apps-restore" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT HUP INT TERM +fail() { echo "FAIL: $*" >&2; exit 1; } +mkdir -p "$TMP/bin" "$TMP/pending" +cat > "$TMP/bin/jsonfilter" <<'SH2' +#!/bin/sh +exec python3 - "$@" <<'PY' +import json, sys +args = sys.argv[1:] +obj = json.load(open(args[args.index('-i') + 1])) +key = args[args.index('-e') + 1].removeprefix('@.') +if key in obj: print(obj[key]) +PY +SH2 +cat > "$TMP/bin/uclient-fetch" <<'SH2' +#!/bin/sh +[ "$APPS_METRICS_TEST_MODE" = fail ] && exit 1 +while [ "$#" -gt 0 ]; do + case "$1" in + -O) out="$2"; shift 2;; + --post-file=*) body="${1#--post-file=}"; shift;; + --header=Authorization:*) auth="$1"; shift;; + *) shift;; + esac +done +[ -n "${auth:-}" ] && [ -n "${body:-}" ] || exit 1 +cp "$body" "$APPS_METRICS_TEST_LAST_BODY" +printf '{"status":"%s"} +' "$APPS_METRICS_TEST_MODE" > "$out" +SH2 +chmod +x "$TMP/bin/"* +export SMARTSAFEHUB_APPS_METRICS_DIR="$TMP/pending" +export SMARTSAFEHUB_APPS_METRICS_CREDENTIAL="$TMP/credential.json" +export SMARTSAFEHUB_APPS_METRICS_FETCH_BIN="$TMP/bin/uclient-fetch" +export SMARTSAFEHUB_APPS_JSONFILTER_BIN="$TMP/bin/jsonfilter" +export APPS_METRICS_TEST_MODE=fail +export APPS_METRICS_TEST_LAST_BODY="$TMP/last-body.json" +EVENT=1793170000-4321-1234567890abcdef +"$HELPER" enqueue retro-arcade 1.0.0-r1 "$EVENT" +[ "$(find "$TMP/pending" -name '*.json' | wc -l | tr -d ' ')" -eq 1 ] || fail 'receipt not queued' +"$HELPER" enqueue retro-arcade 1.0.0-r1 "$EVENT" +[ "$(find "$TMP/pending" -name '*.json' | wc -l | tr -d ' ')" -eq 1 ] || fail 'same event duplicated' +"$HELPER" enqueue '../../etc/passwd' x "$EVENT" >/dev/null 2>&1 && fail 'unsafe app ID accepted' +"$HELPER" enqueue retro-arcade 1.0.0-r1 "$EVENT" invalid >/dev/null 2>&1 && fail 'invalid event kind accepted' +"$HELPER" enqueue retro-arcade 1.0.0-r1 "$EVENT" update extra >/dev/null 2>&1 && fail 'extra event fields accepted' +"$HELPER" enqueue retro-arcade x 'bad/id' >/dev/null 2>&1 && fail 'unsafe event accepted' +"$HELPER" flush +[ -f "$TMP/pending/$EVENT.json" ] || fail 'unregistered device lost receipt' +printf '{"token":"ssh_dev_%064d"} +' 0 > "$TMP/credential.json" +"$HELPER" flush >/dev/null 2>&1 && fail 'failed upload returned success' +[ -f "$TMP/pending/$EVENT.json" ] || fail 'offline request lost receipt' +export APPS_METRICS_TEST_MODE=recorded +"$HELPER" flush +[ ! -f "$TMP/pending/$EVENT.json" ] || fail 'acknowledged event not cleared' +grep -Fq '"app_id":"retro-arcade"' "$TMP/last-body.json" || fail 'app ID lost' +grep -Fq '"kind":"install"' "$TMP/last-body.json" || fail 'legacy install kind lost' +# The same device can report a newer app release as an update; retry must +# preserve the kind and event ID without changing the install counter. +UPDATE_EVENT=1793170001-4322-abcdef1234567890 +"$HELPER" enqueue retro-arcade 1.1.0-r5 "$UPDATE_EVENT" update +[ "$(find "$TMP/pending" -name '*.json' | wc -l | tr -d ' ')" -eq 1 ] || fail 'update receipt not queued' +grep -Fq '"kind":"update"' "$TMP/pending/$UPDATE_EVENT.json" || fail 'update recorded as install' +export APPS_METRICS_TEST_MODE=fail +"$HELPER" flush >/dev/null 2>&1 && fail 'failed update upload returned success' +[ -f "$TMP/pending/$UPDATE_EVENT.json" ] || fail 'update receipt lost on failure' +export APPS_METRICS_TEST_MODE=recorded +"$HELPER" flush +[ ! -f "$TMP/pending/$UPDATE_EVENT.json" ] || fail 'updated receipt not acknowledged' +grep -Fq '"kind":"update"' "$TMP/last-body.json" || fail 'update kind lost over HTTP' +"$HELPER" enqueue retro-arcade 1.0.0-r1 "$EVENT" +export APPS_METRICS_TEST_MODE=duplicate +"$HELPER" flush +[ ! -f "$TMP/pending/$EVENT.json" ] || fail 'deduplicated event not cleared' +! grep -Eiq '(credential|mac|ip_address|device_uuid|user)' "$TMP/last-body.json" || fail 'private metadata uploaded' +grep -Fq '"$METRICS" enqueue "$app" "$version" "$operation_id" "$action"' "$MANAGER" || fail 'successful installation/update not queued' +grep -Fq '[ "$source" = user ]' "$MANAGER" || fail 'restores are included in download count' +grep -Fq 'start install "$id" restore' "$RESTORE" || fail 'restore source missing' +grep -Fq '"$APPS_METRICS" flush' "$DEVICE" || fail 'background queue retry missing' +grep -Fxq '/etc/smartsafehub/apps-metrics-pending/' "$ROOT/root/lib/upgrade/keep.d/smartsafehub-apps" || fail 'sysupgrade lost pending receipts' +sh -n "$HELPER" "$MANAGER" "$DEVICE" "$RESTORE" || fail 'shell syntax invalid' +printf 'PASS: authenticated Apps install/update receipt queue, retry, dedup and restore exclusion +' diff --git a/luci-app-smartsafehub/tests/test-apps-restore.sh b/luci-app-smartsafehub/tests/test-apps-restore.sh new file mode 100755 index 000000000..20eb26155 --- /dev/null +++ b/luci-app-smartsafehub/tests/test-apps-restore.sh @@ -0,0 +1,98 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +set -eu +ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +RESTORE="$ROOT/root/usr/libexec/smartsafehub-apps-restore" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT HUP INT TERM +fail() { echo "FAIL: $*" >&2; exit 1; } +mkdir -p "$TMP/runtime" "$TMP/owned" "$TMP/bin" +cat > "$TMP/bin/apk" <<'MOCK' +#!/bin/sh +[ "$1" = info ] && [ "$2" = -e ] || exit 2 +grep -qxF "$3" "$RESTORE_TEST_INSTALLED" 2>/dev/null +MOCK +cat > "$TMP/bin/manager" <<'MOCK' +#!/bin/sh +[ "$1" = start ] && [ "$2" = install ] && [ "$3" = network-usage-monitor ] && [ "$4" = restore ] || exit 4 +printf '%s\n' "$3" >> "$RESTORE_TEST_CALLS" +case "${RESTORE_TEST_MODE:-success}" in + busy) exit 3;; + offline) + code=E21 + phase=failed;; + revoked) + code=E22 + phase=failed;; + success) + printf 'nlbwmon\nluci-app-nlbwmon\nluci-lib-chartjs\n' > "$RESTORE_TEST_INSTALLED" + code=NONE + phase=success;; +esac +printf 'phase\t%s\naction\tinstall\napp_id\t%s\ncode\t%s\n' "$phase" "$3" "$code" > "$SMARTSAFEHUB_APPS_RUNTIME_DIR/apps-operation.state" +MOCK +cat > "$TMP/bin/sleep" <<'MOCK' +#!/bin/sh +exit 0 +MOCK +chmod +x "$TMP/bin/"* +export SMARTSAFEHUB_APPS_MANAGER_BIN="$TMP/bin/manager" +export SMARTSAFEHUB_APPS_APK_BIN="$TMP/bin/apk" +export SMARTSAFEHUB_APPS_RESTORE_SLEEP_BIN="$TMP/bin/sleep" +export SMARTSAFEHUB_APPS_RESTORE_START_DELAY=0 +export SMARTSAFEHUB_APPS_RESTORE_RETRIES=2 +export SMARTSAFEHUB_APPS_RESTORE_RETRY_DELAY=0 +export SMARTSAFEHUB_APPS_RESTORE_POLL_INTERVAL=0 +export SMARTSAFEHUB_APPS_RESTORE_POLL_LIMIT=2 +export SMARTSAFEHUB_APPS_RUNTIME_DIR="$TMP/runtime" +export SMARTSAFEHUB_APPS_OWNED_DIR="$TMP/owned" +export RESTORE_TEST_INSTALLED="$TMP/installed" +export RESTORE_TEST_CALLS="$TMP/calls" +: > "$RESTORE_TEST_CALLS" +: > "$RESTORE_TEST_INSTALLED" +# No ownership record (or one from a removed app) must never install packages. +"$RESTORE" run +[ ! -s "$RESTORE_TEST_CALLS" ] || fail 'fresh device tried to restore an app' +# A malformed manifest must never invoke the package manager. +printf 'evil\t1\tbadsha\n' > "$TMP/owned/network-usage-monitor" +"$RESTORE" run +[ ! -s "$RESTORE_TEST_CALLS" ] || fail 'invalid manifest triggered package installation' +grep -Fqx "$(printf 'code\tINVALID_MANIFEST')" "$TMP/runtime/apps-restore.state" || fail 'invalid manifest was not diagnosed' +# The persisted manifest contains only Apps-owned packages. A previously shared +# chart dependency must not be recorded as owned. +HASH="$(printf %064d 0)" +printf 'nlbwmon\t2025.06.02~29236be6-r1\t%s\nluci-app-nlbwmon\t26.288.56989~75a306d\t%s\n' "$HASH" "$HASH" > "$TMP/owned/network-usage-monitor" +export RESTORE_TEST_MODE=offline +"$RESTORE" run +[ "$(wc -l < "$RESTORE_TEST_CALLS" | tr -d ' ')" -eq 2 ] || fail 'network error retry was not bounded' +grep -Fqx "$(printf 'code\tE21')" "$TMP/runtime/apps-restore.state" || fail 'network failure state missing' +: > "$RESTORE_TEST_CALLS" +export RESTORE_TEST_MODE=revoked +"$RESTORE" run +[ "$(wc -l < "$RESTORE_TEST_CALLS" | tr -d ' ')" -eq 1 ] || fail 'revoked approval was retried' +grep -Fqx "$(printf 'code\tE22')" "$TMP/runtime/apps-restore.state" || fail 'revoked approval error missing' +: > "$RESTORE_TEST_CALLS" +export RESTORE_TEST_MODE=success +"$RESTORE" run +[ "$(wc -l < "$RESTORE_TEST_CALLS" | tr -d ' ')" -eq 1 ] || fail 'owned app not restored exactly once' +grep -Fqx "$(printf 'phase\tsuccess')" "$TMP/runtime/apps-restore.state" || fail 'restore success not recorded' +"$RESTORE" run +[ "$(wc -l < "$RESTORE_TEST_CALLS" | tr -d ' ')" -eq 1 ] || fail 'already installed app was reinstalled' +# Apps remove deletes the ownership file. Never recreate it on future boots. +rm -f "$TMP/owned/network-usage-monitor" +: > "$RESTORE_TEST_INSTALLED" +"$RESTORE" run +[ "$(wc -l < "$RESTORE_TEST_CALLS" | tr -d ' ')" -eq 1 ] || fail 'removed app was restored' +# Structural contracts: ownership backup, existing firmware service hook and +# shared RPC status. Do not regress to another procd/init daemon. +grep -Fxq '/etc/smartsafehub/apps-installed/' "$ROOT/root/lib/upgrade/keep.d/smartsafehub-apps" || fail 'sysupgrade backup missing' +[ ! -e "$ROOT/root/etc/init.d/smartsafehub-apps-restore" ] || fail 'extra Apps restore init service introduced' +[ ! -e "$ROOT/root/etc/uci-defaults/93-smartsafehub-apps-restore" ] || fail 'extra Apps restore service enable introduced' +grep -Fq 'start_apps_restore_once' "$ROOT/root/usr/libexec/smartsafehub-firmware" || fail 'existing firmware daemon does not schedule restore' +grep -Fq '"$APPS_RESTORE_BIN" run /dev/null 2>&1 &' "$ROOT/root/usr/libexec/smartsafehub-firmware" || fail 'restore is not a detached one-shot' +grep -Fq 'restore: restore_status()' "$ROOT/root/usr/share/rpcd/ucode/smartsafehub/apps-manager.uc" || fail 'restore status missing from RPC' +grep -Fq 'E21|E25|E31|E35|BUSY' "$ROOT/root/etc/hotplug.d/iface/98-smartsafehub-apps-restore" || fail 'WAN retry safety contract missing' +grep -Fq '"$RUNNER" run /dev/null 2>&1 &' "$ROOT/root/etc/hotplug.d/iface/98-smartsafehub-apps-restore" || fail 'WAN should trigger the one-shot worker directly' +! grep -Fq '"$APK" add' "$RESTORE" || fail 'restore runner must not invoke APK directly' +[ -x "$RESTORE" ] || fail 'restore worker is not executable' +printf 'PASS: sysupgrade Apps restoration without new daemon, retry, revocation and ownership\n' diff --git a/luci-app-smartsafehub/tests/test-apps-ui.sh b/luci-app-smartsafehub/tests/test-apps-ui.sh new file mode 100755 index 000000000..fd3fdbeab --- /dev/null +++ b/luci-app-smartsafehub/tests/test-apps-ui.sh @@ -0,0 +1,243 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +set -eu +ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +python3 - "$ROOT" <<'PY' +from pathlib import Path +import sys + +root = Path(sys.argv[1]) +page = (root / 'frontend/src/pages/AppsPage.tsx').read_text() +styles = (root / 'frontend/src/styles/app.css').read_text() +package = (root / 'Makefile').read_text() + + +def check(ok, message): + if not ok: + raise AssertionError(message) + + +# Version/revision validity is covered by test-package-contract.sh. +check('border-teal-300 text-teal-800 hover:bg-teal-50' in page, 'install should stay teal') +check('border-red-300 text-red-700 hover:bg-red-50' in page, 'removal must be red outline') +check('hover\\:bg-red-50:hover' in styles, 'red hover needs an explicit dark-mode style') +check('rgb(239 68 68 / 0.16)' in styles, 'dark-mode removal hover should be subtle') +check('ssh-apps-installed-badge' in page and 'ssh-apps-update-badge' in page and + 'ssh-apps-update-button' in page, + 'installed, update notice and update action need distinct styles') +check('.ssh-apps-update-button' in styles and ".ssh-app[data-theme='dark'] .ssh-apps-update-button" in styles, + 'update action must have a distinct dark-mode style') + + +# App installation storage must refer to the writable APK filesystem. +backend = (root / 'root/usr/share/rpcd/ucode/smartsafehub/apps-manager.uc').read_text() +card = (root / 'frontend/src/components/AppsStorageCard.tsx').read_text() +utils = (root / 'frontend/src/utils/appsStorage.ts').read_text() +check("safe_call('system', 'info', {})" in backend and + "from_system_root(info?.root)" in backend, + 'Apps disk data must prefer procd system.info root stats') +check("from_statvfs(stats, mount)" in backend and "[ '/overlay', '/' ]" in backend, + 'statvfs fallback must support overlay and ext4 rootfs') +storage_module = (root / 'root/usr/share/rpcd/ucode/smartsafehub/apps-storage.uc').read_text() +check("type(value) == 'int'" in storage_module and "type(value) == 'double'" in storage_module, + 'ucode type() emits int/double and must never require integer') +check("root.total * 1024" in storage_module and "root.avail * 1024" in storage_module, + 'procd system.info root counters are KiB, not raw bytes') +check("MAX_BYTES / frsize" in storage_module, + 'unexpected statvfs counters must not overflow safe byte values') +check('stats.bavail' in storage_module and 'stats.frsize' in storage_module and 'storage: storage' in backend, + 'optional Apps metrics must report actual APK-available bytes without spawning df') +check('read_apps_metrics(request)' in backend and 'restore: restore_status(), installed: installed' in backend, + 'critical Apps status must be separate from optional storage metrics') +check("ensure_job('sizes')" in backend and "ensure_job('status')" in backend, + 'APK scan must execute asynchronously and independently from RPC status') +check('apps_metrics:' in (root / 'root/usr/share/rpcd/ucode/smartsafehub.uc').read_text(), + 'metrics RPC must be independently accessible') +import json +acl = json.loads((root / 'root/usr/share/rpcd/acl.d/luci-app-smartsafehub.json').read_text()) +check('apps_metrics' in acl['luci-app-smartsafehub']['read']['ubus']['smartsafehub'], + 'critical regression: Apps metrics RPC is missing from LuCI read ACL') +check('fetchAppsMetrics' in (root / 'frontend/src/app/App.tsx').read_text() and + '!data?.ready ? 3_000 : 60000' in (root / 'frontend/src/app/App.tsx').read_text(), + 'optional Apps metrics should run separately and less frequently') +check("'/bin/df'" not in backend and "'/bin/df'" not in storage_module, 'storage must not spawn a polling command') +check('lg:grid-cols-[minmax(0,1fr)_minmax(0,18.5rem)]' in page, + 'Apps storage should be a compact fixed-width column on desktop, stacked on mobile') +check('ssh-apps-storage-card' in card and 'size-24' in card and 'gap-3' in card and 'sm:p-5' not in card, + 'storage card must be compact without hiding the donut or capacity data') +check('' in page, + 'Apps storage should be read from independent optional metrics') +check('stroke-dasharray=' in card and 'aria-label="앱 설치 저장 공간"' in card, + 'storage donut must be accessible and use SVG') +check('Math.PI * 39' in card and 'r="39"' in card and card.count('size-24') == 2, + 'storage donut needs a larger ring and enough room around the center label') +check('용량 정보를 확인할 수 없습니다.' in card and 'summarizeAppsStorage(storage)' in card, + 'storage errors and older RPC responses need a safe fallback') +check('storage.availableBytes > storage.totalBytes' in utils, + 'malformed storage reports must be rejected') +check('usedText: formatStorageBytes(storage.totalBytes - storage.availableBytes)' in utils, + 'used storage must be derived from validated total and APK-available counters') +check('사용 {summary.usedText} / 전체 {summary.totalText}' in card, + 'storage card must show used and total bytes without increasing its height') +check('파일시스템 예약 공간' in card and '앱 설치 가능 공간

' not in card, + 'capacity breakdown must explain reserved space and avoid redundant footer') +check(".ssh-app[data-theme='dark'] .ssh-apps-storage-track" in styles and + "[data-level='critical']" in styles, + 'donut must support dark mode and dangerously low storage') + +# Per-app size reflects the installed APK package metadata only (no du scan). +manager = (root / 'root/usr/libexec/smartsafehub-apps-manager').read_text() +check('installed_size_bytes() {' in manager and '"$APK" info -s "$name"' in manager, + 'Apps installed size should query each package independently') +check('du -' not in manager and 'find /' not in manager, + 'installed size must not recursively scan the router filesystem') +check('installedSizes[fields[0]]' in backend and 'length(fields) != 2' in backend and + 'installedBytes:' not in backend and 'installedVersion:' in backend, + 'package size data must be parsed separately from the six-column status') +check('sizes) mkdir -p "$RUNTIME"; sizes;;' in manager and 'installed_size_bytes "$name"' in manager, + 'optional sizes command must report app APK totals without modifying status') +check('apps-size-diag' not in manager and 'diagnostic=' not in manager, + 'optional size checks must not create the retired diagnostics TSV') +check('rm -f /tmp/smartsafehub/apps-size-diagnostics.tsv' in package, + 'package upgrade must clean up the retired runtime diagnostic file') +check('app_bytes=' not in manager and 'set --' not in manager.split('sizes() {')[0].split('status() {')[1], + 'status must not invoke optional size processing') +check('sizeLabel !== null' in page and '설치 용량 약 {sizeLabel}' in page, + 'installed app card must show a real size only when known') +check('showOfferedVersion || sizeLabel !== null' in page and + 'showOfferedVersion && sizeLabel !== null' in page and + 'flex-wrap items-center gap-x-1.5' in page, + 'offered version and installed size must share a compact wrapping metadata row') +check(page.count('설치 용량 약 {sizeLabel}') == 1 and page.count('제공 v{app.version}') == 1, + 'Apps metadata must not duplicate version or installed size rows') +check('Number.isSafeInteger(bytes)' in page and 'installed && typeof bytes' in page and 'metrics?.installedSizes' in page, + 'missing or invalid sizes must not be guessed') +check('캐시는 포함되지 않습니다' in page, + 'metadata size must not be misrepresented as actual overlay consumption') +check('installedVersion?: string | null' in (root / 'frontend/src/types/apps.ts').read_text(), + 'installed APK version must have a typed field') +check('installed_release="$(installed_version "$primary_package" || :)"' in manager and + '"$update_ready" "$installed_release"' in manager, + 'current APK version must originate from installed package metadata') +check('installedVersion: length(fields) >= 7' in backend and + 'match(fields[6]' in backend, + 'RPC parser must validate installed APK version') +check('설치 v${installedVersion}' in page and '제공 v{app.version}' in page, + 'installed and approved versions must have independent labels') +check('업데이트 가능' in page and '설치 버전 확인 불가' in page, + 'updates must be distinguished from unknown installed versions') + +# Category icons are local, consistent across cards and filters, and safe +# for unknown/future catalog categories. No external icon service or assets. +icon_source = (root / 'frontend/src/components/AppsCategoryIcon.tsx').read_text() +shared_icons = (root / 'frontend/src/components/Icons.tsx').read_text() +check('AppsCategoryIcon, appsCategoryOf' in page and + '' in page and + '' in page, + 'filters and cards must share category-specific icons in appropriately sized frames') + +check('class="grid size-14 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-600"' in page, + 'app card category icon frame must be 56px and align with the installed size row') +check('' in page and + 'class="grid size-11 shrink-0 place-items-center' not in page, + 'app category icon itself must be 28px while retaining the 56px frame') +check('' in page, + 'category filter icons must remain compact and not change touch targets') +check('appsCategoryOf(app.category) === category' in page, + 'icon categorization and filtering must use the same normalization') +check('aria-label="앱 카테고리"' in page and '>{tab.label}' in page and + 'inline-flex min-h-10 items-center gap-1.5' in page, + 'filter icons must retain text labels, touch targets and wrapping') +for catalog_key, category_key in [('network', 'network'), ('security', 'security'), + ('entertainment', 'entertainment'), ('smart_home', 'smart_home'), + ('iot', 'smart_home')]: + check(f"case '{catalog_key}':" in icon_source, + f'category {catalog_key} must be explicitly mapped to {category_key}') +for tab_key, icon_name in [('all', 'AppsIcon'), ('network', 'NetworkNodesIcon'), + ('security', 'ShieldIcon'), ('smart_home', 'HomeIcon'), + ('entertainment', 'GamepadIcon')]: + check(f"case '{tab_key}': return <{icon_name}" in icon_source, + f'{tab_key} category has the wrong icon') +check("default: return 'other';" in icon_source and + 'default: return ' in shared_icons and + 'M8 12h.01M12 12h.01M16 12h.01' in shared_icons and + '3.106-3.105' in shared_icons, + 'tools use a wrench, other uses a circle ellipsis, not network-like nodes') +check('aria-hidden={title ? undefined : true}' in shared_icons and + 'fill="none"' in shared_icons and 'stroke="currentColor"' in shared_icons, + 'icons must be decorative line SVGs that inherit light/dark text color') +check('https://' not in icon_source and '이전 목록' in page, 'cached catalog should have a compact non-blocking stale marker') +check('state.retired === true && state.managed' in page and '!error && !loading && !refreshing' in page, + 'retirement must require explicit backend proof and a complete catalog') +status_rpc = (root / 'root/usr/share/rpcd/ucode/smartsafehub/apps-manager.uc').read_text() +check("retired: length(fields) == 8 && fields[7] == '1'" in status_rpc, + 'retired status must originate only from a signed-off local owner manifest record') +check('retired?: boolean;' in (root / 'frontend/src/types/apps.ts').read_text(), + 'retired proof must be optional for older router responses') + +# Removal never calls onRemove from a card or from the orphan-list button. +check("onClick={(event) => removable ? openRemoveDialog(app, event.currentTarget)" in page, + 'catalog removal must open confirmation') +check("onClick={(event) => openRemoveDialog({ id, name: id" in page, + 'orphaned apps must open the same confirmation') +check("onClick={() => void requestOperation(removeTarget, 'remove')}" in page, + 'dialog approval must be the only removal confirmation action') +check(page.count('onRemove(app.id)') == 1, 'only the accepted operation may call the remove RPC callback') +check('aria-modal="true"' in page and 'aria-labelledby="apps-remove-title"' in page, + 'remove confirmation must be accessible') +check('취소' in page and '앱 제거' in page, 'dialog must offer both choices') +check("if (event.key === 'Escape')" in page and "event.key !== 'Tab'" in page, + 'dialog must support escape and focus containment') +check("if (action === 'remove') closeRemoveDialog()" in page, + 'confirmed operation should close the modal before starting') +check("if (!installStatus || statusError || pending !== null || running || restoring) return;" in page, + 'operation must preserve status and concurrency guards') + +# Installing still uses the existing confirmation and neither action bypasses RPC guards. +check("action === 'install' && !window.confirm" in page, 'existing install confirmation must remain') +check("" in page, 'loading must show on app and orphan entries') +check("{removing ? '제거 중…' : action === 'update' ? '업데이트 중…' : '설치 중…'}" in page, + 'install, update and remove must show distinct spinner states') +check("job.action === 'remove' ? '앱 제거 중' : job.action === 'update' ? '앱 업데이트 중' : '앱 설치 중'" in page, + 'running job status must use the action name') +check("job.phase === 'failed' ? 'border-rose-200 bg-rose-50 text-rose-800'" in page, + 'errors must keep a red alert') +check('useState(readDismissedAppsJob)' in page, + 'dismissal must be restored after refreshing Apps') +check('saveDismissedAppsJob(key)' in page and 'onClick={() => dismissJob(jobKey)}' in page, + 'dismissing a job should persist the dismissed operation ID') +check("onClick={() => void requestOperation(app, 'update')}" in page, 'approved update action must be shown') +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-changelog-spacing.sh b/luci-app-smartsafehub/tests/test-changelog-spacing.sh new file mode 100755 index 000000000..11890e156 --- /dev/null +++ b/luci-app-smartsafehub/tests/test-changelog-spacing.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +set -eu + +ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +python3 - "$ROOT_DIR/CHANGELOG.md" <<'PYTEST' +from pathlib import Path +import sys + +lines = Path(sys.argv[1]).read_text(encoding="utf-8").splitlines() +releases = [index for index, line in enumerate(lines) if line.startswith("## [")] +if len(releases) < 2: + raise SystemExit("FAIL: CHANGELOG must contain multiple releases") + +for index in releases[1:]: + previous = index - 1 + while previous >= 0 and not lines[previous].strip(): + if lines[previous]: + raise SystemExit(f"FAIL: whitespace-only line before release at {index + 1}") + previous -= 1 + blank_lines = index - previous - 1 + if blank_lines != 2: + raise SystemExit( + f"FAIL: release at line {index + 1} has {blank_lines} blank lines; expected 2" + ) + +print(f"PASS: {len(releases)} CHANGELOG releases have two blank lines between entries") +PYTEST diff --git a/luci-app-smartsafehub/tests/test-daemon-runtime.sh b/luci-app-smartsafehub/tests/test-daemon-runtime.sh index 7ee3e95c3..71b470122 100755 --- a/luci-app-smartsafehub/tests/test-daemon-runtime.sh +++ b/luci-app-smartsafehub/tests/test-daemon-runtime.sh @@ -43,4 +43,4 @@ for service in device health activity-sync; do grep -Fq 'ssh_runtime_execute' "$script" done grep -Eq '^PKG_RELEASE:=[0-9]+$' "$ROOT/Makefile" -echo 'daemon runtime checks: OK' +echo 'PASS: daemon runtime checks' diff --git a/luci-app-smartsafehub/tests/test-dashboard-performance-contract.sh b/luci-app-smartsafehub/tests/test-dashboard-performance-contract.sh index 5481aa39b..ec7e034d8 100755 --- a/luci-app-smartsafehub/tests/test-dashboard-performance-contract.sh +++ b/luci-app-smartsafehub/tests/test-dashboard-performance-contract.sh @@ -15,6 +15,12 @@ assert "useFirmwareUpdates(true)" in app # sidebar firmware badge stays current assert 'if (loading && !data)' in home assert "let info_finished = false" in server and "let wan_finished = false" in server assert server.index("const info_request = defer_call('system', 'info'") < server.index("const wan_request = defer_call('network.interface.wan'") +assert "storage: dashboard_writable_storage(info?.root)" in server +assert "storage: dashboard_writable_storage(info?.root)" in server[server.index('function system_status_payload('):server.index('function collect_system_status(')] +assert "const from_root = from_system_root(root)" in server, 'procd root counters must be the fast path' +fallback = server[server.index('function dashboard_writable_storage(root)'):server.index('function system_status_payload(')] +assert "from_statvfs(fs.statvfs(mount), mount)" in fallback, 'local statvfs should recover missing root counters' +assert "run_command" not in fallback and "defer_call" not in fallback, 'extra process or ubus call not allowed for storage' for hook in ('useSoftwareUpdates', 'useFirmwareUpdates'): source = Path(f'frontend/src/hooks/{hook}.ts').read_text() assert 'const [pollingPhase, setPollingPhase]' in source diff --git a/luci-app-smartsafehub/tests/test-dashboard-ui-contract.sh b/luci-app-smartsafehub/tests/test-dashboard-ui-contract.sh index 73f2ddbc2..e78882ef7 100755 --- a/luci-app-smartsafehub/tests/test-dashboard-ui-contract.sh +++ b/luci-app-smartsafehub/tests/test-dashboard-ui-contract.sh @@ -257,6 +257,68 @@ if grep -Fq '메모리 사용률' "$HOME"; then fail '대시보드는 메모리 사용률을 별도 블록으로 중복 표시하지 않아야 합니다' fi +# Disk belongs to the normal dashboard status payload; no Apps metrics polling. +python3 - "$HOME" "$ROOT_DIR/root/usr/share/rpcd/ucode/smartsafehub/system.uc" "$ROOT_DIR/frontend/src/types/status.ts" "$ROOT_DIR/frontend/src/demo/data.ts" <<'PY_DASH_DISK' +from pathlib import Path +import sys + +home, backend, types, demo = [Path(name).read_text() for name in sys.argv[1:]] +labels = [f'{title}' for title in + ('메모리', '디스크', '시스템 부하', '실행 시간')] +assert all(home.count(label) == 1 for label in labels), '리소스 카드 네 종류가 필요합니다' +assert [home.index(label) for label in labels] == sorted(home.index(label) for label in labels), '카드 순서 오류' +assert 'class="mt-5 grid min-w-0 grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4"' in home, '데스크톱 4열, 태블릿 2열, 모바일 1열이 필요합니다' +assert 'summarizeAppsStorage(data.runtime.storage)' in home, 'Apps 저장 공간 계산을 재사용해야 합니다' +assert 'disk.availablePercent' in home and 'disk.availableText' in home, 'Apps와 동일한 여유 공간 표시가 필요합니다' +assert '사용 {disk.usedText} / 전체 {disk.totalText}' in home, '사용/전체 용량을 표시해야 합니다' +assert "disk.availableRatio * DISK_RING_CIRCUMFERENCE" in home, '실제 여유 비율로 도넛 호를 계산해야 합니다' +assert 'DISK_RING_CIRCUMFERENCE = 2 * Math.PI * 39' in home, 'Apps 도넛과 같은 반지름을 사용해야 합니다' +assert 'ssh-apps-storage-track' in home and 'ssh-apps-storage-free' in home and 'ssh-apps-storage-ring' in home, 'Apps의 테마 색상과 도넛 스타일을 재사용해야 합니다' +assert 'disk && disk.availableRatio > 0' in home, '여유 공간이 0인 경우 가짜 호를 그리면 안 됩니다' +assert "data-level={disk?.level ?? 'unavailable'}" in home, 'Apps와 같은 부족 경고 색상 상태가 필요합니다' +assert 'aria-label={disk' in home and '디스크 여유 공간 ${disk.availablePercent}%' in home, '스크린 리더에 저장 공간 정보를 제공해야 합니다' +assert "{disk ? `${disk.availableText} 남음` : '저장 공간 확인 불가'}" in home, '용량 조회 실패를 0%로 표시하면 안 됩니다' +assert 'diskUsedPercent' not in home, '기존 선형 사용률 막대 로직을 제거해야 합니다' +assert '[a-zA-Z_][a-zA-Z_0-9]*)\s*=\s*["\']?' + r'\$\{[a-zA-Z_][a-zA-Z_0-9]*:-(?P[a-zA-Z_][a-zA-Z_0-9.+-]*)\}' +) +# Shell syntax, builtins, environment assignments, and case patterns. +# Provided by /etc/rc.common and /etc/diag.sh, which are sourced at runtime. +SOURCED_FUNCTIONS = { + 'procd_add_reload_trigger', 'procd_close_instance', 'procd_open_instance', + 'procd_set_param', 'set_state', +} +# OpenWrt's standard rootfs does not ship curl. Device registration has a +# legacy *conditional* curl fallback; it uses the required uclient-fetch when +# curl is absent. Allow that existing branch only, never a new curl caller. +OPTIONAL_CURL_SOURCE = 'root/usr/libexec/smartsafehub-device' + + +def legacy_curl_is_guarded(source: str) -> bool: + """Verify every curl invocation is in the existing guarded branch.""" + match = re.search(r'^post_json\(\)\s*\{(?P.*?)^\}', source, + flags=re.MULTILINE | re.DOTALL) + if not match: + return False + body = match.group('body') + guard = re.search(r'^ if command -v curl >/dev/null 2>&1; then$', + body, flags=re.MULTILINE) + end_guard = re.search(r'^ fi$', body, flags=re.MULTILINE) + invocations = list(re.finditer(r'^\s*curl\s', source, flags=re.MULTILINE)) + if not guard or not end_guard or not invocations: + return False + # The fallback requires both authenticated and unauthenticated requests. + for invocation in invocations: + if not (match.start('body') + guard.end() <= invocation.start() < + match.start('body') + end_guard.start()): + return False + return True +INTERNAL = set(''' +if then elif else fi for in do done while until case esac function return exit +local export readonly set shift trap read eval exec command builtin type hash alias +unalias unset wait break continue cd pwd printf echo test true false time +umask ulimit getopts kill jobs fg bg select coproc +'''.split()) + + +def policy_names(path: Path) -> set[str]: + names = set() + for line in path.read_text(encoding='utf-8').splitlines(): + value = line.partition('#')[0].strip() + if not value: + continue + if not re.fullmatch(r'[a-z][a-z0-9._+-]*', value): + raise ValueError(f'invalid allowlisted command: {value}') + names.add(value) + if not names: + raise ValueError('empty command policy') + return names + + +def router_scripts(root: Path) -> list[Path]: + scripts = [] + for path in sorted((root / 'root').rglob('*')): + if not path.is_file(): + continue + rel = path.relative_to(root).as_posix() + if not rel.startswith(('root/usr/libexec/', 'root/usr/lib/', 'root/etc/', + 'root/www/cgi-bin/')): + continue + with path.open('rb') as stream: + shebang = stream.readline(128).decode('utf-8', errors='replace').strip() + if SHELL.match(shebang): + scripts.append(path) + return scripts + + +def unquoted_shell(line: str, state: list[dict]) -> str: + """Mask shell literals, retain executable command substitutions. + + Each nested $(...) is a separate shell context. Single-quoted awk/sed + source is *not* parsed as shell code, including across source lines. + """ + out = [] + i = 0 + while i < len(line): + ctx = state[-1] + quote = ctx['quote'] + char = line[i] + if char == "\\" and quote != "'": + out.append(' ') + if i + 1 < len(line): + out.append(' ') + i += 2 + else: + i += 1 + continue + if quote != "'" and line.startswith('$((', i): + # Arithmetic expansion is not a command substitution. + end = line.find('))', i + 3) + if end != -1: + out.extend(' ' * (end + 2 - i)) + i = end + 2 + continue + if quote != "'" and line.startswith('$(', i): + out.extend('$(') + state.append({'quote': None, 'depth': 1}) + i += 2 + continue + if quote == "'": + if char == "'": + ctx['quote'] = None + out.append(' ') + elif quote == '"': + if char == '"': + ctx['quote'] = None + out.append(' ') + elif char in ("'", '"'): + ctx['quote'] = char + out.append(' ') + elif char == '#' and (not out or out[-1].isspace()): + break + else: + out.append(char) + if len(state) > 1: + if char == '(': + ctx['depth'] += 1 + if char == ')': + ctx['depth'] -= 1 + if ctx['depth'] == 0: + state.pop() + i += 1 + return ''.join(out) + + +def commands(source: str) -> set[str]: + functions = {match.group(1) for line in source.splitlines() + if (match := FUNCTION.match(line))} + found: set[str] = set() + heredoc_end: str | None = None + state = [{'quote': None, 'depth': 0}] + for raw in source.splitlines(): + if heredoc_end: + if raw.strip() == heredoc_end: + heredoc_end = None + continue + # Check heredoc delimiters on raw shell syntax before quote masking. + heredoc = HEREDOC.search(raw) + if heredoc and state[0]['quote'] is None and len(state) == 1: + heredoc_end = heredoc.group(2) + # A quoted executable path/variable is one command token. Keep a + # placeholder to prevent its first argument being mistaken for a call. + raw = re.sub(r'"\$(?:\{[a-zA-Z0-9_]+\}|[a-zA-Z0-9_]+)"', 'DYNAMIC_EXEC', raw) + line = unquoted_shell(raw, state) + # Case labels are not commands, including multi-pattern arms and + # single-line `case value in one|two) ...` forms. + if re.match(r'^\s*case\b.*\bin\b.*\)', line): + line = line.split(')', 1)[1] + elif re.match(r'^\s*[^#$;={}()]+\)', line): + line = line.split(')', 1)[1] + if not line.strip() or line.lstrip().startswith('#'): + continue + # Ignore shell function definitions, pattern arms, and plain assignments. + for probe in (*PROBE.finditer(line), *POST_KEYWORD.finditer(line)): + name = probe.group('name') + if name in INTERNAL or name in functions or name == 'DYNAMIC_EXEC': + continue + if line[probe.end('name'):].lstrip().startswith('='): + continue + found.add(name) + for match in SITE.finditer(line): + name = match.group('name') + # Shell case arms / function declarations are not executable calls. + suffix = line[match.end('name'):].lstrip() + if suffix.startswith('()') or suffix.startswith(')'): + continue + if name in INTERNAL or name in functions or name == 'DYNAMIC_EXEC': + continue + # An assignment cannot be an executable command name. + if suffix.startswith('='): + continue + # Expressions like $(foo) are real commands but `${foo}` is not. + if line[:match.start('name')].endswith('${'): + continue + found.add(name) + return found + + + +# Performance guardrails for constrained router hardware. These are deliberately +# high-confidence anti-patterns, not a blanket ban on grep/find/sort: bounded +# scans and file processing are legitimate and often faster than alternatives. +# Using the same shipped-script discovery as the compatibility audit ensures +# that every added router entry point receives both checks automatically. +# Detect executable sites, not command names embedded in quotes or comments. +# Assignment prefixes such as LC_ALL=C grep -R must not bypass the audit. +PERFORMANCE_SITE = re.compile( + r'(?:^|[;|`]|\$\(|\bthen\s+|\bdo\s+|\belse\s+|' + r'\bif\s+|\belif\s+|\bwhile\s+|\buntil\s+|\b!\s+)' + r'\s*(?:[a-zA-Z_][a-zA-Z_0-9]*=[^\s;|]+\s+)*' + r'(?Pfind|du|grep|ls|dd|xargs|sleep)\b' +) +INFINITE_LOOP = re.compile( + r'(?:^|[;|]|\$\()\s*(?:while\s+(?:true|:)|until\s+false)\s*;\s*do\b' +) +NESTED_LOOP = re.compile(r'\b(?:for|while|until)\b[^;]*;\s*do\b') +LOOP_DONE = re.compile(r'(?:^|;)\s*done\b') +BLOCKING_ACTION = re.compile(r'\b(?:sleep|wait)\b|\bubus\s+listen\b') +LOOP_EXIT = re.compile(r'\b(?:break|return|exit)\b') +ZERO_SLEEP = re.compile(r'0+(?:\.0+)?[smhd]?$', re.IGNORECASE) + + +def shell_lines(source: str) -> list[tuple[int, str, str]]: + """Return source and quote-masked shell lines, excluding heredoc data. + + The mask retains offsets, so we can lex *real arguments* only after we have + established that a command name is executable shell, not a quoted string. + """ + lines = [] + heredoc_end: str | None = None + state = [{'quote': None, 'depth': 0}] + for number, raw in enumerate(source.splitlines(), 1): + if heredoc_end: + if raw.strip() == heredoc_end: + heredoc_end = None + continue + heredoc = HEREDOC.search(raw) + if heredoc and state[0]['quote'] is None and len(state) == 1: + heredoc_end = heredoc.group(2) + masked = unquoted_shell(raw, state) + lines.append((number, raw, masked)) + return lines + + +def call_arguments(raw: str, masked: str, end: int) -> list[str]: + """Parse arguments up to the next unquoted shell operator.""" + boundary = re.search(r'[;|`]|\$\(', masked[end:]) + stop = end + boundary.start() if boundary else len(masked) + try: + return shlex.split(raw[end:stop], comments=True, posix=True) + except ValueError: + # The command may use a line continuation or multiline quotes. Avoid + # a false positive based on incomplete arguments; build/runtime tests + # remain necessary for cases this static checker cannot resolve. + return [] + + +def command_risk(name: str, args: list[str]) -> str | None: + """Only report expensive or unbounded operations with clear evidence.""" + options = [a for a in args if a.startswith('-')] + shorts = ''.join(a[1:] for a in options if a.startswith('-') and not a.startswith('--')) + recursive = 'R' in shorts or '--recursive' in options + if name == 'find' and args and args[0] == '/': + return 'full filesystem traversal (find /); use a bounded subtree' + if name == 'du' and ('a' in shorts or '--all' in options or '/' in args): + return 'unbounded disk scan (du -a or du /); target a bounded path' + if name == 'grep' and (recursive or 'r' in shorts): + return 'recursive grep; limit the files/directory and scan budget' + if name == 'ls' and recursive: + return 'recursive ls; use a targeted directory listing' + if name == 'dd' and any(a in ('if=/dev/zero', 'if=/dev/urandom') for a in args): + if not any(a.startswith(('count=', 'count_bytes=')) for a in args): + return 'unbounded dd from a generator; specify count=' + if name == 'xargs' and ('-P0' in args or '--max-procs=0' in args or + any(args[i:i + 2] == ['-P', '0'] + for i in range(len(args) - 1))): + return 'unbounded xargs concurrency (-P 0); specify a small limit' + if name == 'sleep' and args and ZERO_SLEEP.fullmatch(args[0]): + return 'zero-delay sleep; set a positive delay to prevent busy polling' + return None + + +def performance_risks(source: str, rel: str) -> list[str]: + errors: list[str] = [] + lines = shell_lines(source) + # Some shell code has case labels (`grep)`) and function declarations; + # they are not command invocations. Raw strings in heredocs are excluded. + for number, raw, masked in lines: + for match in PERFORMANCE_SITE.finditer(masked): + name = match.group('name') + suffix = masked[match.end('name'):].lstrip() + if suffix.startswith((')', '()')): + continue + args = call_arguments(raw, masked, match.end('name')) + reason = command_risk(name, args) + if reason: + errors.append(f'{rel}:{number}: performance risk: {reason}') + + # An unconditional infinite loop needs explicit throttling, blocking + # wait, or an exit path. A zero-delay sleep is separately rejected. A + # bounded `while :` loop with `break` must remain valid (apps-manager). + for i, (number, _, masked) in enumerate(lines): + loop = INFINITE_LOOP.search(masked) + if not loop: + continue + depth = 1 + body: list[str] = [] + closed = False + # Handle both multiline loops and `while true; do :; done`. + for next_line in [masked[loop.end():], *(line[2] for line in lines[i + 1:])]: + closing = LOOP_DONE.search(next_line) + if depth == 1 and closing: + body.append(next_line[:closing.start()]) + closed = True + break + depth += len(NESTED_LOOP.findall(next_line)) + depth -= len(LOOP_DONE.findall(next_line)) + if depth <= 0: + closed = True + break + body.append(next_line) + if closed and not any(BLOCKING_ACTION.search(line) or LOOP_EXIT.search(line) + for line in body): + errors.append(f'{rel}:{number}: performance risk: ' + 'unthrottled infinite loop (no sleep, wait, or exit)') + return errors + + +def audit(root: Path, allowed: set[str]) -> list[str]: + errors = [] + files = router_scripts(root) + if not files: + return ['no router shell scripts found'] + # A newly added script requiring bash/python must not escape the audit just + # because its shebang is not /bin/sh. Ucode files have no shell shebang. + runtime_roots = ('root/usr/libexec/', 'root/usr/lib/', 'root/etc/', + 'root/www/cgi-bin/') + for path in sorted((root / 'root').rglob('*')): + if not path.is_file(): + continue + rel = path.relative_to(root).as_posix() + if not rel.startswith(runtime_roots): + continue + with path.open('rb') as stream: + shebang = stream.readline(128).decode('utf-8', errors='replace').strip() + if re.match(r'^#!\s*(?:/bin/bash|/usr/bin/env\s+(?:bash|python3?))\b', shebang): + errors.append(f'{rel}: unsupported interpreter: {shebang}') + # Shared shell helpers can define functions used by sourced scripts. + shared = set() + for path in files: + shared.update(match.group(1) for line in path.read_text(encoding='utf-8').splitlines() + if (match := FUNCTION.match(line))) + for path in files: + rel = path.relative_to(root).as_posix() + source = path.read_text(encoding='utf-8') + for match in COMMAND_DEFAULT.finditer(source): + variable = match.group('var') + command = match.group('name') + if variable.upper().endswith(('_BIN', '_COMMAND', '_CMD', '_EXEC')): + if command not in allowed and command not in INTERNAL: + errors.append(f'{rel}: unsupported command default: ' + f'{variable}={command}') + for command in sorted(commands(source) - allowed - shared - SOURCED_FUNCTIONS): + if (rel == OPTIONAL_CURL_SOURCE and command == 'curl' and + legacy_curl_is_guarded(source)): + continue + errors.append(f'{rel}: unsupported command: {command}') + errors.extend(performance_risks(source, rel)) + return errors + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--root', type=Path, default=ROOT) + args = parser.parse_args() + try: + errors = audit(args.root, policy_names(POLICY)) + except (OSError, ValueError) as exc: + print(f'FAIL: OpenWrt command policy: {exc}', file=sys.stderr) + return 1 + if errors: + print('FAIL: OpenWrt 25.12 unsupported or high-cost router shell operation(s):', file=sys.stderr) + for error in errors: + print(f' {error}', file=sys.stderr) + return 1 + print('PASS: OpenWrt 25.12 command and performance policy') + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/luci-app-smartsafehub/tools/check_test_coverage.py b/luci-app-smartsafehub/tools/check_test_coverage.py new file mode 100644 index 000000000..4f0149094 --- /dev/null +++ b/luci-app-smartsafehub/tools/check_test_coverage.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""Fail fast when a test is not connected to ShellSpec or GitHub Actions. + +No third-party dependencies: the checker runs before CI installs ShellSpec/Node. +""" + +from __future__ import annotations + +from collections import Counter +from pathlib import Path +import re +import sys +import tempfile + + +ROOT = Path(__file__).resolve().parents[1] +REGISTRATION = re.compile( + r'^\s*When\s+run\s+command\s+sh\s+["\']\$SHELLSPEC_PROJECT_ROOT/' + r'(tests/[a-zA-Z0-9_./-]+\.sh)["\']\s*$' +) +JOB_HEADER = re.compile(r'^ ([a-zA-Z][a-zA-Z0-9_-]*):\s*$') + + +class CoverageError(Exception): + pass + + +def require(condition: bool, message: str) -> None: + if not condition: + raise CoverageError(message) + + +def relative_names(paths, root: Path) -> set[str]: + return {path.relative_to(root).as_posix() for path in paths} + + +def check_shellspec(root: Path) -> int: + tests_dir = root / 'tests' + spec_dir = root / 'spec' + scripts = relative_names(tests_dir.rglob('*.sh'), root) + require(bool(scripts), 'tests/ 안에 셸 테스트가 없습니다.') + specs = sorted(spec_dir.rglob('*_spec.sh')) + unsupported_specs = sorted(spec_dir.rglob('*.sh')) + unsupported_specs = [spec for spec in unsupported_specs if spec not in specs] + require(not unsupported_specs, 'ShellSpec 자동 검색 대상이 아닌 spec 스크립트: ' + + ', '.join(str(spec.relative_to(root)) for spec in unsupported_specs)) + require(bool(specs), 'ShellSpec spec/*_spec.sh 파일이 없습니다.') + + registrations: Counter[str] = Counter() + for spec in specs: + for line in spec.read_text(encoding='utf-8').splitlines(): + match = REGISTRATION.fullmatch(line) + if match: + registrations[match.group(1)] += 1 + + missing = sorted(scripts - registrations.keys()) + stale = sorted(registrations.keys() - scripts) + duplicates = sorted(name for name, count in registrations.items() if count > 1) + require(not missing, 'ShellSpec 미등록 셸 테스트: ' + ', '.join(missing)) + require(not stale, 'ShellSpec 등록 경로에 파일 없음: ' + ', '.join(stale)) + require(not duplicates, 'ShellSpec 중복 등록: ' + ', '.join(duplicates)) + return len(scripts) + + +def check_runtime(root: Path) -> int: + runtime_dir = root / 'tests/runtime' + runtime = sorted(runtime_dir.glob('*.test.mjs')) + require(bool(runtime), 'tests/runtime/*.test.mjs 테스트가 없습니다.') + # The workflow deliberately uses a non-recursive *.test.mjs glob. Nested + # tests or newly introduced .test.ts/.spec.mjs files must not be silent. + for path in (root / 'tests').rglob('*'): + if not path.is_file() or path.suffix == '.sh': + continue + name = path.name + looks_like_test = ( + '.test.' in name or '.spec.' in name or + name.startswith(('test-', 'test_')) or name.endswith('_test.py') + ) + if looks_like_test and path not in runtime: + raise CoverageError( + f'CI 실행 대상이 아닌 테스트 파일: {path.relative_to(root)} ' + '(JavaScript 테스트는 tests/runtime/*.test.mjs에 배치하세요)' + ) + return len(runtime) + + +def job_section(workflow: str, name: str) -> str: + lines = workflow.splitlines() + start = next((i for i, line in enumerate(lines) + if JOB_HEADER.fullmatch(line) and JOB_HEADER.fullmatch(line).group(1) == name), None) + require(start is not None, f'CI jobs.{name} 누락') + end = next((i for i in range(start + 1, len(lines)) + if JOB_HEADER.fullmatch(lines[i])), len(lines)) + return '\n'.join(lines[start:end]) + + +def command_is_unconditional(job: str, command: str) -> bool: + if re.search(r'^ (?:if|continue-on-error)\s*:', job, re.MULTILINE): + return False + # Match exact YAML step commands rather than substrings, comments or + # commands that can swallow failures (e.g. `... || true`). + steps = re.split(r'(?=^ - )', job, flags=re.MULTILINE) + for step in steps: + if re.search(r'^ (?:if|continue-on-error)\s*:', step, re.MULTILINE): + continue + if re.search(r'^ run:[ \t]*' + re.escape(command) + r'[ \t]*$', step, re.MULTILINE): + return True + return False + + +def check_ci(root: Path) -> None: + workflow = (root / '.github/workflows/ci.yml').read_text(encoding='utf-8') + backend = job_section(workflow, 'backend') + frontend = job_section(workflow, 'frontend') + require( + command_is_unconditional(backend, 'sh tests/test-suite-integrity.sh'), + 'Backend CI의 독립 테스트 누락 검사 단계가 없거나 조건부로 실행됩니다.', + ) + require( + command_is_unconditional(backend, 'shellspec'), + 'Backend CI에서 전체 ShellSpec 테스트를 실행하지 않습니다.', + ) + require( + command_is_unconditional(backend, 'sh tests/test-openwrt-commands.sh'), + 'Backend CI에서 OpenWrt 25.12 명령 호환성 검사가 누락되거나 조건부로 실행됩니다.', + ) + require( + command_is_unconditional(frontend, 'node --experimental-strip-types --test ../tests/runtime/*.test.mjs'), + 'Frontend CI에서 모든 *.test.mjs 파일을 자동 실행하지 않습니다.', + ) + + +def check(root: Path) -> tuple[int, int]: + shell_count = check_shellspec(root) + runtime_count = check_runtime(root) + check_ci(root) + return shell_count, runtime_count + + +def self_test() -> None: + """Mutation tests: prove unregistered and skipped tests really fail.""" + with tempfile.TemporaryDirectory(prefix='smartsafehub-ci-gate-') as temp: + root = Path(temp) + for dirname in ('tests/runtime', 'spec', '.github/workflows'): + (root / dirname).mkdir(parents=True) + (root / 'tests/test-existing.sh').write_text('#!/bin/sh\n', encoding='utf-8') + (root / 'tests/runtime/existing.test.mjs').write_text('', encoding='utf-8') + spec_path = root / 'spec/contracts_spec.sh' + spec_base = 'Describe "checks"\n When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-existing.sh"\nEnd\n' + spec_path.write_text(spec_base, encoding='utf-8') + workflow_path = root / '.github/workflows/ci.yml' + workflow_base = '''jobs: + backend: + runs-on: ubuntu-latest + steps: + - name: Guard + run: sh tests/test-suite-integrity.sh + - name: Test + run: shellspec + - name: OpenWrt BusyBox + run: sh tests/test-openwrt-commands.sh + frontend: + runs-on: ubuntu-latest + steps: + - name: Test + run: node --experimental-strip-types --test ../tests/runtime/*.test.mjs +''' + workflow_path.write_text(workflow_base, encoding='utf-8') + require(check(root) == (1, 1), 'self-test fixture 불일치') + + def fails_with(fragment: str) -> None: + try: + check(root) + except CoverageError as exc: + require(fragment in str(exc), f'예상 외 오류: {exc}') + else: + raise CoverageError(f'누락 검출 실패: {fragment}') + + extra = root / 'tests/test-new.sh' + extra.write_text('#!/bin/sh\n', encoding='utf-8') + fails_with('ShellSpec 미등록') + extra.unlink() + + spec_path.write_text(spec_base.replace('End\n', + ' When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-existing.sh"\nEnd\n'), encoding='utf-8') + fails_with('ShellSpec 중복') + spec_path.write_text(spec_base.replace('End\n', + ' When run command sh \"$SHELLSPEC_PROJECT_ROOT/tests/test-deleted.sh\"\nEnd\n'), encoding='utf-8') + fails_with('등록 경로에 파일 없음') + spec_path.write_text(spec_base, encoding='utf-8') + extra_spec = root / 'spec/hidden.sh' + extra_spec.write_text('Describe \"hidden\"\nEnd\n', encoding='utf-8') + fails_with('자동 검색 대상이 아닌') + extra_spec.unlink() + + # A correctly named new runtime test is covered by the CI glob. + included = root / 'tests/runtime/new.test.mjs' + included.write_text('', encoding='utf-8') + require(check(root) == (1, 2), '새 JS 테스트 자동 검색 실패') + included.unlink() + + unsupported = root / 'tests/runtime/new.test.js' + unsupported.write_text('', encoding='utf-8') + fails_with('CI 실행 대상이 아닌') + unsupported.unlink() + unsupported_python = root / 'tests/test_missing.py' + unsupported_python.write_text('', encoding='utf-8') + fails_with('CI 실행 대상이 아닌') + unsupported_python.unlink() + nested = root / 'tests/runtime/nested' + nested.mkdir() + (nested / 'new.test.mjs').write_text('', encoding='utf-8') + fails_with('CI 실행 대상이 아닌') + (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-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', + 'run: node --experimental-strip-types --test ../tests/runtime/existing.test.mjs', '*.test.mjs'), + ('run: shellspec', 'if: false\n run: shellspec', 'ShellSpec 테스트'), + ('run: shellspec', 'run: shellspec || true', 'ShellSpec 테스트'), + ('run: shellspec', 'continue-on-error: true\n run: shellspec', 'ShellSpec 테스트'), + ): + workflow_path.write_text(workflow_base.replace(old, replacement), encoding='utf-8') + fails_with(error) + workflow_path.write_text(workflow_base, encoding='utf-8') + require(check(root) == (1, 1), 'self-test fixture 복구 실패') + + +if __name__ == '__main__': + try: + shell, runtime = check(ROOT) + if '--self-test' in sys.argv[1:]: + self_test() + print(f'PASS: ShellSpec {shell}개, Frontend CI {runtime}개 파일 등록 확인' + + (' (누락/중복/CI 우회 회귀 테스트 통과)' if '--self-test' in sys.argv[1:] else '')) + except (CoverageError, OSError) as exc: + print(f'FAIL: {exc}', file=sys.stderr) + sys.exit(1) diff --git a/luci-app-smartsafehub/tools/openwrt-25.12-commands.txt b/luci-app-smartsafehub/tools/openwrt-25.12-commands.txt new file mode 100644 index 000000000..363957b40 --- /dev/null +++ b/luci-app-smartsafehub/tools/openwrt-25.12-commands.txt @@ -0,0 +1,98 @@ +# OpenWrt 25.12 BusyBox default applets, with explicitly installed OpenWrt base +# commands. Source for BusyBox default y/n: +# https://github.com/openwrt/openwrt/blob/openwrt-25.12/package/utils/busybox/Config-defaults.in +# This is an *allowlist* of supported router commands, not host Ubuntu commands. +# Non-BusyBox commands are documented separately below. Never add a command just +# to silence a failure; verify that each supported firmware image supplies it. +# BusyBox 25.12 default applets needed by SmartSafeHub shell runtime: +awk +basename +cat +chmod +chown +cp +cut +cmp +date +dd +df +flock +dirname +dmesg +du +echo +env +expr +find +grep +hexdump +head +id +kill +killall +ln +logger +ls +md5sum +mkdir +mkfifo +mknod +mktemp +mount +mv +pidof +passwd +ping +printf +ps +readlink +reboot +rm +rmdir +sed +seq +sha256sum +sleep +sort +start-stop-daemon +strings +sync +tail +tar +tee +test +time +touch +tr +true +uname +uniq +uptime +wc +xargs +yes +# Guaranteed OpenWrt base/required-package commands (not BusyBox applets): +# apk: firmware package manager (OpenWrt 25.12) +apk +# ubus / uci: base system +ubus +uci +# jsonfilter: base/explicit LuCI and SafeShield dependency +jsonfilter +# uclient-fetch: LUCI_DEPENDS +uclient-fetch +uclient-fetch +# procd: LUCI_DEPENDS +procd +procd +# networking tools included by OpenWrt base system / firmware +ip +ifstatus +ifup +ifdown +wifi +# fw4 and nft: firewall packages in stock OpenWrt router +fw4 +nft +# system services and init tooling +# factoryreset: OpenWrt 25.12 base-files reset utility + +factoryreset diff --git a/luci-app-wwand/htdocs/luci-static/resources/wwand/modemopts.js b/luci-app-wwand/htdocs/luci-static/resources/wwand/modemopts.js index 6b3fd3678..1ad0d8562 100644 --- a/luci-app-wwand/htdocs/luci-static/resources/wwand/modemopts.js +++ b/luci-app-wwand/htdocs/luci-static/resources/wwand/modemopts.js @@ -702,6 +702,21 @@ return baseclass.extend({ o.depends('gnss', '1'); bind(o); + /* '' is not "back to full": the modem stores the masks, so unsetting + the option changes nothing on it (wwand reference.md, gnss_nmea) — + the labels have to say so, or "unchanged" reads as "default". It is + written only in start_gnss (modem_common.uc, once per bring-up); a + saved change gets there because gnss_nmea is in the daemon's modem + reload signature, which restarts the modem. */ + o = s.taboption(tab, form.ListValue, 'gnss_nmea', _('GNSS NMEA output'), + _('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.')); + o.value('', _('Leave modem setting unchanged')); + o.value('fix', _('Fix only (less CPU)')); + o.value('full', _('All sentences')); + o.optional = true; + o.depends('gnss', '1'); + bind(o); + o = s.taboption(tab, form.Flag, 'auto_correct_config', _('Auto-correct modem config'), _('Let wwand reprogram mismatched modem-side settings (attach profile, etc.) when it detects them.')); o.default = '0'; diff --git a/luci-app-wwand/po/templates/wwand.pot b/luci-app-wwand/po/templates/wwand.pot index 16d70cc4b..208e1de98 100644 --- a/luci-app-wwand/po/templates/wwand.pot +++ b/luci-app-wwand/po/templates/wwand.pot @@ -329,6 +329,10 @@ msgstr "" msgid "All" msgstr "" +#: htdocs/luci-static/resources/wwand/modemopts.js:715 +msgid "All sentences" +msgstr "" + #: htdocs/luci-static/resources/view/wwand/status.js:611 msgid "" "An environmental limit the modem reports (temperature, voltage or charge " @@ -400,7 +404,7 @@ msgstr "" msgid "Authentication type" msgstr "" -#: htdocs/luci-static/resources/wwand/modemopts.js:705 +#: htdocs/luci-static/resources/wwand/modemopts.js:720 msgid "Auto-correct modem config" msgstr "" @@ -931,6 +935,10 @@ msgstr "" msgid "Fix age" msgstr "" +#: htdocs/luci-static/resources/wwand/modemopts.js:714 +msgid "Fix only (less CPU)" +msgstr "" + #: htdocs/luci-static/resources/view/wwand/settings.js:680 msgid "Forbidden" msgstr "" @@ -960,6 +968,10 @@ msgstr "" msgid "GNSS" msgstr "" +#: htdocs/luci-static/resources/wwand/modemopts.js:711 +msgid "GNSS NMEA output" +msgstr "" + #: htdocs/luci-static/resources/wwand/modemopts.js:694 msgid "GNSS receiver" msgstr "" @@ -1212,10 +1224,11 @@ msgid "" msgstr "" #: htdocs/luci-static/resources/wwand/modemopts.js:178 +#: htdocs/luci-static/resources/wwand/modemopts.js:713 msgid "Leave modem setting unchanged" msgstr "" -#: htdocs/luci-static/resources/wwand/modemopts.js:706 +#: htdocs/luci-static/resources/wwand/modemopts.js:721 msgid "" "Let wwand reprogram mismatched modem-side settings (attach profile, etc.) " "when it detects them." @@ -2019,6 +2032,18 @@ msgstr "" msgid "QRTR node" msgstr "" +#: htdocs/luci-static/resources/wwand/modemopts.js:712 +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 " diff --git a/maxminddb-dump-country/Makefile b/maxminddb-dump-country/Makefile index 6f8bdb91c..42ab2b217 100644 --- a/maxminddb-dump-country/Makefile +++ b/maxminddb-dump-country/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=maxminddb-dump-country -PKG_VERSION:=1 -PKG_RELEASE:=1 +PKG_VERSION:=2 +PKG_RELEASE:=2 PKG_MAINTAINER:=Konstantine Shevlakov PKG_LICENSE:=LICENSE @@ -40,12 +40,12 @@ endef define Build/Compile $(TARGET_CC) $(TARGET_CFLAGS) -Wall -lmaxminddb \ - -o $(PKG_BUILD_DIR)/xt_geoip_build_maxmind $(PKG_BUILD_DIR)/xt_geoip_build_maxmind.c + -o $(PKG_BUILD_DIR)/xt_geoip_build $(PKG_BUILD_DIR)/xt_geoip_build_maxmind.c endef define Package/$(PKG_NAME)/install $(INSTALL_DIR) $(1)/usr/bin - $(INSTALL_BIN) $(PKG_BUILD_DIR)/xt_geoip_build_maxmind $(1)/usr/bin/ + $(INSTALL_BIN) $(PKG_BUILD_DIR)/xt_geoip_build $(1)/usr/bin/ endef $(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/maxminddb-dump-country/patches/0001-geoip-add-DB-IP-CSV-input-support.patch b/maxminddb-dump-country/patches/0001-geoip-add-DB-IP-CSV-input-support.patch new file mode 100644 index 000000000..f0c6c3263 --- /dev/null +++ b/maxminddb-dump-country/patches/0001-geoip-add-DB-IP-CSV-input-support.patch @@ -0,0 +1,388 @@ +From 8ea81c36bee0d4eefc023561a423cbfaac7acc41 Mon Sep 17 00:00:00 2001 +From: koshev-msk +Date: Thu, 29 Oct 2026 23:04:25 +0000 +Subject: [PATCH] geoip: add DB-IP CSV input support + +Add a -d option to read DB-IP Country Lite CSV files and generate the +same per-country .iv4/.iv6 binary range files as the existing MMDB +input path. + +The parser accepts the DB-IP start_ip,end_ip,country format, handles +quoted fields and CRLF line endings, skips the header and unknown +country codes, and validates address families and range ordering. + +The existing MMDB processing path and -c CSV export mode remain +unchanged. + +Signed-off-by: OpenWrt Developer +--- + xt_geoip_build_maxmind.c | 292 ++++++++++++++++++++++++++++++++++----- + 1 file changed, 260 insertions(+), 32 deletions(-) + +diff --git a/xt_geoip_build_maxmind.c b/xt_geoip_build_maxmind.c +index 55e86d0..95d2c92 100644 +--- a/xt_geoip_build_maxmind.c ++++ b/xt_geoip_build_maxmind.c +@@ -3,6 +3,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -31,6 +32,8 @@ int opt_v = 0; + int opt_4 = 1; + int opt_6 = 1; + int opt_csv = 0; ++int opt_dbip = 0; ++char *dbip_filename = NULL; + + int opt_asn = 0; + char *opt_asn_val = NULL; +@@ -326,6 +329,204 @@ void p_dump(int l, uint8_t *path, MMDB_entry_s *e) { + fwrite(&path[start],16-start,1,CFD[cind]); + fwrite(&path2[start],16-start,1,CFD[cind]); + } ++/* ++ * DB-IP Country Lite CSV format: ++ * start_ip,end_ip,country ++ * ++ * The fields may optionally be quoted. The CSV has no embedded commas. ++ */ ++static char *csv_trim(char *field) ++{ ++ char *end; ++ ++ while(isspace((unsigned char)*field)) ++ field++; ++ end = field + strlen(field); ++ while(end > field && isspace((unsigned char)end[-1])) ++ *--end = '\0'; ++ if(end - field >= 2 && field[0] == '"' && end[-1] == '"') { ++ field++; ++ end[-1] = '\0'; ++ } ++ return field; ++} ++ ++static int dbip_write_range(int family, const uint8_t *start, ++ const uint8_t *end, const char *country, int cind) ++{ ++ char path[sizeof(output_dir) + 8]; ++ FILE **files; ++ int32_t *counts; ++ size_t addr_len; ++ ++ if(family == AF_INET) { ++ if(!opt_4) ++ return 0; ++ files = CFD4; ++ counts = CCN4; ++ addr_len = 4; ++ } else { ++ if(!opt_6) ++ return 0; ++ files = CFD6; ++ counts = CCN6; ++ addr_len = 16; ++ } ++ ++ if(!files[cind]) { ++ snprintf(path, sizeof(path), "%s%s.iv%c", ++ output_dir, country, family == AF_INET ? '4' : '6'); ++ files[cind] = fopen(path, "wb"); ++ if(!files[cind]) { ++ fprintf(stderr, "create file %s: %s\n", ++ path, strerror(errno)); ++ return -1; ++ } ++ } ++ ++ if(fwrite(start, addr_len, 1, files[cind]) != 1 || ++ fwrite(end, addr_len, 1, files[cind]) != 1) { ++ fprintf(stderr, "write %s range failed: %s\n", ++ country, strerror(errno)); ++ return -1; ++ } ++ ++ counts[cind]++; ++ return 0; ++} ++ ++static int dbip_csv_dump(const char *path) ++{ ++ FILE *input; ++ char line[256]; ++ unsigned long lineno = 0; ++ int first_line = 1; ++ int result = 0; ++ ++ input = fopen(path, "r"); ++ if(!input) { ++ fprintf(stderr, "Can't open DB-IP CSV %s: %s\n", ++ path, strerror(errno)); ++ return -1; ++ } ++ ++ while(fgets(line, sizeof(line), input)) { ++ char *field[3], *p, *comma; ++ char *country; ++ uint8_t start[16], end[16]; ++ char start_text[INET6_ADDRSTRLEN]; ++ char end_text[INET6_ADDRSTRLEN]; ++ int family, end_family, cind; ++ ++ lineno++; ++ if(!strchr(line, '\n') && !feof(input)) { ++ fprintf(stderr, "DB-IP CSV line %lu is too long\n", lineno); ++ result = -1; ++ break; ++ } ++ ++ p = line + strspn(line, " \t"); ++ p[strcspn(p, "\r\n")] = '\0'; ++ if(!*p) ++ continue; ++ ++ field[0] = p; ++ comma = strchr(p, ','); ++ if(!comma) ++ goto malformed; ++ *comma = '\0'; ++ field[1] = comma + 1; ++ ++ comma = strchr(field[1], ','); ++ if(!comma) ++ goto malformed; ++ *comma = '\0'; ++ field[2] = comma + 1; ++ if(strchr(field[2], ',')) ++ goto malformed; ++ ++ field[0] = csv_trim(field[0]); ++ field[1] = csv_trim(field[1]); ++ field[2] = csv_trim(field[2]); ++ country = field[2]; ++ ++ if(first_line) { ++ first_line = 0; ++ if(!strcmp(field[0], "start_ip") || ++ !strcmp(field[0], "ip_start")) ++ continue; ++ } ++ ++ /* DB-IP uses "--" when the country is unknown. */ ++ if(!strcmp(country, "--") || !*country) ++ continue; ++ ++ if(strlen(country) != 2 || ++ country[0] < 'A' || country[0] > 'Z' || ++ country[1] < 'A' || country[1] > 'Z') { ++ fprintf(stderr, "Invalid country code on DB-IP CSV line %lu\n", ++ lineno); ++ result = -1; ++ break; ++ } ++ ++ cind = country2idx(country); ++ if(cind < 0) ++ continue; ++ ++ if(inet_pton(AF_INET, field[0], start) == 1) ++ family = AF_INET; ++ else if(inet_pton(AF_INET6, field[0], start) == 1) ++ family = AF_INET6; ++ else { ++ fprintf(stderr, "Invalid start IP on DB-IP CSV line %lu\n", ++ lineno); ++ result = -1; ++ break; ++ } ++ ++ if(inet_pton(family, field[1], end) != 1) { ++ fprintf(stderr, "Invalid or mismatched end IP on DB-IP CSV line %lu\n", ++ lineno); ++ result = -1; ++ break; ++ } ++ end_family = family; ++ if(end_family != family || ++ memcmp(start, end, family == AF_INET ? 4 : 16) > 0) { ++ fprintf(stderr, "Invalid IP range on DB-IP CSV line %lu\n", ++ lineno); ++ result = -1; ++ break; ++ } ++ ++ if(dbip_write_range(family, start, end, country, cind)) { ++ result = -1; ++ break; ++ } ++ ++ if(opt_v > 1) { ++ inet_ntop(family, start, start_text, sizeof(start_text)); ++ inet_ntop(family, end, end_text, sizeof(end_text)); ++ printf("%s-%s %s\n", start_text, end_text, country); ++ } ++ continue; ++ ++malformed: ++ fprintf(stderr, "Malformed DB-IP CSV line %lu\n", lineno); ++ result = -1; ++ break; ++ } ++ ++ if(ferror(input)) { ++ fprintf(stderr, "Error reading DB-IP CSV %s: %s\n", ++ path, strerror(errno)); ++ result = -1; ++ } ++ fclose(input); ++ return result; ++} ++ + static int skip_n96 = 0; + + void tree_walk(int rn,int l, uint8_t *path) { +@@ -372,9 +573,10 @@ int main(int argc, char **argv) + + progname = strdup(argv[0]); + +- while((c=getopt(argc,argv,"46cvo:A:N:")) != -1) { ++ while((c=getopt(argc,argv,"46cvd:o:A:N:")) != -1) { + switch(c) { + case 'c': opt_csv = 1; break; ++ case 'd': opt_dbip = 1; dbip_filename = optarg; break; + case '4': opt_6 = 0; break; + case '6': opt_4 = 0; break; + case 'v': opt_v++; break; +@@ -394,17 +596,29 @@ int main(int argc, char **argv) + filename = strdup(argv[optind]); + } else { + filename = opt_asn ? asn_name:country_name; +- if(access(filename,R_OK)) usage(); ++ if(!opt_dbip && access(filename,R_OK)) usage(); ++ } ++ ++ if(opt_dbip) { ++ if(opt_csv || opt_asn || !opt_xtgeo || optind < argc) { ++ fprintf(stderr, "%s: -d requires -o and cannot be combined with -c, -A or a positional input file\n", progname); ++ exit(1); ++ } ++ filename = dbip_filename; + } + + if(stat(filename,&st)) { +- perror("mmdb file:"); ++ perror("input file:"); + exit(1); + } + if(!opt_xtgeo && !opt_csv && !opt_asn) { + fprintf(stderr,"%s: -o or -c or -a is required!\n",progname); + exit(1); + } ++ if(opt_dbip && !S_ISREG(st.st_mode)) { ++ fprintf(stderr, "DB-IP input is not a regular file\n"); ++ exit(1); ++ } + if(opt_asn && (opt_xtgeo || opt_csv)) { + fprintf(stderr,"%s: cant use -A with -o or -c\n",progname); + exit(1); +@@ -426,43 +640,55 @@ int main(int argc, char **argv) + output_dir[l+2] = '\0'; + } + +- status = MMDB_open(filename, MMDB_MODE_MMAP, &mmdb); ++ if(opt_dbip) { ++ status = dbip_csv_dump(filename); ++ if(status) ++ exit(1); ++ } else { ++ status = MMDB_open(filename, MMDB_MODE_MMAP, &mmdb); + +- if (MMDB_SUCCESS != status) { +- fprintf(stderr, "\n Can't open %s - %s\n", +- filename, MMDB_strerror(status)); ++ if (MMDB_SUCCESS != status) { ++ fprintf(stderr, "\n Can't open %s - %s\n", ++ filename, MMDB_strerror(status)); + +- if (MMDB_IO_ERROR == status) { +- fprintf(stderr, " IO error: %s\n", strerror(errno)); +- } +- exit(1); +- } +- if(opt_csv) { +- if(opt_4) { +- CSV_IPv4_fd = fopen("GeoLite2-Country-Blocks-IPv4.csv","w"); +- if(CSV_IPv4_fd) +- fprintf(CSV_IPv4_fd,"%s",CSV_IP_hdr); ++ if (MMDB_IO_ERROR == status) { ++ fprintf(stderr, " IO error: %s\n", strerror(errno)); ++ } ++ exit(1); + } +- if(opt_6) { +- CSV_IPv6_fd = fopen("GeoLite2-Country-Blocks-IPv6.csv","w"); +- if(CSV_IPv6_fd) +- fprintf(CSV_IPv6_fd,"%s",CSV_IP_hdr); ++ if(opt_csv) { ++ if(opt_4) { ++ CSV_IPv4_fd = fopen("GeoLite2-Country-Blocks-IPv4.csv","w"); ++ if(CSV_IPv4_fd) ++ fprintf(CSV_IPv4_fd,"%s",CSV_IP_hdr); ++ } ++ if(opt_6) { ++ CSV_IPv6_fd = fopen("GeoLite2-Country-Blocks-IPv6.csv","w"); ++ if(CSV_IPv6_fd) ++ fprintf(CSV_IPv6_fd,"%s",CSV_IP_hdr); ++ } ++ CSV_EN_fd = fopen("GeoLite2-Country-Locations-en.csv","w"); ++ if(CSV_EN_fd) ++ fprintf(CSV_EN_fd,"%s",CSV_EN_hdr); + } +- CSV_EN_fd = fopen("GeoLite2-Country-Locations-en.csv","w"); +- if(CSV_EN_fd) +- fprintf(CSV_EN_fd,"%s",CSV_EN_hdr); ++ memset(path,0,sizeof(path)); ++ if(!opt_6) { ++ tree_walk(mmdb.ipv4_start_node.node_value,mmdb.ipv4_start_node.netmask,path); ++ } else ++ tree_walk(0,0,path); ++ MMDB_close(&mmdb); + } +- memset(path,0,sizeof(path)); +- if(!opt_6) { +- tree_walk(mmdb.ipv4_start_node.node_value,mmdb.ipv4_start_node.netmask,path); +- } else +- tree_walk(0,0,path); +- MMDB_close(&mmdb); + if(opt_asn) exit(0); + for(fd = 0; fd < sizeof(CFD4)/sizeof(CFD4[0]); fd++) { + if(opt_xtgeo) { +- if(CFD4[fd]) fclose(CFD4[fd]); +- if(CFD6[fd]) fclose(CFD6[fd]); ++ if(CFD4[fd] && fclose(CFD4[fd])) { ++ fprintf(stderr, "close IPv4 output failed: %s\n", strerror(errno)); ++ status = 1; ++ } ++ if(CFD6[fd] && fclose(CFD6[fd])) { ++ fprintf(stderr, "close IPv6 output failed: %s\n", strerror(errno)); ++ status = 1; ++ } + } + c_ipv4 += CCN4[fd]; + c_ipv6 += CCN6[fd]; +@@ -482,6 +708,8 @@ int main(int argc, char **argv) + if(CSV_IPv6_fd) fclose(CSV_IPv6_fd); + if(CSV_EN_fd) fclose(CSV_EN_fd); + ++ if(status) ++ exit(1); + exit(0); + } + +-- +2.43.0 + diff --git a/maxminddb-dump-country/patches/0002-add-help-CSV-args.patch b/maxminddb-dump-country/patches/0002-add-help-CSV-args.patch new file mode 100644 index 000000000..29abcbd68 --- /dev/null +++ b/maxminddb-dump-country/patches/0002-add-help-CSV-args.patch @@ -0,0 +1,11 @@ +--- a/xt_geoip_build_maxmind.c ++++ b/xt_geoip_build_maxmind.c +@@ -559,7 +559,7 @@ + } + + void usage(void) { +- printf("%s [-4] [-6] [-v] [-c] [-N ASNUM|string ][-A all|asnum[,asnum...]] [-o outputdir] file.mmdb\n",progname); ++ printf("%s [-4] [-6] [-v] [-c] [-N ASNUM|string ][-A all|asnum[,asnum...]] [-o outputdir] file.mmdb|-d file.dbip.csv\n",progname); + printf("\t asnum: number | number-number\n"); + exit(1); + } diff --git a/netdata/Makefile b/netdata/Makefile index 2374034ca..a14d99761 100644 --- a/netdata/Makefile +++ b/netdata/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=netdata PKG_VERSION:=2.12.2 -PKG_RELEASE:=22 +PKG_RELEASE:=23 PKG_MAINTAINER:=Josef Schlehofer , Daniel Engberg PKG_LICENSE:=GPL-3.0-or-later @@ -49,7 +49,6 @@ define Package/netdata/description python3-urllib3 endef -TARGET_CFLAGS := $(filter-out -O%,$(TARGET_CFLAGS)) -O3 TARGET_LDFLAGS += -L$(STAGING_DIR)/usr/lib TARGET_CFLAGS += $(FPIC) diff --git a/node/Makefile b/node/Makefile index 2f2efbbdb..007a92cc8 100644 --- a/node/Makefile +++ b/node/Makefile @@ -8,25 +8,25 @@ PKG_NAME:=node ifeq ($(CONFIG_NODEJS_22),y) PKG_VERSION:=22.23.4 -PKG_RELEASE:=3 +PKG_RELEASE:=4 PKG_HASH:=skip PATCH_DIR:=./patches/v22.x NODEJS_BIN_SUM:=b8492284c76a54baf58d61668dd0e41ee0fc98adabf9503d30f05d08f55b4406 NODE_MODULE_VERSION:=127 else ifeq ($(CONFIG_NODEJS_26),y) -PKG_VERSION:=26.12.1 -PKG_RELEASE:=3 +PKG_VERSION:=26.13.0 +PKG_RELEASE:=4 PKG_HASH:=skip PATCH_DIR:=./patches/v26.x -NODEJS_BIN_SUM:=0fd1eb7770598f52a21995edf6ed35fc690400fa437c2386af329519e9143132 +NODEJS_BIN_SUM:=5848553868b402d3fabb5b09270649977780a390f832155721025a08b5104455 NODE_MODULE_VERSION:=147 else -PKG_VERSION:=24.22.1 -PKG_RELEASE:=3 +PKG_VERSION:=24.22.2 +PKG_RELEASE:=4 PKG_HASH:=skip PATCH_DIR:=./patches/v24.x -NODEJS_BIN_SUM:=eb6728e77c576a35162c122c45ccadf224d89ff00abc8dae46cc2897c6002d7c +NODEJS_BIN_SUM:=361718c444e49af2ca5a110c83ba6a8901143f23ab285ebbef0341f391f482a9 NODE_MODULE_VERSION:=137 endif endif @@ -104,10 +104,10 @@ define Package/node/config bool "22.x Maintenance LTS" config NODEJS_24 - bool "24.x Active LTS" + bool "24.x Maintenance LTS" config NODEJS_26 - bool "26.x Current" + bool "26.x Active LTS" endchoice if PACKAGE_node diff --git a/node/patches/v24.x/999-v8-icu4c-79.patch b/node/patches/v24.x/999-v8-icu4c-79.patch new file mode 100644 index 000000000..55df32a4b --- /dev/null +++ b/node/patches/v24.x/999-v8-icu4c-79.patch @@ -0,0 +1,220 @@ +From e455d955bd6b7a7549106d1b9d7ae5bb3b9e77b4 Mon Sep 17 00:00:00 2001 +From: Jakob Linke +Date: Fri, 9 Oct 2026 12:41:30 +0200 +Subject: [PATCH] [regexp] Handle supplementary case equivalents of BMP + characters + +Unicode 18 adds U+1DF95, which simple-folds to U+00DF. This creates the +first case-equivalence class spanning BMP and supplementary characters: +{U+00DF, U+1E9E, U+1DF95}. + +Discard supplementary characters from the generator's non-unicode +closures before calling Canonicalize, which requires UTF-16 code units +and otherwise CHECK-fails with ICU 79. Apply the same filtering in +runtime non-unicode closure. + +Generate SupplementaryEquivalentsSet and desugar its BMP characters into +character classes under /iu and /iv, allowing literals to match +surrogate pairs. + +Backreferences with differing UTF-16 character widths remain unsupported +and are tracked in crbug.com/571793821. + +IgnoreSet is unchanged with ICU 78 and with ICU 78.2 patched to upstream +Unicode 18 case data. With the patched data, the generator succeeds, +SupplementaryEquivalentsSet is {U+00DF, U+1E9E}, and regexp tests pass +in release and debug. + +Bug: 571232615, 571668686 +Change-Id: I9eacb273a74c66e17f0cfb1b256e863f2718c130 +Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8548134 +Reviewed-by: Patrick Thier +Commit-Queue: Jakob Linke +Cr-Commit-Position: refs/heads/main@{#110522} +--- + src/regexp/gen-regexp-special-case.cc | 18 ++++++++++ + src/regexp/regexp-parser.cc | 18 +++++++--- + src/regexp/special-case.h | 47 ++++++++++++++++++++++++++- + 3 files changed, 77 insertions(+), 6 deletions(-) + +--- a/deps/v8/src/regexp/gen-regexp-special-case.cc ++++ b/deps/v8/src/regexp/gen-regexp-special-case.cc +@@ -9,6 +9,7 @@ + + #include "src/base/strings.h" + #include "src/regexp/special-case.h" ++#include "unicode/uchar.h" + + namespace v8 { + namespace internal { +@@ -17,6 +18,19 @@ static const base::uc32 kSurrogateStart + static const base::uc32 kSurrogateEnd = 0xdfff; + static const base::uc32 kNonBmpStart = 0x10000; + ++// Removes supplementary code points from |set| and returns whether any were ++// present. Since Unicode 18, the case closure of a BMP character can contain ++// supplementary code points (U+1DF95 LATIN SMALL LIGATURE LONG S WITH ++// DESCENDER S case-folds to U+00DF LATIN SMALL LETTER SHARP S). Non-unicode ++// ignoreCase matching compares UTF-16 code units, so a supplementary code ++// point never matches a BMP character and does not take part in Canonicalize. ++bool RemoveSupplementary(icu::UnicodeSet& set) { ++ constexpr UChar32 kFirstSupplementary = static_cast(kNonBmpStart); ++ if (!set.containsSome(kFirstSupplementary, UCHAR_MAX_VALUE)) return false; ++ set.remove(kFirstSupplementary, UCHAR_MAX_VALUE); ++ return true; ++} ++ + // The following code generates "src/regexp/special-case.cc". + void PrintSet(std::ofstream& out, const char* name, + const icu::UnicodeSet& set) { +@@ -51,6 +65,7 @@ void PrintSpecial(std::ofstream& out) { + icu::UnicodeSet current; + icu::UnicodeSet special_add; + icu::UnicodeSet ignore; ++ icu::UnicodeSet supplementary_equivalents; + UErrorCode status = U_ZERO_ERROR; + icu::UnicodeSet upper("[\\p{Lu}]", status); + CHECK(U_SUCCESS(status)); +@@ -63,6 +78,7 @@ void PrintSpecial(std::ofstream& out) { + } + current.set(i, i); + current.closeOver(USET_CASE_INSENSITIVE); ++ if (RemoveSupplementary(current)) supplementary_equivalents.add(i); + + // Check to see if all characters in the case-folding equivalence + // class as defined by UnicodeSet::closeOver all map to the same +@@ -112,6 +128,7 @@ void PrintSpecial(std::ofstream& out) { + UChar32 canonical = RegExpCaseFolding::Canonicalize(c); + current.set(c, c); + current.closeOver(USET_CASE_INSENSITIVE); ++ RemoveSupplementary(current); + current.removeAll(ignore); + for (int32_t j = 0; j < current.getRangeCount(); j++) { + for (UChar32 c2 = current.getRangeStart(j); +@@ -124,6 +141,7 @@ void PrintSpecial(std::ofstream& out) { + + PrintSet(out, "IgnoreSet", ignore); + PrintSet(out, "SpecialAddSet", special_add); ++ PrintSet(out, "SupplementaryEquivalentsSet", supplementary_equivalents); + } + + void WriteHeader(const char* header_filename) { +--- a/deps/v8/src/regexp/regexp-parser.cc ++++ b/deps/v8/src/regexp/regexp-parser.cc +@@ -9,6 +9,7 @@ + #include "src/regexp/regexp-ast.h" + #include "src/regexp/regexp-macro-assembler.h" + #include "src/regexp/regexp.h" ++#include "src/regexp/special-case.h" + #include "src/strings/char-predicates-inl.h" + #include "src/utils/ostreams.h" + #include "src/utils/utils.h" +@@ -155,6 +156,10 @@ void RegExpTextBuilder::FlushText() { + + void RegExpTextBuilder::AddCharacter(base::uc16 c) { + FlushPendingSurrogate(); ++ if (NeedsDesugaringForIgnoreCase(c)) { ++ AddClassRangesForDesugaring(c); ++ return; ++ } + if (characters_ == nullptr) { + characters_ = zone()->New>(4, zone()); + } +@@ -237,15 +242,18 @@ bool RegExpTextBuilder::NeedsDesugaringF + return false; + } + +-// We only use this for characters made of surrogate pairs. All other +-// characters outside of character classes are made case independent in the +-// code generation. ++// Desugar supplementary characters with case equivalents and BMP characters ++// with supplementary equivalents. Other atoms are folded during code ++// generation. + bool RegExpTextBuilder::NeedsDesugaringForIgnoreCase(base::uc32 c) { + #ifdef V8_INTL_SUPPORT + if (IsUnicodeMode() && ignore_case()) { ++ if (c <= unibrow::Utf16::kMaxNonSurrogateCharCode) { ++ return RegExpCaseFolding::HasSupplementaryEquivalents( ++ static_cast(c)); ++ } + icu::UnicodeSet set(c, c); +- set.closeOver(USET_CASE_INSENSITIVE); +- set.removeAllStrings(); ++ RegExpCaseFolding::CloseOver(set, RegExpCaseFolding::Mode::kUnicode); + return set.size() > 1; + } + // In the case where ICU is not included, we act as if the unicode flag is +--- a/deps/v8/src/regexp/special-case.h ++++ b/deps/v8/src/regexp/special-case.h +@@ -7,8 +7,8 @@ + + #ifdef V8_INTL_SUPPORT + #include "src/base/logging.h" ++#include "src/base/strings.h" + #include "src/common/globals.h" +- + #include "unicode/uchar.h" + #include "unicode/uniset.h" + #include "unicode/unistr.h" +@@ -71,9 +71,17 @@ namespace internal { + // another character. Characters that match no other characters in + // their equivalence class are added to IgnoreSet. Characters that + // match at least one other character are added to SpecialAddSet. ++// Supplementary code points are ignored throughout: non-unicode matching ++// compares UTF-16 code units, so they can never match a BMP character. ++// ++// The generator additionally emits SupplementaryEquivalentsSet, the BMP ++// characters whose Unicode case closure contains supplementary code points. ++// This set is used for unicode-mode matching, see HasSupplementaryEquivalents. + + class RegExpCaseFolding final : public AllStatic { + public: ++ enum class Mode { kNonUnicode, kUnicode }; ++ + static const icu::UnicodeSet& IgnoreSet(); + static const icu::UnicodeSet& SpecialAddSet(); + +@@ -108,6 +116,43 @@ class RegExpCaseFolding final : public A + // h. Return cu. + return cu; + } ++ ++ // Close a set of characters under case equivalence, preserving its ++ // original members. Non-unicode inputs must be UTF-16 code units. ++ static void CloseOver(icu::UnicodeSet& set, Mode mode) { ++ if (mode == Mode::kUnicode) { ++ set.closeOver(USET_SIMPLE_CASE_INSENSITIVE); ++ return; ++ } ++ DCHECK(!set.containsSome(0x10000, 0x10ffff)); ++ if (IgnoreSet().containsNone(set)) { ++ CloseOverNonUnicode(set); ++ return; ++ } ++ if (IgnoreSet().containsAll(set)) return; ++ // Ignored characters neither contribute nor acquire equivalents, but ++ // must remain in the result if they were explicitly present. ++ icu::UnicodeSet ignored(set); ++ ignored.retainAll(IgnoreSet()); ++ set.removeAll(IgnoreSet()); ++ CloseOverNonUnicode(set); ++ set.addAll(ignored); ++ } ++ ++ // Whether c has supplementary Unicode case equivalents. ++ static bool HasSupplementaryEquivalents(base::uc16 c) { ++ return SupplementaryEquivalentsSet().contains(c); ++ } ++ ++ private: ++ static const icu::UnicodeSet& SupplementaryEquivalentsSet(); ++ ++ // Requires a set containing no IgnoreSet characters. ++ static void CloseOverNonUnicode(icu::UnicodeSet& set) { ++ set.closeOver(USET_SIMPLE_CASE_INSENSITIVE); ++ set.removeAll(IgnoreSet()); ++ set.remove(0x10000, 0x10ffff); ++ } + }; + + } // namespace internal diff --git a/node/patches/v26.x/999-v8-icu4c-79.patch b/node/patches/v26.x/999-v8-icu4c-79.patch new file mode 100644 index 000000000..c644fa817 --- /dev/null +++ b/node/patches/v26.x/999-v8-icu4c-79.patch @@ -0,0 +1,220 @@ +From e455d955bd6b7a7549106d1b9d7ae5bb3b9e77b4 Mon Sep 17 00:00:00 2001 +From: Jakob Linke +Date: Fri, 9 Oct 2026 12:41:30 +0200 +Subject: [PATCH] [regexp] Handle supplementary case equivalents of BMP + characters + +Unicode 18 adds U+1DF95, which simple-folds to U+00DF. This creates the +first case-equivalence class spanning BMP and supplementary characters: +{U+00DF, U+1E9E, U+1DF95}. + +Discard supplementary characters from the generator's non-unicode +closures before calling Canonicalize, which requires UTF-16 code units +and otherwise CHECK-fails with ICU 79. Apply the same filtering in +runtime non-unicode closure. + +Generate SupplementaryEquivalentsSet and desugar its BMP characters into +character classes under /iu and /iv, allowing literals to match +surrogate pairs. + +Backreferences with differing UTF-16 character widths remain unsupported +and are tracked in crbug.com/571793821. + +IgnoreSet is unchanged with ICU 78 and with ICU 78.2 patched to upstream +Unicode 18 case data. With the patched data, the generator succeeds, +SupplementaryEquivalentsSet is {U+00DF, U+1E9E}, and regexp tests pass +in release and debug. + +Bug: 571232615, 571668686 +Change-Id: I9eacb273a74c66e17f0cfb1b256e863f2718c130 +Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8548134 +Reviewed-by: Patrick Thier +Commit-Queue: Jakob Linke +Cr-Commit-Position: refs/heads/main@{#110522} +--- + src/regexp/gen-regexp-special-case.cc | 18 ++++++++++ + src/regexp/regexp-parser.cc | 18 +++++++--- + src/regexp/special-case.h | 47 ++++++++++++++++++++++++++- + 3 files changed, 77 insertions(+), 6 deletions(-) + +--- a/deps/v8/src/regexp/gen-regexp-special-case.cc ++++ b/deps/v8/src/regexp/gen-regexp-special-case.cc +@@ -9,6 +9,7 @@ + + #include "src/base/strings.h" + #include "src/regexp/special-case.h" ++#include "unicode/uchar.h" + + namespace v8 { + namespace internal { +@@ -17,6 +18,19 @@ static const base::uc32 kSurrogateStart + static const base::uc32 kSurrogateEnd = 0xdfff; + static const base::uc32 kNonBmpStart = 0x10000; + ++// Removes supplementary code points from |set| and returns whether any were ++// present. Since Unicode 18, the case closure of a BMP character can contain ++// supplementary code points (U+1DF95 LATIN SMALL LIGATURE LONG S WITH ++// DESCENDER S case-folds to U+00DF LATIN SMALL LETTER SHARP S). Non-unicode ++// ignoreCase matching compares UTF-16 code units, so a supplementary code ++// point never matches a BMP character and does not take part in Canonicalize. ++bool RemoveSupplementary(icu::UnicodeSet& set) { ++ constexpr UChar32 kFirstSupplementary = static_cast(kNonBmpStart); ++ if (!set.containsSome(kFirstSupplementary, UCHAR_MAX_VALUE)) return false; ++ set.remove(kFirstSupplementary, UCHAR_MAX_VALUE); ++ return true; ++} ++ + // The following code generates "src/regexp/special-case.cc". + void PrintSet(std::ofstream& out, const char* name, + const icu::UnicodeSet& set) { +@@ -51,6 +65,7 @@ void PrintSpecial(std::ofstream& out) { + icu::UnicodeSet current; + icu::UnicodeSet special_add; + icu::UnicodeSet ignore; ++ icu::UnicodeSet supplementary_equivalents; + UErrorCode status = U_ZERO_ERROR; + icu::UnicodeSet upper("[\\p{Lu}]", status); + CHECK(U_SUCCESS(status)); +@@ -63,6 +78,7 @@ void PrintSpecial(std::ofstream& out) { + } + current.set(i, i); + current.closeOver(USET_CASE_INSENSITIVE); ++ if (RemoveSupplementary(current)) supplementary_equivalents.add(i); + + // Check to see if all characters in the case-folding equivalence + // class as defined by UnicodeSet::closeOver all map to the same +@@ -112,6 +128,7 @@ void PrintSpecial(std::ofstream& out) { + UChar32 canonical = RegExpCaseFolding::Canonicalize(c); + current.set(c, c); + current.closeOver(USET_CASE_INSENSITIVE); ++ RemoveSupplementary(current); + current.removeAll(ignore); + for (int32_t j = 0; j < current.getRangeCount(); j++) { + for (UChar32 c2 = current.getRangeStart(j); +@@ -124,6 +141,7 @@ void PrintSpecial(std::ofstream& out) { + + PrintSet(out, "IgnoreSet", ignore); + PrintSet(out, "SpecialAddSet", special_add); ++ PrintSet(out, "SupplementaryEquivalentsSet", supplementary_equivalents); + } + + void WriteHeader(const char* header_filename) { +--- a/deps/v8/src/regexp/regexp-parser.cc ++++ b/deps/v8/src/regexp/regexp-parser.cc +@@ -10,6 +10,7 @@ + #include "src/regexp/regexp-ast.h" + #include "src/regexp/regexp-macro-assembler.h" + #include "src/regexp/regexp.h" ++#include "src/regexp/special-case.h" + #include "src/strings/char-predicates-inl.h" + #include "src/utils/ostreams.h" + #include "src/utils/utils.h" +@@ -156,6 +157,10 @@ void RegExpTextBuilder::FlushText() { + + void RegExpTextBuilder::AddCharacter(base::uc16 c) { + FlushPendingSurrogate(); ++ if (NeedsDesugaringForIgnoreCase(c)) { ++ AddClassRangesForDesugaring(c); ++ return; ++ } + if (characters_ == nullptr) { + characters_ = zone()->New>(4, zone()); + } +@@ -238,15 +243,18 @@ bool RegExpTextBuilder::NeedsDesugaringF + return false; + } + +-// We only use this for characters made of surrogate pairs. All other +-// characters outside of character classes are made case independent in the +-// code generation. ++// Desugar supplementary characters with case equivalents and BMP characters ++// with supplementary equivalents. Other atoms are folded during code ++// generation. + bool RegExpTextBuilder::NeedsDesugaringForIgnoreCase(base::uc32 c) { + #ifdef V8_INTL_SUPPORT + if (IsUnicodeMode() && ignore_case()) { ++ if (c <= unibrow::Utf16::kMaxNonSurrogateCharCode) { ++ return RegExpCaseFolding::HasSupplementaryEquivalents( ++ static_cast(c)); ++ } + icu::UnicodeSet set(c, c); +- set.closeOver(USET_CASE_INSENSITIVE); +- set.removeAllStrings(); ++ RegExpCaseFolding::CloseOver(set, RegExpCaseFolding::Mode::kUnicode); + return set.size() > 1; + } + // In the case where ICU is not included, we act as if the unicode flag is +--- a/deps/v8/src/regexp/special-case.h ++++ b/deps/v8/src/regexp/special-case.h +@@ -7,8 +7,8 @@ + + #ifdef V8_INTL_SUPPORT + #include "src/base/logging.h" ++#include "src/base/strings.h" + #include "src/common/globals.h" +- + #include "unicode/uchar.h" + #include "unicode/uniset.h" + #include "unicode/unistr.h" +@@ -71,9 +71,17 @@ namespace internal { + // another character. Characters that match no other characters in + // their equivalence class are added to IgnoreSet. Characters that + // match at least one other character are added to SpecialAddSet. ++// Supplementary code points are ignored throughout: non-unicode matching ++// compares UTF-16 code units, so they can never match a BMP character. ++// ++// The generator additionally emits SupplementaryEquivalentsSet, the BMP ++// characters whose Unicode case closure contains supplementary code points. ++// This set is used for unicode-mode matching, see HasSupplementaryEquivalents. + + class RegExpCaseFolding final : public AllStatic { + public: ++ enum class Mode { kNonUnicode, kUnicode }; ++ + static const icu::UnicodeSet& IgnoreSet(); + static const icu::UnicodeSet& SpecialAddSet(); + +@@ -108,6 +116,43 @@ class RegExpCaseFolding final : public A + // h. Return cu. + return cu; + } ++ ++ // Close a set of characters under case equivalence, preserving its ++ // original members. Non-unicode inputs must be UTF-16 code units. ++ static void CloseOver(icu::UnicodeSet& set, Mode mode) { ++ if (mode == Mode::kUnicode) { ++ set.closeOver(USET_SIMPLE_CASE_INSENSITIVE); ++ return; ++ } ++ DCHECK(!set.containsSome(0x10000, 0x10ffff)); ++ if (IgnoreSet().containsNone(set)) { ++ CloseOverNonUnicode(set); ++ return; ++ } ++ if (IgnoreSet().containsAll(set)) return; ++ // Ignored characters neither contribute nor acquire equivalents, but ++ // must remain in the result if they were explicitly present. ++ icu::UnicodeSet ignored(set); ++ ignored.retainAll(IgnoreSet()); ++ set.removeAll(IgnoreSet()); ++ CloseOverNonUnicode(set); ++ set.addAll(ignored); ++ } ++ ++ // Whether c has supplementary Unicode case equivalents. ++ static bool HasSupplementaryEquivalents(base::uc16 c) { ++ return SupplementaryEquivalentsSet().contains(c); ++ } ++ ++ private: ++ static const icu::UnicodeSet& SupplementaryEquivalentsSet(); ++ ++ // Requires a set containing no IgnoreSet characters. ++ static void CloseOverNonUnicode(icu::UnicodeSet& set) { ++ set.closeOver(USET_SIMPLE_CASE_INSENSITIVE); ++ set.removeAll(IgnoreSet()); ++ set.remove(0x10000, 0x10ffff); ++ } + }; + + } // namespace internal diff --git a/wwand/Makefile b/wwand/Makefile index 6ed4f799e..1ed8af285 100644 --- a/wwand/Makefile +++ b/wwand/Makefile @@ -50,12 +50,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=wwand -PKG_VERSION:=1.8.0_pre32 -PKG_RELEASE:=85 +PKG_VERSION:=1.8.0_pre36 +PKG_RELEASE:=86 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git -PKG_SOURCE_VERSION:=4ea1d116311a0205396f3c40dffe9f012c200233 +PKG_SOURCE_VERSION:=d13826e8e19f0e4118d3246fdb021ca04b9ca05e PKG_MIRROR_HASH:=skip PKG_LICENSE:=GPL-2.0-only