diff --git a/Cargo.lock b/Cargo.lock index 38ff26a..04d1761 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,18 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -315,6 +327,7 @@ checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", "axum-core", + "axum-macros", "bytes", "futures-util", "http 1.4.0", @@ -335,7 +348,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tower", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -362,6 +375,47 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04" +dependencies = [ + "axum", + "axum-core", + "bytes", + "fastrand", + "futures-util", + "headers", + "http 1.4.0", + "http-body", + "http-body-util", + "mime", + "multer", + "pin-project-lite", + "serde", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "base32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + [[package]] name = "base64" version = "0.21.7" @@ -410,6 +464,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -654,6 +717,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.4.0" @@ -896,6 +965,12 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + [[package]] name = "der" version = "0.7.10" @@ -1168,6 +1243,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.1" @@ -1894,6 +1978,30 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64 0.22.1", + "bytes", + "headers-core", + "http 1.4.0", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http 1.4.0", +] + [[package]] name = "heck" version = "0.4.1" @@ -2433,6 +2541,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 = "keyboard-types" version = "0.7.0" @@ -2625,6 +2748,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "matches" version = "0.1.10" @@ -2716,6 +2848,23 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.4.0", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + [[package]] name = "native-tls" version = "0.2.18" @@ -2785,6 +2934,25 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -3120,6 +3288,17 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -3132,6 +3311,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -3334,6 +3523,26 @@ dependencies = [ "siphasher 1.0.2", ] +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -3860,7 +4069,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-util", - "tower", + "tower 0.5.3", "tower-http 0.6.8", "tower-service", "url", @@ -3895,7 +4104,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-util", - "tower", + "tower 0.5.3", "tower-http 0.6.8", "tower-service", "url", @@ -4399,6 +4608,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -4431,6 +4649,18 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.18", + "time", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -5261,6 +5491,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.47" @@ -5505,6 +5744,34 @@ version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" +[[package]] +name = "totp-rs" +version = "5.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b36a9dd327e9f401320a2cb4572cc76ff43742bcfc3291f871691050f140ba" +dependencies = [ + "base32", + "constant_time_eq", + "hmac", + "sha1", + "sha2", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.3" @@ -5535,6 +5802,7 @@ dependencies = [ "pin-project-lite", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -5550,7 +5818,7 @@ dependencies = [ "http-body", "iri-string", "pin-project-lite", - "tower", + "tower 0.5.3", "tower-layer", "tower-service", ] @@ -5597,6 +5865,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -5814,6 +6112,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -7124,6 +7428,43 @@ dependencies = [ "zclaw-types", ] +[[package]] +name = "zclaw-saas" +version = "0.1.0" +dependencies = [ + "anyhow", + "argon2", + "axum", + "axum-extra", + "chrono", + "dashmap", + "data-encoding", + "futures", + "hex", + "jsonwebtoken", + "libsqlite3-sys", + "rand 0.8.5", + "reqwest 0.12.28", + "secrecy", + "serde", + "serde_json", + "sha2", + "sqlx", + "tempfile", + "thiserror 2.0.18", + "tokio", + "toml 0.8.2", + "totp-rs", + "tower 0.4.13", + "tower-http 0.5.2", + "tracing", + "tracing-subscriber", + "url", + "urlencoding", + "uuid", + "zclaw-types", +] + [[package]] name = "zclaw-skills" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index d39920c..833b7e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ members = [ "crates/zclaw-growth", # Desktop Application "desktop/src-tauri", + # SaaS Backend + "crates/zclaw-saas", ] [workspace.package] @@ -95,6 +97,16 @@ shlex = "1" # Testing tempfile = "3" +# SaaS dependencies +axum = { version = "0.7", features = ["macros"] } +axum-extra = { version = "0.9", features = ["typed-header"] } +tower = { version = "0.4", features = ["util"] } +tower-http = { version = "0.5", features = ["cors", "trace", "limit"] } +jsonwebtoken = "9" +argon2 = "0.5" +totp-rs = "5" +hex = "0.4" + # Internal crates zclaw-types = { path = "crates/zclaw-types" } zclaw-memory = { path = "crates/zclaw-memory" } @@ -106,6 +118,7 @@ zclaw-channels = { path = "crates/zclaw-channels" } zclaw-protocols = { path = "crates/zclaw-protocols" } zclaw-pipeline = { path = "crates/zclaw-pipeline" } zclaw-growth = { path = "crates/zclaw-growth" } +zclaw-saas = { path = "crates/zclaw-saas" } [profile.release] lto = true diff --git a/admin/.gitignore b/admin/.gitignore new file mode 100644 index 0000000..5b3ad33 --- /dev/null +++ b/admin/.gitignore @@ -0,0 +1,2 @@ +.next/ +node_modules/ diff --git a/admin/next-env.d.ts b/admin/next-env.d.ts new file mode 100644 index 0000000..40c3d68 --- /dev/null +++ b/admin/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/admin/next.config.js b/admin/next.config.js new file mode 100644 index 0000000..767719f --- /dev/null +++ b/admin/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/admin/package.json b/admin/package.json new file mode 100644 index 0000000..c33c6ed --- /dev/null +++ b/admin/package.json @@ -0,0 +1,37 @@ +{ + "name": "zclaw-admin", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-tooltip": "^1.2.7", + "@radix-ui/react-separator": "^1.1.7", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.484.0", + "next": "14.2.29", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "recharts": "^2.15.3", + "tailwind-merge": "^3.0.2" + }, + "devDependencies": { + "@types/node": "^20.17.19", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.3" + }, + "packageManager": "pnpm@10.30.2" +} diff --git a/admin/pnpm-lock.yaml b/admin/pnpm-lock.yaml new file mode 100644 index 0000000..2f8b4ef --- /dev/null +++ b/admin/pnpm-lock.yaml @@ -0,0 +1,2171 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@radix-ui/react-dialog': + specifier: ^1.1.14 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: ^2.2.5 + version: 2.2.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-separator': + specifier: ^1.1.7 + version: 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-switch': + specifier: ^1.2.5 + version: 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tabs': + specifier: ^1.1.12 + version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': + specifier: ^1.2.7 + version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + lucide-react: + specifier: ^0.484.0 + version: 0.484.0(react@18.3.1) + next: + specifier: 14.2.29 + version: 14.2.29(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + recharts: + specifier: ^2.15.3 + version: 2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwind-merge: + specifier: ^3.0.2 + version: 3.5.0 + devDependencies: + '@types/node': + specifier: ^20.17.19 + version: 20.19.37 + '@types/react': + specifier: ^18.3.18 + version: 18.3.28 + '@types/react-dom': + specifier: ^18.3.5 + version: 18.3.7(@types/react@18.3.28) + autoprefixer: + specifier: ^10.4.20 + version: 10.4.27(postcss@8.5.8) + postcss: + specifier: ^8.5.3 + version: 8.5.8 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.19 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@next/env@14.2.29': + resolution: {integrity: sha512-UzgLR2eBfhKIQt0aJ7PWH7XRPYw7SXz0Fpzdl5THjUnvxy4kfBk9OU4RNPNiETewEEtaBcExNFNn1QWH8wQTjg==} + + '@next/swc-darwin-arm64@14.2.29': + resolution: {integrity: sha512-wWtrAaxCVMejxPHFb1SK/PVV1WDIrXGs9ki0C/kUM8ubKHQm+3hU9MouUywCw8Wbhj3pewfHT2wjunLEr/TaLA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@14.2.29': + resolution: {integrity: sha512-7Z/jk+6EVBj4pNLw/JQrvZVrAh9Bv8q81zCFSfvTMZ51WySyEHWVpwCEaJY910LyBftv2F37kuDPQm0w9CEXyg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@14.2.29': + resolution: {integrity: sha512-o6hrz5xRBwi+G7JFTHc+RUsXo2lVXEfwh4/qsuWBMQq6aut+0w98WEnoNwAwt7hkEqegzvazf81dNiwo7KjITw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@14.2.29': + resolution: {integrity: sha512-9i+JEHBOVgqxQ92HHRFlSW1EQXqa/89IVjtHgOqsShCcB/ZBjTtkWGi+SGCJaYyWkr/lzu51NTMCfKuBf7ULNw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@14.2.29': + resolution: {integrity: sha512-B7JtMbkUwHijrGBOhgSQu2ncbCYq9E7PZ7MX58kxheiEOwdkM+jGx0cBb+rN5AeqF96JypEppK6i/bEL9T13lA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@14.2.29': + resolution: {integrity: sha512-yCcZo1OrO3aQ38B5zctqKU1Z3klOohIxug6qdiKO3Q3qNye/1n6XIs01YJ+Uf+TdpZQ0fNrOQI2HrTLF3Zprnw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@14.2.29': + resolution: {integrity: sha512-WnrfeOEtTVidI9Z6jDLy+gxrpDcEJtZva54LYC0bSKQqmyuHzl0ego+v0F/v2aXq0am67BRqo/ybmmt45Tzo4A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-ia32-msvc@14.2.29': + resolution: {integrity: sha512-vkcriFROT4wsTdSeIzbxaZjTNTFKjSYmLd8q/GVH3Dn8JmYjUKOuKXHK8n+lovW/kdcpIvydO5GtN+It2CvKWA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@next/swc-win32-x64-msvc@14.2.29': + resolution: {integrity: sha512-iPPwUEKnVs7pwR0EBLJlwxLD7TTHWS/AoVZx1l9ZQzfQciqaFEr5AlYzA2uB6Fyby1IF18t4PL0nTpB+k4Tzlw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.4': + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.8': + resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.5': + resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/node@20.19.37': + resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.28': + resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + autoprefixer@10.4.27: + resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + baseline-browser-mapping@2.10.11: + resolution: {integrity: sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==} + engines: {node: '>=6.0.0'} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001781: + resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + electron-to-chromium@1.5.327: + resolution: {integrity: sha512-hLxLdIJDf8zIzKoH2TPCs+Botc+wUmj9sp4jVMwklY/sKleM8xxxOExRX3Gxj73nCXmJe3anhG7SvsDDPDvmuQ==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + fast-equals@5.4.0: + resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} + engines: {node: '>=6.0.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lucide-react@0.484.0: + resolution: {integrity: sha512-oZy8coK9kZzvqhSgfbGkPtTgyjpBvs3ukLgDPv14dSOZtBtboryWF5o8i3qen7QbGg7JhiJBz5mK1p8YoMZTLQ==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + 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 + + next@14.2.29: + resolution: {integrity: sha512-s98mCOMOWLGGpGOfgKSnleXLuegvvH415qtRZXpSp00HeEgdmrxmwL9cgKU+h4XrhB16zEI5d/7BnkS3ATInsA==} + engines: {node: '>=18.17.0'} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details. + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + sass: + optional: true + + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.4: + resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + styled-jsx@5.1.1: + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@babel/runtime@7.29.2': {} + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/utils@0.2.11': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@next/env@14.2.29': {} + + '@next/swc-darwin-arm64@14.2.29': + optional: true + + '@next/swc-darwin-x64@14.2.29': + optional: true + + '@next/swc-linux-arm64-gnu@14.2.29': + optional: true + + '@next/swc-linux-arm64-musl@14.2.29': + optional: true + + '@next/swc-linux-x64-gnu@14.2.29': + optional: true + + '@next/swc-linux-x64-musl@14.2.29': + optional: true + + '@next/swc-win32-arm64-msvc@14.2.29': + optional: true + + '@next/swc-win32-ia32-msvc@14.2.29': + optional: true + + '@next/swc-win32-x64-msvc@14.2.29': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-context@1.1.2(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.28)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-direction@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-id@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-primitive@2.1.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.4(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-select@2.2.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.28)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-separator@1.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-slot@1.2.3(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-slot@1.2.4(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-size@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/rect@1.1.1': {} + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.5': + dependencies: + '@swc/counter': 0.1.3 + tslib: 2.8.1 + + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/node@20.19.37': + dependencies: + undici-types: 6.21.0 + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + + '@types/react@18.3.28': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + arg@5.0.2: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + autoprefixer@10.4.27(postcss@8.5.8): + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001781 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.8 + postcss-value-parser: 4.2.0 + + baseline-browser-mapping@2.10.11: {} + + binary-extensions@2.3.0: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.11 + caniuse-lite: 1.0.30001781 + electron-to-chromium: 1.5.327 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001781: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + client-only@0.0.1: {} + + clsx@2.1.1: {} + + commander@4.1.1: {} + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + decimal.js-light@2.5.1: {} + + detect-node-es@1.1.0: {} + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.29.2 + csstype: 3.2.3 + + electron-to-chromium@1.5.327: {} + + escalade@3.2.0: {} + + eventemitter3@4.0.7: {} + + fast-equals@5.4.0: {} + + 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 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + fraction.js@5.3.4: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-nonce@1.0.1: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + graceful-fs@4.2.11: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + internmap@2.0.3: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + jiti@1.21.7: {} + + js-tokens@4.0.0: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + lodash@4.17.23: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lucide-react@0.484.0(react@18.3.1): + dependencies: + react: 18.3.1 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + next@14.2.29(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@next/env': 14.2.29 + '@swc/helpers': 0.5.5 + busboy: 1.6.0 + caniuse-lite: 1.0.30001781 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.1(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 14.2.29 + '@next/swc-darwin-x64': 14.2.29 + '@next/swc-linux-arm64-gnu': 14.2.29 + '@next/swc-linux-arm64-musl': 14.2.29 + '@next/swc-linux-x64-gnu': 14.2.29 + '@next/swc-linux-x64-musl': 14.2.29 + '@next/swc-win32-arm64-msvc': 14.2.29 + '@next/swc-win32-ia32-msvc': 14.2.29 + '@next/swc-win32-x64-msvc': 14.2.29 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-releases@2.0.36: {} + + normalize-path@3.0.0: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + path-parse@1.0.7: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pify@2.3.0: {} + + pirates@4.0.7: {} + + postcss-import@15.1.0(postcss@8.5.8): + dependencies: + postcss: 8.5.8 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-js@4.1.0(postcss@8.5.8): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.8 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.8): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.8 + + postcss-nested@6.2.0(postcss@8.5.8): + dependencies: + postcss: 8.5.8 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + queue-microtask@1.2.3: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-is@16.13.1: {} + + react-is@18.3.1: {} + + react-remove-scroll-bar@2.3.8(@types/react@18.3.28)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.3.28)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.28 + + react-remove-scroll@2.7.2(@types/react@18.3.28)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.28)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.28)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.28)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.28)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + + react-smooth@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + fast-equals: 5.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + react-style-singleton@2.2.3(@types/react@18.3.28)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.28 + + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.29.2 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.23 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + source-map-js@1.2.1: {} + + streamsearch@1.1.0: {} + + styled-jsx@5.1.1(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwind-merge@3.5.0: {} + + tailwindcss@3.4.19: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.8 + postcss-import: 15.1.0(postcss@8.5.8) + postcss-js: 4.1.0(postcss@8.5.8) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8) + postcss-nested: 6.2.0(postcss@8.5.8) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tiny-invariant@1.3.3: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-interface-checker@0.1.13: {} + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-callback-ref@1.3.3(@types/react@18.3.28)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.28 + + use-sidecar@1.1.3(@types/react@18.3.28)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.28 + + util-deprecate@1.0.2: {} + + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 diff --git a/admin/postcss.config.js b/admin/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/admin/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/admin/src/app/(dashboard)/accounts/page.tsx b/admin/src/app/(dashboard)/accounts/page.tsx new file mode 100644 index 0000000..91e1a01 --- /dev/null +++ b/admin/src/app/(dashboard)/accounts/page.tsx @@ -0,0 +1,393 @@ +'use client' + +import { useEffect, useState, useCallback } from 'react' +import { + Search, + Plus, + Loader2, + ChevronLeft, + ChevronRight, + Pencil, + Ban, + CheckCircle2, +} from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Badge } from '@/components/ui/badge' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from '@/components/ui/dialog' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { api } from '@/lib/api-client' +import { ApiRequestError } from '@/lib/api-client' +import { formatDate } from '@/lib/utils' +import type { AccountPublic } from '@/lib/types' + +const PAGE_SIZE = 20 + +const roleLabels: Record = { + super_admin: '超级管理员', + admin: '管理员', + user: '普通用户', +} + +const statusColors: Record = { + active: 'success', + disabled: 'destructive', + suspended: 'warning', +} + +const statusLabels: Record = { + active: '正常', + disabled: '已禁用', + suspended: '已暂停', +} + +export default function AccountsPage() { + const [accounts, setAccounts] = useState([]) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) + const [search, setSearch] = useState('') + const [roleFilter, setRoleFilter] = useState('all') + const [statusFilter, setStatusFilter] = useState('all') + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + + // 编辑 Dialog + const [editTarget, setEditTarget] = useState(null) + const [editForm, setEditForm] = useState({ display_name: '', email: '', role: 'user' }) + const [editSaving, setEditSaving] = useState(false) + + // 确认 Dialog + const [confirmTarget, setConfirmTarget] = useState<{ id: string; action: string; status: string } | null>(null) + const [confirmSaving, setConfirmSaving] = useState(false) + + const fetchAccounts = useCallback(async () => { + setLoading(true) + setError('') + try { + const params: Record = { page, page_size: PAGE_SIZE } + if (search.trim()) params.search = search.trim() + if (roleFilter !== 'all') params.role = roleFilter + if (statusFilter !== 'all') params.status = statusFilter + + const res = await api.accounts.list(params) + setAccounts(res.items) + setTotal(res.total) + } catch (err) { + if (err instanceof ApiRequestError) { + setError(err.body.message) + } else { + setError('加载失败') + } + } finally { + setLoading(false) + } + }, [page, search, roleFilter, statusFilter]) + + useEffect(() => { + fetchAccounts() + }, [fetchAccounts]) + + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) + + function openEditDialog(account: AccountPublic) { + setEditTarget(account) + setEditForm({ + display_name: account.display_name, + email: account.email, + role: account.role, + }) + } + + async function handleEditSave() { + if (!editTarget) return + setEditSaving(true) + try { + await api.accounts.update(editTarget.id, { + display_name: editForm.display_name, + email: editForm.email, + role: editForm.role as AccountPublic['role'], + }) + setEditTarget(null) + fetchAccounts() + } catch (err) { + if (err instanceof ApiRequestError) { + setError(err.body.message) + } + } finally { + setEditSaving(false) + } + } + + function openConfirmDialog(account: AccountPublic) { + const newStatus = account.status === 'active' ? 'disabled' : 'active' + setConfirmTarget({ + id: account.id, + action: newStatus === 'disabled' ? '禁用' : '启用', + status: newStatus, + }) + } + + async function handleConfirmSave() { + if (!confirmTarget) return + setConfirmSaving(true) + try { + await api.accounts.updateStatus(confirmTarget.id, { + status: confirmTarget.status as AccountPublic['status'], + }) + setConfirmTarget(null) + fetchAccounts() + } catch (err) { + if (err instanceof ApiRequestError) { + setError(err.body.message) + } + } finally { + setConfirmSaving(false) + } + } + + return ( +
+ {/* 搜索和筛选 */} +
+
+ + { setSearch(e.target.value); setPage(1) }} + className="pl-10" + /> +
+ + +
+ + {/* 错误提示 */} + {error && ( +
+ {error} + +
+ )} + + {/* 表格 */} + {loading ? ( +
+ +
+ ) : accounts.length === 0 ? ( +
+ 暂无数据 +
+ ) : ( + <> + + + + 用户名 + 邮箱 + 显示名 + 角色 + 状态 + 创建时间 + 操作 + + + + {accounts.map((account) => ( + + {account.username} + {account.email} + {account.display_name || '-'} + + + {roleLabels[account.role] || account.role} + + + + + + {statusLabels[account.status] || account.status} + + + + {formatDate(account.created_at)} + + +
+ + +
+
+
+ ))} +
+
+ + {/* 分页 */} +
+

+ 第 {page} 页 / 共 {totalPages} 页 ({total} 条) +

+
+ + +
+
+ + )} + + {/* 编辑 Dialog */} + setEditTarget(null)}> + + + 编辑账号 + 修改账号信息 + +
+
+ + setEditForm({ ...editForm, display_name: e.target.value })} + /> +
+
+ + setEditForm({ ...editForm, email: e.target.value })} + /> +
+
+ + +
+
+ + + + +
+
+ + {/* 确认 Dialog */} + setConfirmTarget(null)}> + + + 确认{confirmTarget?.action} + + 确定要{confirmTarget?.action}该账号吗?此操作将立即生效。 + + + + + + + + +
+ ) +} diff --git a/admin/src/app/(dashboard)/api-keys/page.tsx b/admin/src/app/(dashboard)/api-keys/page.tsx new file mode 100644 index 0000000..4da1e61 --- /dev/null +++ b/admin/src/app/(dashboard)/api-keys/page.tsx @@ -0,0 +1,351 @@ +'use client' + +import { useEffect, useState, useCallback } from 'react' +import { + Plus, + Loader2, + ChevronLeft, + ChevronRight, + Trash2, + Copy, + Check, + AlertTriangle, +} from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Badge } from '@/components/ui/badge' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from '@/components/ui/dialog' +import { api } from '@/lib/api-client' +import { ApiRequestError } from '@/lib/api-client' +import { formatDate } from '@/lib/utils' +import type { TokenInfo } from '@/lib/types' + +const PAGE_SIZE = 20 + +const allPermissions = [ + { key: 'chat', label: '对话' }, + { key: 'relay', label: '中转' }, + { key: 'admin', label: '管理' }, +] + +export default function ApiKeysPage() { + const [tokens, setTokens] = useState([]) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + + // 创建 Dialog + const [createOpen, setCreateOpen] = useState(false) + const [createForm, setCreateForm] = useState({ name: '', expires_days: '', permissions: ['chat'] as string[] }) + const [creating, setCreating] = useState(false) + + // 创建成功显示 token + const [createdToken, setCreatedToken] = useState(null) + const [copied, setCopied] = useState(false) + + // 撤销确认 + const [revokeTarget, setRevokeTarget] = useState(null) + const [revoking, setRevoking] = useState(false) + + const fetchTokens = useCallback(async () => { + setLoading(true) + setError('') + try { + const res = await api.tokens.list({ page, page_size: PAGE_SIZE }) + setTokens(res.items) + setTotal(res.total) + } catch (err) { + if (err instanceof ApiRequestError) setError(err.body.message) + else setError('加载失败') + } finally { + setLoading(false) + } + }, [page]) + + useEffect(() => { + fetchTokens() + }, [fetchTokens]) + + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) + + function togglePermission(perm: string) { + setCreateForm((prev) => ({ + ...prev, + permissions: prev.permissions.includes(perm) + ? prev.permissions.filter((p) => p !== perm) + : [...prev.permissions, perm], + })) + } + + async function handleCreate() { + if (!createForm.name.trim() || createForm.permissions.length === 0) return + setCreating(true) + try { + const payload = { + name: createForm.name.trim(), + expires_days: createForm.expires_days ? parseInt(createForm.expires_days, 10) : undefined, + permissions: createForm.permissions, + } + const res = await api.tokens.create(payload) + setCreateOpen(false) + setCreatedToken(res) + setCreateForm({ name: '', expires_days: '', permissions: ['chat'] }) + fetchTokens() + } catch (err) { + if (err instanceof ApiRequestError) setError(err.body.message) + } finally { + setCreating(false) + } + } + + async function handleRevoke() { + if (!revokeTarget) return + setRevoking(true) + try { + await api.tokens.revoke(revokeTarget.id) + setRevokeTarget(null) + fetchTokens() + } catch (err) { + if (err instanceof ApiRequestError) setError(err.body.message) + } finally { + setRevoking(false) + } + } + + async function copyToken() { + if (!createdToken?.token) return + try { + await navigator.clipboard.writeText(createdToken.token) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch { + // Fallback + const textarea = document.createElement('textarea') + textarea.value = createdToken.token + document.body.appendChild(textarea) + textarea.select() + document.execCommand('copy') + document.body.removeChild(textarea) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + } + + return ( +
+
+
+ +
+ + {error && ( +
+ {error} + +
+ )} + + {loading ? ( +
+ +
+ ) : tokens.length === 0 ? ( +
+ 暂无数据 +
+ ) : ( + <> + + + + 名称 + 前缀 + 权限 + 最后使用 + 过期时间 + 创建时间 + 操作 + + + + {tokens.map((t) => ( + + {t.name} + + {t.token_prefix}... + + +
+ {t.permissions.map((p) => ( + + {p} + + ))} +
+
+ + {t.last_used_at ? formatDate(t.last_used_at) : '未使用'} + + + {t.expires_at ? formatDate(t.expires_at) : '永不过期'} + + + {formatDate(t.created_at)} + + + + +
+ ))} +
+
+ +
+

+ 第 {page} 页 / 共 {totalPages} 页 ({total} 条) +

+
+ + +
+
+ + )} + + {/* 创建 Dialog */} + + + + 新建 API 密钥 + 创建新的 API 密钥用于接口调用 + +
+
+ + setCreateForm({ ...createForm, name: e.target.value })} + placeholder="例如: 生产环境" + /> +
+
+ + setCreateForm({ ...createForm, expires_days: e.target.value })} + placeholder="365" + /> +
+
+ +
+ {allPermissions.map((perm) => ( + + ))} +
+
+
+ + + + +
+
+ + {/* 创建成功 Dialog */} + setCreatedToken(null)}> + + + + + 密钥已创建 + + + 请立即复制并安全保存此密钥,关闭后将无法再次查看完整密钥。 + + +
+
+

