diff --git a/Cargo.lock b/Cargo.lock index 8744f08..bbdffaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -527,8 +527,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" dependencies = [ "async-trait", - "axum-core", - "axum-macros", + "axum-core 0.4.5", + "axum-macros 0.4.2", "bytes", "futures-util", "http", @@ -537,7 +537,42 @@ dependencies = [ "hyper", "hyper-util", "itoa", - "matchit", + "matchit 0.7.3", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +dependencies = [ + "axum-core 0.5.2", + "axum-macros 0.5.0", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit 0.8.4", "memchr", "mime", "percent-encoding", @@ -576,13 +611,33 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-embed" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "077959a7f8cf438676af90b483304528eb7e16eadadb7f44e9ada4f9dceb9e62" dependencies = [ - "axum-core", + "axum-core 0.4.5", "chrono", "http", "mime_guess", @@ -597,7 +652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5260ed0ecc8ace8e7e61a7406672faba598c8a86b8f4742fcdde0ddc979a318f" dependencies = [ "async-trait", - "axum", + "axum 0.7.7", "form_urlencoded", "serde", "subtle", @@ -621,6 +676,17 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "axum-messages" version = "0.7.0" @@ -628,7 +694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40e85c86a8bd84f54833bca296a0204bd865958ade62bacadeae92dda34cfb8a" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.4.5", "http", "parking_lot", "serde", @@ -1935,6 +2001,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "downcast-rs" version = "1.2.1" @@ -2161,13 +2233,50 @@ dependencies = [ "prost-build", ] +[[package]] +name = "easytier-uptime" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "axum 0.8.4", + "chrono", + "clap", + "dashmap", + "easytier", + "futures", + "jsonwebtoken", + "mockall", + "once_cell", + "parking_lot", + "reqwest", + "sea-orm", + "sea-orm-migration", + "serde", + "serde_json", + "serde_yaml", + "sqlx", + "tempfile", + "thiserror 1.0.63", + "tokio", + "tokio-test", + "tokio-util", + "toml 0.8.19", + "tower 0.5.2", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", + "validator", +] + [[package]] name = "easytier-web" version = "2.4.2" dependencies = [ "anyhow", "async-trait", - "axum", + "axum 0.7.7", "axum-embed", "axum-login", "axum-messages", @@ -2570,6 +2679,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" + [[package]] name = "funty" version = "2.0.0" @@ -3264,7 +3379,7 @@ dependencies = [ "futures-channel", "futures-io", "futures-util", - "idna", + "idna 1.0.3", "ipnet", "once_cell", "rand 0.9.1", @@ -3679,6 +3794,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "1.0.3" @@ -4013,6 +4138,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "kcp-sys" version = "0.1.0" @@ -4340,6 +4480,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "matrixmultiply" version = "0.3.9" @@ -4455,6 +4601,33 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mockall" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "moka" version = "0.12.10" @@ -5868,6 +6041,32 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "prefix-trie" version = "0.7.0" @@ -7332,6 +7531,19 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.7.1", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serde_yml" version = "0.0.11" @@ -7511,6 +7723,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.11", + "time", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -8523,6 +8747,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thiserror" version = "1.0.63" @@ -8757,6 +8987,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + [[package]] name = "tokio-util" version = "0.7.13" @@ -8765,8 +9008,12 @@ checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", + "futures-util", + "hashbrown 0.14.5", "pin-project-lite", + "slab", "tokio", ] @@ -8947,7 +9194,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fd0118512cf0b3768f7fcccf0bef1ae41d68f2b45edc1e77432b36c97c56c6d" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.4.5", "cookie", "futures-util", "http", @@ -9013,7 +9260,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb6abbfcaf6436ec5a772cd9f965401da12db793e404ae6134eac066fa5a04f3" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.4.5", "base64 0.22.1", "futures", "http", @@ -9366,6 +9613,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" @@ -9379,7 +9632,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna", + "idna 1.0.3", "percent-encoding", "serde", ] @@ -9451,6 +9704,36 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "validator" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db79c75af171630a3148bd3e6d7c4f42b6a9a014c2945bc5ed0020cbb8d9478e" +dependencies = [ + "idna 0.5.0", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0bcf92720c40105ac4b2dda2a4ea3aa717d4d6a862cc217da653a4bd5c6b10" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 4ab8771..2101797 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "easytier-rpc-build", "easytier-web", "easytier-contrib/easytier-ffi", + "easytier-contrib/easytier-uptime", ] default-members = ["easytier", "easytier-web"] exclude = [ @@ -14,6 +15,7 @@ exclude = [ [profile.dev] panic = "unwind" +debug = 2 [profile.release] panic = "abort" diff --git a/EasyTier.code-workspace b/EasyTier.code-workspace index ebe4413..808d827 100644 --- a/EasyTier.code-workspace +++ b/EasyTier.code-workspace @@ -27,6 +27,10 @@ "name": "openharmony", "path": "easytier-contrib/easytier-ohrs" }, + { + "name": "uptime", + "path": "easytier-contrib/easytier-uptime" + }, { "name": "vpnservice", "path": "tauri-plugin-vpnservice" diff --git a/easytier-contrib/easytier-uptime/.env.development b/easytier-contrib/easytier-uptime/.env.development new file mode 100644 index 0000000..a38dd97 --- /dev/null +++ b/easytier-contrib/easytier-uptime/.env.development @@ -0,0 +1,17 @@ +# Development Environment Configuration +SERVER_HOST=127.0.0.1 +SERVER_PORT=8080 +DATABASE_PATH=uptime.db +DATABASE_MAX_CONNECTIONS=5 +HEALTH_CHECK_INTERVAL=60 +HEALTH_CHECK_TIMEOUT=15 +HEALTH_CHECK_RETRIES=2 +RUST_LOG=debug +LOG_LEVEL=debug +CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080 +CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS +CORS_ALLOWED_HEADERS=content-type,authorization +NODE_ENV=development +API_BASE_URL=/api +ENABLE_COMPRESSION=true +ENABLE_CORS=true \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/.env.example b/easytier-contrib/easytier-uptime/.env.example new file mode 100644 index 0000000..88aebf4 --- /dev/null +++ b/easytier-contrib/easytier-uptime/.env.example @@ -0,0 +1,29 @@ +# Server Configuration +SERVER_HOST=127.0.0.1 +SERVER_PORT=8080 + +# Database Configuration +DATABASE_PATH=uptime.db +DATABASE_MAX_CONNECTIONS=10 + +# Health Check Configuration +HEALTH_CHECK_INTERVAL=30 +HEALTH_CHECK_TIMEOUT=10 +HEALTH_CHECK_RETRIES=3 + +# Logging Configuration +RUST_LOG=info +LOG_LEVEL=info + +# CORS Configuration +CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080 +CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS +CORS_ALLOWED_HEADERS=content-type,authorization + +# Production Configuration +NODE_ENV=development +API_BASE_URL=/api + +# Security Configuration +ENABLE_COMPRESSION=true +ENABLE_CORS=true \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/.env.production b/easytier-contrib/easytier-uptime/.env.production new file mode 100644 index 0000000..6909ec7 --- /dev/null +++ b/easytier-contrib/easytier-uptime/.env.production @@ -0,0 +1,21 @@ +# Production Environment Configuration +SERVER_HOST=0.0.0.0 +SERVER_PORT=8080 +DATABASE_PATH=/var/lib/easytier-uptime/uptime.db +DATABASE_MAX_CONNECTIONS=20 +HEALTH_CHECK_INTERVAL=30 +HEALTH_CHECK_TIMEOUT=10 +HEALTH_CHECK_RETRIES=3 +RUST_LOG=info +LOG_LEVEL=info +CORS_ALLOWED_ORIGINS=https://yourdomain.com +CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS +CORS_ALLOWED_HEADERS=content-type,authorization +NODE_ENV=production +API_BASE_URL=/api +ENABLE_COMPRESSION=true +ENABLE_CORS=true + +# Security +SECRET_KEY=your-secret-key-here +JWT_SECRET=your-jwt-secret-here \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/.gitignore b/easytier-contrib/easytier-uptime/.gitignore new file mode 100644 index 0000000..81e8b4d --- /dev/null +++ b/easytier-contrib/easytier-uptime/.gitignore @@ -0,0 +1,3 @@ +*.db +*.db-shm +*.db-wal diff --git a/easytier-contrib/easytier-uptime/Cargo.toml b/easytier-contrib/easytier-uptime/Cargo.toml new file mode 100644 index 0000000..559dc33 --- /dev/null +++ b/easytier-contrib/easytier-uptime/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "easytier-uptime" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1.0", features = ["full"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +anyhow = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +chrono = { version = "0.4", features = ["serde"] } +uuid = { version = "1.0", features = ["v4", "serde"] } + +# Axum web framework +axum = { version = "0.8.4", features = ["macros"] } +tower-http = { version = "0.6", features = ["cors", "compression-full"] } +tower = "0.5" + +# SeaORM dependencies +sea-orm = { version = "1.1", features = [ + "sqlx-sqlite", + "runtime-tokio-rustls", + "macros", + "with-chrono", + "with-uuid", + "with-json" +] } +sea-orm-migration = { version = "1.1" } +sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls", "chrono", "uuid"] } + +# Validation +validator = { version = "0.18", features = ["derive"] } +thiserror = "1.0" +jsonwebtoken = "9.0" + +# Configuration and serialization +serde_yaml = "0.9" +toml = "0.8" + +# Network and async +async-trait = "0.1" +futures = "0.3" +tokio-util = { version = "0.7", features = ["full"] } + +# Filesystem operations +tempfile = "3.8" + +# Additional utilities +dashmap = "6.1.0" +clap = { version = "4.0", features = ["derive"] } +parking_lot = "0.12" +once_cell = "1.19" + +# EasyTier core +easytier = { path = "../../easytier" } + +# Testing +[dev-dependencies] +mockall = "0.12" +tokio-test = "0.4" +reqwest = "0.12" diff --git a/easytier-contrib/easytier-uptime/README.md b/easytier-contrib/easytier-uptime/README.md new file mode 100644 index 0000000..59ebaa6 --- /dev/null +++ b/easytier-contrib/easytier-uptime/README.md @@ -0,0 +1,272 @@ +# EasyTier Uptime Monitor + +一个用于监控 EasyTier 实例健康状态和运行时间的系统。 + +## 功能特性 + +- 🏥 **健康监控**: 实时监控 EasyTier 节点的健康状态 +- 📊 **数据统计**: 提供详细的运行时间和响应时间统计 +- 🔧 **实例管理**: 管理多个 EasyTier 实例 +- 🌐 **Web界面**: 直观的 Web 管理界面 +- 🚨 **告警系统**: 支持健康状态异常告警 +- 📈 **图表展示**: 可视化展示监控数据 + +## 系统架构 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Frontend │ │ Backend │ │ Database │ +│ (Vue.js) │◄──►│ (Rust/Axum) │◄──►│ (SQLite) │ +│ │ │ │ │ │ +│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ +│ │ Dashboard │ │ │ │ API Routes │ │ │ │ Nodes │ │ +│ │ Health View │ │ │ │ Health │ │ │ │ Health │ │ +│ │ Node Mgmt │ │ │ │ Instances │ │ │ │ Instances │ │ +│ │ Charts │ │ │ │ Scheduler │ │ │ │ Stats │ │ +│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +## 快速开始 + +### 环境要求 + +- **Rust**: 1.70+ +- **Node.js**: 16+ +- **npm**: 8+ + +### 开发环境 + +1. **克隆项目** + ```bash + git clone + cd easytier-uptime + ``` + +2. **启动开发环境** + ```bash + ./start-dev.sh + ``` + +3. **访问应用** + - 前端界面: http://localhost:3000 + - 后端API: http://localhost:8080 + - 健康检查: http://localhost:8080/health + +### 生产环境 + +1. **启动生产环境** + ```bash + ./start-prod.sh + ``` + +2. **停止生产环境** + ```bash + ./stop-prod.sh + ``` + +## 配置说明 + +### 环境变量 + +#### 后端配置 (.env) + +| 变量名 | 默认值 | 说明 | +|--------|--------|------| +| `SERVER_HOST` | `127.0.0.1` | 服务器监听地址 | +| `SERVER_PORT` | `8080` | 服务器端口 | +| `DATABASE_PATH` | `uptime.db` | 数据库文件路径 | +| `DATABASE_MAX_CONNECTIONS` | `10` | 数据库最大连接数 | +| `HEALTH_CHECK_INTERVAL` | `30` | 健康检查间隔(秒) | +| `HEALTH_CHECK_TIMEOUT` | `10` | 健康检查超时(秒) | +| `HEALTH_CHECK_RETRIES` | `3` | 健康检查重试次数 | +| `RUST_LOG` | `info` | 日志级别 | +| `CORS_ALLOWED_ORIGINS` | `http://localhost:3000` | 允许的跨域来源 | +| `ENABLE_CORS` | `true` | 是否启用CORS | +| `ENABLE_COMPRESSION` | `true` | 是否启用压缩 | + +#### 前端配置 (frontend/.env) + +| 变量名 | 默认值 | 说明 | +|--------|--------|------| +| `VITE_APP_TITLE` | `EasyTier Uptime Monitor` | 应用标题 | +| `VITE_API_BASE_URL` | `/api` | API基础URL | +| `VITE_APP_ENV` | `development` | 应用环境 | +| `VITE_ENABLE_DEV_TOOLS` | `true` | 是否启用开发工具 | +| `VITE_API_TIMEOUT` | `10000` | API超时时间(毫秒) | + +## API 文档 + +### 健康检查 + +```http +GET /health +``` + +### 节点管理 + +```http +# 获取节点列表 +GET /api/nodes + +# 创建节点 +POST /api/nodes + +# 获取节点详情 +GET /api/nodes/{id} + +# 更新节点 +PUT /api/nodes/{id} + +# 删除节点 +DELETE /api/nodes/{id} +``` + +### 健康记录 + +```http +# 获取节点健康历史 +GET /api/nodes/{id}/health + +# 获取节点健康统计 +GET /api/nodes/{id}/health/stats +``` + +### 实例管理 + +```http +# 获取实例列表 +GET /api/instances + +# 创建实例 +POST /api/instances + +# 停止实例 +DELETE /api/instances/{id} +``` + +## 测试 + +### 运行集成测试 + +```bash +./test-integration.sh +``` + +### 运行单元测试 + +```bash +cargo test +``` + +### 测试覆盖率 + +```bash +cargo tarpaulin +``` + +## 部署 + +### Docker 部署 + +```bash +# 构建镜像 +docker build -t easytier-uptime . + +# 运行容器 +docker run -d -p 8080:8080 easytier-uptime +``` + +### 手动部署 + +1. **构建后端** + ```bash + cargo build --release + ``` + +2. **构建前端** + ```bash + cd frontend + npm install + npm run build + cd .. + ``` + +3. **配置环境** + ```bash + cp .env.production .env + # 编辑 .env 文件 + ``` + +4. **启动服务** + ```bash + ./start-prod.sh + ``` + +## 监控和日志 + +### 日志文件 + +- **后端日志**: `logs/backend.log` +- **前端日志**: `logs/frontend.log` +- **测试日志**: `test-results/` + +### 健康检查 + +系统提供以下健康检查端点: + +- `/health` - 基本健康检查 +- `/api/health/stats` - 健康统计信息 +- `/api/health/scheduler/status` - 调度器状态 + +## 故障排除 + +### 常见问题 + +1. **后端启动失败** + - 检查端口是否被占用 + - 确认数据库文件权限 + - 查看日志文件 `logs/backend.log` + +2. **前端连接失败** + - 检查后端服务是否运行 + - 确认API地址配置 + - 检查CORS配置 + +3. **健康检查失败** + - 确认目标节点可访问 + - 检查防火墙设置 + - 验证健康检查配置 + +### 性能优化 + +1. **数据库优化** + - 定期清理过期数据 + - 配置适当的连接池大小 + - 使用索引优化查询 + +2. **前端优化** + - 启用代码分割 + - 配置缓存策略 + - 优化图片和资源 + +3. **网络优化** + - 启用压缩 + - 配置CDN + - 优化API响应时间 + +## 贡献指南 + +1. Fork 项目 +2. 创建特性分支 +3. 提交更改 +4. 推送到分支 +5. 创建 Pull Request + +## 许可证 + +MIT License + +## 支持 + +如有问题或建议,请提交 Issue 或联系开发团队。 \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/frontend/.gitignore b/easytier-contrib/easytier-uptime/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/easytier-contrib/easytier-uptime/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/easytier-contrib/easytier-uptime/frontend/README.md b/easytier-contrib/easytier-uptime/frontend/README.md new file mode 100644 index 0000000..1511959 --- /dev/null +++ b/easytier-contrib/easytier-uptime/frontend/README.md @@ -0,0 +1,5 @@ +# Vue 3 + Vite + +This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` + + diff --git a/easytier-contrib/easytier-uptime/frontend/package-lock.json b/easytier-contrib/easytier-uptime/frontend/package-lock.json new file mode 100644 index 0000000..92f9ad0 --- /dev/null +++ b/easytier-contrib/easytier-uptime/frontend/package-lock.json @@ -0,0 +1,2557 @@ +{ + "name": "easytier-uptime-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "easytier-uptime-frontend", + "version": "0.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.7.9", + "dayjs": "^1.11.13", + "element-plus": "^2.8.8", + "vue": "^3.5.18", + "vue-router": "^4.4.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "unplugin-auto-import": "^0.18.6", + "unplugin-vue-components": "^0.27.4", + "vite": "^7.1.2" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz", + "integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", + "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz", + "integrity": "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", + "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", + "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz", + "integrity": "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.29" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.18.tgz", + "integrity": "sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@vue/shared": "3.5.18", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.18.tgz", + "integrity": "sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.18", + "@vue/shared": "3.5.18" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.18.tgz", + "integrity": "sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@vue/compiler-core": "3.5.18", + "@vue/compiler-dom": "3.5.18", + "@vue/compiler-ssr": "3.5.18", + "@vue/shared": "3.5.18", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.17", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.18.tgz", + "integrity": "sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.18", + "@vue/shared": "3.5.18" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.18.tgz", + "integrity": "sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.18" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.18.tgz", + "integrity": "sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.18", + "@vue/shared": "3.5.18" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.18.tgz", + "integrity": "sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.18", + "@vue/runtime-core": "3.5.18", + "@vue/shared": "3.5.18", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.18.tgz", + "integrity": "sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.18", + "@vue/shared": "3.5.18" + }, + "peerDependencies": { + "vue": "3.5.18" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.18.tgz", + "integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz", + "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.16", + "@vueuse/metadata": "9.13.0", + "@vueuse/shared": "9.13.0", + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz", + "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz", + "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", + "license": "MIT", + "dependencies": { + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.10.7", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.10.7.tgz", + "integrity": "sha512-bL4yhepL8/0NEQA5+N2Q6ZVKLipIDkiQjK2mqtSmGh6CxJk1yaBMdG5HXfYkbk1htNcT3ULk9g23lzT323JGcA==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.3.1", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.14.182", + "@types/lodash-es": "^4.17.6", + "@vueuse/core": "^9.1.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.13", + "escape-html": "^1.0.3", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "lodash-unified": "^1.0.2", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mlly": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", + "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "pathe": "^2.0.1", + "pkg-types": "^1.3.0", + "ufo": "^1.5.4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/quansync": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz", + "integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.46.2", + "@rollup/rollup-android-arm64": "4.46.2", + "@rollup/rollup-darwin-arm64": "4.46.2", + "@rollup/rollup-darwin-x64": "4.46.2", + "@rollup/rollup-freebsd-arm64": "4.46.2", + "@rollup/rollup-freebsd-x64": "4.46.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", + "@rollup/rollup-linux-arm-musleabihf": "4.46.2", + "@rollup/rollup-linux-arm64-gnu": "4.46.2", + "@rollup/rollup-linux-arm64-musl": "4.46.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", + "@rollup/rollup-linux-ppc64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-musl": "4.46.2", + "@rollup/rollup-linux-s390x-gnu": "4.46.2", + "@rollup/rollup-linux-x64-gnu": "4.46.2", + "@rollup/rollup-linux-x64-musl": "4.46.2", + "@rollup/rollup-win32-arm64-msvc": "4.46.2", + "@rollup/rollup-win32-ia32-msvc": "4.46.2", + "@rollup/rollup-win32-x64-msvc": "4.46.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unimport": { + "version": "3.14.6", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-3.14.6.tgz", + "integrity": "sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.4", + "acorn": "^8.14.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "fast-glob": "^3.3.3", + "local-pkg": "^1.0.0", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "pathe": "^2.0.1", + "picomatch": "^4.0.2", + "pkg-types": "^1.3.0", + "scule": "^1.3.0", + "strip-literal": "^2.1.1", + "unplugin": "^1.16.1" + } + }, + "node_modules/unimport/node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unimport/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/unimport/node_modules/local-pkg": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz", + "integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.0.1", + "quansync": "^0.2.8" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unimport/node_modules/local-pkg/node_modules/pkg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz", + "integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unplugin-auto-import": { + "version": "0.18.6", + "resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-0.18.6.tgz", + "integrity": "sha512-LMFzX5DtkTj/3wZuyG5bgKBoJ7WSgzqSGJ8ppDRdlvPh45mx6t6w3OcbExQi53n3xF5MYkNGPNR/HYOL95KL2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.3", + "fast-glob": "^3.3.2", + "local-pkg": "^0.5.1", + "magic-string": "^0.30.14", + "minimatch": "^9.0.5", + "unimport": "^3.13.4", + "unplugin": "^1.16.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@nuxt/kit": "^3.2.2", + "@vueuse/core": "*" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@vueuse/core": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.27.5.tgz", + "integrity": "sha512-m9j4goBeNwXyNN8oZHHxvIIYiG8FQ9UfmKWeNllpDvhU7btKNNELGPt+o3mckQKuPwrE7e0PvCsx+IWuDSD9Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.3", + "chokidar": "^3.6.0", + "debug": "^4.3.7", + "fast-glob": "^3.3.2", + "local-pkg": "^0.5.1", + "magic-string": "^0.30.14", + "minimatch": "^9.0.5", + "mlly": "^1.7.3", + "unplugin": "^1.16.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@babel/parser": "^7.15.8", + "@nuxt/kit": "^3.2.2", + "vue": "2 || 3" + }, + "peerDependenciesMeta": { + "@babel/parser": { + "optional": true + }, + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz", + "integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.6", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.14" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.18.tgz", + "integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.18", + "@vue/compiler-sfc": "3.5.18", + "@vue/runtime-dom": "3.5.18", + "@vue/server-renderer": "3.5.18", + "@vue/shared": "3.5.18" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/easytier-contrib/easytier-uptime/frontend/package.json b/easytier-contrib/easytier-uptime/frontend/package.json new file mode 100644 index 0000000..b94153f --- /dev/null +++ b/easytier-contrib/easytier-uptime/frontend/package.json @@ -0,0 +1,26 @@ +{ + "name": "easytier-uptime-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.7.9", + "dayjs": "^1.11.13", + "easytier-uptime-frontend": "link:", + "element-plus": "^2.8.8", + "vue": "^3.5.18", + "vue-router": "^4.4.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "unplugin-auto-import": "^0.18.6", + "unplugin-vue-components": "^0.27.4", + "vite": "^7.1.2" + } +} diff --git a/easytier-contrib/easytier-uptime/frontend/public/vite.svg b/easytier-contrib/easytier-uptime/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/easytier-contrib/easytier-uptime/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/frontend/src/App.vue b/easytier-contrib/easytier-uptime/frontend/src/App.vue new file mode 100644 index 0000000..9978b10 --- /dev/null +++ b/easytier-contrib/easytier-uptime/frontend/src/App.vue @@ -0,0 +1,326 @@ + + + + + diff --git a/easytier-contrib/easytier-uptime/frontend/src/api/index.js b/easytier-contrib/easytier-uptime/frontend/src/api/index.js new file mode 100644 index 0000000..f721dc2 --- /dev/null +++ b/easytier-contrib/easytier-uptime/frontend/src/api/index.js @@ -0,0 +1,155 @@ +import axios from 'axios' + +// 创建axios实例 +const api = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || '', + timeout: 10000, + headers: { + 'Content-Type': 'application/json' + } +}) + +// 请求拦截器 +api.interceptors.request.use( + config => { + // 只在管理员相关的API请求中添加token + if (config.url && config.url.includes('/api/admin/')) { + const token = localStorage.getItem('admin_token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + } + return config + }, + error => { + return Promise.reject(error) + } +) + +// 响应拦截器 +api.interceptors.response.use( + response => { + // 直接返回完整的response对象,让各个API方法自己处理数据格式 + return response + }, + error => { + console.error('API Error Details:', { + message: error.message, + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + config: { + url: error.config?.url, + method: error.config?.method, + headers: error.config?.headers + } + }) + return Promise.reject(error) + } +) + +// 节点相关API +export const nodeApi = { + // 获取节点列表 + async getNodes(params = {}) { + const response = await api.get('/api/nodes', { params }) + return response.data + }, + + // 创建节点 + async createNode(data) { + const response = await api.post('/api/nodes', data) + return response.data + }, + + // 获取单个节点 + async getNode(id) { + const response = await api.get(`/api/nodes/${id}`) + return response.data + }, + + // 更新节点 + async updateNode(id, data) { + const response = await api.put(`/api/nodes/${id}`, data) + return response.data + }, + + // 删除节点 + async deleteNode(id) { + const response = await api.delete(`/api/nodes/${id}`) + return response.data + }, + + // 获取节点健康记录 + async getNodeHealth(id, params = {}) { + const response = await api.get(`/api/nodes/${id}/health`, { params }) + return response.data + }, + + // 获取节点健康统计 + async getNodeHealthStats(id, params = {}) { + const response = await api.get(`/api/nodes/${id}/health/stats`, { params }) + return response.data + }, + + // 测试节点连接 + async testConnection(data) { + const response = await api.post('/api/test_connection', data) + return response.data + } +} + +// 健康检查API +export const healthApi = { + async check() { + const response = await api.get('/health') + return response.data + } +} + +// 管理员API +export const adminApi = { + // 管理员登录 + async login(password) { + const response = await api.post('/api/admin/login', { password }) + return response.data + }, + + // 验证token有效性 + async verifyToken() { + const response = await api.get('/api/admin/verify') + return response.data + }, + + // 获取所有节点(包括未审批的) + async getNodes(params = {}) { + const response = await api.get('/api/admin/nodes', { params }) + return response.data + }, + + // 审批节点 + async approveNode(id) { + const response = await api.put(`/api/admin/nodes/${id}/approve`) + return response.data + }, + + // 撤销审批节点 + async revokeApproval(id) { + const response = await api.put(`/api/admin/nodes/${id}/revoke`) + return response.data + }, + + // 删除节点 + async deleteNode(id) { + const response = await api.delete(`/api/admin/nodes/${id}`) + return response.data + }, + + // 更新节点 + async updateNode(id, data) { + const response = await api.put(`/api/admin/nodes/${id}`, data) + return response.data + } +} + +export default api \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/frontend/src/assets/vue.svg b/easytier-contrib/easytier-uptime/frontend/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/easytier-contrib/easytier-uptime/frontend/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/frontend/src/components/HealthTimeline.vue b/easytier-contrib/easytier-uptime/frontend/src/components/HealthTimeline.vue new file mode 100644 index 0000000..8bd516b --- /dev/null +++ b/easytier-contrib/easytier-uptime/frontend/src/components/HealthTimeline.vue @@ -0,0 +1,405 @@ + + + + + \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/frontend/src/components/NodeForm.vue b/easytier-contrib/easytier-uptime/frontend/src/components/NodeForm.vue new file mode 100644 index 0000000..a984b54 --- /dev/null +++ b/easytier-contrib/easytier-uptime/frontend/src/components/NodeForm.vue @@ -0,0 +1,507 @@ + + + + + \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/frontend/src/main.js b/easytier-contrib/easytier-uptime/frontend/src/main.js new file mode 100644 index 0000000..5236307 --- /dev/null +++ b/easytier-contrib/easytier-uptime/frontend/src/main.js @@ -0,0 +1,22 @@ +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import router from './router' +import App from './App.vue' +import './style.css' + +const app = createApp(App) + +// 注册Element Plus +app.use(ElementPlus) + +// 注册所有图标 +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +// 注册路由 +app.use(router) + +app.mount('#app') diff --git a/easytier-contrib/easytier-uptime/frontend/src/router/index.js b/easytier-contrib/easytier-uptime/frontend/src/router/index.js new file mode 100644 index 0000000..d115158 --- /dev/null +++ b/easytier-contrib/easytier-uptime/frontend/src/router/index.js @@ -0,0 +1,78 @@ +import { createRouter, createWebHistory } from 'vue-router' +import NodeDashboard from '../views/NodeDashboard.vue' +import SubmitNode from '../views/SubmitNode.vue' +import AdminLogin from '../views/AdminLogin.vue' +import AdminDashboard from '../views/AdminDashboard.vue' + +const routes = [ + { + path: '/', + name: 'Dashboard', + component: NodeDashboard, + meta: { + title: '节点状态监控' + } + }, + { + path: '/submit', + name: 'Submit', + component: SubmitNode, + meta: { + title: '提交共享节点' + } + }, + { + path: '/admin/login', + name: 'AdminLogin', + component: AdminLogin, + meta: { + title: '管理员登录' + } + }, + { + path: '/admin', + name: 'AdminDashboard', + component: AdminDashboard, + meta: { + title: '管理员面板', + requiresAuth: true + } + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +// 路由守卫 +router.beforeEach(async (to, from, next) => { + // 设置页面标题 + if (to.meta.title) { + document.title = `${to.meta.title} - EasyTier Uptime` + } + + // 检查管理员权限 + if (to.meta.requiresAuth) { + const token = localStorage.getItem('admin_token') + if (!token) { + next('/admin/login') + return + } + + // 验证token有效性 + try { + const { adminApi } = await import('../api') + await adminApi.verifyToken() + } catch (error) { + console.error('Token verification failed:', error) + localStorage.removeItem('admin_token') + next('/admin/login') + return + } + } + + next() +}) + +export default router \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/frontend/src/style.css b/easytier-contrib/easytier-uptime/frontend/src/style.css new file mode 100644 index 0000000..8fb1668 --- /dev/null +++ b/easytier-contrib/easytier-uptime/frontend/src/style.css @@ -0,0 +1,243 @@ +/* 自定义样式 */ +:root { + --primary-color: #409EFF; + --success-color: #67C23A; + --warning-color: #E6A23C; + --danger-color: #F56C6C; + --info-color: #909399; + + --text-primary: #303133; + --text-regular: #606266; + --text-secondary: #909399; + --text-placeholder: #C0C4CC; + + --border-base: #DCDFE6; + --border-light: #E4E7ED; + --border-lighter: #EBEEF5; + --border-extra-light: #F2F6FC; + + --background-base: #F5F7FA; + --background-light: #FAFAFA; +} + +/* 滚动条样式 */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + +/* 工具类 */ +.text-center { + text-align: center; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.flex { + display: flex; +} + +.flex-center { + display: flex; + align-items: center; + justify-content: center; +} + +.flex-between { + display: flex; + align-items: center; + justify-content: space-between; +} + +.flex-column { + flex-direction: column; +} + +.flex-1 { + flex: 1; +} + +.mb-10 { + margin-bottom: 10px; +} + +.mb-20 { + margin-bottom: 20px; +} + +.mt-10 { + margin-top: 10px; +} + +.mt-20 { + margin-top: 20px; +} + +.p-10 { + padding: 10px; +} + +.p-20 { + padding: 20px; +} + +/* 动画效果 */ +.fade-in { + animation: fadeIn 0.3s ease-in; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.slide-up { + animation: slideUp 0.3s ease-out; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 响应式断点 */ +@media (max-width: 768px) { + .mobile-hidden { + display: none !important; + } +} + +@media (min-width: 769px) { + .desktop-hidden { + display: none !important; + } +} + +/* 状态指示器 */ +.status-online { + color: var(--success-color); +} + +.status-offline { + color: var(--danger-color); +} + +.status-warning { + color: var(--warning-color); +} + +/* 卡片阴影效果 */ +.card-shadow { + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); + transition: box-shadow 0.3s; +} + +.card-shadow:hover { + box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.15); +} + +/* 加载状态 */ +.loading-overlay { + position: relative; +} + +.loading-overlay::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +/* 表格样式增强 */ +.el-table .el-table__row:hover { + cursor: pointer; +} + +/* 按钮组样式 */ +.button-group { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.button-group .el-button { + margin: 0; +} + +/* 统计卡片样式 */ +.stat-card { + text-align: center; + padding: 10px; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + border-radius: 8px; + transition: transform 0.3s; +} + +.stat-card:hover { + transform: translateY(-2px); +} + +/* 标签样式 */ +.tag-group { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +/* 描述列表样式 */ +.description-list { + display: grid; + grid-template-columns: auto 1fr; + gap: 10px 20px; + align-items: center; +} + +.description-list .label { + font-weight: 600; + color: var(--text-regular); +} + +.description-list .value { + color: var(--text-primary); +} \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/frontend/src/views/AdminDashboard.vue b/easytier-contrib/easytier-uptime/frontend/src/views/AdminDashboard.vue new file mode 100644 index 0000000..372ed3f --- /dev/null +++ b/easytier-contrib/easytier-uptime/frontend/src/views/AdminDashboard.vue @@ -0,0 +1,579 @@ + + + + + \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/frontend/src/views/AdminLogin.vue b/easytier-contrib/easytier-uptime/frontend/src/views/AdminLogin.vue new file mode 100644 index 0000000..f037490 --- /dev/null +++ b/easytier-contrib/easytier-uptime/frontend/src/views/AdminLogin.vue @@ -0,0 +1,251 @@ + + + + + \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/frontend/src/views/NodeDashboard.vue b/easytier-contrib/easytier-uptime/frontend/src/views/NodeDashboard.vue new file mode 100644 index 0000000..461d594 --- /dev/null +++ b/easytier-contrib/easytier-uptime/frontend/src/views/NodeDashboard.vue @@ -0,0 +1,573 @@ + + + + + diff --git a/easytier-contrib/easytier-uptime/frontend/src/views/SubmitNode.vue b/easytier-contrib/easytier-uptime/frontend/src/views/SubmitNode.vue new file mode 100644 index 0000000..7b3cf3d --- /dev/null +++ b/easytier-contrib/easytier-uptime/frontend/src/views/SubmitNode.vue @@ -0,0 +1,351 @@ + + + + + \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/frontend/vite.config.js b/easytier-contrib/easytier-uptime/frontend/vite.config.js new file mode 100644 index 0000000..2b83c87 --- /dev/null +++ b/easytier-contrib/easytier-uptime/frontend/vite.config.js @@ -0,0 +1,30 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + AutoImport({ + resolvers: [ElementPlusResolver()], + }), + Components({ + resolvers: [ElementPlusResolver()], + }), + ], + server: { + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + }, + '/health': { + target: 'http://localhost:8080', + changeOrigin: true, + } + } + } +}) diff --git a/easytier-contrib/easytier-uptime/src/api/error.rs b/easytier-contrib/easytier-uptime/src/api/error.rs new file mode 100644 index 0000000..3eacff1 --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/api/error.rs @@ -0,0 +1,80 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use serde_json::json; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ApiError { + #[error("Database error: {0}")] + Database(#[from] sea_orm::DbErr), + + #[error("Validation error: {0}")] + Validation(String), + + #[error("Not found: {0}")] + NotFound(String), + + #[error("Bad request: {0}")] + BadRequest(String), + + #[error("Internal server error: {0}")] + Internal(String), + + #[error("Unauthorized: {0}")] + Unauthorized(String), + + #[error("Forbidden: {0}")] + Forbidden(String), +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + let (status, error_message) = match self { + ApiError::Database(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Database error: {}", err), + ), + ApiError::Validation(msg) => (StatusCode::BAD_REQUEST, msg), + ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg), + ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg), + ApiError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg), + ApiError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg), + ApiError::Forbidden(msg) => (StatusCode::FORBIDDEN, msg), + }; + + let body = json!({ + "error": { + "code": status.as_u16(), + "message": error_message + } + }); + + (status, axum::Json(body)).into_response() + } +} + +pub type ApiResult = Result; + +impl From for ApiError { + fn from(err: validator::ValidationErrors) -> Self { + let errors: Vec = err + .field_errors() + .iter() + .map(|(field, errors)| { + let error_msgs: Vec = errors + .iter() + .map(|error| { + if let Some(msg) = &error.message { + msg.to_string() + } else { + format!("Validation failed for field: {}", field) + } + }) + .collect(); + error_msgs.join(", ") + }) + .collect(); + + ApiError::Validation(errors.join("; ")) + } +} diff --git a/easytier-contrib/easytier-uptime/src/api/handlers.rs b/easytier-contrib/easytier-uptime/src/api/handlers.rs new file mode 100644 index 0000000..29007d3 --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/api/handlers.rs @@ -0,0 +1,507 @@ +use std::ops::{Div, Mul}; + +use axum::extract::{Path, Query, State}; +use axum::Json; +use sea_orm::{ + ColumnTrait, Condition, EntityTrait, IntoActiveModel, ModelTrait, Order, PaginatorTrait, + QueryFilter, QueryOrder, QuerySelect, Set, TryIntoModel, +}; +use serde::Deserialize; +use validator::Validate; + +use crate::api::{ + error::{ApiError, ApiResult}, + models::*, +}; +use crate::db::entity::{self, health_records, shared_nodes}; +use crate::db::{operations::*, Db}; +use crate::health_checker_manager::HealthCheckerManager; +use std::sync::Arc; + +#[derive(Clone)] +pub struct AppState { + pub db: Db, + pub health_checker_manager: Arc, +} + +pub async fn health_check() -> Json> { + Json(ApiResponse::message("Service is healthy".to_string())) +} + +pub async fn get_nodes( + State(app_state): State, + Query(pagination): Query, + Query(filters): Query, +) -> ApiResult>>> { + let page = pagination.page.unwrap_or(1); + let per_page = pagination.per_page.unwrap_or(20); + + let offset = (page - 1) * per_page; + + let mut query = entity::shared_nodes::Entity::find(); + + // 普通用户只能看到已审核的节点 + query = query.filter(entity::shared_nodes::Column::IsApproved.eq(true)); + + if let Some(is_active) = filters.is_active { + query = query.filter(entity::shared_nodes::Column::IsActive.eq(is_active)); + } + + if let Some(protocol) = filters.protocol { + query = query.filter(entity::shared_nodes::Column::Protocol.eq(protocol)); + } + + if let Some(search) = filters.search { + query = query.filter( + sea_orm::Condition::any() + .add(entity::shared_nodes::Column::Name.contains(&search)) + .add(entity::shared_nodes::Column::Host.contains(&search)) + .add(entity::shared_nodes::Column::Description.contains(&search)), + ); + } + + let total = query.clone().count(app_state.db.orm_db()).await?; + let nodes = query + .order_by_asc(entity::shared_nodes::Column::Id) + .limit(Some(per_page as u64)) + .offset(Some(offset as u64)) + .all(app_state.db.orm_db()) + .await?; + + let mut node_responses: Vec = nodes.into_iter().map(NodeResponse::from).collect(); + let total_pages = total.div_ceil(per_page as u64); + + // 为每个节点添加健康状态信息 + for node_response in &mut node_responses { + if let Some(mut health_record) = app_state + .health_checker_manager + .get_node_memory_record(node_response.id) + { + node_response.current_health_status = + Some(health_record.get_current_health_status().to_string()); + node_response.last_check_time = Some(health_record.get_last_check_time()); + node_response.last_response_time = health_record.get_last_response_time(); + + // 获取24小时健康统计 + if let Some(stats) = app_state + .health_checker_manager + .get_node_health_stats(node_response.id, 24) + { + node_response.health_percentage_24h = Some(stats.health_percentage); + } + + let (total_ring, healthy_ring) = health_record.get_counter_ring(); + node_response.health_record_total_counter_ring = total_ring; + node_response.health_record_healthy_counter_ring = healthy_ring; + node_response.ring_granularity = health_record.get_ring_granularity(); + } + } + + // remove sensitive information + node_responses.iter_mut().for_each(|node| { + tracing::info!("node: {:?}", node); + node.network_name = None; + node.network_secret = None; + + // make cur connection and max conn round to percentage + if node.max_connections != 0 { + node.current_connections = node.current_connections.mul(100).div(node.max_connections); + node.max_connections = 100; + } else { + node.current_connections = 0; + node.max_connections = 0; + } + + node.wechat = None; + node.qq_number = None; + node.mail = None; + }); + + Ok(Json(ApiResponse::success(PaginatedResponse { + items: node_responses, + total, + page, + per_page, + total_pages: total_pages as u32, + }))) +} + +pub async fn create_node( + State(app_state): State, + Json(request): Json, +) -> ApiResult>> { + request.validate()?; + + let node = NodeOperations::create_node(&app_state.db, request).await?; + + Ok(Json(ApiResponse::success(NodeResponse::from(node)))) +} + +pub async fn test_connection( + State(app_state): State, + Json(request): Json, +) -> ApiResult>> { + let mut node = NodeOperations::create_node_model(request); + node.id = Set(0); + let node = node.try_into_model()?; + app_state + .health_checker_manager + .test_connection(&node, std::time::Duration::from_secs(5)) + .await + .map_err(|e| ApiError::Internal(e.to_string()))?; + + Ok(Json(ApiResponse::success(NodeResponse::from(node)))) +} + +pub async fn get_node( + State(app_state): State, + Path(id): Path, +) -> ApiResult>> { + let node = NodeOperations::get_node_by_id(&app_state.db, id) + .await? + .ok_or_else(|| ApiError::NotFound(format!("Node with id {} not found", id)))?; + + Ok(Json(ApiResponse::success(NodeResponse::from(node)))) +} + +pub async fn get_node_health( + State(app_state): State, + Path(node_id): Path, + Query(pagination): Query, + Query(filters): Query, +) -> ApiResult>>> { + let page = pagination.page.unwrap_or(1); + let per_page = pagination.per_page.unwrap_or(20); + let offset = (page - 1) * per_page; + + let mut query = entity::health_records::Entity::find() + .filter(entity::health_records::Column::NodeId.eq(node_id)); + + if let Some(status) = filters.status { + query = query.filter(entity::health_records::Column::Status.eq(status)); + } + + if let Some(since) = filters.since { + query = query.filter(entity::health_records::Column::CheckedAt.gte(since.naive_utc())); + } + + let total = query.clone().count(app_state.db.orm_db()).await?; + let records = query + .order_by_desc(entity::health_records::Column::CheckedAt) + .limit(Some(per_page as u64)) + .offset(Some(offset as u64)) + .all(app_state.db.orm_db()) + .await?; + + let record_responses: Vec = records + .into_iter() + .map(HealthRecordResponse::from) + .collect(); + let total_pages = total.div_ceil(per_page as u64); + + Ok(Json(ApiResponse::success(PaginatedResponse { + items: record_responses, + total, + page, + per_page, + total_pages: total_pages as u32, + }))) +} + +pub async fn get_node_health_stats( + State(app_state): State, + Path(node_id): Path, + Query(params): Query, +) -> ApiResult>> { + let hours = params.hours.unwrap_or(24); + let stats = HealthOperations::get_health_stats(&app_state.db, node_id, hours).await?; + + Ok(Json(ApiResponse::success(HealthStatsResponse::from(stats)))) +} + +#[derive(Debug, Deserialize)] +pub struct HealthStatsParams { + pub hours: Option, +} + +#[derive(Debug, Deserialize)] +pub struct InstanceFilterParams { + pub node_id: Option, + pub status: Option, +} + +// 管理员相关处理器 +use crate::config::AppConfig; +use axum::http::{HeaderMap, StatusCode}; +use chrono::{Duration, Utc}; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; +use serde::Serialize; + +#[derive(Debug, Serialize, Deserialize)] +struct AdminClaims { + sub: String, + exp: usize, + iat: usize, +} + +pub async fn get_node_connect_url( + State(app_state): State, + Path(id): Path, +) -> ApiResult { + let node = NodeOperations::get_node_by_id(&app_state.db, id) + .await? + .ok_or_else(|| ApiError::NotFound(format!("Node with id {} not found", id)))?; + let connect_url = format!("{}://{}:{}", node.protocol, node.host, node.port); + Ok(connect_url) +} + +pub async fn admin_login( + Json(request): Json, +) -> ApiResult>> { + request + .validate() + .map_err(|e| ApiError::Validation(e.to_string()))?; + + let config = AppConfig::default(); + + if request.password != config.security.admin_password { + return Err(ApiError::Unauthorized("Invalid password".to_string())); + } + + let now = Utc::now(); + let expires_at = now + Duration::hours(24); + + let claims = AdminClaims { + sub: "admin".to_string(), + exp: expires_at.timestamp() as usize, + iat: now.timestamp() as usize, + }; + + let token = encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(config.security.jwt_secret.as_ref()), + ) + .map_err(|e| ApiError::Internal(format!("Token generation failed: {}", e)))?; + + Ok(Json(ApiResponse::success(AdminLoginResponse { + token, + expires_at, + }))) +} + +pub async fn admin_get_nodes( + State(app_state): State, + Query(pagination): Query, + Query(filters): Query, + headers: HeaderMap, +) -> ApiResult>>> { + verify_admin_token(&headers)?; + + let page = pagination.page.unwrap_or(1); + let per_page = pagination.per_page.unwrap_or(20); + let offset = (page - 1) * per_page; + + let mut query = entity::shared_nodes::Entity::find(); + + if let Some(is_active) = filters.is_active { + query = query.filter(entity::shared_nodes::Column::IsActive.eq(is_active)); + } + + if let Some(is_approved) = filters.is_approved { + query = query.filter(entity::shared_nodes::Column::IsApproved.eq(is_approved)); + } + + if let Some(protocol) = filters.protocol { + query = query.filter(entity::shared_nodes::Column::Protocol.eq(protocol)); + } + + if let Some(search) = filters.search { + query = query.filter( + sea_orm::Condition::any() + .add(entity::shared_nodes::Column::Name.contains(&search)) + .add(entity::shared_nodes::Column::Host.contains(&search)) + .add(entity::shared_nodes::Column::Description.contains(&search)), + ); + } + + let total = query.clone().count(app_state.db.orm_db()).await?; + + let nodes = query + .order_by(entity::shared_nodes::Column::CreatedAt, Order::Desc) + .offset(offset as u64) + .limit(per_page as u64) + .all(app_state.db.orm_db()) + .await?; + + let node_responses: Vec = nodes.into_iter().map(NodeResponse::from).collect(); + + let total_pages = (total as f64 / per_page as f64).ceil() as u32; + + Ok(Json(ApiResponse::success(PaginatedResponse { + items: node_responses, + total, + page, + per_page, + total_pages, + }))) +} + +pub async fn admin_approve_node( + State(app_state): State, + Path(id): Path, + headers: HeaderMap, +) -> ApiResult>> { + verify_admin_token(&headers)?; + + let node = entity::shared_nodes::Entity::find_by_id(id) + .one(app_state.db.orm_db()) + .await? + .ok_or_else(|| ApiError::NotFound("Node not found".to_string()))?; + + let mut active_model = node.into_active_model(); + active_model.is_approved = sea_orm::Set(true); + + let updated_node = entity::shared_nodes::Entity::update(active_model) + .exec(app_state.db.orm_db()) + .await?; + + Ok(Json(ApiResponse::success(NodeResponse::from(updated_node)))) +} + +pub async fn admin_update_node( + State(app_state): State, + Path(id): Path, + headers: HeaderMap, + Json(request): Json, +) -> ApiResult>> { + verify_admin_token(&headers)?; + request.validate()?; + + let mut node = NodeOperations::get_node_by_id(&app_state.db, id) + .await? + .ok_or_else(|| ApiError::NotFound(format!("Node with id {} not found", id)))?; + + let mut node = node.into_active_model(); + + if let Some(name) = request.name { + node.name = Set(name); + } + if let Some(host) = request.host { + node.host = Set(host); + } + if let Some(port) = request.port { + node.port = Set(port); + } + if let Some(protocol) = request.protocol { + node.protocol = Set(protocol); + } + if let Some(description) = request.description { + node.description = Set(description); + } + if let Some(max_connections) = request.max_connections { + node.max_connections = Set(max_connections); + } + if let Some(is_active) = request.is_active { + node.is_active = Set(is_active); + } + if let Some(allow_relay) = request.allow_relay { + node.allow_relay = Set(allow_relay); + } + if let Some(network_name) = request.network_name { + node.network_name = Set(network_name); + } + if let Some(network_secret) = request.network_secret { + node.network_secret = Set(network_secret); + } + if let Some(wechat) = request.wechat { + node.wechat = Set(wechat); + } + if let Some(mail) = request.mail { + node.mail = Set(mail); + } + if let Some(qq_number) = request.qq_number { + node.qq_number = Set(qq_number); + } + + node.updated_at = Set(chrono::Utc::now().fixed_offset()); + + tracing::info!("updated node: {:?}", node); + + let updated_node = entity::shared_nodes::Entity::update(node) + .exec(app_state.db.orm_db()) + .await?; + + Ok(Json(ApiResponse::success(NodeResponse::from(updated_node)))) +} + +pub async fn admin_revoke_approval( + State(app_state): State, + Path(id): Path, + headers: HeaderMap, +) -> ApiResult>> { + verify_admin_token(&headers)?; + + let node = entity::shared_nodes::Entity::find_by_id(id) + .one(app_state.db.orm_db()) + .await? + .ok_or_else(|| ApiError::NotFound("Node not found".to_string()))?; + + let mut active_model = node.into_active_model(); + active_model.is_approved = sea_orm::Set(false); + + let updated_node = entity::shared_nodes::Entity::update(active_model) + .exec(app_state.db.orm_db()) + .await?; + + Ok(Json(ApiResponse::success(NodeResponse::from(updated_node)))) +} + +pub async fn admin_delete_node( + State(app_state): State, + Path(id): Path, + headers: HeaderMap, +) -> ApiResult>> { + verify_admin_token(&headers)?; + + let node = entity::shared_nodes::Entity::find_by_id(id) + .one(app_state.db.orm_db()) + .await? + .ok_or_else(|| ApiError::NotFound("Node not found".to_string()))?; + + node.delete(app_state.db.orm_db()).await?; + + Ok(Json(ApiResponse::message( + "Node deleted successfully".to_string(), + ))) +} + +pub async fn admin_verify_token(headers: HeaderMap) -> ApiResult>> { + verify_admin_token(&headers)?; + Ok(Json(ApiResponse::message("Token is valid".to_string()))) +} + +fn verify_admin_token(headers: &HeaderMap) -> ApiResult<()> { + let config = AppConfig::default(); + + let auth_header = headers + .get("authorization") + .ok_or_else(|| ApiError::Unauthorized("Missing authorization header".to_string()))?; + + let auth_str = auth_header + .to_str() + .map_err(|_| ApiError::Unauthorized("Invalid authorization header".to_string()))?; + + let token = auth_str + .strip_prefix("Bearer ") + .ok_or_else(|| ApiError::Unauthorized("Invalid authorization format".to_string()))?; + + let _claims = decode::( + token, + &DecodingKey::from_secret(config.security.jwt_secret.as_ref()), + &Validation::default(), + ) + .map_err(|_| ApiError::Unauthorized("Invalid token".to_string()))?; + + Ok(()) +} diff --git a/easytier-contrib/easytier-uptime/src/api/mod.rs b/easytier-contrib/easytier-uptime/src/api/mod.rs new file mode 100644 index 0000000..e5b5acb --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/api/mod.rs @@ -0,0 +1,8 @@ +pub mod error; +pub mod handlers; +pub mod models; +pub mod routes; + +pub use error::{ApiError, ApiResult}; +pub use handlers::*; +pub use models::*; diff --git a/easytier-contrib/easytier-uptime/src/api/models.rs b/easytier-contrib/easytier-uptime/src/api/models.rs new file mode 100644 index 0000000..abf4348 --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/api/models.rs @@ -0,0 +1,316 @@ +use crate::db::entity; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use validator::Validate; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ApiResponse { + pub success: bool, + pub data: Option, + pub error: Option, + pub message: Option, +} + +impl ApiResponse { + pub fn success(data: T) -> Self { + Self { + success: true, + data: Some(data), + error: None, + message: None, + } + } + + pub fn error(error: String) -> Self { + Self { + success: false, + data: None, + error: Some(error), + message: None, + } + } + + pub fn message(message: String) -> Self { + Self { + success: true, + data: None, + error: None, + message: Some(message), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PaginatedResponse { + pub items: Vec, + pub total: u64, + pub page: u32, + pub per_page: u32, + pub total_pages: u32, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PaginationParams { + pub page: Option, + pub per_page: Option, +} + +impl Default for PaginationParams { + fn default() -> Self { + Self { + page: Some(1), + per_page: Some(20), + } + } +} + +#[derive(Debug, Serialize, Deserialize, Validate)] +#[validate(schema(function = "validate_contact_info", skip_on_field_errors = false))] +pub struct CreateNodeRequest { + #[validate(length(min = 1, max = 100))] + pub name: String, + + #[validate(length(min = 1, max = 255))] + pub host: String, + + #[validate(range(min = 1, max = 65535))] + pub port: i32, + + #[validate(length(min = 1, max = 20))] + pub protocol: String, + + #[validate(length(max = 500))] + pub description: Option, + + #[validate(range(min = 1, max = 10000))] + pub max_connections: i32, + + pub allow_relay: bool, + + #[validate(length(min = 1, max = 100))] + pub network_name: String, + + #[validate(length(max = 100))] + pub network_secret: Option, + + // 联系方式字段 + #[validate(length(max = 20))] + pub qq_number: Option, + + #[validate(length(max = 50))] + pub wechat: Option, + + #[validate(email)] + pub mail: Option, +} + +// 自定义验证函数:确保至少填写一种联系方式 +fn validate_contact_info(request: &CreateNodeRequest) -> Result<(), validator::ValidationError> { + let has_qq = request + .qq_number + .as_ref() + .is_some_and(|s| !s.trim().is_empty()); + let has_wechat = request + .wechat + .as_ref() + .is_some_and(|s| !s.trim().is_empty()); + let has_mail = request.mail.as_ref().is_some_and(|s| !s.trim().is_empty()); + + if !has_qq && !has_wechat && !has_mail { + return Err(validator::ValidationError::new("contact_required")); + } + + Ok(()) +} + +#[derive(Debug, Serialize, Deserialize, Validate)] +pub struct UpdateNodeRequest { + #[validate(length(min = 1, max = 100))] + pub name: Option, + + #[validate(length(min = 1, max = 255))] + pub host: Option, + + #[validate(range(min = 1, max = 65535))] + pub port: Option, + + #[validate(length(min = 1, max = 20))] + pub protocol: Option, + + #[validate(length(max = 500))] + pub description: Option, + + #[validate(range(min = 1, max = 10000))] + pub max_connections: Option, + + pub is_active: Option, + + pub allow_relay: Option, + + #[validate(length(min = 1, max = 100))] + pub network_name: Option, + + #[validate(length(max = 100))] + pub network_secret: Option, + + // 联系方式字段 + #[validate(length(max = 20))] + pub qq_number: Option, + + #[validate(length(max = 50))] + pub wechat: Option, + + #[validate(email)] + pub mail: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct NodeResponse { + pub id: i32, + pub name: String, + pub host: String, + pub port: i32, + pub protocol: String, + pub version: Option, + pub description: Option, + pub max_connections: i32, + pub current_connections: i32, + pub is_active: bool, + pub is_approved: bool, + pub allow_relay: bool, + pub network_name: Option, + pub network_secret: Option, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub address: String, + pub usage_percentage: f64, + // 健康状态相关字段 + pub current_health_status: Option, + pub last_check_time: Option>, + pub last_response_time: Option, + pub health_percentage_24h: Option, + + pub health_record_total_counter_ring: Vec, + pub health_record_healthy_counter_ring: Vec, + pub ring_granularity: u32, + + // 联系方式字段 + pub qq_number: Option, + pub wechat: Option, + pub mail: Option, +} + +impl From for NodeResponse { + fn from(node: entity::shared_nodes::Model) -> Self { + Self { + id: node.id, + name: node.name.clone(), + host: node.host.clone(), + port: node.port, + protocol: node.protocol.clone(), + version: Some(node.version.clone()), + description: Some(node.description.clone()), + max_connections: node.max_connections, + current_connections: node.current_connections, + is_active: node.is_active, + is_approved: node.is_approved, + allow_relay: node.allow_relay, + network_name: Some(node.network_name.clone()), + network_secret: Some(node.network_secret.clone()), + created_at: node.created_at.into(), + updated_at: node.updated_at.into(), + address: format!("{}://{}:{}", node.protocol, node.host, node.port), + usage_percentage: node.current_connections as f64 / node.max_connections as f64 * 100.0, + // 健康状态字段初始化为 None,将在 handlers 中填充 + current_health_status: None, + last_check_time: None, + last_response_time: None, + health_percentage_24h: None, + + health_record_healthy_counter_ring: Vec::new(), + health_record_total_counter_ring: Vec::new(), + ring_granularity: 0, + + // 联系方式字段 + qq_number: if node.qq_number.is_empty() { + None + } else { + Some(node.qq_number) + }, + wechat: if node.wechat.is_empty() { + None + } else { + Some(node.wechat) + }, + mail: if node.mail.is_empty() { + None + } else { + Some(node.mail) + }, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HealthRecordResponse { + pub id: i32, + pub node_id: i32, + pub status: String, + pub response_time: Option, + pub error_message: Option, + pub checked_at: chrono::DateTime, +} + +impl From for HealthRecordResponse { + fn from(record: entity::health_records::Model) -> Self { + Self { + id: record.id, + node_id: record.node_id, + status: record.status.to_string(), + response_time: Some(record.response_time), + error_message: Some(record.error_message), + checked_at: record.checked_at.into(), + } + } +} + +pub type HealthStatsResponse = crate::db::HealthStats; + +#[derive(Debug, Serialize, Deserialize)] +pub struct NodeFilterParams { + pub is_active: Option, + pub protocol: Option, + pub search: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HealthFilterParams { + pub status: Option, + pub since: Option>, +} + +// 管理员相关模型 +#[derive(Debug, Serialize, Deserialize, Validate)] +pub struct AdminLoginRequest { + #[validate(length(min = 1))] + pub password: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AdminLoginResponse { + pub token: String, + pub expires_at: DateTime, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ApproveNodeRequest { + pub approved: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AdminNodeFilterParams { + pub is_active: Option, + pub is_approved: Option, + pub protocol: Option, + pub search: Option, +} diff --git a/easytier-contrib/easytier-uptime/src/api/routes.rs b/easytier-contrib/easytier-uptime/src/api/routes.rs new file mode 100644 index 0000000..e4e07d1 --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/api/routes.rs @@ -0,0 +1,64 @@ +use axum::routing::{delete, get, post, put}; +use axum::Router; +use tower_http::compression::CompressionLayer; +use tower_http::cors::CorsLayer; + +use super::handlers::AppState; +use super::handlers::{ + admin_approve_node, admin_delete_node, admin_get_nodes, admin_login, admin_revoke_approval, + admin_update_node, admin_verify_token, create_node, get_node, get_node_health, + get_node_health_stats, get_nodes, health_check, +}; +use crate::api::{get_node_connect_url, test_connection}; +use crate::config::AppConfig; +use crate::db::Db; + +pub fn create_routes() -> Router { + let config = AppConfig::default(); + + let compression_layer = if config.security.enable_compression { + Some( + CompressionLayer::new() + .br(true) + .deflate(true) + .gzip(true) + .zstd(true), + ) + } else { + None + }; + + let cors_layer = if config.cors.enabled { + Some(CorsLayer::very_permissive()) + } else { + None + }; + + let mut router = Router::new() + .route("/node/{id}", get(get_node_connect_url)) + .route("/health", get(health_check)) + .route("/api/nodes", get(get_nodes).post(create_node)) + .route("/api/test_connection", post(test_connection)) + .route("/api/nodes/{id}/health", get(get_node_health)) + .route("/api/nodes/{id}/health/stats", get(get_node_health_stats)) + // 管理员路由 + .route("/api/admin/login", post(admin_login)) + .route("/api/admin/verify", get(admin_verify_token)) + .route("/api/admin/nodes", get(admin_get_nodes)) + .route("/api/admin/nodes/{id}/approve", put(admin_approve_node)) + .route("/api/admin/nodes/{id}/revoke", put(admin_revoke_approval)) + .route( + "/api/admin/nodes/{id}", + put(admin_update_node).delete(admin_delete_node), + ); + + if let Some(layer) = compression_layer { + router = router.layer(layer); + } + + if let Some(layer) = cors_layer { + router = router.layer(layer); + } + + router +} diff --git a/easytier-contrib/easytier-uptime/src/config.rs b/easytier-contrib/easytier-uptime/src/config.rs new file mode 100644 index 0000000..e3c9811 --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/config.rs @@ -0,0 +1,198 @@ +use std::env; +use std::net::{IpAddr, SocketAddr}; +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub struct AppConfig { + pub server: ServerConfig, + pub database: DatabaseConfig, + pub health_check: HealthCheckConfig, + pub logging: LoggingConfig, + pub cors: CorsConfig, + pub security: SecurityConfig, +} + +#[derive(Debug, Clone)] +pub struct ServerConfig { + pub host: String, + pub port: u16, + pub addr: SocketAddr, +} + +#[derive(Debug, Clone)] +pub struct DatabaseConfig { + pub path: PathBuf, + pub max_connections: u32, +} + +#[derive(Debug, Clone)] +pub struct HealthCheckConfig { + pub interval_seconds: u64, + pub timeout_seconds: u64, + pub max_retries: u32, +} + +#[derive(Debug, Clone)] +pub struct LoggingConfig { + pub level: String, + pub rust_log: String, +} + +#[derive(Debug, Clone)] +pub struct CorsConfig { + pub allowed_origins: Vec, + pub allowed_methods: Vec, + pub allowed_headers: Vec, + pub enabled: bool, +} + +#[derive(Debug, Clone)] +pub struct SecurityConfig { + pub enable_compression: bool, + pub secret_key: String, + pub jwt_secret: String, + pub admin_password: String, +} + +impl Default for AppConfig { + fn default() -> Self { + Self::from_env().unwrap_or_else(|_| Self::default_config()) + } +} + +impl AppConfig { + pub fn from_env() -> Result { + let server_config = ServerConfig { + host: env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()), + port: env::var("SERVER_PORT") + .map(|s| s.parse().unwrap_or(8080)) + .unwrap_or(8080), + addr: SocketAddr::from(( + env::var("SERVER_HOST") + .unwrap_or_else(|_| "127.0.0.1".to_string()) + .parse::() + .unwrap(), + env::var("SERVER_PORT") + .map(|s| s.parse().unwrap_or(8080)) + .unwrap_or(8080), + )), + }; + + let database_config = DatabaseConfig { + path: PathBuf::from( + env::var("DATABASE_PATH").unwrap_or_else(|_| "uptime.db".to_string()), + ), + max_connections: env::var("DATABASE_MAX_CONNECTIONS") + .map(|s| s.parse().unwrap_or(10)) + .unwrap_or(10), + }; + + let health_check_config = HealthCheckConfig { + interval_seconds: env::var("HEALTH_CHECK_INTERVAL") + .map(|s| s.parse().unwrap_or(30)) + .unwrap_or(30), + timeout_seconds: env::var("HEALTH_CHECK_TIMEOUT") + .map(|s| s.parse().unwrap_or(10)) + .unwrap_or(10), + max_retries: env::var("HEALTH_CHECK_RETRIES") + .map(|s| s.parse().unwrap_or(3)) + .unwrap_or(3), + }; + + let logging_config = LoggingConfig { + level: env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string()), + rust_log: env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()), + }; + + let cors_config = CorsConfig { + allowed_origins: env::var("CORS_ALLOWED_ORIGINS") + .unwrap_or_else(|_| "http://localhost:3000,http://localhost:8080".to_string()) + .split(',') + .map(|s| s.trim().to_string()) + .collect(), + allowed_methods: env::var("CORS_ALLOWED_METHODS") + .unwrap_or_else(|_| "GET,POST,PUT,DELETE,OPTIONS".to_string()) + .split(',') + .map(|s| s.trim().to_string()) + .collect(), + allowed_headers: env::var("CORS_ALLOWED_HEADERS") + .unwrap_or_else(|_| "content-type,authorization".to_string()) + .split(',') + .map(|s| s.trim().to_string()) + .collect(), + enabled: env::var("ENABLE_CORS") + .map(|s| s.parse().unwrap_or(true)) + .unwrap_or(true), + }; + + let security_config = SecurityConfig { + enable_compression: env::var("ENABLE_COMPRESSION") + .map(|s| s.parse().unwrap_or(true)) + .unwrap_or(true), + secret_key: env::var("SECRET_KEY").unwrap_or_else(|_| "default-secret-key".to_string()), + jwt_secret: env::var("JWT_SECRET").unwrap_or_else(|_| "default-jwt-secret".to_string()), + admin_password: env::var("ADMIN_PASSWORD").unwrap_or_else(|_| "admin123".to_string()), + }; + + Ok(AppConfig { + server: server_config, + database: database_config, + health_check: health_check_config, + logging: logging_config, + cors: cors_config, + security: security_config, + }) + } + + pub fn default_config() -> Self { + Self { + server: ServerConfig { + host: "127.0.0.1".to_string(), + port: 8080, + addr: SocketAddr::from(([127, 0, 0, 1], 8080)), + }, + database: DatabaseConfig { + path: PathBuf::from("uptime.db"), + max_connections: 10, + }, + health_check: HealthCheckConfig { + interval_seconds: 30, + timeout_seconds: 10, + max_retries: 3, + }, + logging: LoggingConfig { + level: "info".to_string(), + rust_log: "info".to_string(), + }, + cors: CorsConfig { + allowed_origins: vec![ + "http://localhost:3000".to_string(), + "http://localhost:8080".to_string(), + ], + allowed_methods: vec![ + "GET".to_string(), + "POST".to_string(), + "PUT".to_string(), + "DELETE".to_string(), + "OPTIONS".to_string(), + ], + allowed_headers: vec!["content-type".to_string(), "authorization".to_string()], + enabled: true, + }, + security: SecurityConfig { + enable_compression: true, + secret_key: "default-secret-key".to_string(), + jwt_secret: "default-jwt-secret".to_string(), + admin_password: "admin123".to_string(), + }, + } + } + + pub fn is_development(&self) -> bool { + env::var("NODE_ENV").unwrap_or_else(|_| "development".to_string()) == "development" + } + + pub fn is_production(&self) -> bool { + env::var("NODE_ENV").unwrap_or_else(|_| "development".to_string()) == "production" + } +} diff --git a/easytier-contrib/easytier-uptime/src/db/cleanup.rs b/easytier-contrib/easytier-uptime/src/db/cleanup.rs new file mode 100644 index 0000000..0645b1f --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/db/cleanup.rs @@ -0,0 +1,360 @@ +use crate::db::entity::*; +use crate::db::Db; +use sea_orm::*; +use tokio::time::{sleep, Duration}; +use tracing::{error, info, warn}; + +/// 数据清理策略配置 +#[derive(Debug, Clone)] +pub struct CleanupConfig { + /// 健康记录保留天数 + pub health_record_retention_days: i64, + /// 每个节点保留的健康记录最大数量 + pub max_health_records_per_node: u64, + /// 清理任务运行间隔(秒) + pub cleanup_interval_seconds: u64, + /// 是否启用自动清理 + pub auto_cleanup_enabled: bool, +} + +impl Default for CleanupConfig { + fn default() -> Self { + Self { + health_record_retention_days: 30, + max_health_records_per_node: 70000, + cleanup_interval_seconds: 1200, // 20分钟 + auto_cleanup_enabled: true, + } + } +} + +/// 数据清理管理器 +pub struct CleanupManager { + db: Db, + config: CleanupConfig, + running: std::sync::Arc, +} + +impl CleanupManager { + /// 创建新的清理管理器 + pub fn new(db: Db, config: CleanupConfig) -> Self { + Self { + db, + config, + running: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)), + } + } + + /// 使用默认配置创建清理管理器 + pub fn with_default_config(db: Db) -> Self { + Self::new(db, CleanupConfig::default()) + } + + /// 启动自动清理任务 + pub async fn start_auto_cleanup(&self) -> anyhow::Result<()> { + if self.config.auto_cleanup_enabled { + let running = self.running.clone(); + let db = self.db.clone(); + let config = self.config.clone(); + + running.store(true, std::sync::atomic::Ordering::SeqCst); + + tokio::spawn(async move { + info!("Auto cleanup task started"); + + while running.load(std::sync::atomic::Ordering::SeqCst) { + if let Err(e) = Self::perform_cleanup(&db, &config).await { + error!("Auto cleanup failed: {}", e); + } + + sleep(Duration::from_secs(config.cleanup_interval_seconds)).await; + } + + info!("Auto cleanup task stopped"); + }); + } + + Ok(()) + } + + /// 停止自动清理任务 + pub fn stop_auto_cleanup(&self) { + self.running + .store(false, std::sync::atomic::Ordering::SeqCst); + } + + /// 执行一次完整的清理操作 + pub async fn perform_cleanup(db: &Db, config: &CleanupConfig) -> anyhow::Result { + let mut result = CleanupResult::default(); + + // 清理旧的健康记录 + let health_cleanup_result = + Self::cleanup_old_health_records(db, config.health_record_retention_days).await?; + result.old_health_records_cleaned = health_cleanup_result.records_removed; + + // 清理过量的健康记录 + let excess_cleanup_result = + Self::cleanup_excess_health_records(db, config.max_health_records_per_node).await?; + result.excess_health_records_cleaned = excess_cleanup_result.records_removed; + + // 数据库维护 + let maintenance_result = Self::perform_database_maintenance(db).await?; + result.vacuum_performed = maintenance_result.vacuum_performed; + result.analyze_performed = maintenance_result.analyze_performed; + + info!("Cleanup completed: {:?}", result); + + Ok(result) + } + + /// 清理旧的健康记录 + async fn cleanup_old_health_records( + db: &Db, + days: i64, + ) -> anyhow::Result { + let cutoff = chrono::Local::now().fixed_offset() - chrono::Duration::days(days); + + let result = health_records::Entity::delete_many() + .filter(health_records::Column::CheckedAt.lt(cutoff)) + .exec(db.orm_db()) + .await?; + + let records_removed = result.rows_affected; + + if records_removed > 0 { + info!( + "Cleaned {} old health records (older than {} days)", + records_removed, days + ); + } + + Ok(CleanupHealthRecordsResult { records_removed }) + } + + /// 清理过量的健康记录 + async fn cleanup_excess_health_records( + db: &Db, + max_records: u64, + ) -> anyhow::Result { + // 获取所有节点 + let nodes = shared_nodes::Entity::find().all(db.orm_db()).await?; + + let mut total_removed = 0; + + for node in nodes { + // 计算需要删除的记录数量 + let total_count = health_records::Entity::find() + .filter(health_records::Column::NodeId.eq(node.id)) + .count(db.orm_db()) + .await?; + + if total_count > max_records { + let to_remove = total_count - max_records; + + // 获取需要保留的最小ID + let keep_id = health_records::Entity::find() + .filter(health_records::Column::NodeId.eq(node.id)) + .order_by_desc(health_records::Column::CheckedAt) + .offset(max_records) + .limit(1) + .into_model::() + .one(db.orm_db()) + .await?; + + info!( + "Node {}: total count: {}, to remove: {}, last keep record: {:?}", + node.id, total_count, to_remove, keep_id + ); + + if let Some(keep_record) = keep_id { + // 删除比保留记录更早的记录 + let result = health_records::Entity::delete_many() + .filter(health_records::Column::NodeId.eq(node.id)) + .filter(health_records::Column::Id.lt(keep_record.id)) + .exec(db.orm_db()) + .await?; + + total_removed += result.rows_affected; + } + } + } + + if total_removed > 0 { + info!( + "Cleaned {} excess health records (max {} per node)", + total_removed, max_records + ); + } + + Ok(CleanupExcessRecordsResult { + records_removed: total_removed, + }) + } + + /// 执行数据库维护操作 + async fn perform_database_maintenance(db: &Db) -> anyhow::Result { + let mut vacuum_performed = false; + let mut analyze_performed = false; + + // 执行 ANALYZE + match db + .orm_db() + .execute(Statement::from_string( + DatabaseBackend::Sqlite, + "ANALYZE".to_string(), + )) + .await + { + Ok(_) => { + analyze_performed = true; + info!("Database ANALYZE completed"); + } + Err(e) => { + warn!("Database ANALYZE failed: {}", e); + } + } + + // 执行 VACUUM(仅在需要时) + if vacuum_performed || analyze_performed { + match db + .orm_db() + .execute(Statement::from_string( + DatabaseBackend::Sqlite, + "VACUUM".to_string(), + )) + .await + { + Ok(_) => { + vacuum_performed = true; + info!("Database VACUUM completed"); + } + Err(e) => { + warn!("Database VACUUM failed: {}", e); + } + } + } + + Ok(DatabaseMaintenanceResult { + vacuum_performed, + analyze_performed, + }) + } + + /// 获取数据库统计信息 + pub async fn get_database_stats(db: &Db) -> anyhow::Result { + let total_nodes = shared_nodes::Entity::find().count(db.orm_db()).await?; + + let total_health_records = health_records::Entity::find().count(db.orm_db()).await?; + + let active_nodes = shared_nodes::Entity::find() + .filter(shared_nodes::Column::IsActive.eq(true)) + .count(db.orm_db()) + .await?; + + Ok(DatabaseStats { + total_nodes, + active_nodes, + total_health_records, + }) + } + + /// 获取清理配置 + pub fn get_config(&self) -> &CleanupConfig { + &self.config + } + + /// 更新清理配置 + pub fn update_config(&mut self, config: CleanupConfig) { + self.config = config; + } +} + +/// 清理结果 +#[derive(Default, Debug, Clone, serde::Serialize)] +pub struct CleanupResult { + pub old_health_records_cleaned: u64, + pub old_instances_cleaned: u64, + pub excess_health_records_cleaned: u64, + pub vacuum_performed: bool, + pub analyze_performed: bool, +} + +/// 健康记录清理结果 +#[derive(Debug, Clone, serde::Serialize)] +pub struct CleanupHealthRecordsResult { + pub records_removed: u64, +} + +/// 停止实例清理结果 +#[derive(Debug, Clone, serde::Serialize)] +pub struct CleanupStoppedInstancesResult { + pub instances_removed: u64, +} + +/// 过量记录清理结果 +#[derive(Debug, Clone, serde::Serialize)] +pub struct CleanupExcessRecordsResult { + pub records_removed: u64, +} + +/// 数据库维护结果 +#[derive(Debug, Clone, serde::Serialize)] +pub struct DatabaseMaintenanceResult { + pub vacuum_performed: bool, + pub analyze_performed: bool, +} + +/// 数据库统计信息 +#[derive(Debug, Clone, serde::Serialize)] +pub struct DatabaseStats { + pub total_nodes: u64, + pub active_nodes: u64, + pub total_health_records: u64, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Db; + + #[tokio::test] + async fn test_cleanup_manager() { + let db = Db::memory_db().await; + let cleanup_manager = CleanupManager::with_default_config(db.clone()); + + // 测试获取配置 + let config = cleanup_manager.get_config(); + assert_eq!(config.health_record_retention_days, 30); + + // 测试清理操作 + let result = CleanupManager::perform_cleanup(&db, config).await.unwrap(); + println!("Cleanup result: {:?}", result); + + // 测试获取统计信息 + let stats = CleanupManager::get_database_stats(&db).await.unwrap(); + println!("Database stats: {:?}", stats); + } + + #[tokio::test] + async fn test_cleanup_config() { + let config = CleanupConfig { + health_record_retention_days: 7, + max_health_records_per_node: 500, + cleanup_interval_seconds: 1800, + auto_cleanup_enabled: false, + }; + + let db = Db::memory_db().await; + let mut cleanup_manager = CleanupManager::new(db, config.clone()); + + assert_eq!(cleanup_manager.get_config().health_record_retention_days, 7); + + // 测试更新配置 + let new_config = CleanupConfig::default(); + cleanup_manager.update_config(new_config); + assert_eq!( + cleanup_manager.get_config().health_record_retention_days, + 30 + ); + } +} diff --git a/easytier-contrib/easytier-uptime/src/db/entity/connection_instances.rs b/easytier-contrib/easytier-uptime/src/db/entity/connection_instances.rs new file mode 100644 index 0000000..1101910 --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/db/entity/connection_instances.rs @@ -0,0 +1,39 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "connection_instances")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub node_id: i32, + #[sea_orm(unique)] + pub instance_id: String, + pub status: String, + #[sea_orm(column_type = "Text")] + pub config: String, + pub started_at: DateTimeWithTimeZone, + pub stopped_at: DateTimeWithTimeZone, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::shared_nodes::Entity", + from = "Column::NodeId", + to = "super::shared_nodes::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + SharedNodes, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::SharedNodes.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/easytier-contrib/easytier-uptime/src/db/entity/health_records.rs b/easytier-contrib/easytier-uptime/src/db/entity/health_records.rs new file mode 100644 index 0000000..2dbbe2e --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/db/entity/health_records.rs @@ -0,0 +1,37 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "health_records")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub node_id: i32, + pub status: String, + pub response_time: i32, + #[sea_orm(column_type = "Text")] + pub error_message: String, + pub checked_at: DateTimeWithTimeZone, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::shared_nodes::Entity", + from = "Column::NodeId", + to = "super::shared_nodes::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + SharedNodes, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::SharedNodes.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/easytier-contrib/easytier-uptime/src/db/entity/mod.rs b/easytier-contrib/easytier-uptime/src/db/entity/mod.rs new file mode 100644 index 0000000..8b427a8 --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/db/entity/mod.rs @@ -0,0 +1,6 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 + +pub mod prelude; + +pub mod health_records; +pub mod shared_nodes; diff --git a/easytier-contrib/easytier-uptime/src/db/entity/prelude.rs b/easytier-contrib/easytier-uptime/src/db/entity/prelude.rs new file mode 100644 index 0000000..c1b7ed1 --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/db/entity/prelude.rs @@ -0,0 +1,4 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 + +pub use super::health_records::Entity as HealthRecords; +pub use super::shared_nodes::Entity as SharedNodes; diff --git a/easytier-contrib/easytier-uptime/src/db/entity/shared_nodes.rs b/easytier-contrib/easytier-uptime/src/db/entity/shared_nodes.rs new file mode 100644 index 0000000..e5baa11 --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/db/entity/shared_nodes.rs @@ -0,0 +1,44 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "shared_nodes")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + pub host: String, + pub port: i32, + pub protocol: String, + pub version: String, + pub allow_relay: bool, + pub network_name: String, + pub network_secret: String, + #[sea_orm(column_type = "Text")] + pub description: String, + pub max_connections: i32, + pub current_connections: i32, + pub is_active: bool, + pub is_approved: bool, + pub qq_number: String, + pub wechat: String, + pub mail: String, + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::health_records::Entity")] + HealthRecords, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::HealthRecords.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/easytier-contrib/easytier-uptime/src/db/mod.rs b/easytier-contrib/easytier-uptime/src/db/mod.rs new file mode 100644 index 0000000..e25a940 --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/db/mod.rs @@ -0,0 +1,351 @@ +pub mod cleanup; +pub mod entity; +pub mod operations; + +use std::fmt; + +use sea_orm::{ + prelude::*, sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait, + QueryFilter as _, Set, SqlxSqliteConnector, Statement, TransactionTrait as _, +}; +use sea_orm_migration::MigratorTrait as _; +use serde::{Deserialize, Serialize}; +use sqlx::{migrate::MigrateDatabase as _, Sqlite, SqlitePool}; + +use crate::migrator; + +#[derive(Debug, Clone)] +pub struct Db { + db_path: String, + db: SqlitePool, + orm_db: DatabaseConnection, +} + +impl Db { + pub async fn new(db_path: T) -> anyhow::Result { + let db = Self::prepare_db(db_path.to_string().as_str()).await?; + let orm_db = SqlxSqliteConnector::from_sqlx_sqlite_pool(db.clone()); + + // 运行数据库迁移 + migrator::Migrator::up(&orm_db, None).await?; + + // 优化 SQLite 性能 + Self::optimize_sqlite(&orm_db).await?; + + Ok(Self { + db_path: db_path.to_string(), + db, + orm_db, + }) + } + + pub async fn memory_db() -> Self { + Self::new(":memory:").await.unwrap() + } + + #[tracing::instrument(ret)] + async fn prepare_db(db_path: &str) -> anyhow::Result { + if !Sqlite::database_exists(db_path).await.unwrap_or(false) { + tracing::info!("Database not found, creating a new one"); + Sqlite::create_database(db_path).await?; + } + + let db = sqlx::pool::PoolOptions::new() + .max_lifetime(None) + .idle_timeout(None) + .connect(db_path) + .await?; + + Ok(db) + } + + async fn optimize_sqlite(db: &DatabaseConnection) -> Result<(), DbErr> { + // 优化 SQLite 性能 + let pragmas = vec![ + "PRAGMA journal_mode = WAL", // 使用 WAL 模式提高并发性能 + "PRAGMA synchronous = NORMAL", // 平衡性能和数据安全 + "PRAGMA cache_size = 10000", // 增加缓存大小 + "PRAGMA temp_store = memory", // 临时存储使用内存 + "PRAGMA mmap_size = 268435456", // 内存映射大小 256MB + "PRAGMA foreign_keys = ON", // 启用外键约束 + ]; + + for pragma in pragmas { + db.execute(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Sqlite, + pragma.to_string(), + )) + .await?; + } + + Ok(()) + } + + pub fn inner(&self) -> SqlitePool { + self.db.clone() + } + + pub fn orm_db(&self) -> &DatabaseConnection { + &self.orm_db + } + + /// 清理旧的健康度记录(删除30天前的记录) + pub async fn cleanup_old_health_records(&self) -> Result { + use chrono::Duration; + use entity::health_records; + + let cutoff_date = chrono::Utc::now().naive_utc() - Duration::days(30); + + let result = health_records::Entity::delete_many() + .filter(health_records::Column::CheckedAt.lt(cutoff_date)) + .exec(self.orm_db()) + .await?; + + Ok(result.rows_affected) + } + + /// 获取数据库统计信息 + pub async fn get_database_stats(&self) -> anyhow::Result { + use entity::{health_records, shared_nodes}; + + let node_count = shared_nodes::Entity::find().count(self.orm_db()).await?; + + let health_record_count = health_records::Entity::find().count(self.orm_db()).await?; + + let active_nodes_count = shared_nodes::Entity::find() + .filter(shared_nodes::Column::IsActive.eq(true)) + .count(self.orm_db()) + .await?; + + Ok(DatabaseStats { + total_nodes: node_count, + active_nodes: active_nodes_count, + total_health_records: health_record_count, + }) + } +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct DatabaseStats { + pub total_nodes: u64, + pub active_nodes: u64, + pub total_health_records: u64, +} + +/// 健康状态枚举 +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum HealthStatus { + /// 健康状态 + Healthy, + /// 不健康状态 + Unhealthy, + /// 超时状态 + Timeout, + /// 连接错误 + ConnectionError, + /// 未知错误 + Unknown, +} + +impl fmt::Display for HealthStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + HealthStatus::Healthy => write!(f, "healthy"), + HealthStatus::Unhealthy => write!(f, "unhealthy"), + HealthStatus::Timeout => write!(f, "timeout"), + HealthStatus::ConnectionError => write!(f, "connection_error"), + HealthStatus::Unknown => write!(f, "unknown"), + } + } +} + +impl From for HealthStatus { + fn from(s: String) -> Self { + match s.to_lowercase().as_str() { + "healthy" => HealthStatus::Healthy, + "unhealthy" => HealthStatus::Unhealthy, + "timeout" => HealthStatus::Timeout, + "connection_error" => HealthStatus::ConnectionError, + _ => HealthStatus::Unknown, + } + } +} + +impl From<&str> for HealthStatus { + fn from(s: &str) -> Self { + HealthStatus::from(s.to_string()) + } +} + +/// 健康统计信息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HealthStats { + /// 总检查次数 + pub total_checks: u64, + /// 健康检查次数 + pub healthy_count: u64, + /// 不健康检查次数 + pub unhealthy_count: u64, + /// 健康百分比 + pub health_percentage: f64, + /// 平均响应时间(毫秒) + pub average_response_time: Option, + /// 正常运行时间百分比 + pub uptime_percentage: f64, + /// 最后检查时间 + pub last_check_time: Option>, + /// 最后健康状态 + pub last_status: Option, +} + +impl Default for HealthStats { + fn default() -> Self { + Self { + total_checks: 0, + healthy_count: 0, + unhealthy_count: 0, + health_percentage: 0.0, + average_response_time: None, + uptime_percentage: 0.0, + last_check_time: None, + last_status: None, + } + } +} + +impl HealthStats { + /// 从健康记录列表创建统计信息 + pub fn from_records(records: &[self::entity::health_records::Model]) -> Self { + if records.is_empty() { + return Self::default(); + } + + let total_checks = records.len() as u64; + let healthy_count = records.iter().filter(|r| r.is_healthy()).count() as u64; + let unhealthy_count = total_checks - healthy_count; + + let health_percentage = if total_checks > 0 { + (healthy_count as f64 / total_checks as f64) * 100.0 + } else { + 0.0 + }; + + // 计算平均响应时间(只计算健康状态的记录) + let healthy_records: Vec<_> = records + .iter() + .filter(|r| r.is_healthy() && r.response_time > 0) + .collect(); + + let average_response_time = if !healthy_records.is_empty() { + let total_time: i32 = healthy_records.iter().map(|r| r.response_time).sum(); + Some(total_time as f64 / healthy_records.len() as f64) + } else { + None + }; + + // 正常运行时间百分比(基于健康状态) + let uptime_percentage = health_percentage; + + // 获取最后的检查信息 + let last_record = records.first(); // records 应该按时间倒序排列 + let last_check_time = last_record.map(|r| r.checked_at.into()); + let last_status = last_record.map(|r| HealthStatus::from(r.status.clone())); + + Self { + total_checks, + healthy_count, + unhealthy_count, + health_percentage, + average_response_time, + uptime_percentage, + last_check_time, + last_status, + } + } +} + +/// Model 的扩展方法 +impl entity::health_records::Model { + /// 检查记录是否为健康状态 + pub fn is_healthy(&self) -> bool { + let status = HealthStatus::from(self.status.clone()); + matches!(status, HealthStatus::Healthy) + } + + /// 创建新的活动模型 + pub fn new_active_model( + node_id: i32, + status: HealthStatus, + response_time: Option, + error_message: Option, + ) -> entity::health_records::ActiveModel { + entity::health_records::ActiveModel { + node_id: Set(node_id), + status: Set(status.to_string()), + response_time: Set(response_time.unwrap_or(0)), + error_message: Set(error_message.unwrap_or_default()), + checked_at: Set(chrono::Utc::now().fixed_offset()), + ..Default::default() + } + } + + /// 获取健康状态 + pub fn get_status(&self) -> HealthStatus { + HealthStatus::from(self.status.clone()) + } +} + +/// Model 的扩展方法 +impl entity::shared_nodes::Model { + /// 创建新的活动模型 + #[allow(clippy::too_many_arguments)] + pub fn new_active_model( + name: String, + host: String, + port: i32, + protocol: String, + version: Option, + description: Option, + max_connections: i32, + allow_relay: bool, + network_name: String, + network_secret: Option, + ) -> entity::shared_nodes::ActiveModel { + let now = chrono::Utc::now().fixed_offset(); + entity::shared_nodes::ActiveModel { + name: Set(name), + host: Set(host), + port: Set(port), + protocol: Set(protocol), + version: Set(version.unwrap_or_default()), + description: Set(description.unwrap_or_default()), + max_connections: Set(max_connections), + current_connections: Set(0), + is_active: Set(true), + is_approved: Set(false), + allow_relay: Set(allow_relay), + network_name: Set(network_name), + network_secret: Set(network_secret.unwrap_or_default()), + created_at: Set(now), + updated_at: Set(now), + ..Default::default() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _}; + + #[tokio::test] + async fn test_database_creation() { + let db = Db::memory_db().await; + let stats = db.get_database_stats().await.unwrap(); + + // 初始状态下应该没有记录 + assert_eq!(stats.total_nodes, 0); + assert_eq!(stats.active_nodes, 0); + assert_eq!(stats.total_health_records, 0); + } +} diff --git a/easytier-contrib/easytier-uptime/src/db/operations.rs b/easytier-contrib/easytier-uptime/src/db/operations.rs new file mode 100644 index 0000000..94af0f5 --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/db/operations.rs @@ -0,0 +1,343 @@ +use crate::api::CreateNodeRequest; +use crate::db::entity::*; +use crate::db::Db; +use crate::db::HealthStats; +use crate::db::HealthStatus; +use sea_orm::*; + +/// 节点管理操作 +pub struct NodeOperations; + +impl NodeOperations { + pub fn create_node_model(req: CreateNodeRequest) -> shared_nodes::ActiveModel { + shared_nodes::ActiveModel { + id: NotSet, + name: Set(req.name), + host: Set(req.host), + port: Set(req.port), + protocol: Set(req.protocol), + version: Set("".to_string()), + description: Set(req.description.unwrap_or_default()), + max_connections: Set(req.max_connections), + current_connections: Set(0), + is_active: Set(false), + is_approved: Set(false), + allow_relay: Set(req.allow_relay), + network_name: Set(req.network_name), + network_secret: Set(req.network_secret.unwrap_or_default()), + qq_number: Set(req.qq_number.unwrap_or_default()), + wechat: Set(req.wechat.unwrap_or_default()), + mail: Set(req.mail.unwrap_or_default()), + created_at: Set(chrono::Utc::now().fixed_offset()), + updated_at: Set(chrono::Utc::now().fixed_offset()), + } + } + + /// 创建新节点 + pub async fn create_node( + db: &Db, + req: CreateNodeRequest, + ) -> Result { + let node = Self::create_node_model(req); + let insert_result = shared_nodes::Entity::insert(node).exec(db.orm_db()).await?; + + shared_nodes::Entity::find_by_id(insert_result.last_insert_id) + .one(db.orm_db()) + .await? + .ok_or(DbErr::RecordNotFound( + "Failed to retrieve created node".to_string(), + )) + } + + /// 获取所有节点 + pub async fn get_all_nodes(db: &Db) -> Result, DbErr> { + shared_nodes::Entity::find() + .order_by_asc(shared_nodes::Column::Id) + .all(db.orm_db()) + .await + } + + /// 根据ID获取节点 + pub async fn get_node_by_id(db: &Db, id: i32) -> Result, DbErr> { + shared_nodes::Entity::find_by_id(id).one(db.orm_db()).await + } + + /// 更新节点状态 + pub async fn update_node_status( + db: &Db, + id: i32, + is_active: bool, + current_connections: Option, + ) -> Result { + let mut node = shared_nodes::Entity::find_by_id(id) + .one(db.orm_db()) + .await? + .ok_or(DbErr::RecordNotFound("Node not found".to_string()))?; + + let mut node = node.into_active_model(); + + node.is_active = Set(is_active); + if let Some(connections) = current_connections { + node.current_connections = Set(connections); + } + node.updated_at = Set(chrono::Utc::now().fixed_offset()); + + let updated_node = shared_nodes::Entity::update(node).exec(db.orm_db()).await?; + + Ok(updated_node) + } + + /// 删除节点 + pub async fn delete_node(db: &Db, id: i32) -> Result { + let result = shared_nodes::Entity::delete_by_id(id) + .exec(db.orm_db()) + .await?; + Ok(result.rows_affected) + } + + /// 获取活跃节点 + pub async fn get_active_nodes(db: &Db) -> Result, DbErr> { + shared_nodes::Entity::find() + .filter(shared_nodes::Column::IsActive.eq(true)) + .order_by_asc(shared_nodes::Column::Id) + .all(db.orm_db()) + .await + } + + /// 检查节点是否存在(根据host、port、protocol) + pub async fn node_exists( + db: &Db, + host: &str, + port: i32, + protocol: &str, + ) -> Result { + let count = shared_nodes::Entity::find() + .filter(shared_nodes::Column::Host.eq(host)) + .filter(shared_nodes::Column::Port.eq(port)) + .filter(shared_nodes::Column::Protocol.eq(protocol)) + .count(db.orm_db()) + .await?; + + Ok(count > 0) + } + + pub async fn update_node_version( + db: &Db, + node_id: i32, + version: String, + ) -> Result { + let mut node = shared_nodes::Entity::find_by_id(node_id) + .one(db.orm_db()) + .await? + .ok_or(DbErr::RecordNotFound("Node not found".to_string()))?; + + let mut node = node.into_active_model(); + + node.version = Set(version); + node.updated_at = Set(chrono::Utc::now().fixed_offset()); + + let updated_node = shared_nodes::Entity::update(node).exec(db.orm_db()).await?; + + Ok(updated_node) + } +} + +/// 健康记录操作 +pub struct HealthOperations; + +impl HealthOperations { + /// 创建健康记录 + pub async fn create_health_record( + db: &Db, + node_id: i32, + status: HealthStatus, + response_time: Option, + error_message: Option, + ) -> Result { + let record = + health_records::Model::new_active_model(node_id, status, response_time, error_message); + + let insert_result = health_records::Entity::insert(record) + .exec(db.orm_db()) + .await?; + + health_records::Entity::find_by_id(insert_result.last_insert_id) + .one(db.orm_db()) + .await? + .ok_or(DbErr::RecordNotFound( + "Failed to retrieve created health record".to_string(), + )) + } + + /// 获取节点的健康记录 + pub async fn get_node_health_records( + db: &Db, + node_id: i32, + from_date: Option, + limit: Option, + ) -> Result, DbErr> { + let mut query = health_records::Entity::find() + .filter(health_records::Column::NodeId.eq(node_id)) + .order_by_desc(health_records::Column::CheckedAt); + + if let Some(from_date) = from_date { + query = query.filter(health_records::Column::CheckedAt.gte(from_date)); + } + + if let Some(limit) = limit { + query = query.limit(Some(limit)); + } + + query.all(db.orm_db()).await + } + + /// 获取节点最近的健康状态 + pub async fn get_latest_health_status( + db: &Db, + node_id: i32, + ) -> Result, DbErr> { + health_records::Entity::find() + .filter(health_records::Column::NodeId.eq(node_id)) + .order_by_desc(health_records::Column::CheckedAt) + .one(db.orm_db()) + .await + } + + /// 获取健康统计信息 + pub async fn get_health_stats(db: &Db, node_id: i32, hours: i64) -> Result { + let since = chrono::Utc::now().naive_utc() - chrono::Duration::hours(hours); + + let records = health_records::Entity::find() + .filter(health_records::Column::NodeId.eq(node_id)) + .filter(health_records::Column::CheckedAt.gte(since)) + .order_by_desc(health_records::Column::CheckedAt) + .all(db.orm_db()) + .await?; + + Ok(HealthStats::from_records(&records)) + } + + /// 清理旧的健康记录 + pub async fn cleanup_old_records(db: &Db, days: i64) -> Result { + let cutoff = chrono::Utc::now().naive_utc() - chrono::Duration::days(days); + + let result = health_records::Entity::delete_many() + .filter(health_records::Column::CheckedAt.lt(cutoff)) + .exec(db.orm_db()) + .await?; + + Ok(result.rows_affected) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Db; + + #[tokio::test] + async fn test_node_operations() { + let db = Db::memory_db().await; + + let req = CreateNodeRequest { + name: "Test Node".to_string(), + host: "test.example.com".to_string(), + port: 11010, + protocol: "tcp".to_string(), + description: Some("Test node".to_string()), + max_connections: 100, + allow_relay: false, + network_name: "test-network".to_string(), + network_secret: Some("test-secret".to_string()), + qq_number: Some("123456789".to_string()), + wechat: Some("test_wechat".to_string()), + mail: Some("test@example.com".to_string()), + }; + + // 测试创建节点 + let node = NodeOperations::create_node(&db, req).await.unwrap(); + + assert_eq!(node.name, "Test Node"); + assert_eq!(node.host, "test.example.com"); + assert_eq!(node.port, 11010); + assert!(node.is_active); + + // 测试获取节点 + let found_node = NodeOperations::get_node_by_id(&db, node.id).await.unwrap(); + assert!(found_node.is_some()); + assert_eq!(found_node.unwrap().id, node.id); + + // 测试获取所有节点 + let all_nodes = NodeOperations::get_all_nodes(&db).await.unwrap(); + assert_eq!(all_nodes.len(), 1); + + // 测试节点存在性检查 + let exists = NodeOperations::node_exists(&db, "test.example.com", 11010, "tcp") + .await + .unwrap(); + assert!(exists); + + let not_exists = NodeOperations::node_exists(&db, "nonexistent.com", 8080, "tcp") + .await + .unwrap(); + assert!(!not_exists); + } + + #[tokio::test] + async fn test_health_operations() { + let db = Db::memory_db().await; + + let req = CreateNodeRequest { + name: "Test Node".to_string(), + host: "test.example.com".to_string(), + port: 11010, + protocol: "tcp".to_string(), + description: Some("Test node".to_string()), + max_connections: 100, + allow_relay: false, + network_name: "test-network".to_string(), + network_secret: Some("test-secret".to_string()), + qq_number: Some("123456789".to_string()), + wechat: Some("test_wechat".to_string()), + mail: Some("test@example.com".to_string()), + }; + + // 创建测试节点 + let node = NodeOperations::create_node(&db, req).await.unwrap(); + // 测试创建健康记录 + let record = HealthOperations::create_health_record( + &db, + node.id, + HealthStatus::Healthy, + Some(100), + None, + ) + .await + .unwrap(); + + assert_eq!(record.node_id, node.id); + assert!(record.is_healthy()); + assert_eq!(record.response_time, 100); + + // 测试获取健康记录 + let records = HealthOperations::get_node_health_records(&db, node.id, None, None) + .await + .unwrap(); + assert_eq!(records.len(), 1); + + // 测试获取最新状态 + let latest = HealthOperations::get_latest_health_status(&db, node.id) + .await + .unwrap(); + assert!(latest.is_some()); + assert_eq!(latest.unwrap().id, record.id); + + // 测试健康统计 + let stats = HealthOperations::get_health_stats(&db, node.id, 24) + .await + .unwrap(); + assert_eq!(stats.total_checks, 1); + assert_eq!(stats.healthy_count, 1); + assert_eq!(stats.health_percentage, 100.0); + } +} diff --git a/easytier-contrib/easytier-uptime/src/health_checker.rs b/easytier-contrib/easytier-uptime/src/health_checker.rs new file mode 100644 index 0000000..8a7fbca --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/health_checker.rs @@ -0,0 +1,660 @@ +use std::{ + ops::{DerefMut, Div}, + sync::Arc, + time::{Duration, Instant}, +}; + +use anyhow::Context as _; +use dashmap::DashMap; +use easytier::{ + common::{ + config::{ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader}, + scoped_task::ScopedTask, + }, + defer, + instance_manager::NetworkInstanceManager, + launcher::ConfigSource, +}; +use serde::{Deserialize, Serialize}; +use sqlx::any; +use tracing::{debug, error, info, instrument, warn}; + +use crate::db::{ + entity::shared_nodes, + operations::{HealthOperations, NodeOperations}, + Db, HealthStatus, +}; + +pub struct HealthCheckOneNode { + node_id: String, +} + +const HEALTH_CHECK_RING_GRANULARITY_SEC: usize = 60 * 15; // 15分钟 +const HEALTH_CHECK_RING_MAX_DURATION_SEC: usize = 60 * 60 * 24; // 最多一天 + +// const HEALTH_CHECK_RING_GRANULARITY_SEC: usize = 10; +// const HEALTH_CHECK_RING_MAX_DURATION_SEC: usize = 60; + +const HEALTH_CHECK_RING_SIZE: usize = + HEALTH_CHECK_RING_MAX_DURATION_SEC / HEALTH_CHECK_RING_GRANULARITY_SEC; + +#[derive(Debug, Default, Clone)] +struct RingItem { + counter: u64, + round: u64, +} + +impl RingItem { + fn try_update_round(&mut self, timestamp: u64) { + let cur_round = + timestamp.div((HEALTH_CHECK_RING_GRANULARITY_SEC * HEALTH_CHECK_RING_SIZE) as u64); + if self.round != cur_round { + self.round = cur_round; + self.counter = 0; + } + } + + fn inc(&mut self, timestamp: u64) { + self.try_update_round(timestamp); + self.counter += 1; + } + + fn get(&mut self, timestamp: u64) -> u64 { + self.try_update_round(timestamp); + self.counter + } +} + +#[derive(Debug, Clone)] +pub struct HealthyMemRecord { + node_id: i32, + current_health_status: HealthStatus, + last_error_info: Option, + last_check_time: chrono::DateTime, + last_response_time: Option, + + // the current time is corresponding to the index by modulo with UNIX-timestamp. + total_check_counter_ring: Vec, + healthy_counter_ring: Vec, +} + +impl HealthyMemRecord { + pub fn new(node_id: i32) -> Self { + Self { + node_id, + current_health_status: HealthStatus::Unknown, + last_error_info: None, + last_check_time: chrono::Utc::now(), + last_response_time: None, + total_check_counter_ring: vec![Default::default(); HEALTH_CHECK_RING_SIZE], + healthy_counter_ring: vec![Default::default(); HEALTH_CHECK_RING_SIZE], + } + } + + /// 从数据库记录初始化内存记录 + pub fn from_db_records( + node_id: i32, + records: &[crate::db::entity::health_records::Model], + ) -> Self { + let mut mem_record = Self::new(node_id); + + if let Some(latest) = records.first() { + mem_record.current_health_status = latest.get_status(); + mem_record.last_check_time = latest.checked_at.to_utc(); + mem_record.last_response_time = if latest.response_time == 0 { + None + } else { + Some(latest.response_time) + }; + mem_record.last_error_info = if latest.error_message.is_empty() { + None + } else { + Some(latest.error_message.clone()) + }; + } + + // 填充环形缓冲区 + mem_record.populate_ring_from_records(records); + mem_record + } + + /// 从历史记录填充环形缓冲区 + fn populate_ring_from_records(&mut self, records: &[crate::db::entity::health_records::Model]) { + let now = chrono::Utc::now().timestamp() as usize; + + for record in records { + let record_time = record.checked_at.to_utc().timestamp() as usize; + let time_diff = now.saturating_sub(record_time); + + // 只处理在环形缓冲区时间范围内的记录 + if time_diff < HEALTH_CHECK_RING_MAX_DURATION_SEC { + let ring_index = + (record_time / HEALTH_CHECK_RING_GRANULARITY_SEC) % HEALTH_CHECK_RING_SIZE; + self.total_check_counter_ring[ring_index].inc(record_time as u64); + + if record.get_status() == HealthStatus::Healthy { + self.healthy_counter_ring[ring_index].inc(record_time as u64); + } + } + } + } + + /// 更新健康状态并记录到环形缓冲区 + pub fn update_health_status( + &mut self, + status: HealthStatus, + response_time: Option, + error_message: Option, + ) { + self.current_health_status = status.clone(); + self.last_check_time = chrono::Utc::now(); + self.last_response_time = response_time; + self.last_error_info = error_message; + + // 更新环形缓冲区 + let now = chrono::Utc::now().timestamp() as usize; + let ring_index = (now / HEALTH_CHECK_RING_GRANULARITY_SEC) % HEALTH_CHECK_RING_SIZE; + + self.total_check_counter_ring[ring_index].inc(now as u64); + self.healthy_counter_ring[ring_index].try_update_round(now as u64); + if status == HealthStatus::Healthy { + self.healthy_counter_ring[ring_index].inc(now as u64); + } + } + + /// 获取健康统计信息 + pub fn get_health_stats(&self, hours: u64) -> crate::db::HealthStats { + let now = chrono::Utc::now().timestamp() as usize; + + let mut total_checks = 0; + let mut healthy_count = 0; + + for ring_index in 0..HEALTH_CHECK_RING_SIZE { + total_checks += self.total_check_counter_ring[ring_index].counter; + healthy_count += self.healthy_counter_ring[ring_index].counter; + } + + let health_percentage = if total_checks > 0 { + (healthy_count as f64 / total_checks as f64) * 100.0 + } else { + 0.0 + }; + + crate::db::HealthStats { + total_checks, + healthy_count, + unhealthy_count: total_checks - healthy_count, + health_percentage, + average_response_time: self.last_response_time.map(|rt| rt as f64), + uptime_percentage: health_percentage, + last_check_time: Some(self.last_check_time), + last_status: Some(self.current_health_status.clone()), + } + } + + /// 获取当前健康状态 + pub fn get_current_health_status(&self) -> &HealthStatus { + &self.current_health_status + } + + /// 获取最后检查时间 + pub fn get_last_check_time(&self) -> chrono::DateTime { + self.last_check_time + } + + /// 获取最后响应时间 + pub fn get_last_response_time(&self) -> Option { + self.last_response_time + } + + /// 获取最后错误信息 + pub fn get_last_error_info(&self) -> &Option { + &self.last_error_info + } + + pub fn get_counter_ring(&mut self) -> (Vec, Vec) { + let now = self.last_check_time.timestamp() as usize; + + let mut total_ring = vec![0; HEALTH_CHECK_RING_SIZE]; + let mut healthy_ring = vec![0; HEALTH_CHECK_RING_SIZE]; + + let mut total_checks = 0; + let mut healthy_count = 0; + + for i in 0..HEALTH_CHECK_RING_SIZE { + let ring_time = now - (i * HEALTH_CHECK_RING_GRANULARITY_SEC); + let ring_index = + ring_time.div_euclid(HEALTH_CHECK_RING_GRANULARITY_SEC) % HEALTH_CHECK_RING_SIZE; + total_ring[i] = self.total_check_counter_ring[ring_index].get(ring_time as u64); + healthy_ring[i] = self.healthy_counter_ring[ring_index].counter; + } + + (total_ring, healthy_ring) + } + + pub fn get_ring_granularity(&self) -> u32 { + HEALTH_CHECK_RING_GRANULARITY_SEC as u32 + } +} + +pub struct HealthChecker { + db: Db, + instance_mgr: Arc, + inst_id_map: DashMap, + node_tasks: DashMap>, + node_records: Arc>, + node_cfg: Arc>, +} + +impl HealthChecker { + pub fn new(db: Db) -> Self { + let instance_mgr = Arc::new(NetworkInstanceManager::new()); + Self { + db, + instance_mgr, + inst_id_map: DashMap::new(), + node_tasks: DashMap::new(), + node_records: Arc::new(DashMap::new()), + node_cfg: Arc::new(DashMap::new()), + } + } + + /// 启动时从数据库加载所有节点的健康记录到内存 + pub async fn load_health_records_from_db(&self) -> anyhow::Result<()> { + info!("Loading health records from database..."); + + // 获取所有活跃节点 + let nodes = NodeOperations::get_all_nodes(&self.db) + .await + .with_context(|| "Failed to get all nodes from database")?; + + let from_date = chrono::Utc::now().naive_utc() + - chrono::Duration::seconds(HEALTH_CHECK_RING_MAX_DURATION_SEC as i64); + + for node in nodes { + // 获取每个节点最近的健康记录(用于初始化环形缓冲区) + let records = + HealthOperations::get_node_health_records(&self.db, node.id, Some(from_date), None) + .await + .with_context(|| { + format!("Failed to get health records for node {}", node.id) + })?; + + // 创建内存记录 + let mem_record = HealthyMemRecord::from_db_records(node.id, &records); + self.node_records.insert(node.id, mem_record); + + debug!( + "Loaded {} health records for node {} ({})", + records.len(), + node.id, + node.name + ); + } + + info!( + "Loaded health records for {} nodes", + self.node_records.len() + ); + Ok(()) + } + + /// 获取节点的内存健康记录 + pub fn get_node_memory_record(&self, node_id: i32) -> Option { + self.node_records.get(&node_id).map(|entry| entry.clone()) + } + + /// 获取节点的健康统计信息(从内存) + pub fn get_node_health_stats( + &self, + node_id: i32, + hours: u64, + ) -> Option { + self.node_records + .get(&node_id) + .map(|record| record.get_health_stats(hours)) + } + + /// 获取所有节点的当前健康状态(从内存) + pub fn get_all_nodes_health_status(&self) -> Vec<(i32, HealthStatus, Option)> { + self.node_records + .iter() + .map(|entry| { + let record = entry.value(); + ( + record.node_id, + record.current_health_status.clone(), + record.last_error_info.clone(), + ) + }) + .collect() + } + + pub async fn try_update_node(&self, node_id: i32) -> anyhow::Result<()> { + let old_cfg = self + .node_cfg + .get(&node_id) + .ok_or_else(|| anyhow::anyhow!("old node cfg not found, node_id: {}", node_id))? + .clone(); + let new_cfg = self.get_node_cfg(node_id, Some(old_cfg.get_id())).await?; + + if new_cfg.dump() != old_cfg.dump() { + self.remove_node(node_id).await?; + self.add_node(node_id).await?; + info!("node {} cfg updated", node_id); + } + + Ok(()) + } + + async fn get_node_cfg_with_model( + &self, + node_info: &shared_nodes::Model, + inst_id: Option, + ) -> anyhow::Result { + let cfg = TomlConfigLoader::default(); + cfg.set_peers(vec![PeerConfig { + uri: format!( + "{}://{}:{}", + node_info.protocol, node_info.host, node_info.port + ) + .parse() + .with_context(|| "failed to parse peer uri")?, + }]); + + let inst_id = inst_id.unwrap_or(uuid::Uuid::new_v4()); + cfg.set_id(inst_id); + cfg.set_network_identity(NetworkIdentity::new( + node_info.network_name.clone(), + node_info.network_secret.clone(), + )); + + cfg.set_hostname(Some("HealthCheckNode".to_string())); + + let mut flags = cfg.get_flags(); + flags.no_tun = true; + flags.disable_p2p = true; + flags.disable_udp_hole_punching = true; + cfg.set_flags(flags); + + Ok(cfg) + } + + pub async fn test_connection( + &self, + node_info: &shared_nodes::Model, + max_time: Duration, + ) -> anyhow::Result<()> { + let cfg = self.get_node_cfg_with_model(node_info, None).await?; + defer!({ + let _ = self + .instance_mgr + .delete_network_instance(vec![cfg.get_id()]); + }); + self.instance_mgr + .run_network_instance(cfg.clone(), ConfigSource::FFI) + .with_context(|| "failed to run network instance")?; + + let now = Instant::now(); + let mut err = None; + while now.elapsed() < max_time { + match Self::test_node_healthy(cfg.get_id(), self.instance_mgr.clone()).await { + Ok(_) => { + return Ok(()); + } + Err(e) => { + warn!( + "test node healthy failed, node_info: {:?}, err: {}", + node_info, e + ); + err = Some(e); + } + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + Err(anyhow::anyhow!("test node healthy failed, err: {:?}", err)) + } + + async fn get_node_cfg( + &self, + node_id: i32, + inst_id: Option, + ) -> anyhow::Result { + let node_info = NodeOperations::get_node_by_id(&self.db, node_id) + .await + .with_context(|| format!("failed to get node by id: {}", node_id))? + .ok_or_else(|| anyhow::anyhow!("node not found"))?; + self.get_node_cfg_with_model(&node_info, inst_id).await + } + + pub async fn add_node(&self, node_id: i32) -> anyhow::Result<()> { + let cfg = self.get_node_cfg(node_id, None).await?; + info!( + "Add node {} to health checker, cfg: {}", + node_id, + cfg.dump() + ); + + self.instance_mgr + .run_network_instance(cfg.clone(), ConfigSource::FFI) + .with_context(|| "failed to run network instance")?; + self.inst_id_map.insert(node_id, cfg.get_id()); + + // 初始化内存记录(如果不存在) + if !self.node_records.contains_key(&node_id) { + // 从数据库加载历史记录 + let from_date = chrono::Utc::now().naive_utc() + - chrono::Duration::seconds(HEALTH_CHECK_RING_MAX_DURATION_SEC as i64); + if let Ok(records) = + HealthOperations::get_node_health_records(&self.db, node_id, Some(from_date), None) + .await + { + let mem_record = HealthyMemRecord::from_db_records(node_id, &records); + self.node_records.insert(node_id, mem_record); + info!( + "Initialized memory record for node {} with {} historical records", + node_id, + records.len() + ); + } else { + self.node_records + .insert(node_id, HealthyMemRecord::new(node_id)); + info!("Initialized new memory record for node {}", node_id); + } + } + + // 启动健康检查任务 + let task = ScopedTask::from(tokio::spawn(Self::node_health_check_task( + node_id, + cfg.get_id(), + Arc::clone(&self.instance_mgr), + self.db.clone(), + Arc::clone(&self.node_records), + ))); + self.node_tasks.insert(node_id, task); + self.node_cfg.insert(node_id, cfg.clone()); + + Ok(()) + } + + pub async fn remove_node(&self, node_id: i32) -> anyhow::Result<()> { + self.node_tasks.remove(&node_id); + if let Some(inst_id) = self.inst_id_map.remove(&node_id) { + let _ = self.instance_mgr.delete_network_instance(vec![inst_id.1]); + } + self.node_cfg.remove(&node_id); + // 保留内存记录,不删除,以便后续查询历史数据 + info!( + "Removed health check task for node {}, memory record retained", + node_id + ); + Ok(()) + } + + #[instrument(err, ret, skip(instance_mgr))] + async fn test_node_healthy( + inst_id: uuid::Uuid, + instance_mgr: Arc, + // return version, response time on healthy, conn_count + ) -> anyhow::Result<(String, u64, u32)> { + let Some(instance) = instance_mgr.get_network_info(&inst_id) else { + anyhow::bail!("healthy check node is not started"); + }; + + let running = instance.running; + // health check node is not running, update db + if !running { + anyhow::bail!("healthy check node is not running"); + } + + if let Some(err) = instance.error_msg { + anyhow::bail!("healthy check node has error: {}", err); + } + + let p = instance.peer_route_pairs; + // dst node is not online + let Some(dst_node) = p.iter().find(|x| { + // we disable p2p, so we only check direct connected peer + x.route.as_ref().is_some_and(|route| { + !route.feature_flag.unwrap().is_public_server && route.hostname != "HealthCheckNode" + }) && x.peer.as_ref().is_some_and(|p| !p.conns.is_empty()) + }) else { + anyhow::bail!("dst node is not online"); + }; + + let Some(route_info) = &dst_node.route else { + anyhow::bail!("dst node route is not found"); + }; + + let Some(peer_info) = &dst_node.peer else { + anyhow::bail!("dst node peer is not found"); + }; + + let version = route_info + .version + .clone() + .split("-") + .next() + .unwrap_or("") + .to_string(); + + // 计算响应时间(这里可以根据实际需要实现) + let response_time = peer_info + .conns + .iter() + .filter_map(|x| x.stats) + .map(|x| x.latency_us) + .min() + .unwrap_or(0); + + let peer_id = peer_info.peer_id; + + let conn_count = if let Some(summary) = instance.foreign_network_summary { + summary + .info_map + .get(&peer_id) + .map(|x| x.network_count) + .unwrap_or(0) + } else { + 0 + }; + + Ok((version, response_time, conn_count)) + } + + async fn node_health_check_task( + node_id: i32, + inst_id: uuid::Uuid, + instance_mgr: Arc, + db: Db, + node_records: Arc>, + ) { + /// 记录健康状态到数据库和内存 + async fn record_health_status( + db: &Db, + node_records: &Arc>, + node_id: i32, + status: HealthStatus, + response_time: Option, + error_message: Option, + ) { + // 写入数据库 + if let Err(e) = HealthOperations::create_health_record( + db, + node_id, + status.clone(), + response_time, + error_message.clone(), + ) + .await + { + error!("Failed to create health record for node {}: {}", node_id, e); + } + + // 更新内存记录 + if let Some(mut record) = node_records.get_mut(&node_id) { + record.update_health_status(status, response_time, error_message); + } else { + let mut new_record = HealthyMemRecord::new(node_id); + new_record.update_health_status(status, response_time, error_message); + node_records.insert(node_id, new_record); + } + } + let mut tick = tokio::time::interval(Duration::from_secs(5)); + let mut counter: u64 = 0; + loop { + if counter != 0 { + tick.tick().await; + } + counter += 1; + + match Self::test_node_healthy(inst_id, instance_mgr.clone()).await { + Ok((version, response_time, conn_count)) => { + if let Err(e) = NodeOperations::update_node_status( + &db, + node_id, + true, + Some(conn_count as i32), + ) + .await + { + error!("Failed to update node status for node {}: {}", node_id, e); + } + + record_health_status( + &db, + &node_records, + node_id, + HealthStatus::Healthy, + Some(response_time as i32), + None, + ) + .await; + + // update node version + if let Err(e) = NodeOperations::update_node_version(&db, node_id, version).await + { + error!("Failed to update node version for node {}: {}", node_id, e); + } + } + Err(e) => { + if let Err(e) = + NodeOperations::update_node_status(&db, node_id, false, None).await + { + error!("Failed to update node status for node {}: {}", node_id, e); + } + + record_health_status( + &db, + &node_records, + node_id, + HealthStatus::Unhealthy, + None, + Some(e.to_string()), + ) + .await; + } + } + } + } +} diff --git a/easytier-contrib/easytier-uptime/src/health_checker_manager.rs b/easytier-contrib/easytier-uptime/src/health_checker_manager.rs new file mode 100644 index 0000000..fe26f05 --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/health_checker_manager.rs @@ -0,0 +1,160 @@ +use std::{collections::HashSet, sync::Arc, time::Duration}; + +use anyhow::Context as _; +use tokio::time::{interval, Interval}; +use tracing::{error, info}; + +use crate::{ + db::{entity::shared_nodes, operations::NodeOperations, Db}, + health_checker::HealthChecker, +}; + +/// HealthChecker的封装器,用于监控数据库中节点的添加和删除 +pub struct HealthCheckerManager { + health_checker: Arc, + db: Db, + current_nodes: Arc>>, + monitor_interval: Duration, +} + +impl HealthCheckerManager { + /// 创建新的HealthCheckerManager实例 + pub fn new(health_checker: Arc, db: Db) -> Self { + Self { + health_checker, + db, + current_nodes: Arc::new(tokio::sync::RwLock::new(HashSet::new())), + monitor_interval: Duration::from_secs(1), // 默认每1秒检查一次 + } + } + + /// 设置监控间隔 + pub fn with_monitor_interval(mut self, interval: Duration) -> Self { + self.monitor_interval = interval; + self + } + + /// 启动监控任务 + pub async fn start_monitoring(&self) -> anyhow::Result<()> { + // 启动定期检查任务 + let health_checker = Arc::clone(&self.health_checker); + let db = self.db.clone(); + let current_nodes = Arc::clone(&self.current_nodes); + let monitor_interval = self.monitor_interval; + + tokio::spawn(async move { + let mut ticker = interval(monitor_interval); + loop { + if let Err(e) = Self::check_node_changes(&health_checker, &db, ¤t_nodes).await + { + tracing::error!("Error checking node changes: {}", e); + } + ticker.tick().await; + } + }); + + Ok(()) + } + + /// 检查节点变化并更新监控 + async fn check_node_changes( + health_checker: &Arc, + db: &Db, + current_nodes: &Arc>>, + ) -> anyhow::Result<()> { + // 获取数据库中当前的所有节点 + let db_nodes = NodeOperations::get_all_nodes(db) + .await + .with_context(|| "Failed to get all nodes from database")?; + + let db_node_ids: HashSet = db_nodes.iter().map(|node| node.id).collect(); + + let mut current_nodes_guard = current_nodes.write().await; + + // 检查新增的节点 + for &node_id in &db_node_ids { + if !current_nodes_guard.contains(&node_id) { + // 新节点,添加到监控 + if let Err(e) = health_checker.add_node(node_id).await { + error!("Failed to add node {} to health checker: {}", node_id, e); + continue; + } + current_nodes_guard.insert(node_id); + info!("Added new node {} to health monitoring", node_id); + } else if let Err(e) = health_checker.try_update_node(node_id).await { + error!("Failed to add node {} to health checker: {}", node_id, e); + } + } + + // 检查删除的节点 + let nodes_to_remove: Vec = current_nodes_guard + .iter() + .filter(|&&node_id| !db_node_ids.contains(&node_id)) + .copied() + .collect(); + + for node_id in nodes_to_remove { + // 节点已删除,从监控中移除 + if let Err(e) = health_checker.remove_node(node_id).await { + error!( + "Failed to remove node {} from health checker: {}", + node_id, e + ); + continue; + } + current_nodes_guard.remove(&node_id); + info!("Removed node {} from health monitoring", node_id); + } + + Ok(()) + } + + /// 手动触发节点变化检查 + pub async fn refresh_nodes(&self) -> anyhow::Result<()> { + Self::check_node_changes(&self.health_checker, &self.db, &self.current_nodes).await + } + + /// 获取当前监控的节点数量 + pub async fn get_monitored_node_count(&self) -> usize { + self.current_nodes.read().await.len() + } + + /// 获取当前监控的节点ID列表 + pub async fn get_monitored_nodes(&self) -> Vec { + self.current_nodes.read().await.iter().copied().collect() + } + + /// 获取节点的内存健康记录 + pub fn get_node_memory_record( + &self, + node_id: i32, + ) -> Option { + self.health_checker.get_node_memory_record(node_id) + } + + /// 获取节点的健康统计信息 + pub fn get_node_health_stats( + &self, + node_id: i32, + hours: u64, + ) -> Option { + self.health_checker.get_node_health_stats(node_id, hours) + } + + /// 获取所有节点的当前健康状态 + pub fn get_all_nodes_health_status( + &self, + ) -> Vec<(i32, crate::db::HealthStatus, Option)> { + self.health_checker.get_all_nodes_health_status() + } + + pub async fn test_connection( + &self, + node_info: &shared_nodes::Model, + max_time: Duration, + ) -> anyhow::Result<()> { + self.health_checker + .test_connection(node_info, max_time) + .await + } +} diff --git a/easytier-contrib/easytier-uptime/src/main.rs b/easytier-contrib/easytier-uptime/src/main.rs new file mode 100644 index 0000000..910814c --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/main.rs @@ -0,0 +1,153 @@ +#![allow(unused)] + +mod api; +mod config; +mod db; +mod health_checker; +mod health_checker_manager; +mod migrator; + +use api::routes::create_routes; +use clap::Parser; +use config::AppConfig; +use db::{operations::NodeOperations, Db}; +use health_checker::HealthChecker; +use health_checker_manager::HealthCheckerManager; +use std::env; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; +use tracing_subscriber::EnvFilter; + +use crate::db::cleanup::{CleanupConfig, CleanupManager}; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Admin password for management access + #[arg(long, env = "ADMIN_PASSWORD")] + admin_password: Option, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // 加载配置 + let config = AppConfig::default(); + + // 初始化日志 + tracing_subscriber::fmt() + .with_max_level(match config.logging.level.as_str() { + "debug" => tracing::Level::DEBUG, + "info" => tracing::Level::INFO, + "warn" => tracing::Level::WARN, + "error" => tracing::Level::ERROR, + _ => tracing::Level::INFO, + }) + .with_target(false) + .with_thread_ids(true) + .with_env_filter(EnvFilter::new("easytier_uptime")) + .init(); + + // 解析命令行参数 + let args = Args::parse(); + + // 如果提供了管理员密码,设置环境变量 + if let Some(password) = args.admin_password { + env::set_var("ADMIN_PASSWORD", password); + } + + tracing::info!( + "Admin password configured: {}", + !config.security.admin_password.is_empty() + ); + + // 创建数据库连接 + let db = Db::new(&config.database.path.to_string_lossy()).await?; + + // 获取数据库统计信息 + let stats = db.get_database_stats().await?; + tracing::info!("Database initialized successfully!"); + tracing::info!("Database stats: {:?}", stats); + + // 创建配置目录 + let config_dir = PathBuf::from("./configs"); + tokio::fs::create_dir_all(&config_dir).await?; + + // 创建健康检查器和管理器 + let health_checker = Arc::new(HealthChecker::new(db.clone())); + let health_checker_manager = HealthCheckerManager::new(health_checker, db.clone()) + .with_monitor_interval(Duration::from_secs(1)); // 每30秒检查一次节点变化 + + let cleanup_manager = CleanupManager::new(db.clone(), CleanupConfig::default()); + cleanup_manager.start_auto_cleanup().await?; + + // 启动节点监控 + health_checker_manager.start_monitoring().await?; + tracing::info!("Health checker manager started successfully!"); + + let monitored_count = health_checker_manager.get_monitored_node_count().await; + tracing::info!("Currently monitoring {} nodes", monitored_count); + + // 创建应用状态 + let app_state = crate::api::handlers::AppState { + db: db.clone(), + health_checker_manager: Arc::new(health_checker_manager), + }; + + // 创建 API 路由 + let app = create_routes().with_state(app_state); + + // 配置服务器地址 + let addr = config.server.addr; + + tracing::info!("Starting server on http://{}", addr); + tracing::info!("Available endpoints:"); + tracing::info!(" GET /health - Health check"); + tracing::info!(" GET /api/nodes - Get nodes (paginated, approved only)"); + tracing::info!(" POST /api/nodes - Create node (pending approval)"); + tracing::info!(" GET /api/nodes/:id - Get node by ID"); + tracing::info!(" PUT /api/nodes/:id - Update node"); + tracing::info!(" DELETE /api/nodes/:id - Delete node"); + tracing::info!(" GET /api/nodes/:id/health - Get node health history"); + tracing::info!(" GET /api/nodes/:id/health/stats - Get node health stats"); + tracing::info!("Admin endpoints:"); + tracing::info!(" POST /api/admin/login - Admin login"); + tracing::info!(" GET /api/admin/nodes - Get all nodes (including pending)"); + tracing::info!(" PUT /api/admin/nodes/:id/approve - Approve/reject node"); + tracing::info!(" DELETE /api/admin/nodes/:id - Delete node (admin only)"); + + // 启动服务器 + let listener = tokio::net::TcpListener::bind(addr).await?; + + // 设置优雅关闭 + let shutdown_signal = Arc::new(tokio::sync::Notify::new()); + let server_shutdown_signal = shutdown_signal.clone(); + + // 启动服务器任务 + let server_handle = tokio::spawn(async move { + axum::serve(listener, app) + .with_graceful_shutdown(async move { + server_shutdown_signal.notified().await; + }) + .await + .unwrap(); + }); + + // 等待 Ctrl+C 信号 + tokio::select! { + _ = tokio::signal::ctrl_c() => { + tracing::info!("Received shutdown signal"); + } + _ = server_handle => { + tracing::info!("Server task completed"); + } + } + + // 优雅关闭 + tracing::info!("Shutting down gracefully..."); + shutdown_signal.notify_waiters(); + + tracing::info!("Shutdown complete"); + Ok(()) +} diff --git a/easytier-contrib/easytier-uptime/src/migrator/m20250101_000001_create_tables.rs b/easytier-contrib/easytier-uptime/src/migrator/m20250101_000001_create_tables.rs new file mode 100644 index 0000000..5c88876 --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/migrator/m20250101_000001_create_tables.rs @@ -0,0 +1,181 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20250101_000001_create_tables" + } +} + +#[derive(DeriveIden)] +pub enum SharedNodes { + Table, + Id, + Name, + Host, + Port, + Protocol, + Version, + AllowRelay, + NetworkName, + NetworkSecret, + Description, + MaxConnections, + CurrentConnections, + IsActive, + IsApproved, + QQNumber, + Wechat, + Mail, + CreatedAt, + UpdatedAt, +} + +#[derive(DeriveIden)] +pub enum HealthRecords { + Table, + Id, + NodeId, + Status, + ResponseTime, + ErrorMessage, + CheckedAt, +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // 创建共享节点表 + manager + .create_table( + Table::create() + .if_not_exists() + .table(SharedNodes::Table) + .col(pk_auto(SharedNodes::Id).not_null()) + .col(string(SharedNodes::Name).not_null()) + .col(string(SharedNodes::Host).not_null()) + .col(integer(SharedNodes::Port).not_null()) + .col(string(SharedNodes::Protocol).not_null().default("tcp")) + .col(string(SharedNodes::Version)) + .col(boolean(SharedNodes::AllowRelay).default(false)) + .col(string(SharedNodes::NetworkName)) + .col(string(SharedNodes::NetworkSecret)) + .col(text(SharedNodes::Description)) + .col(integer(SharedNodes::MaxConnections).default(100)) + .col(integer(SharedNodes::CurrentConnections).default(0)) + .col(boolean(SharedNodes::IsActive).default(true)) + .col(boolean(SharedNodes::IsApproved).default(false)) + .col(string(SharedNodes::QQNumber)) + .col(string(SharedNodes::Wechat)) + .col(string(SharedNodes::Mail)) + .col( + timestamp_with_time_zone(SharedNodes::CreatedAt) + .default(Expr::current_timestamp()), + ) + .col( + timestamp_with_time_zone(SharedNodes::UpdatedAt) + .default(Expr::current_timestamp()), + ) + .to_owned(), + ) + .await?; + + // 创建唯一约束 + manager + .create_index( + Index::create() + .name("idx_shared_nodes_host_port_protocol") + .table(SharedNodes::Table) + .col(SharedNodes::Host) + .col(SharedNodes::Port) + .col(SharedNodes::Protocol) + .unique() + .to_owned(), + ) + .await?; + + // 创建健康度记录表 + manager + .create_table( + Table::create() + .if_not_exists() + .table(HealthRecords::Table) + .col(pk_auto(HealthRecords::Id).not_null()) + .col(integer(HealthRecords::NodeId).not_null()) + .col(string(HealthRecords::Status).not_null()) + .col(integer(HealthRecords::ResponseTime)) + .col(text(HealthRecords::ErrorMessage).null()) + .col( + timestamp_with_time_zone(HealthRecords::CheckedAt) + .default(Expr::current_timestamp()), + ) + .foreign_key( + ForeignKey::create() + .name("fk_health_records_node_id_to_shared_nodes_id") + .from(HealthRecords::Table, HealthRecords::NodeId) + .to(SharedNodes::Table, SharedNodes::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + // 创建健康度记录索引 + manager + .create_index( + Index::create() + .name("idx_health_records_node_id") + .table(HealthRecords::Table) + .col(HealthRecords::NodeId) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("idx_health_records_checked_at") + .table(HealthRecords::Table) + .col(HealthRecords::CheckedAt) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("idx_health_records_node_time") + .table(HealthRecords::Table) + .col(HealthRecords::NodeId) + .col(HealthRecords::CheckedAt) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("idx_health_records_status") + .table(HealthRecords::Table) + .col(HealthRecords::Status) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(HealthRecords::Table).to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(SharedNodes::Table).to_owned()) + .await?; + + Ok(()) + } +} diff --git a/easytier-contrib/easytier-uptime/src/migrator/mod.rs b/easytier-contrib/easytier-uptime/src/migrator/mod.rs new file mode 100644 index 0000000..1d492e9 --- /dev/null +++ b/easytier-contrib/easytier-uptime/src/migrator/mod.rs @@ -0,0 +1,12 @@ +use sea_orm_migration::prelude::*; + +mod m20250101_000001_create_tables; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20250101_000001_create_tables::Migration)] + } +} diff --git a/easytier-contrib/easytier-uptime/start-dev.sh b/easytier-contrib/easytier-uptime/start-dev.sh new file mode 100755 index 0000000..b336b97 --- /dev/null +++ b/easytier-contrib/easytier-uptime/start-dev.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +# EasyTier Uptime Monitor 开发环境启动脚本 + +set -e + +echo "🚀 Starting EasyTier Uptime Monitor Development Environment..." + +# 检查依赖 +echo "📦 Checking dependencies..." + +# 检查 Rust +if ! command -v cargo &> /dev/null; then + echo "❌ Rust is not installed. Please install Rust first." + exit 1 +fi + +# 检查 Node.js +if ! command -v node &> /dev/null; then + echo "❌ Node.js is not installed. Please install Node.js first." + exit 1 +fi + +# 检查 npm +if ! command -v npm &> /dev/null; then + echo "❌ npm is not installed. Please install npm first." + exit 1 +fi + +# 设置环境变量 +export RUST_LOG=debug +export NODE_ENV=development + +# 创建必要的目录 +echo "📁 Creating directories..." +mkdir -p logs +mkdir -p configs +mkdir -p frontend/dist + +# 复制环境配置文件 +if [ ! -f .env ]; then + echo "📝 Creating environment configuration..." + cp .env.development .env +fi + +# 安装前端依赖 +echo "📦 Installing frontend dependencies..." +cd frontend +if [ ! -d "node_modules" ]; then + npm install +fi +cd .. + +# 启动后端服务 +echo "🔧 Starting backend server..." +cargo run & +BACKEND_PID=$! + +# 等待后端服务启动 +echo "⏳ Waiting for backend server to start..." +sleep 5 + +# 启动前端开发服务器 +echo "🎨 Starting frontend development server..." +cd frontend +npm run dev & +FRONTEND_PID=$! +cd .. + +# 等待前端服务启动 +echo "⏳ Waiting for frontend server to start..." +sleep 3 + +echo "✅ Development environment started successfully!" +echo "🌐 Frontend: http://localhost:3000" +echo "🔧 Backend API: http://localhost:8080" +echo "📊 API Health Check: http://localhost:8080/health" +echo "" +echo "Press Ctrl+C to stop all services" + +# 清理函数 +cleanup() { + echo "" + echo "🛑 Stopping services..." + kill $BACKEND_PID 2>/dev/null || true + kill $FRONTEND_PID 2>/dev/null || true + echo "✅ All services stopped" + exit 0 +} + +# 设置信号处理 +trap cleanup SIGINT SIGTERM + +# 等待用户中断 +wait \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/start-prod.sh b/easytier-contrib/easytier-uptime/start-prod.sh new file mode 100755 index 0000000..67152fa --- /dev/null +++ b/easytier-contrib/easytier-uptime/start-prod.sh @@ -0,0 +1,98 @@ +#!/bin/bash + +# EasyTier Uptime Monitor 生产环境启动脚本 + +set -e + +echo "🚀 Starting EasyTier Uptime Monitor Production Environment..." + +# 检查依赖 +echo "📦 Checking dependencies..." + +# 检查 Rust +if ! command -v cargo &> /dev/null; then + echo "❌ Rust is not installed. Please install Rust first." + exit 1 +fi + +# 检查 Node.js +if ! command -v node &> /dev/null; then + echo "❌ Node.js is not installed. Please install Node.js first." + exit 1 +fi + +# 检查 npm +if ! command -v npm &> /dev/null; then + echo "❌ npm is not installed. Please install npm first." + exit 1 +fi + +# 设置环境变量 +export RUST_LOG=info +export NODE_ENV=production + +# 创建必要的目录 +echo "📁 Creating directories..." +mkdir -p logs +mkdir -p configs +mkdir -p /var/lib/easytier-uptime +mkdir -p frontend/dist + +# 复制环境配置文件 +if [ ! -f .env ]; then + echo "📝 Creating environment configuration..." + cp .env.production .env +fi + +# 构建后端 +echo "🔧 Building backend..." +cargo build --release + +# 构建前端 +echo "🎨 Building frontend..." +cd frontend +if [ ! -d "node_modules" ]; then + npm install +fi +npm run build +cd .. + +# 启动后端服务 +echo "🔧 Starting backend server..." +nohup ./target/release/easytier-uptime > logs/backend.log 2>&1 & +BACKEND_PID=$! + +# 等待后端服务启动 +echo "⏳ Waiting for backend server to start..." +sleep 5 + +# 设置静态文件服务 +echo "🌐 Setting up static file server..." +cd frontend/dist +python3 -m http.server 8081 > ../../logs/frontend.log 2>&1 & +FRONTEND_PID=$! +cd ../.. + +# 等待前端服务启动 +echo "⏳ Waiting for frontend server to start..." +sleep 3 + +echo "✅ Production environment started successfully!" +echo "🌐 Frontend: http://localhost:8081" +echo "🔧 Backend API: http://localhost:8080" +echo "📊 API Health Check: http://localhost:8080/health" +echo "" +echo "Backend PID: $BACKEND_PID" +echo "Frontend PID: $FRONTEND_PID" +echo "" +echo "To stop services:" +echo " kill $BACKEND_PID" +echo " kill $FRONTEND_PID" +echo "" +echo "Or use the stop script: ./stop-prod.sh" + +# 保存PID到文件 +echo $BACKEND_PID > logs/backend.pid +echo $FRONTEND_PID > logs/frontend.pid + +echo "✅ PIDs saved to logs/" \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/stop-prod.sh b/easytier-contrib/easytier-uptime/stop-prod.sh new file mode 100755 index 0000000..69adc99 --- /dev/null +++ b/easytier-contrib/easytier-uptime/stop-prod.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# EasyTier Uptime Monitor 停止服务脚本 + +set -e + +echo "🛑 Stopping EasyTier Uptime Monitor services..." + +# 检查PID文件 +if [ -f "logs/backend.pid" ]; then + BACKEND_PID=$(cat logs/backend.pid) + echo "🔧 Stopping backend server (PID: $BACKEND_PID)..." + kill $BACKEND_PID 2>/dev/null || true + rm logs/backend.pid + echo "✅ Backend server stopped" +else + echo "⚠️ Backend PID file not found" +fi + +if [ -f "logs/frontend.pid" ]; then + FRONTEND_PID=$(cat logs/frontend.pid) + echo "🌐 Stopping frontend server (PID: $FRONTEND_PID)..." + kill $FRONTEND_PID 2>/dev/null || true + rm logs/frontend.pid + echo "✅ Frontend server stopped" +else + echo "⚠️ Frontend PID file not found" +fi + +# 强制杀死可能残留的进程 +echo "🔍 Checking for remaining processes..." +REMAINING_BACKEND=$(ps aux | grep 'easytier-uptime' | grep -v grep | awk '{print $2}' || true) +if [ ! -z "$REMAINING_BACKEND" ]; then + echo "🔧 Killing remaining backend processes..." + echo $REMAINING_BACKEND | xargs kill -9 2>/dev/null || true + echo "✅ Remaining backend processes killed" +fi + +REMAINING_FRONTEND=$(ps aux | grep 'python3 -m http.server' | grep -v grep | awk '{print $2}' || true) +if [ ! -z "$REMAINING_FRONTEND" ]; then + echo "🌐 Killing remaining frontend processes..." + echo $REMAINING_FRONTEND | xargs kill -9 2>/dev/null || true + echo "✅ Remaining frontend processes killed" +fi + +echo "✅ All services stopped successfully!" \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/test-integration.sh b/easytier-contrib/easytier-uptime/test-integration.sh new file mode 100755 index 0000000..6865994 --- /dev/null +++ b/easytier-contrib/easytier-uptime/test-integration.sh @@ -0,0 +1,144 @@ +#!/bin/bash + +# EasyTier Uptime Monitor 集成测试脚本 + +set -e + +echo "🧪 Running EasyTier Uptime Monitor Integration Tests..." + +# 检查依赖 +echo "📦 Checking dependencies..." + +# 检查 Rust +if ! command -v cargo &> /dev/null; then + echo "❌ Rust is not installed. Please install Rust first." + exit 1 +fi + +# 检查 curl +if ! command -v curl &> /dev/null; then + echo "❌ curl is not installed. Please install curl first." + exit 1 +fi + +# 设置环境变量 +export RUST_LOG=info +export NODE_ENV=test + +# 创建测试目录 +echo "📁 Creating test directories..." +mkdir -p test-results +mkdir -p test-logs + +# 复制测试环境配置 +if [ ! -f .env ]; then + echo "📝 Creating test environment configuration..." + cp .env.development .env +fi + +# 构建项目 +echo "🔧 Building project..." +cargo build + +# 启动后端服务进行测试 +echo "🚀 Starting backend server for testing..." +cargo run & +BACKEND_PID=$! + +# 等待后端服务启动 +echo "⏳ Waiting for backend server to start..." +sleep 5 + +# 检查服务是否运行 +echo "🔍 Checking if server is running..." +if curl -f http://localhost:8080/health > /dev/null 2>&1; then + echo "✅ Backend server is running" +else + echo "❌ Backend server failed to start" + kill $BACKEND_PID 2>/dev/null || true + exit 1 +fi + +# 运行API测试 +echo "🧪 Running API tests..." +if cargo test api_test --lib -- --nocapture > test-results/api-test.log 2>&1; then + echo "✅ API tests passed" +else + echo "❌ API tests failed" + echo "Check test-results/api-test.log for details" +fi + +# 运行健康检查测试 +echo "🏥 Running health check tests..." +curl -s http://localhost:8080/health | jq . > test-results/health-check.json +if [ $? -eq 0 ]; then + echo "✅ Health check test passed" +else + echo "❌ Health check test failed" +fi + +# 运行节点管理测试 +echo "🔧 Running node management tests..." +# 创建测试节点 +curl -s -X POST http://localhost:8080/api/nodes \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Test Node", + "host": "127.0.0.1", + "port": 11010, + "protocol": "tcp", + "version": "1.0.0", + "description": "Test node for integration testing", + "max_connections": 100 + }' > test-results/create-node.json + +# 获取节点列表 +curl -s http://localhost:8080/api/nodes > test-results/get-nodes.json + +echo "✅ Node management tests completed" + +# 停止后端服务 +echo "🛑 Stopping backend server..." +kill $BACKEND_PID 2>/dev/null || true +sleep 2 + +# 强制杀死可能残留的进程 +pkill -f easytier-uptime 2>/dev/null || true + +echo "✅ Integration tests completed!" +echo "📊 Test results saved to test-results/" +echo "📋 Test logs saved to test-logs/" + +# 生成测试报告 +echo "📝 Generating test report..." +cat > test-results/test-report.md << EOF +# EasyTier Uptime Monitor Integration Test Report + +## Test Summary +- **Test Date**: $(date) +- **Test Environment**: Integration +- **Backend PID**: $BACKEND_PID + +## Test Results + +### API Tests +- Status: $(grep -q "test result: ok" test-results/api-test.log && echo "PASSED" || echo "FAILED") +- Log: [api-test.log](api-test.log) + +### Health Check +- Status: $(jq -r '.success' test-results/health-check.json 2>/dev/null || echo "FAILED") +- Response: $(cat test-results/health-check.json 2>/dev/null || echo "No response") + +### Node Management +- Status: COMPLETED +- Create Node: [create-node.json](create-node.json) +- Get Nodes: [get-nodes.json](get-nodes.json) + +## System Information +- **Rust Version**: $(rustc --version) +- **Cargo Version**: $(cargo --version) +- **System**: $(uname -a) + +EOF + +echo "✅ Test report generated: test-results/test-report.md" \ No newline at end of file diff --git a/easytier-contrib/easytier-uptime/test_health_stats.rs b/easytier-contrib/easytier-uptime/test_health_stats.rs new file mode 100644 index 0000000..863286b --- /dev/null +++ b/easytier-contrib/easytier-uptime/test_health_stats.rs @@ -0,0 +1,66 @@ +//! 测试 HealthyStats 功能的示例代码 + +use easytier_uptime::db::entity::health_records::{HealthStatus, HealthStats, Model}; +use sea_orm::prelude::*; + +fn main() { + // 创建一些模拟的健康记录 + let records = vec![ + Model { + id: 1, + node_id: 1, + status: HealthStatus::Healthy.to_string(), + response_time: 100, + error_message: String::new(), + checked_at: chrono::Utc::now().fixed_offset(), + }, + Model { + id: 2, + node_id: 1, + status: HealthStatus::Healthy.to_string(), + response_time: 150, + error_message: String::new(), + checked_at: chrono::Utc::now().fixed_offset(), + }, + Model { + id: 3, + node_id: 1, + status: HealthStatus::Unhealthy.to_string(), + response_time: 0, + error_message: "Connection failed".to_string(), + checked_at: chrono::Utc::now().fixed_offset(), + }, + ]; + + // 从记录创建统计信息 + let stats = HealthStats::from_records(&records); + + println!("健康统计信息:"); + println!("总检查次数: {}", stats.total_checks); + println!("健康检查次数: {}", stats.healthy_count); + println!("不健康检查次数: {}", stats.unhealthy_count); + println!("健康百分比: {:.2}%", stats.health_percentage); + println!("平均响应时间: {:?} ms", stats.average_response_time); + println!("正常运行时间百分比: {:.2}%", stats.uptime_percentage); + println!("最后检查时间: {:?}", stats.last_check_time); + println!("最后状态: {:?}", stats.last_status); + + // 测试健康状态转换 + println!("\n健康状态测试:"); + let status_healthy = HealthStatus::from("healthy"); + let status_unhealthy = HealthStatus::from("unhealthy"); + let status_timeout = HealthStatus::from("timeout"); + let status_unknown = HealthStatus::from("invalid_status"); + + println!("healthy -> {:?}", status_healthy); + println!("unhealthy -> {:?}", status_unhealthy); + println!("timeout -> {:?}", status_timeout); + println!("invalid_status -> {:?}", status_unknown); + + // 测试记录的健康状态检查 + println!("\n记录健康状态检查:"); + for record in &records { + println!("记录 {} 是否健康: {}", record.id, record.is_healthy()); + println!("记录 {} 状态: {:?}", record.id, record.get_status()); + } +} \ No newline at end of file diff --git a/easytier/build.rs b/easytier/build.rs index 6827387..9ef3d7c 100644 --- a/easytier/build.rs +++ b/easytier/build.rs @@ -169,6 +169,14 @@ fn main() -> Result<(), Box> { .type_attribute("peer_rpc.DirectConnectedPeerInfo", "#[derive(Hash)]") .type_attribute("peer_rpc.PeerInfoForGlobalMap", "#[derive(Hash)]") .type_attribute("peer_rpc.ForeignNetworkRouteInfoKey", "#[derive(Hash, Eq)]") + .type_attribute( + "peer_rpc.RouteForeignNetworkSummary.Info", + "#[derive(Hash, Eq, serde::Serialize, serde::Deserialize)]", + ) + .type_attribute( + "peer_rpc.RouteForeignNetworkSummary", + "#[derive(Hash, Eq, serde::Serialize, serde::Deserialize)]", + ) .type_attribute("common.RpcDescriptor", "#[derive(Hash, Eq)]") .field_attribute(".web.NetworkConfig", "#[serde(default)]") .service_generator(Box::new(rpc_build::ServiceGenerator::new())) diff --git a/easytier/src/instance_manager.rs b/easytier/src/instance_manager.rs index 1ec399b..59fe11d 100644 --- a/easytier/src/instance_manager.rs +++ b/easytier/src/instance_manager.rs @@ -134,6 +134,12 @@ impl NetworkInstanceManager { Ok(ret) } + pub fn get_network_info(&self, instance_id: &uuid::Uuid) -> Option { + self.instance_map + .get(instance_id) + .and_then(|instance| instance.value().get_running_info()) + } + pub fn list_network_instance_ids(&self) -> Vec { self.instance_map.iter().map(|item| *item.key()).collect() } diff --git a/easytier/src/launcher.rs b/easytier/src/launcher.rs index 9f68d65..fcaffae 100644 --- a/easytier/src/launcher.rs +++ b/easytier/src/launcher.rs @@ -1,4 +1,5 @@ use crate::common::config::PortForwardConfig; +use crate::proto::peer_rpc::RouteForeignNetworkSummary; use crate::proto::web; use crate::{ common::{ @@ -36,6 +37,7 @@ struct EasyTierData { my_node_info: RwLock, routes: RwLock>, peers: RwLock>, + foreign_network_summary: RwLock, tun_fd: Arc>>, tun_dev_name: RwLock, event_subscriber: RwLock>, @@ -51,6 +53,7 @@ impl Default for EasyTierData { my_node_info: RwLock::new(MyNodeInfo::default()), routes: RwLock::new(Vec::new()), peers: RwLock::new(Vec::new()), + foreign_network_summary: RwLock::new(RouteForeignNetworkSummary::default()), tun_fd: Arc::new(RwLock::new(None)), tun_dev_name: RwLock::new(String::new()), instance_stop_notifier: Arc::new(tokio::sync::Notify::new()), @@ -195,6 +198,8 @@ impl EasyTierLauncher { *data_c.routes.write().unwrap() = peer_mgr_c.list_routes().await; *data_c.peers.write().unwrap() = PeerManagerRpcService::list_peers(&peer_mgr_c).await; + *data_c.foreign_network_summary.write().unwrap() = + peer_mgr_c.get_foreign_network_summary().await; tokio::time::sleep(std::time::Duration::from_secs(1)).await; } }); @@ -323,6 +328,10 @@ impl EasyTierLauncher { pub fn get_peers(&self) -> Vec { self.data.peers.read().unwrap().clone() } + + pub fn get_foreign_network_summary(&self) -> RouteForeignNetworkSummary { + self.data.foreign_network_summary.read().unwrap().clone() + } } impl Drop for EasyTierLauncher { @@ -401,6 +410,7 @@ impl NetworkInstance { peer_route_pairs, running: launcher.running(), error_msg: launcher.error_msg(), + foreign_network_summary: Some(launcher.get_foreign_network_summary()), }) } diff --git a/easytier/src/peers/peer_manager.rs b/easytier/src/peers/peer_manager.rs index f9264e8..e845c57 100644 --- a/easytier/src/peers/peer_manager.rs +++ b/easytier/src/peers/peer_manager.rs @@ -40,7 +40,9 @@ use crate::{ self, list_global_foreign_network_response::OneForeignNetwork, ListGlobalForeignNetworkResponse, }, - peer_rpc::{ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey}, + peer_rpc::{ + ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, RouteForeignNetworkSummary, + }, }, tunnel::{ self, @@ -953,6 +955,10 @@ impl PeerManager { resp } + pub async fn get_foreign_network_summary(&self) -> RouteForeignNetworkSummary { + self.get_route().get_foreign_network_summary().await + } + async fn run_nic_packet_process_pipeline(&self, data: &mut ZCPacket) { if !self .global_ctx diff --git a/easytier/src/peers/peer_ospf_route.rs b/easytier/src/peers/peer_ospf_route.rs index 0a96991..1712337 100644 --- a/easytier/src/peers/peer_ospf_route.rs +++ b/easytier/src/peers/peer_ospf_route.rs @@ -1,5 +1,5 @@ use std::{ - collections::BTreeSet, + collections::{BTreeMap, BTreeSet}, fmt::Debug, net::{Ipv4Addr, Ipv6Addr}, sync::{ @@ -35,9 +35,10 @@ use crate::{ proto::{ common::{Ipv4Inet, NatType, StunInfo}, peer_rpc::{ - route_foreign_network_infos, ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, - OspfRouteRpc, OspfRouteRpcClientFactory, OspfRouteRpcServer, PeerIdVersion, - RouteForeignNetworkInfos, RoutePeerInfo, RoutePeerInfos, SyncRouteInfoError, + route_foreign_network_infos, route_foreign_network_summary, + ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, OspfRouteRpc, + OspfRouteRpcClientFactory, OspfRouteRpcServer, PeerIdVersion, RouteForeignNetworkInfos, + RouteForeignNetworkSummary, RoutePeerInfo, RoutePeerInfos, SyncRouteInfoError, SyncRouteInfoRequest, SyncRouteInfoResponse, }, rpc_types::{ @@ -2320,6 +2321,16 @@ impl Route for PeerRoute { foreign_networks } + async fn get_foreign_network_summary(&self) -> RouteForeignNetworkSummary { + let mut info_map: BTreeMap = BTreeMap::new(); + for item in self.service_impl.synced_route_info.foreign_network.iter() { + let entry = info_map.entry(item.key().peer_id).or_default(); + entry.network_count += 1; + entry.peer_count += item.value().foreign_peer_ids.len() as u32; + } + RouteForeignNetworkSummary { info_map } + } + async fn list_peers_own_foreign_network( &self, network_identity: &NetworkIdentity, diff --git a/easytier/src/peers/route_trait.rs b/easytier/src/peers/route_trait.rs index 831c2ba..278183d 100644 --- a/easytier/src/peers/route_trait.rs +++ b/easytier/src/peers/route_trait.rs @@ -9,7 +9,7 @@ use crate::{ common::{global_ctx::NetworkIdentity, PeerId}, proto::peer_rpc::{ ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, RouteForeignNetworkInfos, - RoutePeerInfo, + RouteForeignNetworkSummary, RoutePeerInfo, }, }; @@ -102,6 +102,10 @@ pub trait Route { Default::default() } + async fn get_foreign_network_summary(&self) -> RouteForeignNetworkSummary { + Default::default() + } + // my peer id in foreign network is different from the one in local network // this function is used to get the peer id in local network async fn get_origin_my_peer_id( diff --git a/easytier/src/proto/peer_rpc.proto b/easytier/src/proto/peer_rpc.proto index 0bfb432..cae2f60 100644 --- a/easytier/src/proto/peer_rpc.proto +++ b/easytier/src/proto/peer_rpc.proto @@ -60,6 +60,16 @@ message RouteForeignNetworkInfos { repeated Info infos = 1; } +message RouteForeignNetworkSummary { + message Info { + uint32 peer_id = 1; + uint32 network_count = 2; + uint32 peer_count = 3; + } + + map info_map = 1; +} + message SyncRouteInfoRequest { uint32 my_peer_id = 1; uint64 my_session_id = 2; diff --git a/easytier/src/proto/web.proto b/easytier/src/proto/web.proto index 68d1225..78a30f9 100644 --- a/easytier/src/proto/web.proto +++ b/easytier/src/proto/web.proto @@ -102,6 +102,7 @@ message NetworkInstanceRunningInfo { repeated cli.PeerRoutePair peer_route_pairs = 6; bool running = 7; optional string error_msg = 8; + peer_rpc.RouteForeignNetworkSummary foreign_network_summary = 9; } message NetworkInstanceRunningInfoMap { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e29dad..635f35d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,43 @@ settings: importers: + easytier-contrib/easytier-uptime/frontend: + dependencies: + '@element-plus/icons-vue': + specifier: ^2.3.1 + version: 2.3.2(vue@3.5.18(typescript@5.6.3)) + axios: + specifier: ^1.7.9 + version: 1.11.0 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 + easytier-uptime-frontend: + specifier: 'link:' + version: 'link:' + element-plus: + specifier: ^2.8.8 + version: 2.10.7(vue@3.5.18(typescript@5.6.3)) + vue: + specifier: ^3.5.18 + version: 3.5.18(typescript@5.6.3) + vue-router: + specifier: ^4.4.5 + version: 4.4.5(vue@3.5.18(typescript@5.6.3)) + devDependencies: + '@vitejs/plugin-vue': + specifier: ^6.0.1 + version: 6.0.1(vite@7.1.2(jiti@2.4.0)(tsx@4.19.2)(yaml@2.6.0))(vue@3.5.18(typescript@5.6.3)) + unplugin-auto-import: + specifier: ^0.18.6 + version: 0.18.6(@vueuse/core@11.2.0(vue@3.5.18(typescript@5.6.3)))(rollup@4.46.2) + unplugin-vue-components: + specifier: ^0.27.4 + version: 0.27.4(@babel/parser@7.28.0)(rollup@4.46.2)(vue@3.5.18(typescript@5.6.3)) + vite: + specifier: ^7.1.2 + version: 7.1.2(jiti@2.4.0)(tsx@4.19.2)(yaml@2.6.0) + easytier-gui: dependencies: '@primevue/themes': @@ -429,10 +466,18 @@ packages: resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.25.9': resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.25.9': resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} engines: {node: '>=6.9.0'} @@ -446,6 +491,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-proposal-decorators@7.25.9': resolution: {integrity: sha512-smkNLL/O1ezy9Nhy4CNosc4Va+1wo5w4gzSZeLe6y6dM4mmHfYOCPolXQPHQxonZCF+ZyebxN9vqOolkYrSn5g==} engines: {node: '>=6.9.0'} @@ -499,6 +549,10 @@ packages: resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} engines: {node: '>=6.9.0'} + '@babel/types@7.28.2': + resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} + engines: {node: '>=6.9.0'} + '@clack/core@0.3.4': resolution: {integrity: sha512-H4hxZDXgHtWTwV3RAVenqcC4VbJZNegbBjlPvzOzCouXtS2y3sDvlO3IsbrPNWuLWPPlYVYPghQdSF64683Ldw==} @@ -507,6 +561,10 @@ packages: bundledDependencies: - is-unicode-supported + '@ctrl/tinycolor@3.6.1': + resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} + engines: {node: '>=10'} + '@dprint/formatter@0.3.0': resolution: {integrity: sha512-N9fxCxbaBOrDkteSOzaCqwWjso5iAe+WJPsHC021JfHNj2ThInPNEF13ORDKta3llq5D1TlclODCvOvipH7bWQ==} @@ -516,6 +574,11 @@ packages: '@dprint/toml@0.6.3': resolution: {integrity: sha512-zQ42I53sb4WVHA+5yoY1t59Zk++Ot02AvUgtNKLzTT8mPyVqVChFcePa3on/xIoKEgH+RoepgPHzqfk9837YFw==} + '@element-plus/icons-vue@2.3.2': + resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} + peerDependencies: + vue: ^3.2.0 + '@emnapi/core@1.3.1': resolution: {integrity: sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==} @@ -545,6 +608,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.25.9': + resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} @@ -557,6 +626,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.25.9': + resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} @@ -569,6 +644,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.25.9': + resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} @@ -581,6 +662,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.25.9': + resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} @@ -593,6 +680,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.25.9': + resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} @@ -605,6 +698,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.25.9': + resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} @@ -617,6 +716,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.25.9': + resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} @@ -629,6 +734,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.9': + resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} @@ -641,6 +752,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.25.9': + resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} @@ -653,6 +770,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.25.9': + resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} @@ -665,6 +788,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.25.9': + resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} @@ -677,6 +806,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.25.9': + resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} @@ -689,6 +824,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.25.9': + resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} @@ -701,6 +842,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.25.9': + resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} @@ -713,6 +860,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.25.9': + resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} @@ -725,6 +878,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.25.9': + resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} @@ -737,6 +896,18 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.25.9': + resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.9': + resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} @@ -749,12 +920,24 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.9': + resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.23.1': resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.25.9': + resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} @@ -767,6 +950,18 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.9': + resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.9': + resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} @@ -779,6 +974,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.25.9': + resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} @@ -791,6 +992,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.25.9': + resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} @@ -803,6 +1010,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.25.9': + resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} @@ -815,6 +1028,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.25.9': + resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-plugin-eslint-comments@4.4.1': resolution: {integrity: sha512-lb/Z/MzbTf7CaVYM9WCFNQZ4L1yi3ev2fsFPF99h31ljhSEyUoyEsKsNWiU+qD1glbYTDJdqgyaLKtyTkkqtuQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1138,6 +1357,9 @@ packages: resolution: {integrity: sha512-LiYlSXsHeA8DFm8+yGyiDFQc3SEQwHcESTN1/rV+rrZ+UPuPisHY9fNIGRFQKA5XUQPDTQDQjtwYGx25Jikwhg==} engines: {node: '>=12.11.0'} + '@rolldown/pluginutils@1.0.0-beta.29': + resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==} + '@rollup/plugin-typescript@11.1.6': resolution: {integrity: sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==} engines: {node: '>=14.0.0'} @@ -1169,96 +1391,205 @@ packages: rollup: optional: true + '@rollup/pluginutils@5.2.0': + resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.24.3': resolution: {integrity: sha512-ufb2CH2KfBWPJok95frEZZ82LtDl0A6QKTa8MoM+cWwDZvVGl5/jNb79pIhRvAalUu+7LD91VYR0nwRD799HkQ==} cpu: [arm] os: [android] + '@rollup/rollup-android-arm-eabi@4.46.2': + resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} + cpu: [arm] + os: [android] + '@rollup/rollup-android-arm64@4.24.3': resolution: {integrity: sha512-iAHpft/eQk9vkWIV5t22V77d90CRofgR2006UiCjHcHJFVI1E0oBkQIAbz+pLtthFw3hWEmVB4ilxGyBf48i2Q==} cpu: [arm64] os: [android] + '@rollup/rollup-android-arm64@4.46.2': + resolution: {integrity: sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==} + cpu: [arm64] + os: [android] + '@rollup/rollup-darwin-arm64@4.24.3': resolution: {integrity: sha512-QPW2YmkWLlvqmOa2OwrfqLJqkHm7kJCIMq9kOz40Zo9Ipi40kf9ONG5Sz76zszrmIZZ4hgRIkez69YnTHgEz1w==} cpu: [arm64] os: [darwin] + '@rollup/rollup-darwin-arm64@4.46.2': + resolution: {integrity: sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==} + cpu: [arm64] + os: [darwin] + '@rollup/rollup-darwin-x64@4.24.3': resolution: {integrity: sha512-KO0pN5x3+uZm1ZXeIfDqwcvnQ9UEGN8JX5ufhmgH5Lz4ujjZMAnxQygZAVGemFWn+ZZC0FQopruV4lqmGMshow==} cpu: [x64] os: [darwin] + '@rollup/rollup-darwin-x64@4.46.2': + resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==} + cpu: [x64] + os: [darwin] + '@rollup/rollup-freebsd-arm64@4.24.3': resolution: {integrity: sha512-CsC+ZdIiZCZbBI+aRlWpYJMSWvVssPuWqrDy/zi9YfnatKKSLFCe6fjna1grHuo/nVaHG+kiglpRhyBQYRTK4A==} cpu: [arm64] os: [freebsd] + '@rollup/rollup-freebsd-arm64@4.46.2': + resolution: {integrity: sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==} + cpu: [arm64] + os: [freebsd] + '@rollup/rollup-freebsd-x64@4.24.3': resolution: {integrity: sha512-F0nqiLThcfKvRQhZEzMIXOQG4EeX61im61VYL1jo4eBxv4aZRmpin6crnBJQ/nWnCsjH5F6J3W6Stdm0mBNqBg==} cpu: [x64] os: [freebsd] + '@rollup/rollup-freebsd-x64@4.46.2': + resolution: {integrity: sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==} + cpu: [x64] + os: [freebsd] + '@rollup/rollup-linux-arm-gnueabihf@4.24.3': resolution: {integrity: sha512-KRSFHyE/RdxQ1CSeOIBVIAxStFC/hnBgVcaiCkQaVC+EYDtTe4X7z5tBkFyRoBgUGtB6Xg6t9t2kulnX6wJc6A==} cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-gnueabihf@4.46.2': + resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.24.3': resolution: {integrity: sha512-h6Q8MT+e05zP5BxEKz0vi0DhthLdrNEnspdLzkoFqGwnmOzakEHSlXfVyA4HJ322QtFy7biUAVFPvIDEDQa6rw==} cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.46.2': + resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.24.3': resolution: {integrity: sha512-fKElSyXhXIJ9pqiYRqisfirIo2Z5pTTve5K438URf08fsypXrEkVmShkSfM8GJ1aUyvjakT+fn2W7Czlpd/0FQ==} cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.46.2': + resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-arm64-musl@4.24.3': resolution: {integrity: sha512-YlddZSUk8G0px9/+V9PVilVDC6ydMz7WquxozToozSnfFK6wa6ne1ATUjUvjin09jp34p84milxlY5ikueoenw==} cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-musl@4.46.2': + resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} + cpu: [loong64] + os: [linux] + '@rollup/rollup-linux-powerpc64le-gnu@4.24.3': resolution: {integrity: sha512-yNaWw+GAO8JjVx3s3cMeG5Esz1cKVzz8PkTJSfYzE5u7A+NvGmbVFEHP+BikTIyYWuz0+DX9kaA3pH9Sqxp69g==} cpu: [ppc64] os: [linux] + '@rollup/rollup-linux-ppc64-gnu@4.46.2': + resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} + cpu: [ppc64] + os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.24.3': resolution: {integrity: sha512-lWKNQfsbpv14ZCtM/HkjCTm4oWTKTfxPmr7iPfp3AHSqyoTz5AgLemYkWLwOBWc+XxBbrU9SCokZP0WlBZM9lA==} cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.46.2': + resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.46.2': + resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.24.3': resolution: {integrity: sha512-HoojGXTC2CgCcq0Woc/dn12wQUlkNyfH0I1ABK4Ni9YXyFQa86Fkt2Q0nqgLfbhkyfQ6003i3qQk9pLh/SpAYw==} cpu: [s390x] os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.46.2': + resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} + cpu: [s390x] + os: [linux] + '@rollup/rollup-linux-x64-gnu@4.24.3': resolution: {integrity: sha512-mnEOh4iE4USSccBOtcrjF5nj+5/zm6NcNhbSEfR3Ot0pxBwvEn5QVUXcuOwwPkapDtGZ6pT02xLoPaNv06w7KQ==} cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-gnu@4.46.2': + resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} + cpu: [x64] + os: [linux] + '@rollup/rollup-linux-x64-musl@4.24.3': resolution: {integrity: sha512-rMTzawBPimBQkG9NKpNHvquIUTQPzrnPxPbCY1Xt+mFkW7pshvyIS5kYgcf74goxXOQk0CP3EoOC1zcEezKXhw==} cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-musl@4.46.2': + resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} + cpu: [x64] + os: [linux] + '@rollup/rollup-win32-arm64-msvc@4.24.3': resolution: {integrity: sha512-2lg1CE305xNvnH3SyiKwPVsTVLCg4TmNCF1z7PSHX2uZY2VbUpdkgAllVoISD7JO7zu+YynpWNSKAtOrX3AiuA==} cpu: [arm64] os: [win32] + '@rollup/rollup-win32-arm64-msvc@4.46.2': + resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} + cpu: [arm64] + os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.24.3': resolution: {integrity: sha512-9SjYp1sPyxJsPWuhOCX6F4jUMXGbVVd5obVpoVEi8ClZqo52ViZewA6eFz85y8ezuOA+uJMP5A5zo6Oz4S5rVQ==} cpu: [ia32] os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.46.2': + resolution: {integrity: sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==} + cpu: [ia32] + os: [win32] + '@rollup/rollup-win32-x64-msvc@4.24.3': resolution: {integrity: sha512-HGZgRFFYrMrP3TJlq58nR1xy8zHKId25vhmm5S9jETEfDf6xybPxsavFTJaufe2zgOGYJBskGlj49CwtEuFhWQ==} cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-msvc@4.46.2': + resolution: {integrity: sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==} + cpu: [x64] + os: [win32] + '@rushstack/node-core-library@5.9.0': resolution: {integrity: sha512-MMsshEWkTbXqxqFxD4gcIUWQOCeBChlGczdZbHfqmNZQFLHB3yWxDFSMHFUdu2/OB9NUk7Awn5qRL+rws4HQNg==} peerDependencies: @@ -1287,6 +1618,9 @@ packages: peerDependencies: eslint: '>=8.40.0' + '@sxzz/popperjs-es@2.11.7': + resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==} + '@tauri-apps/api@2.0.0-rc.0': resolution: {integrity: sha512-v454Qs3REHc3Za59U+/eSmBsdmF+3NE5+76+lFDaitVqN4ZglDHENDaMARYKGJVZuxiSkzyqG0SeG7lLQjVkPA==} engines: {node: '>= 18.18', npm: '>= 6.6.0', yarn: '>= 1.19.1'} @@ -1395,12 +1729,21 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/linkify-it@5.0.0': resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + '@types/markdown-it@14.1.2': resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} @@ -1425,6 +1768,9 @@ packages: '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@types/web-bluetooth@0.0.16': + resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==} + '@types/web-bluetooth@0.0.20': resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} @@ -1513,6 +1859,13 @@ packages: vite: ^5.0.0 vue: ^3.2.25 + '@vitejs/plugin-vue@6.0.1': + resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vue: ^3.2.25 + '@vitest/eslint-plugin@1.1.7': resolution: {integrity: sha512-pTWGW3y6lH2ukCuuffpan6kFxG6nIuoesbhMiQxskyQMRcCN5t9SXsKrNHvEw3p8wcCsgJoRqFZVkOTn6TjclA==} peerDependencies: @@ -1717,15 +2070,27 @@ packages: '@vue/compiler-core@3.5.12': resolution: {integrity: sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==} + '@vue/compiler-core@3.5.18': + resolution: {integrity: sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==} + '@vue/compiler-dom@3.5.12': resolution: {integrity: sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==} + '@vue/compiler-dom@3.5.18': + resolution: {integrity: sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==} + '@vue/compiler-sfc@3.5.12': resolution: {integrity: sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==} + '@vue/compiler-sfc@3.5.18': + resolution: {integrity: sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==} + '@vue/compiler-ssr@3.5.12': resolution: {integrity: sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==} + '@vue/compiler-ssr@3.5.18': + resolution: {integrity: sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==} + '@vue/compiler-vue2@2.7.16': resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} @@ -1762,29 +2127,55 @@ packages: '@vue/reactivity@3.5.12': resolution: {integrity: sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==} + '@vue/reactivity@3.5.18': + resolution: {integrity: sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==} + '@vue/runtime-core@3.5.12': resolution: {integrity: sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==} + '@vue/runtime-core@3.5.18': + resolution: {integrity: sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==} + '@vue/runtime-dom@3.5.12': resolution: {integrity: sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==} + '@vue/runtime-dom@3.5.18': + resolution: {integrity: sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==} + '@vue/server-renderer@3.5.12': resolution: {integrity: sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==} peerDependencies: vue: 3.5.12 + '@vue/server-renderer@3.5.18': + resolution: {integrity: sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==} + peerDependencies: + vue: 3.5.18 + '@vue/shared@3.5.12': resolution: {integrity: sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==} + '@vue/shared@3.5.18': + resolution: {integrity: sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==} + '@vueuse/core@11.2.0': resolution: {integrity: sha512-JIUwRcOqOWzcdu1dGlfW04kaJhW3EXnnjJJfLTtddJanymTL7lF1C0+dVVZ/siLfc73mWn+cGP1PE1PKPruRSA==} + '@vueuse/core@9.13.0': + resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==} + '@vueuse/metadata@11.2.0': resolution: {integrity: sha512-L0ZmtRmNx+ZW95DmrgD6vn484gSpVeRbgpWevFKXwqqQxW9hnSi2Ppuh2BzMjnbv4aJRiIw8tQatXT9uOB23dQ==} + '@vueuse/metadata@9.13.0': + resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==} + '@vueuse/shared@11.2.0': resolution: {integrity: sha512-VxFjie0EanOudYSgMErxXfq6fo8vhr5ICI+BuE3I9FnX7ePllEsVrRQ7O6Q1TLgApeLuPKcHQxAXpP+KnlrJsg==} + '@vueuse/shared@9.13.0': + resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1871,6 +2262,9 @@ packages: resolution: {integrity: sha512-1UWOyC50xI3QZkRuDj6PqDtpm1oHWtYs+NQGwqL/2R11eN3Q81PHAHPM0SWW3BNQm53UDwS//Jv8L4CCVLM1bQ==} engines: {node: '>=16.14.0'} + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -1881,6 +2275,9 @@ packages: peerDependencies: postcss: ^8.1.0 + axios@1.11.0: + resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} + axios@1.7.7: resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} @@ -1926,6 +2323,10 @@ packages: peerDependencies: esbuild: '>=0.18' + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1998,6 +2399,9 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -2020,6 +2424,9 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} @@ -2090,12 +2497,21 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} electron-to-chromium@1.5.50: resolution: {integrity: sha512-eMVObiUQ2LdgeO1F/ySTXsvqvxb6ZH2zPGaMYsWzRDdOddUa77tdmI0ltg+L16UpbWdhPmuF3wIQYyQq65WfZw==} + element-plus@2.10.7: + resolution: {integrity: sha512-bL4yhepL8/0NEQA5+N2Q6ZVKLipIDkiQjK2mqtSmGh6CxJk1yaBMdG5HXfYkbk1htNcT3ULk9g23lzT323JGcA==} + peerDependencies: + vue: ^3.2.0 + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2116,9 +2532,25 @@ packages: error-stack-parser-es@0.1.5: resolution: {integrity: sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.5.4: resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -2129,10 +2561,18 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.25.9: + resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -2368,6 +2808,9 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -2382,6 +2825,10 @@ packages: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -2391,6 +2838,14 @@ packages: fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2444,6 +2899,10 @@ packages: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -2471,6 +2930,14 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -2514,6 +2981,10 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -2528,6 +2999,14 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -2664,6 +3143,9 @@ packages: js-tokens@9.0.0: resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -2747,6 +3229,14 @@ packages: resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} engines: {node: '>=14'} + local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + + local-pkg@1.1.1: + resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} + engines: {node: '>=14'} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -2755,6 +3245,16 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -2781,6 +3281,9 @@ packages: magic-string@0.30.12: resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + make-synchronized@0.2.9: resolution: {integrity: sha512-4wczOs8SLuEdpEvp3vGo83wh8rjJ78UsIk7DIX5fxdfmfMJGog4bQzxfvOwq7Q3yCHLC4jp1urPHIxRS/A93gA==} @@ -2791,6 +3294,10 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.1: resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} @@ -2827,6 +3334,9 @@ packages: mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2958,6 +3468,9 @@ packages: mlly@1.7.2: resolution: {integrity: sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==} + mlly@1.7.4: + resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} @@ -2971,6 +3484,11 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2996,6 +3514,9 @@ packages: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + npm-run-path@5.3.0: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3097,6 +3618,9 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -3111,6 +3635,10 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -3134,6 +3662,12 @@ packages: pkg-types@1.2.1: resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.2.0: + resolution: {integrity: sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==} + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -3195,6 +3729,10 @@ packages: resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3226,6 +3764,9 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + quansync@0.2.10: + resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -3291,6 +3832,11 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rollup@4.46.2: + resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + run-applescript@7.0.0: resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} engines: {node: '>=18'} @@ -3432,6 +3978,9 @@ packages: strip-literal@2.1.0: resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} + strip-literal@2.1.1: + resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + sucrase@3.35.0: resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} engines: {node: '>=16 || 14 >=14.17'} @@ -3491,6 +4040,10 @@ packages: tinyexec@0.3.1: resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3569,6 +4122,9 @@ packages: unimport@3.13.1: resolution: {integrity: sha512-nNrVzcs93yrZQOW77qnyOVHtb68LegvhYFwxFMfuuWScmwQmyVCG/NBuN8tYsaGzgQUVYv34E/af+Cc9u4og4A==} + unimport@3.14.6: + resolution: {integrity: sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==} + unist-util-is@6.0.0: resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} @@ -3601,6 +4157,18 @@ packages: '@vueuse/core': optional: true + unplugin-auto-import@0.18.6: + resolution: {integrity: sha512-LMFzX5DtkTj/3wZuyG5bgKBoJ7WSgzqSGJ8ppDRdlvPh45mx6t6w3OcbExQi53n3xF5MYkNGPNR/HYOL95KL2A==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': ^3.2.2 + '@vueuse/core': '*' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@vueuse/core': + optional: true + unplugin-combine@1.0.3: resolution: {integrity: sha512-vCpXdYCTcGwRGv7iF/COh7dupqyIrRxwe5kTKF3ZiVnO4toyvU+tpoTj570Bf9SpJG4JspGnfjcZIU6SBIKryA==} engines: {node: '>=16.14.0'} @@ -3670,6 +4238,10 @@ packages: webpack-sources: optional: true + unplugin@1.16.1: + resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} + engines: {node: '>=14.0.0'} + update-browserslist-db@1.1.1: resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} hasBin: true @@ -3774,6 +4346,46 @@ packages: terser: optional: true + vite@7.1.2: + resolution: {integrity: sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} @@ -3830,6 +4442,14 @@ packages: typescript: optional: true + vue@3.5.18: + resolution: {integrity: sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -4060,8 +4680,12 @@ snapshots: '@babel/helper-string-parser@7.25.9': {} + '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-validator-identifier@7.25.9': {} + '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-option@7.25.9': {} '@babel/helpers@7.26.0': @@ -4073,6 +4697,10 @@ snapshots: dependencies: '@babel/types': 7.26.0 + '@babel/parser@7.28.0': + dependencies: + '@babel/types': 7.28.2 + '@babel/plugin-proposal-decorators@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -4141,6 +4769,11 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@babel/types@7.28.2': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@clack/core@0.3.4': dependencies: picocolors: 1.1.1 @@ -4152,12 +4785,18 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 + '@ctrl/tinycolor@3.6.1': {} + '@dprint/formatter@0.3.0': {} '@dprint/markdown@0.17.8': {} '@dprint/toml@0.6.3': {} + '@element-plus/icons-vue@2.3.2(vue@3.5.18(typescript@5.6.3))': + dependencies: + vue: 3.5.18(typescript@5.6.3) + '@emnapi/core@1.3.1': dependencies: '@emnapi/wasi-threads': 1.0.1 @@ -4192,141 +4831,219 @@ snapshots: '@esbuild/aix-ppc64@0.23.1': optional: true + '@esbuild/aix-ppc64@0.25.9': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true '@esbuild/android-arm64@0.23.1': optional: true + '@esbuild/android-arm64@0.25.9': + optional: true + '@esbuild/android-arm@0.21.5': optional: true '@esbuild/android-arm@0.23.1': optional: true + '@esbuild/android-arm@0.25.9': + optional: true + '@esbuild/android-x64@0.21.5': optional: true '@esbuild/android-x64@0.23.1': optional: true + '@esbuild/android-x64@0.25.9': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true '@esbuild/darwin-arm64@0.23.1': optional: true + '@esbuild/darwin-arm64@0.25.9': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true '@esbuild/darwin-x64@0.23.1': optional: true + '@esbuild/darwin-x64@0.25.9': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true '@esbuild/freebsd-arm64@0.23.1': optional: true + '@esbuild/freebsd-arm64@0.25.9': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true '@esbuild/freebsd-x64@0.23.1': optional: true + '@esbuild/freebsd-x64@0.25.9': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true '@esbuild/linux-arm64@0.23.1': optional: true + '@esbuild/linux-arm64@0.25.9': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true '@esbuild/linux-arm@0.23.1': optional: true + '@esbuild/linux-arm@0.25.9': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true '@esbuild/linux-ia32@0.23.1': optional: true + '@esbuild/linux-ia32@0.25.9': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true '@esbuild/linux-loong64@0.23.1': optional: true + '@esbuild/linux-loong64@0.25.9': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true '@esbuild/linux-mips64el@0.23.1': optional: true + '@esbuild/linux-mips64el@0.25.9': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true '@esbuild/linux-ppc64@0.23.1': optional: true + '@esbuild/linux-ppc64@0.25.9': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true '@esbuild/linux-riscv64@0.23.1': optional: true + '@esbuild/linux-riscv64@0.25.9': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true '@esbuild/linux-s390x@0.23.1': optional: true + '@esbuild/linux-s390x@0.25.9': + optional: true + '@esbuild/linux-x64@0.21.5': optional: true '@esbuild/linux-x64@0.23.1': optional: true + '@esbuild/linux-x64@0.25.9': + optional: true + + '@esbuild/netbsd-arm64@0.25.9': + optional: true + '@esbuild/netbsd-x64@0.21.5': optional: true '@esbuild/netbsd-x64@0.23.1': optional: true + '@esbuild/netbsd-x64@0.25.9': + optional: true + '@esbuild/openbsd-arm64@0.23.1': optional: true + '@esbuild/openbsd-arm64@0.25.9': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true '@esbuild/openbsd-x64@0.23.1': optional: true + '@esbuild/openbsd-x64@0.25.9': + optional: true + + '@esbuild/openharmony-arm64@0.25.9': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true '@esbuild/sunos-x64@0.23.1': optional: true + '@esbuild/sunos-x64@0.25.9': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true '@esbuild/win32-arm64@0.23.1': optional: true + '@esbuild/win32-arm64@0.25.9': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true '@esbuild/win32-ia32@0.23.1': optional: true + '@esbuild/win32-ia32@0.25.9': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true '@esbuild/win32-x64@0.23.1': optional: true + '@esbuild/win32-x64@0.25.9': + optional: true + '@eslint-community/eslint-plugin-eslint-comments@4.4.1(eslint@9.14.0(jiti@2.4.0))': dependencies: escape-string-regexp: 4.0.0 @@ -4673,6 +5390,8 @@ snapshots: '@primeuix/styled': 0.5.1 '@primeuix/themes': 1.0.2 + '@rolldown/pluginutils@1.0.0-beta.29': {} + '@rollup/plugin-typescript@11.1.6(rollup@4.24.3)(tslib@2.8.1)(typescript@5.6.3)': dependencies: '@rollup/pluginutils': 5.1.3(rollup@4.24.3) @@ -4698,60 +5417,136 @@ snapshots: optionalDependencies: rollup: 4.24.3 + '@rollup/pluginutils@5.1.3(rollup@4.46.2)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.46.2 + + '@rollup/pluginutils@5.2.0(rollup@4.46.2)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.46.2 + '@rollup/rollup-android-arm-eabi@4.24.3': optional: true + '@rollup/rollup-android-arm-eabi@4.46.2': + optional: true + '@rollup/rollup-android-arm64@4.24.3': optional: true + '@rollup/rollup-android-arm64@4.46.2': + optional: true + '@rollup/rollup-darwin-arm64@4.24.3': optional: true + '@rollup/rollup-darwin-arm64@4.46.2': + optional: true + '@rollup/rollup-darwin-x64@4.24.3': optional: true + '@rollup/rollup-darwin-x64@4.46.2': + optional: true + '@rollup/rollup-freebsd-arm64@4.24.3': optional: true + '@rollup/rollup-freebsd-arm64@4.46.2': + optional: true + '@rollup/rollup-freebsd-x64@4.24.3': optional: true + '@rollup/rollup-freebsd-x64@4.46.2': + optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.24.3': optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.46.2': + optional: true + '@rollup/rollup-linux-arm-musleabihf@4.24.3': optional: true + '@rollup/rollup-linux-arm-musleabihf@4.46.2': + optional: true + '@rollup/rollup-linux-arm64-gnu@4.24.3': optional: true + '@rollup/rollup-linux-arm64-gnu@4.46.2': + optional: true + '@rollup/rollup-linux-arm64-musl@4.24.3': optional: true + '@rollup/rollup-linux-arm64-musl@4.46.2': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + optional: true + '@rollup/rollup-linux-powerpc64le-gnu@4.24.3': optional: true + '@rollup/rollup-linux-ppc64-gnu@4.46.2': + optional: true + '@rollup/rollup-linux-riscv64-gnu@4.24.3': optional: true + '@rollup/rollup-linux-riscv64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.46.2': + optional: true + '@rollup/rollup-linux-s390x-gnu@4.24.3': optional: true + '@rollup/rollup-linux-s390x-gnu@4.46.2': + optional: true + '@rollup/rollup-linux-x64-gnu@4.24.3': optional: true + '@rollup/rollup-linux-x64-gnu@4.46.2': + optional: true + '@rollup/rollup-linux-x64-musl@4.24.3': optional: true + '@rollup/rollup-linux-x64-musl@4.46.2': + optional: true + '@rollup/rollup-win32-arm64-msvc@4.24.3': optional: true + '@rollup/rollup-win32-arm64-msvc@4.46.2': + optional: true + '@rollup/rollup-win32-ia32-msvc@4.24.3': optional: true + '@rollup/rollup-win32-ia32-msvc@4.46.2': + optional: true + '@rollup/rollup-win32-x64-msvc@4.24.3': optional: true + '@rollup/rollup-win32-x64-msvc@4.46.2': + optional: true + '@rushstack/node-core-library@5.9.0(@types/node@22.8.6)': dependencies: ajv: 8.13.0 @@ -4798,6 +5593,8 @@ snapshots: - supports-color - typescript + '@sxzz/popperjs-es@2.11.7': {} + '@tauri-apps/api@2.0.0-rc.0': {} '@tauri-apps/api@2.7.0': {} @@ -4884,10 +5681,18 @@ snapshots: '@types/estree@1.0.6': {} + '@types/estree@1.0.8': {} + '@types/json-schema@7.0.15': {} '@types/linkify-it@5.0.0': {} + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.20 + + '@types/lodash@4.17.20': {} + '@types/markdown-it@14.1.2': dependencies: '@types/linkify-it': 5.0.0 @@ -4911,6 +5716,8 @@ snapshots: '@types/uuid@10.0.0': {} + '@types/web-bluetooth@0.0.16': {} + '@types/web-bluetooth@0.0.20': {} '@typescript-eslint/eslint-plugin@8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.0))(typescript@5.6.3))(eslint@9.14.0(jiti@2.4.0))(typescript@5.6.3)': @@ -5026,6 +5833,12 @@ snapshots: vite: 5.4.10(@types/node@22.8.6) vue: 3.5.12(typescript@5.6.3) + '@vitejs/plugin-vue@6.0.1(vite@7.1.2(jiti@2.4.0)(tsx@4.19.2)(yaml@2.6.0))(vue@3.5.18(typescript@5.6.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.29 + vite: 7.1.2(jiti@2.4.0)(tsx@4.19.2)(yaml@2.6.0) + vue: 3.5.18(typescript@5.6.3) + '@vitest/eslint-plugin@1.1.7(@typescript-eslint/utils@8.13.0(eslint@9.14.0(jiti@2.4.0))(typescript@5.6.3))(eslint@9.14.0(jiti@2.4.0))(typescript@5.6.3)': dependencies: '@typescript-eslint/utils': 8.13.0(eslint@9.14.0(jiti@2.4.0))(typescript@5.6.3) @@ -5378,11 +6191,24 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.1 + '@vue/compiler-core@3.5.18': + dependencies: + '@babel/parser': 7.28.0 + '@vue/shared': 3.5.18 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + '@vue/compiler-dom@3.5.12': dependencies: '@vue/compiler-core': 3.5.12 '@vue/shared': 3.5.12 + '@vue/compiler-dom@3.5.18': + dependencies: + '@vue/compiler-core': 3.5.18 + '@vue/shared': 3.5.18 + '@vue/compiler-sfc@3.5.12': dependencies: '@babel/parser': 7.26.2 @@ -5395,11 +6221,28 @@ snapshots: postcss: 8.4.47 source-map-js: 1.2.1 + '@vue/compiler-sfc@3.5.18': + dependencies: + '@babel/parser': 7.28.0 + '@vue/compiler-core': 3.5.18 + '@vue/compiler-dom': 3.5.18 + '@vue/compiler-ssr': 3.5.18 + '@vue/shared': 3.5.18 + estree-walker: 2.0.2 + magic-string: 0.30.17 + postcss: 8.5.6 + source-map-js: 1.2.1 + '@vue/compiler-ssr@3.5.12': dependencies: '@vue/compiler-dom': 3.5.12 '@vue/shared': 3.5.12 + '@vue/compiler-ssr@3.5.18': + dependencies: + '@vue/compiler-dom': 3.5.18 + '@vue/shared': 3.5.18 + '@vue/compiler-vue2@2.7.16': dependencies: de-indent: 1.0.2 @@ -5463,11 +6306,20 @@ snapshots: dependencies: '@vue/shared': 3.5.12 + '@vue/reactivity@3.5.18': + dependencies: + '@vue/shared': 3.5.18 + '@vue/runtime-core@3.5.12': dependencies: '@vue/reactivity': 3.5.12 '@vue/shared': 3.5.12 + '@vue/runtime-core@3.5.18': + dependencies: + '@vue/reactivity': 3.5.18 + '@vue/shared': 3.5.18 + '@vue/runtime-dom@3.5.12': dependencies: '@vue/reactivity': 3.5.12 @@ -5475,14 +6327,29 @@ snapshots: '@vue/shared': 3.5.12 csstype: 3.1.3 + '@vue/runtime-dom@3.5.18': + dependencies: + '@vue/reactivity': 3.5.18 + '@vue/runtime-core': 3.5.18 + '@vue/shared': 3.5.18 + csstype: 3.1.3 + '@vue/server-renderer@3.5.12(vue@3.5.12(typescript@5.6.3))': dependencies: '@vue/compiler-ssr': 3.5.12 '@vue/shared': 3.5.12 vue: 3.5.12(typescript@5.6.3) + '@vue/server-renderer@3.5.18(vue@3.5.18(typescript@5.6.3))': + dependencies: + '@vue/compiler-ssr': 3.5.18 + '@vue/shared': 3.5.18 + vue: 3.5.18(typescript@5.6.3) + '@vue/shared@3.5.12': {} + '@vue/shared@3.5.18': {} + '@vueuse/core@11.2.0(vue@3.5.12(typescript@5.6.3))': dependencies: '@types/web-bluetooth': 0.0.20 @@ -5493,8 +6360,31 @@ snapshots: - '@vue/composition-api' - vue + '@vueuse/core@11.2.0(vue@3.5.18(typescript@5.6.3))': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 11.2.0 + '@vueuse/shared': 11.2.0(vue@3.5.18(typescript@5.6.3)) + vue-demi: 0.14.10(vue@3.5.18(typescript@5.6.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + optional: true + + '@vueuse/core@9.13.0(vue@3.5.18(typescript@5.6.3))': + dependencies: + '@types/web-bluetooth': 0.0.16 + '@vueuse/metadata': 9.13.0 + '@vueuse/shared': 9.13.0(vue@3.5.18(typescript@5.6.3)) + vue-demi: 0.14.10(vue@3.5.18(typescript@5.6.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + '@vueuse/metadata@11.2.0': {} + '@vueuse/metadata@9.13.0': {} + '@vueuse/shared@11.2.0(vue@3.5.12(typescript@5.6.3))': dependencies: vue-demi: 0.14.10(vue@3.5.12(typescript@5.6.3)) @@ -5502,6 +6392,21 @@ snapshots: - '@vue/composition-api' - vue + '@vueuse/shared@11.2.0(vue@3.5.18(typescript@5.6.3))': + dependencies: + vue-demi: 0.14.10(vue@3.5.18(typescript@5.6.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + optional: true + + '@vueuse/shared@9.13.0(vue@3.5.18(typescript@5.6.3))': + dependencies: + vue-demi: 0.14.10(vue@3.5.18(typescript@5.6.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: acorn: 8.14.0 @@ -5578,6 +6483,8 @@ snapshots: '@babel/parser': 7.26.2 ast-kit: 1.3.1 + async-validator@4.2.5: {} + asynckit@0.4.0: {} autoprefixer@10.4.20(postcss@8.4.47): @@ -5590,6 +6497,14 @@ snapshots: postcss: 8.4.47 postcss-value-parser: 4.2.0 + axios@1.11.0: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axios@1.7.7: dependencies: follow-redirects: 1.15.9 @@ -5637,6 +6552,11 @@ snapshots: esbuild: 0.23.1 load-tsconfig: 0.2.5 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + callsites@3.1.0: {} camelcase-css@2.0.1: {} @@ -5702,6 +6622,8 @@ snapshots: confbox@0.1.8: {} + confbox@0.2.2: {} + convert-source-map@2.0.0: {} copy-anything@3.0.5: @@ -5722,6 +6644,8 @@ snapshots: csstype@3.1.3: {} + dayjs@1.11.13: {} + de-indent@1.0.2: {} debug@3.2.7: @@ -5773,10 +6697,37 @@ snapshots: dependencies: esutils: 2.0.3 + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + eastasianwidth@0.2.0: {} electron-to-chromium@1.5.50: {} + element-plus@2.10.7(vue@3.5.18(typescript@5.6.3)): + dependencies: + '@ctrl/tinycolor': 3.6.1 + '@element-plus/icons-vue': 2.3.2(vue@3.5.18(typescript@5.6.3)) + '@floating-ui/dom': 1.1.1 + '@popperjs/core': '@sxzz/popperjs-es@2.11.7' + '@types/lodash': 4.17.20 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 9.13.0(vue@3.5.18(typescript@5.6.3)) + async-validator: 4.2.5 + dayjs: 1.11.13 + escape-html: 1.0.3 + lodash: 4.17.21 + lodash-es: 4.17.21 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.18(typescript@5.6.3) + transitivePeerDependencies: + - '@vue/composition-api' + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -5794,8 +6745,23 @@ snapshots: error-stack-parser-es@0.1.5: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.5.4: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -5849,8 +6815,39 @@ snapshots: '@esbuild/win32-ia32': 0.23.1 '@esbuild/win32-x64': 0.23.1 + esbuild@0.25.9: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.9 + '@esbuild/android-arm': 0.25.9 + '@esbuild/android-arm64': 0.25.9 + '@esbuild/android-x64': 0.25.9 + '@esbuild/darwin-arm64': 0.25.9 + '@esbuild/darwin-x64': 0.25.9 + '@esbuild/freebsd-arm64': 0.25.9 + '@esbuild/freebsd-x64': 0.25.9 + '@esbuild/linux-arm': 0.25.9 + '@esbuild/linux-arm64': 0.25.9 + '@esbuild/linux-ia32': 0.25.9 + '@esbuild/linux-loong64': 0.25.9 + '@esbuild/linux-mips64el': 0.25.9 + '@esbuild/linux-ppc64': 0.25.9 + '@esbuild/linux-riscv64': 0.25.9 + '@esbuild/linux-s390x': 0.25.9 + '@esbuild/linux-x64': 0.25.9 + '@esbuild/netbsd-arm64': 0.25.9 + '@esbuild/netbsd-x64': 0.25.9 + '@esbuild/openbsd-arm64': 0.25.9 + '@esbuild/openbsd-x64': 0.25.9 + '@esbuild/openharmony-arm64': 0.25.9 + '@esbuild/sunos-x64': 0.25.9 + '@esbuild/win32-arm64': 0.25.9 + '@esbuild/win32-ia32': 0.25.9 + '@esbuild/win32-x64': 0.25.9 + escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@1.0.5: {} escape-string-regexp@4.0.0: {} @@ -6193,6 +7190,8 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + exsolve@1.0.7: {} + extend-shallow@2.0.1: dependencies: is-extendable: 0.1.1 @@ -6209,6 +7208,14 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -6217,6 +7224,10 @@ snapshots: dependencies: reusify: 1.0.4 + fdir@6.4.6(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -6263,6 +7274,14 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + fraction.js@4.3.7: {} fs-extra@11.2.0: @@ -6286,6 +7305,24 @@ snapshots: get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@6.0.1: {} get-stream@8.0.1: {} @@ -6330,6 +7367,8 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} graphemer@1.4.0: {} @@ -6343,6 +7382,12 @@ snapshots: has-flag@4.0.0: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -6445,6 +7490,8 @@ snapshots: js-tokens@9.0.0: {} + js-tokens@9.0.1: {} + js-yaml@3.14.1: dependencies: argparse: 1.0.10 @@ -6517,6 +7564,17 @@ snapshots: mlly: 1.7.2 pkg-types: 1.2.1 + local-pkg@0.5.1: + dependencies: + mlly: 1.7.4 + pkg-types: 1.2.1 + + local-pkg@1.1.1: + dependencies: + mlly: 1.7.4 + pkg-types: 2.2.0 + quansync: 0.2.10 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -6525,6 +7583,14 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.17.21: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.17.21 + lodash-es: 4.17.21 + lodash.merge@4.6.2: {} lodash@4.17.21: {} @@ -6549,6 +7615,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + make-synchronized@0.2.9: {} markdown-it@14.1.0: @@ -6562,6 +7632,8 @@ snapshots: markdown-table@3.0.4: {} + math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.1: dependencies: '@types/mdast': 4.0.4 @@ -6666,6 +7738,8 @@ snapshots: mdurl@2.0.0: {} + memoize-one@6.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -6899,6 +7973,13 @@ snapshots: pkg-types: 1.2.1 ufo: 1.5.4 + mlly@1.7.4: + dependencies: + acorn: 8.14.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.5.4 + mrmime@2.0.0: {} ms@2.1.3: {} @@ -6911,6 +7992,8 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nanoid@3.3.11: {} + nanoid@3.3.7: {} natural-compare-lite@1.4.0: {} @@ -6930,6 +8013,8 @@ snapshots: normalize-range@0.1.2: {} + normalize-wheel-es@1.2.0: {} + npm-run-path@5.3.0: dependencies: path-key: 4.0.0 @@ -7035,6 +8120,8 @@ snapshots: pathe@1.1.2: {} + pathe@2.0.3: {} + perfect-debounce@1.0.0: {} picocolors@1.1.1: {} @@ -7043,6 +8130,8 @@ snapshots: picomatch@4.0.2: {} + picomatch@4.0.3: {} + pify@2.3.0: {} pinia@2.2.6(typescript@5.6.3)(vue@3.5.12(typescript@5.6.3)): @@ -7061,6 +8150,18 @@ snapshots: mlly: 1.7.2 pathe: 1.1.2 + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.4 + pathe: 2.0.3 + + pkg-types@2.2.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + pluralize@8.0.0: {} postcss-import@15.1.0(postcss@8.4.47): @@ -7117,6 +8218,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.0: @@ -7143,6 +8250,8 @@ snapshots: punycode@2.3.1: {} + quansync@0.2.10: {} + queue-microtask@1.2.3: {} read-cache@1.0.0: @@ -7223,6 +8332,32 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.24.3 fsevents: 2.3.3 + rollup@4.46.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.46.2 + '@rollup/rollup-android-arm64': 4.46.2 + '@rollup/rollup-darwin-arm64': 4.46.2 + '@rollup/rollup-darwin-x64': 4.46.2 + '@rollup/rollup-freebsd-arm64': 4.46.2 + '@rollup/rollup-freebsd-x64': 4.46.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.46.2 + '@rollup/rollup-linux-arm-musleabihf': 4.46.2 + '@rollup/rollup-linux-arm64-gnu': 4.46.2 + '@rollup/rollup-linux-arm64-musl': 4.46.2 + '@rollup/rollup-linux-loongarch64-gnu': 4.46.2 + '@rollup/rollup-linux-ppc64-gnu': 4.46.2 + '@rollup/rollup-linux-riscv64-gnu': 4.46.2 + '@rollup/rollup-linux-riscv64-musl': 4.46.2 + '@rollup/rollup-linux-s390x-gnu': 4.46.2 + '@rollup/rollup-linux-x64-gnu': 4.46.2 + '@rollup/rollup-linux-x64-musl': 4.46.2 + '@rollup/rollup-win32-arm64-msvc': 4.46.2 + '@rollup/rollup-win32-ia32-msvc': 4.46.2 + '@rollup/rollup-win32-x64-msvc': 4.46.2 + fsevents: 2.3.3 + run-applescript@7.0.0: {} run-parallel@1.2.0: @@ -7345,6 +8480,10 @@ snapshots: dependencies: js-tokens: 9.0.0 + strip-literal@2.1.1: + dependencies: + js-tokens: 9.0.1 + sucrase@3.35.0: dependencies: '@jridgewell/gen-mapping': 0.3.5 @@ -7425,6 +8564,11 @@ snapshots: tinyexec@0.3.1: {} + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -7501,6 +8645,25 @@ snapshots: - rollup - webpack-sources + unimport@3.14.6(rollup@4.46.2): + dependencies: + '@rollup/pluginutils': 5.2.0(rollup@4.46.2) + acorn: 8.14.0 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + fast-glob: 3.3.3 + local-pkg: 1.1.1 + magic-string: 0.30.17 + mlly: 1.7.4 + pathe: 2.0.3 + picomatch: 4.0.2 + pkg-types: 1.3.1 + scule: 1.3.0 + strip-literal: 2.1.1 + unplugin: 1.16.1 + transitivePeerDependencies: + - rollup + unist-util-is@6.0.0: dependencies: '@types/unist': 3.0.3 @@ -7540,6 +8703,21 @@ snapshots: - rollup - webpack-sources + unplugin-auto-import@0.18.6(@vueuse/core@11.2.0(vue@3.5.18(typescript@5.6.3)))(rollup@4.46.2): + dependencies: + '@antfu/utils': 0.7.10 + '@rollup/pluginutils': 5.1.3(rollup@4.46.2) + fast-glob: 3.3.2 + local-pkg: 0.5.1 + magic-string: 0.30.17 + minimatch: 9.0.5 + unimport: 3.14.6(rollup@4.46.2) + unplugin: 1.16.1 + optionalDependencies: + '@vueuse/core': 11.2.0(vue@3.5.18(typescript@5.6.3)) + transitivePeerDependencies: + - rollup + unplugin-combine@1.0.3(esbuild@0.23.1)(rollup@4.24.3)(vite@5.4.10(@types/node@22.8.6)): dependencies: '@antfu/utils': 0.7.10 @@ -7571,6 +8749,26 @@ snapshots: - supports-color - webpack-sources + unplugin-vue-components@0.27.4(@babel/parser@7.28.0)(rollup@4.46.2)(vue@3.5.18(typescript@5.6.3)): + dependencies: + '@antfu/utils': 0.7.10 + '@rollup/pluginutils': 5.1.3(rollup@4.46.2) + chokidar: 3.6.0 + debug: 4.3.7 + fast-glob: 3.3.2 + local-pkg: 0.5.0 + magic-string: 0.30.12 + minimatch: 9.0.5 + mlly: 1.7.2 + unplugin: 1.15.0 + vue: 3.5.18(typescript@5.6.3) + optionalDependencies: + '@babel/parser': 7.28.0 + transitivePeerDependencies: + - rollup + - supports-color + - webpack-sources + unplugin-vue-define-options@1.5.2(rollup@4.24.3)(vue@3.5.12(typescript@5.6.3)): dependencies: '@vue-macros/common': 1.15.0(rollup@4.24.3)(vue@3.5.12(typescript@5.6.3)) @@ -7671,6 +8869,11 @@ snapshots: acorn: 8.14.0 webpack-virtual-modules: 0.6.2 + unplugin@1.16.1: + dependencies: + acorn: 8.14.0 + webpack-virtual-modules: 0.6.2 + update-browserslist-db@1.1.1(browserslist@4.24.2): dependencies: browserslist: 4.24.2 @@ -7787,12 +8990,30 @@ snapshots: '@types/node': 22.8.6 fsevents: 2.3.3 + vite@7.1.2(jiti@2.4.0)(tsx@4.19.2)(yaml@2.6.0): + dependencies: + esbuild: 0.25.9 + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.46.2 + tinyglobby: 0.2.14 + optionalDependencies: + fsevents: 2.3.3 + jiti: 2.4.0 + tsx: 4.19.2 + yaml: 2.6.0 + vscode-uri@3.0.8: {} vue-demi@0.14.10(vue@3.5.12(typescript@5.6.3)): dependencies: vue: 3.5.12(typescript@5.6.3) + vue-demi@0.14.10(vue@3.5.18(typescript@5.6.3)): + dependencies: + vue: 3.5.18(typescript@5.6.3) + vue-eslint-parser@9.4.3(eslint@9.14.0(jiti@2.4.0)): dependencies: debug: 4.3.7 @@ -7829,6 +9050,11 @@ snapshots: '@vue/devtools-api': 6.6.4 vue: 3.5.12(typescript@5.6.3) + vue-router@4.4.5(vue@3.5.18(typescript@5.6.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.18(typescript@5.6.3) + vue-tsc@2.1.10(typescript@5.6.3): dependencies: '@volar/typescript': 2.4.8 @@ -7846,6 +9072,16 @@ snapshots: optionalDependencies: typescript: 5.6.3 + vue@3.5.18(typescript@5.6.3): + dependencies: + '@vue/compiler-dom': 3.5.18 + '@vue/compiler-sfc': 3.5.18 + '@vue/runtime-dom': 3.5.18 + '@vue/server-renderer': 3.5.18(vue@3.5.18(typescript@5.6.3)) + '@vue/shared': 3.5.18 + optionalDependencies: + typescript: 5.6.3 + webpack-virtual-modules@0.6.2: {} which@2.0.2: