feat(kernel): add core installation and management

This commit is contained in:
lzt404 committed 2026-07-15 15:48:42 +08:00
1 parent 8c8cb02f14
commit 3bb5ff7dd4
10 files changed
+3275 -67

No files matched your search

+1
View File
@@ -3,6 +3,7 @@ dist/
src-tauri/gen/
src-tauri/target/
bin-work/
cpa-core/
*.log
*.tmp
@@ -0,0 +1,8 @@
CLIProxyAPI is the core file of this project. After downloading it from the GitHub repository, you can simply extract it locally and use it directly.
Latest release page: https://github.com/router-for-me/CLIProxyAPI/releases/latest
After extraction, you will find the cli-proxy-api binary file and the config.example.yaml configuration template.
You need to copy config.example.yaml to config.yaml and modify the DIY configuration before running it. Of course, the default configuration can also run directly.
+456 -13
View File
@@ -47,6 +47,15 @@ version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "atk"
version = "0.18.2"
@@ -309,6 +318,23 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chacha20"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core",
]
[[package]]
name = "chrono"
version = "0.4.45"
@@ -385,8 +411,18 @@ dependencies = [
name = "cpa-gui"
version = "0.1.0"
dependencies = [
"flate2",
"futures-util",
"reqwest 0.12.28",
"serde",
"serde_json",
"sha2",
"tar",
"tauri",
"tauri-build",
"tokio",
"tokio-util",
"zip",
]
[[package]]
@@ -398,6 +434,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.5.0"
@@ -525,6 +570,17 @@ dependencies = [
"serde_core",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
]
[[package]]
name = "derive_more"
version = "2.1.1"
@@ -698,7 +754,7 @@ dependencies = [
"cc",
"memchr",
"rustc_version",
"toml 1.1.2+spec-1.1.0",
"toml 1.1.3+spec-1.1.0",
"vswhom",
"winreg",
]
@@ -726,6 +782,16 @@ dependencies = [
"typeid",
]
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "fastrand"
version = "2.4.1"
@@ -751,6 +817,16 @@ dependencies = [
"rustc_version",
]
[[package]]
name = "filetime"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
dependencies = [
"cfg-if",
"libc",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@@ -1002,8 +1078,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
@@ -1025,8 +1103,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 6.0.0",
"rand_core",
"wasm-bindgen",
]
[[package]]
@@ -1276,6 +1357,22 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.27.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
dependencies = [
"http",
"hyper",
"hyper-util",
"rustls",
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
@@ -1655,6 +1752,12 @@ dependencies = [
"libc",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
version = "0.8.2"
@@ -1676,6 +1779,12 @@ version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "markup5ever"
version = "0.38.0"
@@ -2229,7 +2338,7 @@ version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
"toml_edit 0.25.12+spec-1.1.0",
"toml_edit 0.25.13+spec-1.1.0",
]
[[package]]
@@ -2274,6 +2383,62 @@ dependencies = [
"memchr",
]
[[package]]
name = "quinn"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.18",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
dependencies = [
"bytes",
"getrandom 0.4.3",
"lru-slab",
"rand",
"rand_pcg",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.18",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.61.2",
]
[[package]]
name = "quote"
version = "1.0.46"
@@ -2295,6 +2460,32 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [
"chacha20",
"getrandom 0.4.3",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_pcg"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
dependencies = [
"rand_core",
]
[[package]]
name = "raw-window-handle"
version = "0.6.2"
@@ -2370,6 +2561,47 @@ version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "reqwest"
version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tokio-util",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams 0.4.2",
"web-sys",
"webpki-roots",
]
[[package]]
name = "reqwest"
version = "0.13.4"
@@ -2400,10 +2632,24 @@ dependencies = [
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"wasm-streams 0.5.0",
"web-sys",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rustc-hash"
version = "2.1.3"
@@ -2419,12 +2665,66 @@ dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags 2.13.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
dependencies = [
"web-time",
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "same-file"
version = "1.0.6"
@@ -2615,6 +2915,18 @@ dependencies = [
"serde_core",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_with"
version = "3.21.0"
@@ -2685,7 +2997,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -2813,6 +3125,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "swift-rs"
version = "1.0.7"
@@ -2929,6 +3247,17 @@ dependencies = [
"syn 2.0.118",
]
[[package]]
name = "tar"
version = "0.4.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]]
name = "target-lexicon"
version = "0.12.16"
@@ -2965,7 +3294,7 @@ dependencies = [
"percent-encoding",
"plist",
"raw-window-handle",
"reqwest",
"reqwest 0.13.4",
"serde",
"serde_json",
"serde_repr",
@@ -3130,7 +3459,7 @@ dependencies = [
"serde_with",
"swift-rs",
"thiserror 2.0.18",
"toml 1.1.2+spec-1.1.0",
"toml 1.1.3+spec-1.1.0",
"url",
"urlpattern",
"uuid",
@@ -3145,7 +3474,7 @@ checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6"
dependencies = [
"dunce",
"embed-resource",
"toml 1.1.2+spec-1.1.0",
"toml 1.1.3+spec-1.1.0",
]
[[package]]
@@ -3263,9 +3592,31 @@ dependencies = [
"mio",
"pin-project-lite",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.18"
@@ -3308,9 +3659,9 @@ dependencies = [
[[package]]
name = "toml"
version = "1.1.2+spec-1.1.0"
version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c"
dependencies = [
"indexmap 2.14.0",
"serde_core",
@@ -3374,9 +3725,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.25.12+spec-1.1.0"
version = "0.25.13+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
dependencies = [
"indexmap 2.14.0",
"toml_datetime 1.1.1+spec-1.1.0",
@@ -3395,9 +3746,9 @@ dependencies = [
[[package]]
name = "toml_writer"
version = "1.1.1+spec-1.1.0"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
[[package]]
name = "tower"
@@ -3556,6 +3907,12 @@ version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.8"
@@ -3720,6 +4077,19 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-streams"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "wasm-streams"
version = "0.5.0"
@@ -3743,6 +4113,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "web_atoms"
version = "0.2.5"
@@ -3799,6 +4179,15 @@ dependencies = [
"system-deps",
]
[[package]]
name = "webpki-roots"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webview2-com"
version = "0.38.2"
@@ -4029,6 +4418,15 @@ dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
@@ -4297,6 +4695,16 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "xattr"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
dependencies = [
"libc",
"rustix",
]
[[package]]
name = "yoke"
version = "0.8.3"
@@ -4341,6 +4749,12 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[[package]]
name = "zerotrie"
version = "0.2.4"
@@ -4374,8 +4788,37 @@ dependencies = [
"syn 2.0.118",
]
[[package]]
name = "zip"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
dependencies = [
"arbitrary",
"crc32fast",
"crossbeam-utils",
"displaydoc",
"flate2",
"indexmap 2.14.0",
"memchr",
"thiserror 2.0.18",
"zopfli",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
[[package]]
name = "zopfli"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
dependencies = [
"bumpalo",
"crc32fast",
"log",
"simd-adler32",
]
+10
View File
@@ -9,4 +9,14 @@ edition = "2021"
tauri-build = { version = "2", features = [] }
[dependencies]
flate2 = "1"
futures-util = "0.3"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
tar = "0.4"
tauri = { version = "2", features = [] }
tokio = { version = "1", features = ["macros", "sync", "time"] }
tokio-util = "0.7"
zip = { version = "2", default-features = false, features = ["deflate"] }
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default permissions for the main CPA GUI window.",
"windows": ["main"],
"permissions": ["core:event:allow-listen", "core:event:allow-unlisten"]
}
+1230 -1
View File
File diff suppressed because it is too large. Load diff
+26 -7
View File
@@ -3,8 +3,16 @@ import { ConfigPanelPage } from './pages/ConfigPanel';
import { KernelPage } from './pages/Kernel';
const pages = [
{ id: 'kernel', label: '内核管理', component: KernelPage },
{ id: 'config', label: '配置面板', component: ConfigPanelPage },
{
id: 'kernel',
label: '内核',
component: KernelPage,
},
{
id: 'config',
label: '配置',
component: ConfigPanelPage,
},
] as const;
type PageId = (typeof pages)[number]['id'];
@@ -22,10 +30,14 @@ function App() {
<div className="app-shell">
<aside className="sidebar">
<div className="sidebar-brand" title="CLI Proxy API GUI">
<strong>Easy CLIProxyAPI</strong>
<span className="brand-mark">CP</span>
<div>
<strong>Easy CLIProxyAPI</strong>
<span>Desktop Console</span>
</div>
</div>
<nav className="nav-section">
<nav className="nav-section" aria-label="主导航">
{pages.map((page) => (
<button
key={page.id}
@@ -37,11 +49,18 @@ function App() {
</button>
))}
</nav>
<div className="sidebar-footer">
<span>CPA GUI</span>
<strong>0.1</strong>
</div>
</aside>
<main className="content">
<ActivePage />
</main>
<div className="workspace">
<main className="content">
<ActivePage />
</main>
</div>
</div>
);
}
+40 -8
View File
@@ -1,14 +1,46 @@
export function ConfigPanelPage() {
return (
<section className="page">
<h1>配置面板</h1>
<section className="page config-page">
<div className="config-layout">
<div className="panel">
<div className="panel-heading">
<div>
<h2>基础配置</h2>
<p>配置文件和工作目录</p>
</div>
</div>
<div className="panel">
<h2>基础配置</h2>
<label className="field">
配置文件
<input placeholder="待接入配置文件" />
</label>
<div className="form-grid">
<label className="field">
配置文件
<input placeholder="待接入配置文件" />
</label>
<label className="field">
工作目录
<input placeholder="待接入工作目录" />
</label>
</div>
</div>
<div className="panel quiet-panel">
<div className="panel-heading">
<div>
<h2>连接信息</h2>
<p>当前前端占位状态</p>
</div>
</div>
<dl className="info-list">
<div>
<dt>配置源</dt>
<dd>未接入</dd>
</div>
<div>
<dt>保存状态</dt>
<dd>待实现</dd>
</div>
</dl>
</div>
</div>
</section>
);
+696 -11
View File
@@ -1,23 +1,708 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
type CorePlatform = {
os: string;
arch: string;
assetOs: string;
assetArch: string;
archiveKind: 'tar.gz' | 'zip';
};
type CoreStatus = {
installed: boolean;
running: boolean;
managed: boolean;
processId: number | null;
currentVersion: string | null;
installDir: string;
binaryPath: string | null;
message: string;
};
type CoreLatest = {
version: string;
assetName: string;
};
type CoreInstallResult = {
version: string;
assetName: string;
installDir: string;
binaryPath: string | null;
};
type CoreInstallTask = {
running: boolean;
cancellable: boolean;
phase: string;
downloaded: number;
total: number | null;
percent: number | null;
message: string | null;
result: CoreInstallResult | null;
};
type MessageType = 'info' | 'success' | 'error';
let latestAutoCheckStarted = false;
let cachedLatest: CoreLatest | null = null;
let cachedLatestError = '';
let latestCheckPromise: Promise<CoreLatest> | null = null;
function requestLatestCore() {
if (latestCheckPromise) {
return latestCheckPromise;
}
latestCheckPromise = invoke<CoreLatest>('check_latest_core')
.then((result) => {
cachedLatest = result;
cachedLatestError = '';
return result;
})
.catch((error) => {
cachedLatest = null;
cachedLatestError = String(error);
throw error;
})
.finally(() => {
latestCheckPromise = null;
});
return latestCheckPromise;
}
export function KernelPage() {
const [status, setStatus] = useState('检测中');
const [coreStatus, setCoreStatus] = useState<CoreStatus | null>(null);
const [statusError, setStatusError] = useState('');
const [platform, setPlatform] = useState<CorePlatform | null>(null);
const [platformError, setPlatformError] = useState('');
const [latest, setLatest] = useState<CoreLatest | null>(cachedLatest);
const [latestError, setLatestError] = useState(cachedLatestError);
const [checkingLatest, setCheckingLatest] = useState(Boolean(latestCheckPromise));
const [allowLanAccess, setAllowLanAccess] = useState(false);
const [customPort, setCustomPort] = useState('');
const [installing, setInstalling] = useState(false);
const [processBusy, setProcessBusy] = useState(false);
const [processMessage, setProcessMessage] = useState('');
const [processMessageType, setProcessMessageType] = useState<MessageType>('info');
const [message, setMessage] = useState('');
const [messageType, setMessageType] = useState<MessageType>('info');
const [progress, setProgress] = useState<CoreInstallTask | null>(null);
const [installDialogOpen, setInstallDialogOpen] = useState(false);
const [cancellingInstall, setCancellingInstall] = useState(false);
const installDialogRef = useRef<HTMLDivElement>(null);
const applyInstallTask = (task: CoreInstallTask, showFinishedDialog = true) => {
if (!task.running && !task.message && !task.result) {
setProgress(null);
setInstalling(false);
setCancellingInstall(false);
return;
}
setInstalling(task.running);
if (!task.running) {
setCancellingInstall(false);
}
if (task.running || showFinishedDialog) {
setProgress(task);
setInstallDialogOpen(true);
} else {
setProgress(null);
}
if (task.result) {
setMessage(task.message || `${task.result.version} 安装完成`);
setMessageType('success');
void loadCoreStatus();
return;
}
if (task.message) {
setMessage(task.message);
setMessageType(task.phase === '安装失败' ? 'error' : 'info');
}
};
useEffect(() => {
invoke<string>('health_check')
.then(setStatus)
.catch(() => setStatus('需要在 Tauri 窗口中运行'));
let disposed = false;
let unlisten: (() => void) | null = null;
listen<CoreInstallTask>('core-install-progress', (event) => {
applyInstallTask(event.payload);
})
.then((unlistenProgress) => {
if (disposed) {
unlistenProgress();
} else {
unlisten = unlistenProgress;
}
})
.catch((error) => {
if (!disposed) {
setMessage(`监听下载进度失败: ${String(error)}`);
setMessageType('error');
}
});
loadCoreStatus();
loadPlatform();
loadInstallTask();
if (!latestAutoCheckStarted) {
latestAutoCheckStarted = true;
void checkLatest();
} else if (latestCheckPromise) {
void checkLatest();
}
return () => {
disposed = true;
unlisten?.();
};
}, []);
return (
<section className="page">
<h1>内核管理</h1>
useEffect(() => {
if (!installDialogOpen) {
return;
}
<div className="panel">
<h2>运行状态</h2>
<p>{status}</p>
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
installDialogRef.current?.focus();
const preventEscapeClose = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
event.preventDefault();
}
};
document.addEventListener('keydown', preventEscapeClose);
return () => {
document.body.style.overflow = previousOverflow;
document.removeEventListener('keydown', preventEscapeClose);
};
}, [installDialogOpen]);
const loadCoreStatus = async () => {
try {
const result = await invoke<CoreStatus>('get_core_status');
setCoreStatus(result);
setStatusError('');
} catch (error) {
setCoreStatus(null);
setStatusError(String(error));
}
};
const loadPlatform = async () => {
try {
const result = await invoke<CorePlatform>('detect_core_platform');
setPlatform(result);
setPlatformError('');
} catch (error) {
setPlatform(null);
setPlatformError(String(error));
}
};
const checkLatest = async () => {
setCheckingLatest(true);
setLatestError('');
setMessage('');
setMessageType('info');
try {
const result = await requestLatestCore();
setLatest(result);
} catch (error) {
setLatest(null);
setLatestError(String(error));
} finally {
setCheckingLatest(false);
}
};
const loadInstallTask = async () => {
try {
const task = await invoke<CoreInstallTask>('get_core_install_task');
applyInstallTask(task, false);
} catch (error) {
setMessage(`读取安装任务失败: ${String(error)}`);
setMessageType('error');
}
};
const installVersion = async (version: string) => {
setInstalling(true);
setMessage(`正在安装 ${version}`);
setMessageType('info');
setCancellingInstall(false);
setInstallDialogOpen(true);
setProgress({
running: true,
cancellable: true,
phase: '准备下载',
downloaded: 0,
total: null,
percent: null,
message: null,
result: null,
});
try {
const result = await invoke<CoreInstallResult>('install_core_version', { version });
setMessage(`${result.version} 安装完成`);
setMessageType('success');
setProgress({
running: false,
cancellable: false,
phase: '安装完成',
downloaded: 1,
total: 1,
percent: 100,
message: `${result.version} 安装完成`,
result,
});
await loadCoreStatus();
} catch (error) {
const errorMessage = String(error);
setMessage(errorMessage);
setMessageType(errorMessage.includes('取消') ? 'info' : 'error');
setProgress((current) => ({
running: false,
cancellable: false,
phase: errorMessage.includes('取消') ? '已取消' : '安装失败',
downloaded: current?.downloaded ?? 0,
total: current?.total ?? null,
percent: current?.percent ?? null,
message: errorMessage,
result: null,
}));
} finally {
setInstalling(false);
}
};
const cancelInstall = async () => {
if (cancellingInstall || !progress?.running || !progress.cancellable) {
return;
}
setCancellingInstall(true);
setMessage('正在取消下载');
setMessageType('info');
try {
await invoke('cancel_core_install');
} catch (error) {
setCancellingInstall(false);
setMessage(String(error));
setMessageType('error');
}
};
const closeInstallDialog = () => {
if (installing || progress?.running) {
return;
}
setInstallDialogOpen(false);
setProgress(null);
setCancellingInstall(false);
};
const runCoreProcessCommand = async (command: string, doing: string, done: string) => {
setProcessBusy(true);
setProcessMessage(doing);
setProcessMessageType('info');
try {
const result = await invoke<CoreStatus>(command);
setCoreStatus(result);
setStatusError('');
setProcessMessage(done);
setProcessMessageType('success');
} catch (error) {
setProcessMessage(String(error));
setProcessMessageType('error');
await loadCoreStatus();
} finally {
setProcessBusy(false);
}
};
const latestVersion = latest?.version ?? '';
const currentVersion = coreStatus?.currentVersion ?? '';
const coreInstalled = Boolean(coreStatus?.installed);
const coreRunning = Boolean(coreStatus?.running);
const busy = checkingLatest || installing || processBusy;
const installDisabled = busy || Boolean(coreStatus?.running);
const computedPercent =
progress?.percent ??
(progress?.total && progress.total > 0 ? (progress.downloaded / progress.total) * 100 : null);
const progressKnown = computedPercent !== null;
const progressPercent = clampPercent(computedPercent ?? 0);
const progressText = progress
? progress.phase === '安装完成'
? '已完成'
: progress.phase === '解压中'
? '正在解压文件'
: progress.total
? `${formatBytes(progress.downloaded)} / ${formatBytes(progress.total)}`
: progress.downloaded > 0
? formatBytes(progress.downloaded)
: '等待进度信息'
: '';
const statusTone = statusError ? 'error' : coreRunning ? 'success' : 'neutral';
const statusLabel = coreStatus
? coreRunning
? '运行中'
: coreInstalled
? '已停止'
: '未安装'
: statusError
? '检测失败'
: '检测中';
const statusMessage = coreStatus?.message || statusError || '正在检测 CPA 内核';
const latestLabel = checkingLatest
? '检查中'
: latestVersion || (latestError ? '检查失败' : '未检查');
const updateStateLabel = checkingLatest
? '正在检查'
: latestError
? '检查失败'
: !latestVersion
? '尚未检查'
: currentVersion === latestVersion
? '已是最新版本'
: '可安装新版本';
const platformLabel = platform ? `${platform.os} / ${platform.arch}` : platformError || '检测中';
const platformOsLabel = platform?.os || (platformError ? '检测失败' : '检测中');
const platformArchLabel = platform?.arch || (platformError ? '检测失败' : '检测中');
const installTaskRunning = Boolean(installing || progress?.running);
const controlSubtitle = processMessage || statusMessage;
const controlSubtitleTone: MessageType = processMessage
? processMessageType
: statusError
? 'error'
: 'info';
const versionStatusLabel = installTaskRunning
? cancellingInstall
? '正在取消下载'
: progress?.phase || '正在安装'
: message || updateStateLabel;
const versionStatusTone: MessageType = installTaskRunning
? 'info'
: message
? messageType
: latestError
? 'error'
: currentVersion && currentVersion === latestVersion
? 'success'
: 'info';
const installDialogTone: MessageType = progress?.result
? 'success'
: progress?.phase === '安装失败'
? 'error'
: 'info';
const installDialogTitle = installTaskRunning
? cancellingInstall
? '正在取消下载'
: '正在安装内核'
: progress?.result
? '安装完成'
: progress?.phase === '已取消'
? '下载已取消'
: '安装失败';
const installDialogMessage = cancellingInstall
? '正在等待下载任务停止'
: progress?.message || (installTaskRunning ? message || '安装任务正在进行' : '');
const installDialogAction = installTaskRunning
? cancellingInstall
? '正在取消'
: progress?.cancellable
? '取消下载'
: '处理中'
: '关闭';
const installDialogActionDisabled =
installTaskRunning && (cancellingInstall || !progress?.cancellable);
return (
<section className="page kernel-page">
<div className="status-strip">
<div className="status-card status-card-primary">
<span className={`status-dot ${statusTone}`} />
<div>
<span>内核状态</span>
<strong>{statusLabel}</strong>
</div>
</div>
<div className="status-card">
<span>当前版本</span>
<strong>{currentVersion || '未安装'}</strong>
</div>
<div className="status-card">
<span>最新版本</span>
<strong>{latestLabel}</strong>
</div>
<div className="status-card">
<span>运行平台</span>
<strong>{platformLabel}</strong>
</div>
</div>
<div className="kernel-layout">
<div className="panel control-panel">
<div className="panel-heading">
<div>
<h2>运行控制</h2>
<p className={controlSubtitleTone} title={controlSubtitle}>
{controlSubtitle}
</p>
</div>
<span className={`state-pill ${statusTone}`}>{statusLabel}</span>
</div>
<dl className="panel-detail-grid">
<div className="panel-detail-row">
<dt>安装状态</dt>
<dd>{coreStatus ? (coreInstalled ? '已安装' : '未安装') : '检测中'}</dd>
</div>
<div className="panel-detail-row">
<dt>运行状态</dt>
<dd>{coreStatus ? (coreRunning ? '运行中' : '未运行') : '检测中'}</dd>
</div>
<div className="panel-detail-row">
<dt>进程 PID</dt>
<dd>{coreStatus?.processId || '无'}</dd>
</div>
<div className="panel-detail-row">
<dt>局域网</dt>
<dd className="detail-control-cell">
<span className="switch-control" title="允许局域网访问">
<input
type="checkbox"
aria-label="允许局域网访问"
checked={allowLanAccess}
onChange={(event) => setAllowLanAccess(event.currentTarget.checked)}
/>
<span className="switch-track" />
</span>
</dd>
</div>
<div className="panel-detail-row">
<dt>
<label htmlFor="custom-port">端口</label>
</dt>
<dd className="detail-control-cell">
<input
id="custom-port"
className="compact-text-input"
type="text"
inputMode="numeric"
pattern="[0-9]*"
maxLength={5}
placeholder="端口号"
value={customPort}
onChange={(event) =>
setCustomPort(event.currentTarget.value.replace(/\D/g, '').slice(0, 5))
}
/>
</dd>
</div>
</dl>
<div className="button-row panel-action-row control-action-row">
<button
type="button"
className="primary-button"
disabled={!coreInstalled || coreRunning || installing || processBusy}
onClick={() =>
runCoreProcessCommand('start_core_process', '正在启动 CPA 内核', 'CPA 内核已启动')
}
>
{processBusy ? '处理中' : '启动'}
</button>
<button
type="button"
className="danger-button"
disabled={!coreInstalled || !coreRunning || installing || processBusy}
onClick={() =>
runCoreProcessCommand('stop_core_process', '正在关闭 CPA 内核', 'CPA 内核已关闭')
}
>
关闭
</button>
<button
type="button"
className="secondary-button"
disabled={!coreInstalled || !coreRunning || installing || processBusy}
onClick={() =>
runCoreProcessCommand(
'restart_core_process',
'正在重启 CPA 内核',
'CPA 内核已重启',
)
}
>
重启
</button>
<button
type="button"
className="secondary-button"
disabled={processBusy}
onClick={loadCoreStatus}
>
刷新状态
</button>
</div>
</div>
<div className="panel version-panel">
<div className="panel-heading">
<div>
<h2>版本管理</h2>
<p>检查和安装 CPA 内核版本</p>
</div>
</div>
<dl className="panel-detail-grid">
<div className="panel-detail-row">
<dt>当前版本</dt>
<dd>{currentVersion || '未安装'}</dd>
</div>
<div className="panel-detail-row">
<dt>最新版本</dt>
<dd>{latestLabel}</dd>
</div>
<div className="panel-detail-row">
<dt>操作系统</dt>
<dd title={platformError || undefined}>{platformOsLabel}</dd>
</div>
<div className="panel-detail-row">
<dt>系统架构</dt>
<dd title={platformError || undefined}>{platformArchLabel}</dd>
</div>
<div className="panel-detail-row">
<dt>更新状态</dt>
<dd className={versionStatusTone} title={versionStatusLabel}>
{versionStatusLabel}
</dd>
</div>
</dl>
<div className="button-row panel-action-row version-action-row">
<button
type="button"
className="secondary-button"
disabled={busy}
onClick={checkLatest}
>
{checkingLatest ? '检查中' : '检查更新'}
</button>
<button
type="button"
className="primary-button"
title={latestVersion ? `安装 ${latestVersion}` : '安装最新版'}
disabled={!latestVersion || installDisabled}
onClick={() => installVersion(latestVersion)}
>
安装最新
</button>
<button
type="button"
className="secondary-button"
title="重新安装当前版本"
disabled={!currentVersion || installDisabled}
onClick={() => installVersion(currentVersion)}
>
重新安装
</button>
</div>
</div>
</div>
{installDialogOpen && progress ? (
<div className="install-dialog-backdrop">
<div
ref={installDialogRef}
className={`install-dialog ${installDialogTone}`}
role="dialog"
aria-modal="true"
aria-labelledby="install-dialog-title"
aria-describedby="install-dialog-message"
aria-busy={installTaskRunning}
tabIndex={-1}
>
<div className="install-dialog-heading">
<span>内核安装</span>
<h2 id="install-dialog-title">{installDialogTitle}</h2>
</div>
<div className="install-dialog-phase">
<span>当前阶段</span>
<strong>{cancellingInstall ? '正在取消' : progress.phase}</strong>
</div>
<div
className={`install-progress-track ${
progressKnown ? '' : installTaskRunning ? 'unknown is-running' : 'unknown'
}`}
>
<span
className="install-progress-fill"
style={progressKnown ? { width: `${progressPercent}%` } : undefined}
/>
</div>
<div className="install-progress-meta">
<strong>{progressKnown ? `${progressPercent.toFixed(1)}%` : '进度未知'}</strong>
<span>{progressText}</span>
</div>
<div
id="install-dialog-message"
className={`install-dialog-message ${installDialogTone}`}
aria-live="polite"
>
{installDialogMessage || '\u00a0'}
</div>
<button
type="button"
className={installTaskRunning ? 'danger-button' : 'primary-button'}
disabled={installDialogActionDisabled}
onClick={installTaskRunning ? cancelInstall : closeInstallDialog}
>
{installDialogAction}
</button>
</div>
</div>
) : null}
</section>
);
}
function clampPercent(percent: number) {
return Math.min(100, Math.max(0, percent));
}
function formatBytes(bytes: number) {
if (bytes < 1024) {
return `${bytes} B`;
}
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
}
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
+801 -27
View File
@@ -3,6 +3,8 @@
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
color: #2d2a26;
background: #faf9f5;
font-synthesis: none;
text-rendering: optimizeLegibility;
}
* {
@@ -22,111 +24,883 @@ body {
margin: 0;
}
button {
button,
input {
font: inherit;
}
button {
cursor: pointer;
}
button:focus-visible,
input:focus-visible {
outline: 3px solid rgba(45, 42, 38, 0.18);
outline-offset: 2px;
}
.app-shell {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-columns: 236px minmax(0, 1fr);
min-height: 100vh;
background: #faf9f5;
}
.sidebar {
position: sticky;
top: 0;
display: flex;
flex-direction: column;
height: 100vh;
padding: 16px;
padding: 18px;
background: #f0eee8;
border-right: 1px solid #e3e1db;
color: #2d2a26;
}
.sidebar-brand {
display: flex;
flex-direction: column;
padding: 8px 10px 20px;
display: grid;
grid-template-columns: 42px minmax(0, 1fr);
align-items: center;
gap: 12px;
min-height: 54px;
margin-bottom: 22px;
}
.brand-mark {
display: grid;
width: 42px;
height: 42px;
place-items: center;
border: 1px solid #d5d2cb;
border-radius: 8px;
background: #2d2a26;
color: #fffdf8;
font-size: 14px;
font-weight: 800;
}
.sidebar-brand strong,
.sidebar-brand span {
display: block;
min-width: 0;
}
.sidebar-brand strong {
overflow: hidden;
color: #2d2a26;
font-size: 15px;
text-overflow: ellipsis;
white-space: nowrap;
}
.sidebar-brand div > span {
margin-top: 4px;
color: #7a746c;
font-size: 12px;
}
.nav-section {
display: flex;
flex-direction: column;
display: grid;
gap: 6px;
}
.nav-section button {
width: 100%;
padding: 11px 12px;
min-height: 42px;
padding: 0 12px;
border: 1px solid transparent;
border-radius: 8px;
background: transparent;
color: #2d2a26;
text-align: left;
}
.nav-section button:hover {
background: #e8e5dc;
color: #2d2a26;
}
.nav-section button.active {
background: #dfdcd4;
border-color: #d5d2cb;
background: #dfdcd4;
color: #2d2a26;
font-weight: 700;
}
.sidebar-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: auto;
padding-top: 18px;
border-top: 1px solid #e3e1db;
color: #7a746c;
font-size: 12px;
}
.sidebar-footer strong {
color: #2d2a26;
}
.workspace {
min-width: 0;
}
.state-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 28px;
padding: 0 10px;
border: 1px solid #d5d2cb;
border-radius: 999px;
background: #fffdf8;
color: #5f5a53;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.state-pill.success {
border-color: #b6d3bd;
background: #eef7f0;
color: #2f6b3f;
}
.state-pill.error {
border-color: #dfb7b0;
background: #fff1ee;
color: #9f2d20;
}
.content {
padding: 48px;
width: 100%;
padding: 24px 40px 42px;
}
.page {
max-width: 860px;
width: min(100%, 1180px);
}
.page h1 {
margin: 0 0 24px;
font-size: 28px;
.kernel-page,
.config-page {
display: grid;
gap: 18px;
}
.panel {
min-width: 0;
padding: 20px;
border: 1px solid #e3e1db;
border-radius: 8px;
background: #fffdf8;
box-shadow: 0 1px 2px rgba(45, 42, 38, 0.04);
}
.quiet-panel {
background: #faf9f5;
}
.panel-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
min-height: 49px;
margin-bottom: 16px;
}
.panel-heading > div {
min-width: 0;
}
.panel h2 {
margin: 0;
color: #2d2a26;
font-size: 16px;
line-height: 1.25;
}
.panel-heading p {
min-height: 19px;
margin: 5px 0 0;
overflow: hidden;
color: #5f5a53;
font-size: 13px;
line-height: 1.45;
text-overflow: ellipsis;
white-space: nowrap;
}
.panel-heading p.success {
color: #2f6b3f;
}
.panel-heading p.error {
color: #9f2d20;
}
.status-strip {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.status-card {
display: grid;
gap: 6px;
min-width: 0;
min-height: 84px;
padding: 16px;
border: 1px solid #e3e1db;
border-radius: 8px;
background: #fffdf8;
}
.panel h2 {
margin: 0 0 12px;
font-size: 16px;
.status-card-primary {
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 12px;
}
.panel p {
.status-card-primary > div {
display: grid;
gap: 6px;
min-width: 0;
}
.status-card span {
overflow: hidden;
color: #7a746c;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-card strong {
overflow: hidden;
color: #2d2a26;
font-size: 17px;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-dot {
display: block;
width: 12px;
height: 12px;
border-radius: 999px;
background: #9b958c;
box-shadow: 0 0 0 5px rgba(155, 149, 140, 0.14);
}
.status-dot.success {
background: #2f6b3f;
box-shadow: 0 0 0 5px rgba(47, 107, 63, 0.15);
}
.status-dot.error {
background: #9f2d20;
box-shadow: 0 0 0 5px rgba(159, 45, 32, 0.15);
}
.status-dot.neutral {
background: #9b958c;
}
.kernel-layout {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
align-items: stretch;
gap: 12px;
}
.config-layout {
display: grid;
grid-template-columns: minmax(420px, 1.15fr) minmax(340px, 0.85fr);
align-items: start;
gap: 18px;
}
.control-panel {
grid-column: span 2;
}
.version-panel {
grid-column: span 2;
}
.kernel-layout > .panel {
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: 65px 232px 56px;
height: 395px;
}
.kernel-layout > .panel > .panel-heading {
min-height: 0;
height: 65px;
margin: 0;
padding-bottom: 16px;
}
.kernel-layout .panel-action-row {
height: 56px;
margin: 0;
padding-top: 18px;
}
.panel-detail-grid {
display: grid;
grid-template-rows: repeat(5, 40px);
gap: 8px;
min-height: 0;
margin: 0;
}
.panel-detail-row {
display: grid;
grid-template-columns: 92px minmax(0, 1fr);
align-items: center;
gap: 14px;
min-width: 0;
min-height: 40px;
}
.panel-detail-row dt {
color: #7a746c;
font-size: 13px;
}
.panel-detail-row dd {
min-width: 0;
margin: 0;
overflow: hidden;
color: #2d2a26;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
.panel-detail-row dd.success {
color: #2f6b3f;
}
.panel-detail-row dd.error {
color: #9f2d20;
}
.detail-control-cell {
display: flex;
align-items: center;
justify-content: flex-end;
height: 40px;
overflow: visible !important;
}
.compact-text-input {
width: min(180px, 100%);
height: 34px;
padding: 0 10px;
border: 1px solid #d5d2cb;
border-radius: 6px;
background: #ffffff;
color: #2d2a26;
text-align: right;
}
.compact-text-input::placeholder {
color: #9b958c;
}
.control-action-row {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.version-action-row {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.info-list {
display: grid;
gap: 10px;
grid-template-rows: repeat(4, 20px);
min-height: 110px;
margin: 0;
}
.info-list div {
display: grid;
grid-template-columns: 92px minmax(0, 1fr);
gap: 14px;
align-items: start;
min-height: 20px;
}
.info-list dt {
color: #7a746c;
font-size: 13px;
}
.info-list dd {
min-width: 0;
margin: 0;
overflow: hidden;
color: #2d2a26;
text-overflow: ellipsis;
white-space: nowrap;
}
.empty-state {
display: grid;
gap: 6px;
padding: 18px;
border: 1px dashed #d5d2cb;
border-radius: 8px;
background: #fffdf8;
color: #5f5a53;
}
.empty-state strong {
color: #2d2a26;
}
.button-row {
display: grid;
grid-auto-columns: minmax(0, 1fr);
grid-auto-flow: column;
gap: 10px;
min-height: 38px;
margin-top: 18px;
}
.primary-button,
.secondary-button,
.danger-button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 38px;
min-width: 0;
overflow: hidden;
padding: 0 10px;
border-radius: 6px;
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.primary-button {
border: 1px solid #2d2a26;
background: #2d2a26;
color: #fffdf8;
}
.primary-button:hover:not(:disabled) {
background: #433f39;
}
.secondary-button {
border: 1px solid #d5d2cb;
background: #fffdf8;
color: #2d2a26;
}
.secondary-button:hover:not(:disabled) {
background: #f4f1ea;
}
.danger-button {
border: 1px solid #a83f32;
background: #fffdf8;
color: #9f2d20;
}
.danger-button:hover:not(:disabled) {
background: #fff1ee;
}
.primary-button:disabled,
.secondary-button:disabled,
.danger-button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.switch-control {
position: relative;
display: block;
width: 44px;
height: 24px;
flex: none;
}
.switch-control input {
position: absolute;
inset: 0;
z-index: 1;
width: 100%;
height: 100%;
margin: 0;
cursor: pointer;
opacity: 0;
}
.switch-track {
display: block;
width: 100%;
height: 100%;
border: 1px solid #d5d2cb;
border-radius: 999px;
background: #e8e5dc;
transition:
background 140ms ease,
border-color 140ms ease;
}
.switch-track::after {
display: block;
width: 18px;
height: 18px;
margin: 2px;
border-radius: 999px;
background: #fffdf8;
box-shadow: 0 1px 2px rgba(45, 42, 38, 0.18);
transition: transform 140ms ease;
content: '';
}
.switch-control input:checked + .switch-track {
border-color: #2d2a26;
background: #2d2a26;
}
.switch-control input:checked + .switch-track::after {
transform: translateX(20px);
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.field {
display: grid;
gap: 8px;
max-width: 420px;
min-width: 0;
color: #5f5a53;
font-size: 14px;
font-weight: 700;
}
.field input {
width: 100%;
padding: 10px 12px;
min-height: 40px;
padding: 0 12px;
border: 1px solid #d5d2cb;
border-radius: 6px;
background: #ffffff;
color: #2d2a26;
font: inherit;
}
@media (max-width: 860px) {
.field input::placeholder {
color: #9b958c;
}
.field input:disabled {
cursor: not-allowed;
background: #f4f1ea;
color: #9b958c;
}
.install-dialog-backdrop {
position: fixed;
inset: 0;
z-index: 50;
display: grid;
place-items: center;
padding: 24px;
background: rgba(45, 42, 38, 0.42);
backdrop-filter: blur(3px);
}
.install-dialog {
display: grid;
grid-template-rows: 48px 42px 10px 22px 72px 40px;
gap: 15px;
width: min(420px, 100%);
height: 360px;
padding: 24px;
border: 1px solid #d5d2cb;
border-radius: 8px;
outline: none;
background: #fffdf8;
box-shadow: 0 18px 48px rgba(45, 42, 38, 0.24);
}
.install-dialog-heading {
min-width: 0;
}
.install-dialog-heading > span {
display: block;
margin-bottom: 5px;
color: #7a746c;
font-size: 12px;
font-weight: 700;
}
.install-dialog-heading h2 {
margin: 0;
overflow: hidden;
color: #2d2a26;
font-size: 20px;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.install-dialog-phase {
display: grid;
grid-template-columns: 76px minmax(0, 1fr);
align-items: center;
gap: 12px;
min-width: 0;
padding: 0 12px;
border-radius: 6px;
background: #f4f1ea;
}
.install-dialog-phase span {
color: #7a746c;
font-size: 13px;
}
.install-dialog-phase strong {
overflow: hidden;
color: #2d2a26;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
.install-progress-track {
width: 100%;
height: 10px;
overflow: hidden;
border-radius: 999px;
background: #e8e5dc;
}
.install-progress-fill {
display: block;
width: 0;
height: 100%;
border-radius: inherit;
background: #2d2a26;
transition: width 160ms ease;
}
.install-dialog.success .install-progress-fill {
background: #2f6b3f;
}
.install-dialog.error .install-progress-fill {
background: #9f2d20;
}
.install-progress-track.unknown .install-progress-fill {
visibility: hidden;
width: 36%;
}
.install-progress-track.unknown.is-running .install-progress-fill {
visibility: visible;
animation: install-progress-slide 1.1s ease-in-out infinite;
}
.install-progress-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-width: 0;
color: #6d6760;
font-size: 13px;
}
.install-progress-meta strong,
.install-progress-meta span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.install-progress-meta strong {
color: #2d2a26;
}
.install-dialog-message {
overflow: auto;
padding: 10px 12px;
border-radius: 6px;
background: #faf9f5;
color: #5f5a53;
font-size: 13px;
line-height: 1.45;
overflow-wrap: anywhere;
}
.install-dialog-message.success {
background: #eef7f0;
color: #2f6b3f;
}
.install-dialog-message.error {
background: #fff1ee;
color: #9f2d20;
}
.install-dialog > button {
width: 100%;
height: 40px;
}
@keyframes install-progress-slide {
from {
transform: translateX(-120%);
}
to {
transform: translateX(300%);
}
}
@media (max-width: 1080px) {
.app-shell {
grid-template-columns: 160px 1fr;
grid-template-columns: 204px minmax(0, 1fr);
}
.status-strip {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.kernel-layout {
grid-template-columns: 1fr;
}
.config-layout {
grid-template-columns: 1fr;
}
.control-panel,
.version-panel {
grid-column: auto;
}
}
@media (max-width: 760px) {
.app-shell {
grid-template-columns: 1fr;
}
.sidebar {
padding: 12px;
position: static;
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 12px;
height: auto;
padding: 14px;
}
.sidebar-brand {
margin-bottom: 0;
}
.nav-section {
display: flex;
overflow-x: auto;
padding-bottom: 2px;
}
.nav-section button {
width: auto;
min-width: 96px;
text-align: center;
}
.sidebar-footer {
display: none;
}
.content {
padding: 24px 16px;
padding: 18px;
}
.status-strip,
.form-grid {
grid-template-columns: 1fr;
}
.panel {
padding: 18px;
}
.kernel-layout > .panel {
padding: 20px;
}
}
@media (max-width: 520px) {
.panel-heading {
flex-direction: column;
}
.button-row {
grid-auto-columns: auto;
grid-auto-flow: row;
grid-template-columns: 1fr;
}
.primary-button,
.secondary-button,
.danger-button {
width: 100%;
}
.kernel-layout .panel-heading {
flex-direction: row;
}
.kernel-layout .panel-action-row {
grid-auto-flow: column;
}
.kernel-layout .control-action-row {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.kernel-layout .version-action-row {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.info-list div {
grid-template-columns: 1fr;
gap: 4px;
}
.install-dialog-backdrop {
padding: 16px;
}
}
@media (prefers-reduced-motion: reduce) {
.install-progress-track.unknown.is-running .install-progress-fill {
animation-duration: 2.2s;
}
}