完整密钥

+

+ {createdToken?.token} +

+
+
+ 此密钥仅显示一次。请确保已保存到安全的位置。 +
+
+ + + + +
+
+ + {/* 撤销确认 */} + setRevokeTarget(null)}> + + + 确认撤销 + + 确定要撤销密钥 "{revokeTarget?.name}" 吗?使用此密钥的应用将立即失去访问权限。 + + + + + + + + +
+ ) +} diff --git a/admin/src/app/(dashboard)/config/page.tsx b/admin/src/app/(dashboard)/config/page.tsx new file mode 100644 index 0000000..204d257 --- /dev/null +++ b/admin/src/app/(dashboard)/config/page.tsx @@ -0,0 +1,270 @@ +'use client' + +import { useEffect, useState, useCallback } from 'react' +import { + Loader2, + Pencil, + RotateCcw, +} from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Badge } from '@/components/ui/badge' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from '@/components/ui/dialog' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { api } from '@/lib/api-client' +import { ApiRequestError } from '@/lib/api-client' +import type { ConfigItem } from '@/lib/types' + +const sourceLabels: Record = { + default: '默认值', + env: '环境变量', + db: '数据库', +} + +const sourceVariants: Record = { + default: 'secondary', + env: 'info', + db: 'default', +} + +export default function ConfigPage() { + const [configs, setConfigs] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + const [activeTab, setActiveTab] = useState('all') + + // 编辑 Dialog + const [editTarget, setEditTarget] = useState(null) + const [editValue, setEditValue] = useState('') + const [saving, setSaving] = useState(false) + + const fetchConfigs = useCallback(async (category?: string) => { + setLoading(true) + setError('') + try { + const params: Record = {} + if (category && category !== 'all') params.category = category + const res = await api.config.list(params) + setConfigs(res) + } catch (err) { + if (err instanceof ApiRequestError) setError(err.body.message) + else setError('加载失败') + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + fetchConfigs(activeTab) + }, [fetchConfigs, activeTab]) + + function openEditDialog(config: ConfigItem) { + setEditTarget(config) + setEditValue(config.current_value !== undefined ? String(config.current_value) : '') + } + + async function handleSave() { + if (!editTarget) return + setSaving(true) + try { + let parsedValue: string | number | boolean = editValue + if (editTarget.value_type === 'number') { + parsedValue = parseFloat(editValue) || 0 + } else if (editTarget.value_type === 'boolean') { + parsedValue = editValue === 'true' + } + await api.config.update(editTarget.id, { value: parsedValue }) + setEditTarget(null) + fetchConfigs(activeTab) + } catch (err) { + if (err instanceof ApiRequestError) setError(err.body.message) + } finally { + setSaving(false) + } + } + + function formatValue(value: unknown): string { + if (value === undefined || value === null) return '-' + if (typeof value === 'boolean') return value ? 'true' : 'false' + return String(value) + } + + const categories = ['all', 'auth', 'relay', 'model', 'system'] + + return ( +
+ {/* 分类 Tabs */} + + + {categories.map((cat) => ( + + {cat === 'all' ? '全部' : cat} + + ))} + + + + {error && ( +
+ {error} + +
+ )} + + {loading ? ( +
+ +
+ ) : configs.length === 0 ? ( +
+ 暂无配置项 +
+ ) : ( + + + + 分类 + Key + 当前值 + 默认值 + 来源 + 需重启 + 描述 + 操作 + + + + {configs.map((config) => ( + + + {config.category} + + {config.key_path} + + {formatValue(config.current_value)} + + + {formatValue(config.default_value)} + + + + {sourceLabels[config.source] || config.source} + + + + {config.requires_restart ? ( + + ) : ( + + )} + + + {config.description || '-'} + + + + + + ))} + +
+ )} + + {/* 编辑 Dialog */} + setEditTarget(null)}> + + + 编辑配置 + + 修改 {editTarget?.key_path} 的值 + {editTarget?.requires_restart && ( + + 注意: 修改此配置需要重启服务才能生效 + + )} + + +
+
+ + +
+
+ + +
+
+ + {editTarget?.value_type === 'boolean' ? ( + + ) : ( + setEditValue(e.target.value)} + /> + )} +
+
+ + + + + +
+
+
+ ) +} diff --git a/admin/src/app/(dashboard)/layout.tsx b/admin/src/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..5c847c6 --- /dev/null +++ b/admin/src/app/(dashboard)/layout.tsx @@ -0,0 +1,218 @@ +'use client' + +import { useState, type ReactNode } from 'react' +import Link from 'next/link' +import { usePathname, useRouter } from 'next/navigation' +import { + LayoutDashboard, + Users, + Server, + Cpu, + Key, + BarChart3, + ArrowLeftRight, + Settings, + FileText, + LogOut, + ChevronLeft, + Menu, + Bell, +} from 'lucide-react' +import { AuthGuard, useAuth } from '@/components/auth-guard' +import { logout } from '@/lib/auth' +import { cn } from '@/lib/utils' + +const navItems = [ + { href: '/', label: '仪表盘', icon: LayoutDashboard }, + { href: '/accounts', label: '账号管理', icon: Users }, + { href: '/providers', label: '服务商', icon: Server }, + { href: '/models', label: '模型管理', icon: Cpu }, + { href: '/api-keys', label: 'API 密钥', icon: Key }, + { href: '/usage', label: '用量统计', icon: BarChart3 }, + { href: '/relay', label: '中转任务', icon: ArrowLeftRight }, + { href: '/config', label: '系统配置', icon: Settings }, + { href: '/logs', label: '操作日志', icon: FileText }, +] + +function Sidebar({ + collapsed, + onToggle, +}: { + collapsed: boolean + onToggle: () => void +}) { + const pathname = usePathname() + const router = useRouter() + const { account } = useAuth() + + function handleLogout() { + logout() + router.replace('/login') + } + + return ( + + ) +} + +function Header() { + const pathname = usePathname() + const currentNav = navItems.find( + (item) => + item.href === '/' + ? pathname === '/' + : pathname.startsWith(item.href), + ) + + return ( +
+ {/* 移动端菜单按钮 */} + + + {/* 页面标题 */} +

+ {currentNav?.label || '仪表盘'} +

+ +
+ {/* 通知 */} + +
+
+ ) +} + +function MobileMenuButton() { + // Placeholder for mobile menu toggle + return ( + + ) +} + +export default function DashboardLayout({ children }: { children: ReactNode }) { + const [sidebarCollapsed, setSidebarCollapsed] = useState(false) + + return ( + +
+ setSidebarCollapsed(!sidebarCollapsed)} + /> +
+
+
+ {children} +
+
+
+
+ ) +} diff --git a/admin/src/app/(dashboard)/models/page.tsx b/admin/src/app/(dashboard)/models/page.tsx new file mode 100644 index 0000000..ee7e74d --- /dev/null +++ b/admin/src/app/(dashboard)/models/page.tsx @@ -0,0 +1,436 @@ +'use client' + +import { useEffect, useState, useCallback } from 'react' +import { + Plus, + Loader2, + ChevronLeft, + ChevronRight, + Pencil, + Trash2, +} from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Badge } from '@/components/ui/badge' +import { Switch } from '@/components/ui/switch' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from '@/components/ui/dialog' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { api } from '@/lib/api-client' +import { ApiRequestError } from '@/lib/api-client' +import { formatNumber } from '@/lib/utils' +import type { Model, Provider } from '@/lib/types' + +const PAGE_SIZE = 20 + +interface ModelForm { + provider_id: string + model_id: string + alias: string + context_window: string + max_output_tokens: string + supports_streaming: boolean + supports_vision: boolean + enabled: boolean + pricing_input: string + pricing_output: string +} + +const emptyForm: ModelForm = { + provider_id: '', + model_id: '', + alias: '', + context_window: '4096', + max_output_tokens: '4096', + supports_streaming: true, + supports_vision: false, + enabled: true, + pricing_input: '', + pricing_output: '', +} + +export default function ModelsPage() { + const [models, setModels] = useState([]) + const [providers, setProviders] = useState([]) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) + const [providerFilter, setProviderFilter] = useState('all') + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + + // Dialog + const [dialogOpen, setDialogOpen] = useState(false) + const [editTarget, setEditTarget] = useState(null) + const [form, setForm] = useState(emptyForm) + const [saving, setSaving] = useState(false) + + // 删除 + const [deleteTarget, setDeleteTarget] = useState(null) + const [deleting, setDeleting] = useState(false) + + const fetchModels = useCallback(async () => { + setLoading(true) + setError('') + try { + const params: Record = { page, page_size: PAGE_SIZE } + if (providerFilter !== 'all') params.provider_id = providerFilter + const res = await api.models.list(params) + setModels(res.items) + setTotal(res.total) + } catch (err) { + if (err instanceof ApiRequestError) setError(err.body.message) + else setError('加载失败') + } finally { + setLoading(false) + } + }, [page, providerFilter]) + + const fetchProviders = useCallback(async () => { + try { + const res = await api.providers.list({ page: 1, page_size: 100 }) + setProviders(res.items) + } catch { + // ignore + } + }, []) + + useEffect(() => { + fetchModels() + fetchProviders() + }, [fetchModels, fetchProviders]) + + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) + + const providerMap = new Map(providers.map((p) => [p.id, p.display_name || p.name])) + + function openCreateDialog() { + setEditTarget(null) + setForm(emptyForm) + setDialogOpen(true) + } + + function openEditDialog(model: Model) { + setEditTarget(model) + setForm({ + provider_id: model.provider_id, + model_id: model.model_id, + alias: model.alias, + context_window: model.context_window.toString(), + max_output_tokens: model.max_output_tokens.toString(), + supports_streaming: model.supports_streaming, + supports_vision: model.supports_vision, + enabled: model.enabled, + pricing_input: model.pricing_input.toString(), + pricing_output: model.pricing_output.toString(), + }) + setDialogOpen(true) + } + + async function handleSave() { + if (!form.model_id.trim() || !form.provider_id) return + setSaving(true) + try { + const payload = { + provider_id: form.provider_id, + model_id: form.model_id.trim(), + alias: form.alias.trim(), + context_window: parseInt(form.context_window, 10) || 4096, + max_output_tokens: parseInt(form.max_output_tokens, 10) || 4096, + supports_streaming: form.supports_streaming, + supports_vision: form.supports_vision, + enabled: form.enabled, + pricing_input: parseFloat(form.pricing_input) || 0, + pricing_output: parseFloat(form.pricing_output) || 0, + } + if (editTarget) { + await api.models.update(editTarget.id, payload) + } else { + await api.models.create(payload) + } + setDialogOpen(false) + fetchModels() + } catch (err) { + if (err instanceof ApiRequestError) setError(err.body.message) + } finally { + setSaving(false) + } + } + + async function handleDelete() { + if (!deleteTarget) return + setDeleting(true) + try { + await api.models.delete(deleteTarget.id) + setDeleteTarget(null) + fetchModels() + } catch (err) { + if (err instanceof ApiRequestError) setError(err.body.message) + } finally { + setDeleting(false) + } + } + + return ( +
+
+ + +
+ + {error && ( +
+ {error} + +
+ )} + + {loading ? ( +
+ +
+ ) : models.length === 0 ? ( +
+ 暂无数据 +
+ ) : ( + <> + + + + 模型 ID + 别名 + 服务商 + 上下文窗口 + 最大输出 + 流式 + 视觉 + 启用 + 操作 + + + + {models.map((m) => ( + + {m.model_id} + {m.alias || '-'} + + {providerMap.get(m.provider_id) || m.provider_id.slice(0, 8)} + + + {formatNumber(m.context_window)} + + + {formatNumber(m.max_output_tokens)} + + + + {m.supports_streaming ? '是' : '否'} + + + + + {m.supports_vision ? '是' : '否'} + + + + + {m.enabled ? '启用' : '禁用'} + + + +
+ + +
+
+
+ ))} +
+
+ +
+

+ 第 {page} 页 / 共 {totalPages} 页 ({total} 条) +

+
+ + +
+
+ + )} + + {/* 创建/编辑 Dialog */} + + + + {editTarget ? '编辑模型' : '新建模型'} + + {editTarget ? '修改模型配置' : '添加新的 AI 模型'} + + +
+
+ + +
+
+ + setForm({ ...form, model_id: e.target.value })} + placeholder="gpt-4o" + disabled={!!editTarget} + /> +
+
+ + setForm({ ...form, alias: e.target.value })} + placeholder="GPT-4o" + /> +
+
+
+ + setForm({ ...form, context_window: e.target.value })} + /> +
+
+ + setForm({ ...form, max_output_tokens: e.target.value })} + /> +
+
+
+
+ + setForm({ ...form, pricing_input: e.target.value })} + placeholder="0" + /> +
+
+ + setForm({ ...form, pricing_output: e.target.value })} + placeholder="0" + /> +
+
+
+
+ setForm({ ...form, supports_streaming: v })} /> + +
+
+ setForm({ ...form, supports_vision: v })} /> + +
+
+ setForm({ ...form, enabled: v })} /> + +
+
+
+ + + + +
+
+ + {/* 删除确认 */} + setDeleteTarget(null)}> + + + 确认删除 + + 确定要删除模型 "{deleteTarget?.alias || deleteTarget?.model_id}" 吗?此操作不可撤销。 + + + + + + + + +
+ ) +} diff --git a/admin/src/app/(dashboard)/page.tsx b/admin/src/app/(dashboard)/page.tsx new file mode 100644 index 0000000..cd52172 --- /dev/null +++ b/admin/src/app/(dashboard)/page.tsx @@ -0,0 +1,336 @@ +'use client' + +import { useEffect, useState } from 'react' +import { + Users, + Server, + ArrowLeftRight, + Zap, + Loader2, + TrendingUp, +} from 'lucide-react' +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + BarChart, + Bar, + Legend, +} from 'recharts' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { api } from '@/lib/api-client' +import { formatNumber, formatDate } from '@/lib/utils' +import type { + DashboardStats, + UsageRecord, + OperationLog, +} from '@/lib/types' + +interface StatCardProps { + title: string + value: string | number + icon: React.ReactNode + color: string + subtitle?: string +} + +function StatCard({ title, value, icon, color, subtitle }: StatCardProps) { + return ( + + +
+
+

{title}

+

{value}

+ {subtitle && ( +

{subtitle}

+ )} +
+
+ {icon} +
+
+
+
+ ) +} + +function StatusBadge({ status }: { status: string }) { + const variantMap: Record = { + active: 'success', + completed: 'success', + disabled: 'destructive', + failed: 'destructive', + processing: 'info', + queued: 'warning', + suspended: 'destructive', + } + return ( + {status} + ) +} + +export default function DashboardPage() { + const [stats, setStats] = useState(null) + const [usageData, setUsageData] = useState([]) + const [recentLogs, setRecentLogs] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + + useEffect(() => { + async function fetchData() { + try { + const [statsRes, usageRes, logsRes] = await Promise.allSettled([ + api.stats.dashboard(), + api.usage.daily({ days: 30 }), + api.logs.list({ page: 1, page_size: 5 }), + ]) + + if (statsRes.status === 'fulfilled') setStats(statsRes.value) + if (usageRes.status === 'fulfilled') setUsageData(usageRes.value) + if (logsRes.status === 'fulfilled') setRecentLogs(logsRes.value.items) + } catch (err) { + setError('加载数据失败,请检查后端服务是否启动') + } finally { + setLoading(false) + } + } + fetchData() + }, []) + + if (loading) { + return ( +
+
+ +

加载中...

+
+
+ ) + } + + if (error) { + return ( +
+
+

{error}

+ +
+
+ ) + } + + const chartData = usageData.map((r) => ({ + day: r.day.slice(5), // MM-DD + 请求量: r.count, + Input: r.input_tokens, + Output: r.output_tokens, + })) + + return ( +
+ {/* 统计卡片 */} +
+ } + color="bg-blue-500/10" + subtitle={`活跃 ${stats?.active_accounts ?? 0}`} + /> + } + color="bg-green-500/10" + subtitle={`模型 ${stats?.active_models ?? 0}`} + /> + } + color="bg-purple-500/10" + subtitle="中转任务" + /> + } + color="bg-orange-500/10" + subtitle={`In: ${formatNumber(stats?.tokens_today_input ?? 0)} / Out: ${formatNumber(stats?.tokens_today_output ?? 0)}`} + /> +
+ + {/* 图表 */} +
+ {/* 请求趋势 */} + + + + + 请求趋势 (30 天) + + + + {chartData.length > 0 ? ( + + + + + + + + + + + + + + + + ) : ( +
+ 暂无数据 +
+ )} +
+
+ + {/* Token 用量 */} + + + + + Token 用量 (30 天) + + + + {chartData.length > 0 ? ( + + + + + + + + + + + + ) : ( +
+ 暂无数据 +
+ )} +
+
+
+ + {/* 最近操作日志 */} + + + 最近操作 + + + {recentLogs.length > 0 ? ( + + + + 时间 + 账号 ID + 操作 + 目标类型 + 目标 ID + + + + {recentLogs.map((log) => ( + + + {formatDate(log.created_at)} + + + {log.account_id.slice(0, 8)}... + + + {log.action} + + + {log.target_type} + + + {log.target_id.slice(0, 8)}... + + + ))} + +
+ ) : ( +
+ 暂无操作日志 +
+ )} +
+
+
+ ) +} diff --git a/admin/src/app/(dashboard)/providers/page.tsx b/admin/src/app/(dashboard)/providers/page.tsx new file mode 100644 index 0000000..f2541f6 --- /dev/null +++ b/admin/src/app/(dashboard)/providers/page.tsx @@ -0,0 +1,386 @@ +'use client' + +import { useEffect, useState, useCallback } from 'react' +import { + Plus, + Loader2, + ChevronLeft, + ChevronRight, + Pencil, + Trash2, +} from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Badge } from '@/components/ui/badge' +import { Switch } from '@/components/ui/switch' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from '@/components/ui/dialog' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { api } from '@/lib/api-client' +import { ApiRequestError } from '@/lib/api-client' +import { formatDate, maskApiKey } from '@/lib/utils' +import type { Provider } from '@/lib/types' + +const PAGE_SIZE = 20 + +interface ProviderForm { + name: string + display_name: string + base_url: string + api_protocol: 'openai' | 'anthropic' + api_key: string + enabled: boolean + rate_limit_rpm: string + rate_limit_tpm: string +} + +const emptyForm: ProviderForm = { + name: '', + display_name: '', + base_url: '', + api_protocol: 'openai', + api_key: '', + enabled: true, + rate_limit_rpm: '', + rate_limit_tpm: '', +} + +export default function ProvidersPage() { + const [providers, setProviders] = useState([]) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + + // 创建/编辑 Dialog + const [dialogOpen, setDialogOpen] = useState(false) + const [editTarget, setEditTarget] = useState(null) + const [form, setForm] = useState(emptyForm) + const [saving, setSaving] = useState(false) + + // 删除确认 Dialog + const [deleteTarget, setDeleteTarget] = useState(null) + const [deleting, setDeleting] = useState(false) + + const fetchProviders = useCallback(async () => { + setLoading(true) + setError('') + try { + const res = await api.providers.list({ page, page_size: PAGE_SIZE }) + setProviders(res.items) + setTotal(res.total) + } catch (err) { + if (err instanceof ApiRequestError) setError(err.body.message) + else setError('加载失败') + } finally { + setLoading(false) + } + }, [page]) + + useEffect(() => { + fetchProviders() + }, [fetchProviders]) + + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) + + function openCreateDialog() { + setEditTarget(null) + setForm(emptyForm) + setDialogOpen(true) + } + + function openEditDialog(provider: Provider) { + setEditTarget(provider) + setForm({ + name: provider.name, + display_name: provider.display_name, + base_url: provider.base_url, + api_protocol: provider.api_protocol, + api_key: provider.api_key || '', + enabled: provider.enabled, + rate_limit_rpm: provider.rate_limit_rpm?.toString() || '', + rate_limit_tpm: provider.rate_limit_tpm?.toString() || '', + }) + setDialogOpen(true) + } + + async function handleSave() { + if (!form.name.trim() || !form.base_url.trim()) return + setSaving(true) + try { + const payload = { + name: form.name.trim(), + display_name: form.display_name.trim(), + base_url: form.base_url.trim(), + api_protocol: form.api_protocol, + api_key: form.api_key.trim() || undefined, + enabled: form.enabled, + rate_limit_rpm: form.rate_limit_rpm ? parseInt(form.rate_limit_rpm, 10) : undefined, + rate_limit_tpm: form.rate_limit_tpm ? parseInt(form.rate_limit_tpm, 10) : undefined, + } + if (editTarget) { + await api.providers.update(editTarget.id, payload) + } else { + await api.providers.create(payload) + } + setDialogOpen(false) + fetchProviders() + } catch (err) { + if (err instanceof ApiRequestError) setError(err.body.message) + } finally { + setSaving(false) + } + } + + async function handleDelete() { + if (!deleteTarget) return + setDeleting(true) + try { + await api.providers.delete(deleteTarget.id) + setDeleteTarget(null) + fetchProviders() + } catch (err) { + if (err instanceof ApiRequestError) setError(err.body.message) + } finally { + setDeleting(false) + } + } + + return ( +
+ {/* 工具栏 */} +
+
+ +
+ + {error && ( +
+ {error} + +
+ )} + + {loading ? ( +
+ +
+ ) : providers.length === 0 ? ( +
+ 暂无数据 +
+ ) : ( + <> + + + + 名称 + 显示名 + Base URL + 协议 + API Key + 启用 + RPM 限制 + 创建时间 + 操作 + + + + {providers.map((p) => ( + + {p.name} + {p.display_name || '-'} + + {p.base_url} + + + + {p.api_protocol} + + + + {maskApiKey(p.api_key)} + + + + {p.enabled ? '是' : '否'} + + + + {p.rate_limit_rpm ?? '-'} + + + {formatDate(p.created_at)} + + +
+ + +
+
+
+ ))} +
+
+ +
+

+ 第 {page} 页 / 共 {totalPages} 页 ({total} 条) +

+
+ + +
+
+ + )} + + {/* 创建/编辑 Dialog */} + + + + {editTarget ? '编辑服务商' : '新建服务商'} + + {editTarget ? '修改服务商配置' : '添加新的 AI 服务商'} + + +
+
+ + setForm({ ...form, name: e.target.value })} + placeholder="例如: openai" + disabled={!!editTarget} + /> +
+
+ + setForm({ ...form, display_name: e.target.value })} + placeholder="例如: OpenAI" + /> +
+
+ + setForm({ ...form, base_url: e.target.value })} + placeholder="https://api.openai.com/v1" + /> +
+
+ + +
+
+ + setForm({ ...form, api_key: e.target.value })} + placeholder={editTarget ? '留空则不修改' : 'sk-...'} + /> +
+
+ setForm({ ...form, enabled: v })} + /> + +
+
+
+ + setForm({ ...form, rate_limit_rpm: e.target.value })} + placeholder="不限" + /> +
+
+ + setForm({ ...form, rate_limit_tpm: e.target.value })} + placeholder="不限" + /> +
+
+
+ + + + +
+
+ + {/* 删除确认 Dialog */} + setDeleteTarget(null)}> + + + 确认删除 + + 确定要删除服务商 "{deleteTarget?.display_name || deleteTarget?.name}" 吗?此操作不可撤销。 + + + + + + + + +
+ ) +} diff --git a/admin/src/app/(dashboard)/relay/page.tsx b/admin/src/app/(dashboard)/relay/page.tsx new file mode 100644 index 0000000..0568e5f --- /dev/null +++ b/admin/src/app/(dashboard)/relay/page.tsx @@ -0,0 +1,245 @@ +'use client' + +import { useEffect, useState, useCallback } from 'react' +import { + Search, + Loader2, + ChevronLeft, + ChevronRight, + ChevronDown, + ChevronUp, +} from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { api } from '@/lib/api-client' +import { ApiRequestError } from '@/lib/api-client' +import { formatDate, formatNumber } from '@/lib/utils' +import type { RelayTask } from '@/lib/types' + +const PAGE_SIZE = 20 + +const statusVariants: Record = { + queued: 'warning', + processing: 'info', + completed: 'success', + failed: 'destructive', +} + +const statusLabels: Record = { + queued: '排队中', + processing: '处理中', + completed: '已完成', + failed: '失败', +} + +export default function RelayPage() { + const [tasks, setTasks] = useState([]) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) + const [statusFilter, setStatusFilter] = useState('all') + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + const [expandedId, setExpandedId] = useState(null) + + const fetchTasks = useCallback(async () => { + setLoading(true) + setError('') + try { + const params: Record = { page, page_size: PAGE_SIZE } + if (statusFilter !== 'all') params.status = statusFilter + const res = await api.relay.list(params) + setTasks(res.items) + setTotal(res.total) + } catch (err) { + if (err instanceof ApiRequestError) setError(err.body.message) + else setError('加载失败') + } finally { + setLoading(false) + } + }, [page, statusFilter]) + + useEffect(() => { + fetchTasks() + }, [fetchTasks]) + + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) + + function toggleExpand(id: string) { + setExpandedId((prev) => (prev === id ? null : id)) + } + + return ( +
+ {/* 筛选 */} +
+ +
+ + {error && ( +
+ {error} + +
+ )} + + {loading ? ( +
+ +
+ ) : tasks.length === 0 ? ( +
+ 暂无数据 +
+ ) : ( + <> + + + + + 任务 ID + 模型 + 状态 + 优先级 + 重试次数 + Input Tokens + Output Tokens + 错误信息 + 创建时间 + + + + {tasks.map((task) => ( + <> + toggleExpand(task.id)}> + + {expandedId === task.id ? ( + + ) : ( + + )} + + + {task.id.slice(0, 8)}... + + + {task.model_id} + + + + {statusLabels[task.status] || task.status} + + + {task.priority} + {task.attempt_count} + + {formatNumber(task.input_tokens)} + + + {formatNumber(task.output_tokens)} + + + {task.error_message || '-'} + + + {formatDate(task.created_at)} + + + {expandedId === task.id && ( + + +
+
+

任务 ID

+

{task.id}

+
+
+

账号 ID

+

{task.account_id}

+
+
+

服务商 ID

+

{task.provider_id}

+
+
+

模型 ID

+

{task.model_id}

+
+ {task.queued_at && ( +
+

排队时间

+

{formatDate(task.queued_at)}

+
+ )} + {task.started_at && ( +
+

开始时间

+

{formatDate(task.started_at)}

+
+ )} + {task.completed_at && ( +
+

完成时间

+

{formatDate(task.completed_at)}

+
+ )} + {task.error_message && ( +
+

错误信息

+

{task.error_message}

+
+ )} +
+
+
+ )} + + ))} +
+
+ +
+

+ 第 {page} 页 / 共 {totalPages} 页 ({total} 条) +

+
+ + +
+
+ + )} +
+ ) +} diff --git a/admin/src/app/(dashboard)/usage/page.tsx b/admin/src/app/(dashboard)/usage/page.tsx new file mode 100644 index 0000000..168eb59 --- /dev/null +++ b/admin/src/app/(dashboard)/usage/page.tsx @@ -0,0 +1,235 @@ +'use client' + +import { useEffect, useState, useCallback } from 'react' +import { Loader2, Zap } from 'lucide-react' +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + BarChart, + Bar, + Legend, +} from 'recharts' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { api } from '@/lib/api-client' +import { ApiRequestError } from '@/lib/api-client' +import { formatNumber } from '@/lib/utils' +import type { UsageRecord, UsageByModel } from '@/lib/types' + +export default function UsagePage() { + const [days, setDays] = useState(7) + const [dailyData, setDailyData] = useState([]) + const [modelData, setModelData] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + + const fetchData = useCallback(async () => { + setLoading(true) + setError('') + try { + const [dailyRes, modelRes] = await Promise.allSettled([ + api.usage.daily({ days }), + api.usage.byModel({ days }), + ]) + if (dailyRes.status === 'fulfilled') setDailyData(dailyRes.value) + else throw new Error('Failed to fetch daily usage') + if (modelRes.status === 'fulfilled') setModelData(modelRes.value) + } catch (err) { + if (err instanceof ApiRequestError) setError(err.body.message) + else setError('加载数据失败') + } finally { + setLoading(false) + } + }, [days]) + + useEffect(() => { + fetchData() + }, [fetchData]) + + const lineChartData = dailyData.map((r) => ({ + day: r.day.slice(5), + Input: r.input_tokens, + Output: r.output_tokens, + })) + + const barChartData = modelData.map((r) => ({ + model: r.model_id, + 请求量: r.count, + Input: r.input_tokens, + Output: r.output_tokens, + })) + + const totalInput = dailyData.reduce((s, r) => s + r.input_tokens, 0) + const totalOutput = dailyData.reduce((s, r) => s + r.output_tokens, 0) + const totalRequests = dailyData.reduce((s, r) => s + r.count, 0) + + if (loading) { + return ( +
+
+ +

加载中...

+
+
+ ) + } + + if (error) { + return ( +
+
+

{error}

+ +
+
+ ) + } + + return ( +
+ {/* 时间范围 */} +
+ 时间范围: + +
+ + {/* 汇总统计 */} +
+ + +

总请求数

+

+ {formatNumber(totalRequests)} +

+
+
+ + +

Input Tokens

+

+ {formatNumber(totalInput)} +

+
+
+ + +

Output Tokens

+

+ {formatNumber(totalOutput)} +

+
+
+
+ + {/* Token 用量趋势 */} + + + + + Token 用量趋势 + + + + {lineChartData.length > 0 ? ( + + + + + + + + + + + + ) : ( +
+ 暂无数据 +
+ )} +
+
+ + {/* 按模型分布 */} + + + 按模型分布 + + + {barChartData.length > 0 ? ( + + + + + + + + + + + + ) : ( +
+ 暂无数据 +
+ )} +
+
+
+ ) +} diff --git a/admin/src/app/globals.css b/admin/src/app/globals.css new file mode 100644 index 0000000..db4d952 --- /dev/null +++ b/admin/src/app/globals.css @@ -0,0 +1,66 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 222 47% 5%; + --foreground: 210 40% 98%; + --card: 222 47% 8%; + --card-foreground: 210 40% 98%; + --primary: 142 71% 45%; + --primary-foreground: 222 47% 5%; + --muted: 217 33% 17%; + --muted-foreground: 215 20% 65%; + --accent: 215 28% 23%; + --accent-foreground: 210 40% 98%; + --destructive: 0 84% 60%; + --destructive-foreground: 210 40% 98%; + --border: 217 33% 17%; + --input: 217 33% 17%; + --ring: 142 71% 45%; + } + + * { + border-color: hsl(var(--border)); + } + + body { + background-color: hsl(var(--background)); + color: hsl(var(--foreground)); + font-family: 'Inter', system-ui, -apple-system, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } +} + +@layer utilities { + .scrollbar-thin { + scrollbar-width: thin; + scrollbar-color: hsl(var(--muted)) transparent; + } + + .scrollbar-thin::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + .scrollbar-thin::-webkit-scrollbar-track { + background: transparent; + } + + .scrollbar-thin::-webkit-scrollbar-thumb { + background-color: hsl(var(--muted)); + border-radius: 3px; + } + + .scrollbar-thin::-webkit-scrollbar-thumb:hover { + background-color: hsl(var(--accent)); + } +} + +@layer components { + .glass-card { + @apply bg-card/80 backdrop-blur-sm border border-border rounded-lg; + } +} diff --git a/admin/src/app/layout.tsx b/admin/src/app/layout.tsx new file mode 100644 index 0000000..35840e6 --- /dev/null +++ b/admin/src/app/layout.tsx @@ -0,0 +1,27 @@ +import type { Metadata } from 'next' +import './globals.css' + +export const metadata: Metadata = { + title: 'ZCLAW Admin', + description: 'ZCLAW AI Agent 管理平台', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + + + + {children} + + + ) +} diff --git a/admin/src/app/login/page.tsx b/admin/src/app/login/page.tsx new file mode 100644 index 0000000..249ad6a --- /dev/null +++ b/admin/src/app/login/page.tsx @@ -0,0 +1,199 @@ +'use client' + +import { useState, type FormEvent } from 'react' +import { useRouter } from 'next/navigation' +import { Lock, User, Loader2, Eye, EyeOff } from 'lucide-react' +import { api } from '@/lib/api-client' +import { login } from '@/lib/auth' +import { ApiRequestError } from '@/lib/api-client' + +export default function LoginPage() { + const router = useRouter() + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [showPassword, setShowPassword] = useState(false) + const [remember, setRemember] = useState(false) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + + async function handleSubmit(e: FormEvent) { + e.preventDefault() + setError('') + + if (!username.trim()) { + setError('请输入用户名') + return + } + if (!password.trim()) { + setError('请输入密码') + return + } + + setLoading(true) + try { + const res = await api.auth.login({ username: username.trim(), password }) + login(res.token, res.account) + router.replace('/') + } catch (err) { + if (err instanceof ApiRequestError) { + setError(err.body.message || '登录失败,请检查用户名和密码') + } else { + setError('网络错误,请稍后重试') + } + } finally { + setLoading(false) + } + } + + return ( +
+ {/* 左侧品牌区域 */} +
+ {/* 装饰性背景 */} +
+
+
+
+
+
+ + {/* 品牌内容 */} +
+
+

+ ZCLAW +

+

+ AI Agent 管理平台 +

+
+
+
+
+
+

+ 统一管理 AI 服务商、模型配置、API 密钥、用量监控与系统配置 +

+
+
+
+ + {/* 右侧登录表单 */} +
+
+ {/* 移动端 Logo */} +
+

+ ZCLAW +

+

AI Agent 管理平台

+
+ +
+

登录

+

+ 输入您的账号信息以继续 +

+
+ +
+ {/* 用户名 */} +
+ +
+ + setUsername(e.target.value)} + className="flex h-10 w-full rounded-md border border-input bg-transparent pl-10 pr-3 py-2 text-sm shadow-sm transition-colors duration-200 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" + autoComplete="username" + /> +
+
+ + {/* 密码 */} +
+ +
+ + setPassword(e.target.value)} + className="flex h-10 w-full rounded-md border border-input bg-transparent pl-10 pr-10 py-2 text-sm shadow-sm transition-colors duration-200 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" + autoComplete="current-password" + /> + +
+
+ + {/* 记住我 */} +
+ setRemember(e.target.checked)} + className="h-4 w-4 rounded border-input bg-transparent accent-primary cursor-pointer" + /> + +
+ + {/* 错误信息 */} + {error && ( +
+ {error} +
+ )} + + {/* 登录按钮 */} + +
+
+
+
+ ) +} diff --git a/admin/src/components/auth-guard.tsx b/admin/src/components/auth-guard.tsx new file mode 100644 index 0000000..7e5b1ca --- /dev/null +++ b/admin/src/components/auth-guard.tsx @@ -0,0 +1,48 @@ +'use client' + +import { useEffect, useState, type ReactNode } from 'react' +import { useRouter } from 'next/navigation' +import { isAuthenticated, getAccount } from '@/lib/auth' +import type { AccountPublic } from '@/lib/types' + +interface AuthGuardProps { + children: ReactNode +} + +export function AuthGuard({ children }: AuthGuardProps) { + const router = useRouter() + const [authorized, setAuthorized] = useState(false) + const [account, setAccount] = useState(null) + + useEffect(() => { + if (!isAuthenticated()) { + router.replace('/login') + return + } + setAccount(getAccount()) + setAuthorized(true) + }, [router]) + + if (!authorized) { + return ( +
+
+
+ ) + } + + return <>{children} +} + +export function useAuth() { + const [account, setAccount] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const acc = getAccount() + setAccount(acc) + setLoading(false) + }, []) + + return { account, loading, isAuthenticated: isAuthenticated() } +} diff --git a/admin/src/components/ui/badge.tsx b/admin/src/components/ui/badge.tsx new file mode 100644 index 0000000..cc1fb36 --- /dev/null +++ b/admin/src/components/ui/badge.tsx @@ -0,0 +1,42 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', + { + variants: { + variant: { + default: + 'border-transparent bg-primary/15 text-primary', + secondary: + 'border-transparent bg-muted text-muted-foreground', + destructive: + 'border-transparent bg-destructive/15 text-destructive', + outline: + 'text-foreground border-border', + success: + 'border-transparent bg-green-500/15 text-green-400', + warning: + 'border-transparent bg-yellow-500/15 text-yellow-400', + info: + 'border-transparent bg-blue-500/15 text-blue-400', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/admin/src/components/ui/button.tsx b/admin/src/components/ui/button.tsx new file mode 100644 index 0000000..0335248 --- /dev/null +++ b/admin/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +'use client' + +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + default: + 'bg-primary text-primary-foreground hover:bg-primary-hover shadow-sm', + secondary: + 'bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground', + destructive: + 'bg-destructive text-destructive-foreground hover:bg-red-600 shadow-sm', + outline: + 'border border-border bg-transparent hover:bg-accent hover:text-accent-foreground', + ghost: + 'hover:bg-accent hover:text-accent-foreground', + link: + 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2', + sm: 'h-8 rounded-md px-3 text-xs', + lg: 'h-10 rounded-md px-8', + icon: 'h-9 w-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps {} + +const Button = React.forwardRef( + ({ className, variant, size, ...props }, ref) => { + return ( +