commit 1b1816d4db265e80d17dbb2a0b3b8f6f8ba009af Author: imengying Date: Mon Aug 10 19:19:13 2026 +0800 Initial release diff --git a/.dev.vars.example b/.dev.vars.example new file mode 100644 index 0000000..446a52b --- /dev/null +++ b/.dev.vars.example @@ -0,0 +1,7 @@ +ADMIN_USERNAME=admin +ADMIN_PASSWORD=change-this-local-password +# Optional: set a stable signing key for admin sessions in production. +# SESSION_SECRET=replace-with-a-random-secret +# Optional: enable Cloudflare usage statistics in the admin data workspace. +# CLOUDFLARE_ACCOUNT_ID=your-account-id +# CLOUDFLARE_API_TOKEN=token-with-account-analytics-read diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c29ae4d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,142 @@ +name: Build tagged release + +on: + push: + tags: + - "*.*.*" + workflow_dispatch: + inputs: + version: + description: Three-part release version, for example 0.1.0 + required: true + default: 0.1.0 + +permissions: + contents: write + +jobs: + metadata: + name: Validate version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - uses: actions/checkout@v4 + - id: version + name: Validate three-part version + shell: bash + env: + RAW_VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }} + run: | + version="${RAW_VERSION#v}" + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Tag must be a three-part version such as v1.2.3 or 1.2.3" >&2 + exit 1 + fi + + root_version=$(node -p "require('./package.json').version") + frontend_version=$(node -p "require('./frontend/package.json').version") + worker_version=$(awk -F '"' '/^version = / { print $2; exit }' Cargo.toml) + agent_version=$(awk -F '"' '/^version = / { print $2; exit }' agent/Cargo.toml) + for package_version in "$root_version" "$frontend_version" "$worker_version" "$agent_version"; do + if [ "$package_version" != "$version" ]; then + echo "Tag version $version does not match package version $package_version" >&2 + exit 1 + fi + done + echo "version=$version" >> "$GITHUB_OUTPUT" + + agent: + name: Agent (${{ matrix.platform }}) + needs: metadata + runs-on: ${{ matrix.runner }} + env: + CARGO_TARGET_DIR: target/agent + strategy: + fail-fast: false + matrix: + include: + - platform: Linux x86_64 + runner: ubuntu-latest + target: x86_64-unknown-linux-gnu + asset: agent-linux-x86_64 + extension: "" + cross: true + - platform: Linux ARM64 + runner: ubuntu-latest + target: aarch64-unknown-linux-gnu + asset: agent-linux-aarch64 + extension: "" + cross: true + - platform: Windows x86_64 + runner: windows-latest + target: x86_64-pc-windows-msvc + asset: agent-windows-x86_64.exe + extension: .exe + cross: false + - platform: macOS ARM64 + runner: macos-14 + target: aarch64-apple-darwin + asset: agent-macos-aarch64 + extension: "" + cross: false + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - name: Install Linux cross compiler + if: matrix.cross + run: cargo install cross --version 0.2.5 --locked + - name: Build Rust Agent with cross + if: matrix.cross + run: cross build --manifest-path agent/Cargo.toml --release --locked --target ${{ matrix.target }} + - name: Build Rust Agent with Cargo + if: matrix.cross == false + run: cargo build --manifest-path agent/Cargo.toml --release --locked --target ${{ matrix.target }} + - name: Stage Agent binary + shell: bash + run: | + mkdir -p release + cp "target/agent/${{ matrix.target }}/release/cf-monitor-agent${{ matrix.extension }}" "release/${{ matrix.asset }}" + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.asset }} + path: release/${{ matrix.asset }} + if-no-files-found: error + + frontend: + name: Frontend + needs: metadata + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.14 + - name: Install dependencies + run: bun install --frozen-lockfile + - name: Build public frontend + run: bun run --cwd frontend build:public + - name: Package frontend + run: tar -czf "cf-monitor-frontend-${{ needs.metadata.outputs.version }}.tar.gz" -C frontend/dist . + - uses: actions/upload-artifact@v4 + with: + name: frontend + path: cf-monitor-frontend-${{ needs.metadata.outputs.version }}.tar.gz + if-no-files-found: error + + release: + name: Publish GitHub Release + if: github.event_name == 'push' + needs: [metadata, agent, frontend] + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + path: release-assets + merge-multiple: true + - uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true + files: release-assets/** diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc8ad3e --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/build/ +/target/ +/node_modules/ +/frontend/public/agent-* +/frontend/dist/ +/frontend/admin-dist/ +/frontend/node_modules/ +/.wrangler/ +/.dev.vars +*.log +*.tsbuildinfo diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..636564e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,874 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cf-monitor-worker" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "sha2", + "worker", + "worker-macros", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "js-sys", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[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 = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[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 = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7d3be8814f5ba5f074491a469eed3d73c273ffad955f25ed1635efac4b0d269" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "worker" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f8adbf6c9ae45b665dee995c5e3a342c2bd7d58a2e8ca5c75b50ce8b1b8bfd9" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "futures-channel", + "futures-util", + "http", + "http-body", + "js-sys", + "matchit", + "pin-project", + "serde", + "serde-wasm-bindgen", + "serde_json", + "serde_urlencoded", + "strum", + "tokio", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "worker-macros", + "worker-sys", +] + +[[package]] +name = "worker-macros" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d908735d273dd7f9c325a842623f4e5a745e0686187ce465b34dc162ad348df" +dependencies = [ + "async-trait", + "proc-macro2", + "quote", + "strum", + "syn 2.0.119", + "wasm-bindgen", + "wasm-bindgen-macro-support", + "worker-sys", +] + +[[package]] +name = "worker-sys" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33faa1a8fa6c7eec67b196e008859c44d468a5ad4f991855cdc856f119e0e98f" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..59b5357 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "cf-monitor-worker" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "Rust/WASM server monitor for Cloudflare Workers" +repository = "https://github.com/imengying/CF-Monitor" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sha2 = "0.10" +worker = { version = "0.8", features = ["d1"] } +worker-macros = "0.8" + +[profile.release] +codegen-units = 1 +lto = true +opt-level = "s" +strip = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ca791ec --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2026 CF Monitor contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..5c31c14 --- /dev/null +++ b/README.md @@ -0,0 +1,163 @@ +# CF Monitor + +基于 Rust、WebAssembly 和 Cloudflare Workers 的轻量服务器监控。单个 Worker 同时提供 API、WebSocket 实时推送和静态前端,D1 保存节点与历史指标,Durable Object 负责实时广播;Linux、Windows 和 macOS agent 同样由 Rust 独立实现。 + +## 能力 + +- CPU/多 GPU、三段负载、内存/Swap、多磁盘容量与 IO、磁盘 await/利用率、网络、连接数、进程和系统信息 +- 独立延迟任务,支持 TCP/ICMP、按节点分配、默认分配给新节点和独立测试周期 +- Komari-Glass 式资产总览、高密度节点卡、可开关的搜索/分组筛选和响应式深浅主题 +- 独立节点详情页,支持实时以及 1/4/24/168/720 小时负载和延迟图表 +- Durable Object WebSocket 实时刷新,支持总览或单节点订阅,断线时 60 秒轮询兜底 +- 节点计费/到期、币种、流量计算口径、公开备注和隐藏控制;后端每日更新汇率并在 D1 保存原子快照,混合币种资产可选 12 种展示币种 +- 用户名+密码管理登录,可选 Cloudflare Turnstile 登录验证和全站盾验证 +- 后台工作区:节点增删改、批量删除、拖拽排序和独立 Agent 密钥轮换 +- 后台安全、外观、主题、告警、数据维护工作区,支持 Telegram 通知测试、离线/到期提醒,以及按节点、指标、时间窗口和平均/持续方式配置的资源告警规则 +- 接入 CFSM Theme Store,支持 Emerald、Pulse、指定 GitHub 构建目录的签名预览、应用和恢复内置主题 +- 可配置总览项、资产折算币种、毛玻璃效果、默认主题、明暗独立背景图、历史保留和私有仪表盘 +- 节点 Agent 采样间隔、批量上报间隔、统计网卡、延迟任务和上下行流量修正可在线下发 +- Agent 默认每 5 秒采样、每 60 秒批量上报;D1 每分钟保存一个代表点,7 天后压缩为小时数据,保留周期可配置 +- 可聚合最多 16 个公开 CF Monitor 站点,远端节点、详情和 WebSocket 数据保持各自来源隔离 +- 公开前端支持简体中文和 English,可配置 favicon、API/WebSocket CORS 来源和第三方主题 CSP 来源 +- 汇率由 Worker 每日从 ER API 拉取、Frankfurter 兜底;失败时保留旧快照并按小时退避重试 +- 独立 Rust Agent,支持 Linux x86_64/ARM64、Windows x86_64 和 macOS ARM64,支持后台按节点开启自动更新 + +## 架构 + +```text +Rust cross-platform agent -- HTTPS report --> Rust Worker (WASM) -- batch write --> D1 + | + +-- push --> Durable Object --> WebSocket clients + | + +-- daily FX refresh -----------> D1 snapshot + | +Browser <-- static assets / REST API -+ +``` + +## 本地开发 + +需要 Bun 1.3+、Rust stable、`wasm32-unknown-unknown` target 和 Cloudflare Wrangler。 + +```bash +rustup target add wasm32-unknown-unknown +bun install +cp .dev.vars.example .dev.vars +bun run dev +``` + +首次运行时,`bun run dev` 会编译 Agent 和两个前端入口,自动创建本地 D1 并应用尚未执行的 migration,然后启动 Worker。访问 `http://localhost:8787`。仅调试前端时运行 `bun run dev:frontend`,Vite 会把 `/api` 代理到 `8787`。使用 `http://localhost:5173/?demo=1` 可查看完整演示数据。 + +## 部署到 Cloudflare + +1. 登录 Cloudflare: + +```bash +bunx wrangler login +``` + +2. 设置初始管理员凭据。生产环境至少设置密码,用户名默认为 `admin`: + +```bash +bunx wrangler secret put ADMIN_USERNAME +bunx wrangler secret put ADMIN_PASSWORD +# 可选:固定会话签名密钥,避免密码修改后旧会话全部失效 +bunx wrangler secret put SESSION_SECRET +# 可选:后台查询 D1/Workers 用量,Token 需要 Account Analytics Read 权限 +bunx wrangler secret put CLOUDFLARE_ACCOUNT_ID +bunx wrangler secret put CLOUDFLARE_API_TOKEN +``` + +3. 部署: + +```bash +bun install +bun run deploy +``` + +Wrangler 4.45+ 的自动资源配置会根据 `wrangler.toml` 中的 `DB` binding 创建并绑定 D1,不需要手动创建数据库或填写 `database_id`。`bun run deploy` 会先检查 binding;仅在部署环境尚未预配置 D1 时,执行一次用于触发自动配置的初始上传;随后应用所有待执行 migration,并完成最终 Worker 部署。Cloudflare Deploy Button 会预先配置资源,普通 CLI 与 Workers Builds 是否需要初始上传则由脚本自动判断。自动资源配置目前仍是 Cloudflare Beta 能力;如需使用既有 D1,再显式填入对应的 `database_id`。参见 [Wrangler 自动资源配置](https://developers.cloudflare.com/workers/wrangler/configuration/)、[Deploy Button](https://developers.cloudflare.com/workers/platform/deploy-buttons/) 和 [D1 migrations](https://developers.cloudflare.com/d1/reference/migrations/)。 + +部署后打开站点,点击右上角管理按钮添加节点。新建节点或重置密钥时会显示平台安装命令:Linux 自动安装 systemd 或 OpenRC 服务,Windows 使用 SYSTEM 启动任务,macOS ARM64 使用 LaunchDaemon;均支持开机启动、异常重启和 Agent 自动更新。TCP 延迟由 Rust 直接测试,ICMP 任务需要系统 `ping`;Linux 的 `ip`、`df` 及各平台的 `nvidia-smi` 是可选采集依赖。 + +Linux 保留 `/proc` 提供的磁盘 IO 和 TCP/UDP 连接统计。Windows 与 macOS 使用跨平台系统接口采集 CPU、内存、Swap、磁盘、网络、进程和系统信息;当前没有可靠统一来源的磁盘 IO 与连接数会上报为 `0`。自动更新优先下载 Worker 同源文件,未提供相应平台文件时回退到 `imengying/CF-Monitor` 最新正式 GitHub Release。 + +第三方主题只替换公开首页,`/admin` 始终使用内置管理界面。应用前会验证 GitHub `tree` 目录中的构建产物,主题资源设置长效浏览器缓存。第三方主题代码与站点同源运行,只应安装可信作者提供的主题。 + +定时任务每 5 分钟检查一次汇率快照,但只有成功数据超过 24 小时才访问上游。ER API 失败时自动尝试 Frankfurter;两者都失败不会清空 D1,下一次尝试至少间隔一小时。后台“监控数据库”工作区可以查看来源、日期和 12 种资产币种,也可以手动刷新。Cloudflare 用量可在同一页面填写 Account ID 和具有 Account Analytics Read 权限的 API Token;输入留空时回退到 `CLOUDFLARE_ACCOUNT_ID` 与 `CLOUDFLARE_API_TOKEN` Worker Secret。 + +后台“通知”工作区可创建最多 20 条资源规则。指标支持 CPU、内存、磁盘、上行和下行;网络阈值单位为 MiB/s。空服务器列表表示应用到全部服务器,也可逐台指定。时间窗口为 1-1440 分钟,“平均”按窗口平均值判断,“持续”要求窗口内所有采样均超过阈值。 + +后台“登录与安全”中的 CORS 列表同时保护 API 和 WebSocket Origin。配置多站点聚合时,应在每个远端站点的允许来源中加入主站 Origin。CSP 外部资源列表用于可信第三方主题所需的脚本、样式、字体和 API 域名。 + +后台“延迟”工作区管理独立任务。每个任务可选择 TCP 或 ICMP、目标、30-3600 秒周期和任意节点;“默认分配给新服务器”只影响以后添加的节点,不会覆盖当前选择。TCP 目标支持域名、IPv4 和 `host:port`,省略端口时使用 443;ICMP 目标只接受域名或 IPv4。每轮测试四次,保存成功 RTT 中位数和丢包率,配置会在 Agent 下一次上报后生效。 + +## GitHub Actions + +推送三段版本 Tag(`1.2.3` 或 `v1.2.3`)时,`release.yml` 会先校验 Tag 与根项目、前端、Worker 和 Agent 的包版本完全一致,再构建 Linux x86_64/ARM64、Windows x86_64、macOS ARM64 Agent 以及公开前端,将产物附加到同一个 GitHub Release。发布前应同步修改四个 `package.version`;GitHub Actions 不编译或部署 Rust/WASM Worker,Worker 仍由 Cloudflare 在代码 push 或同步时构建。 + +登录页和管理面板使用独立前端入口,并在 Rust Worker release 构建时嵌入 WASM;`/admin` 和 `/admin-assets/*` 由 Worker 直接响应,不受第三方公开主题影响。直接执行 Worker 构建前应先运行 `bun run build:frontend`,项目提供的构建脚本会自动保证该顺序。 + +## Cloudflare Git 部署 + +Worker 由 Cloudflare Workers Builds 直接从 GitHub 仓库构建。创建仓库并推送代码后,在 Cloudflare Workers 中导入 `imengying/CF-Monitor`,使用以下设置: + +- Worker 名称:`cf-monitor` +- 生产分支:`main` +- 根目录:`/` +- 构建命令:留空 +- 部署命令:`bun run deploy` + +`bun run deploy` 会依次编译 Rust Agent、公开前端、嵌入式管理端,确保 D1 已自动配置,应用 migration,最后编译和发布 Rust/WASM Worker。以后每次 push 到 `main`,以及 Cloudflare 同步到新的提交时,都会重复同一套可幂等流程,不再需要单独的 D1 初始化步骤。 + +项目尚未发布,数据库和 Agent 协议按当前版本直接演进,不包含旧 schema 或旧单样本上报协议的兼容层。开发期间若初始 schema 发生变化,请重新创建未投入使用的 D1 数据库后执行唯一的 `0001_initial.sql`。 + +## 配置 + +`wrangler.toml` 中可调整: + +| 变量 | 默认值 | 说明 | +| --- | ---: | --- | +| `SITE_NAME` | `CF Monitor` | 初始站点名,可在管理端覆盖 | +| `OFFLINE_THRESHOLD_SECONDS` | `180` | 初始离线判定秒数 | +| `HISTORY_RETENTION_DAYS` | `30` | 历史保留天数,范围 1-365 | + +管理凭据使用 Worker secrets 或后台“安全”工作区保存,不要写进 `wrangler.toml`。开启 Turnstile 后,在“安全”工作区填写站点密钥和密钥;服务端会调用 Cloudflare Siteverify 校验令牌。部署前请把本地测试密钥替换为正式密钥。 + +本地调试可以使用 Cloudflare 官方 Turnstile 测试站点密钥和密钥;测试令牌固定为 `XXXX.DUMMY.TOKEN.XXXX`,只适用于开发环境。 + +## API + +| 方法 | 路径 | 用途 | +| --- | --- | --- | +| `GET` | `/api/config` | 公开站点配置 | +| `GET` | `/api/themes` | 第三方主题商店 | +| `GET` | `/api/exchange-rates` | D1 中的 CNY 基准汇率快照 | +| `GET` | `/api/servers` | 公开节点和最新指标 | +| `GET` | `/api/history/:id?hours=24` | 节点历史 | +| `GET` | `/api/latency/:id?hours=24` | 节点延迟任务历史 | +| `GET` | `/api/ws` | 实时 WebSocket | +| `POST` | `/api/agent/report` | 探针上报,节点 Bearer token | +| `POST` | `/api/admin/login` | 管理登录 | +| `GET/POST` | `/api/admin/servers` | 管理节点 | +| `GET/POST` | `/api/admin/latency-tasks` | 查询或创建延迟任务 | +| `PATCH/DELETE` | `/api/admin/latency-tasks/:id` | 编辑或删除延迟任务 | +| `GET/POST` | `/api/admin/alert-rules` | 查询或创建资源告警规则 | +| `PATCH/DELETE` | `/api/admin/alert-rules/:id` | 编辑或删除资源告警规则 | +| `PATCH/DELETE` | `/api/admin/servers/:id` | 编辑或删除节点 | +| `PATCH` | `/api/admin/servers/order` | 更新全部节点顺序 | +| `POST` | `/api/admin/servers/:id/token` | 轮换探针密钥 | +| `DELETE` | `/api/admin/servers` | 批量删除节点 | +| `GET/PATCH` | `/api/admin/settings` | 读写站点与展示设置 | +| `GET` | `/api/admin/theme-settings` | 读取当前前端提供的主题设置描述 | +| `POST` | `/api/admin/themes/preview` | 创建短时签名主题预览 | +| `POST` | `/api/turnstile/verify` | 校验全站 Turnstile 令牌 | +| `GET` | `/api/admin/database` | 查询 D1 节点、历史和数据库统计 | +| `GET` | `/api/admin/cloudflare-usage` | 查询 UTC 今日/昨日的 D1 与 Workers 用量 | +| `POST` | `/api/admin/exchange-rates/refresh` | 立即拉取并更新 D1 汇率快照 | +| `DELETE` | `/api/admin/history` | 清理历史指标 | +| `POST` | `/api/admin/notifications/test` | 发送一条通知测试消息 | + +第三方主题可在构建目录根部提供 `theme-settings.json`。文件使用 `{ "schema": 1, "settings": [...] }`,字段支持 `text`、`textarea`、`url`、`color`、`select`、`toggle` 和 `number` 类型。后台会按描述生成控件,保存值通过 `/api/config` 的 `theme_options` 返回给主题。 + +## License + +MIT diff --git a/agent/Cargo.lock b/agent/Cargo.lock new file mode 100644 index 0000000..5729bcd --- /dev/null +++ b/agent/Cargo.lock @@ -0,0 +1,1032 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cf-monitor-agent" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "sysinfo", + "ureq", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[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 = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "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 = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sysinfo" +version = "0.39.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "log", + "native-tls", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.9", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/agent/Cargo.toml b/agent/Cargo.toml new file mode 100644 index 0000000..34bf3ad --- /dev/null +++ b/agent/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "cf-monitor-agent" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "Independent Rust telemetry agent for CF Monitor" +repository = "https://github.com/imengying/CF-Monitor" + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[target.'cfg(target_os = "linux")'.dependencies] +ureq = { version = "2.12", default-features = false, features = ["json", "tls"] } + +[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies] +sysinfo = { version = "0.39.6", default-features = false, features = ["disk", "network", "system"] } +ureq = { version = "2.12", default-features = false, features = ["json", "native-tls"] } + +[profile.release] +codegen-units = 1 +lto = true +opt-level = "s" +strip = true diff --git a/agent/agent.sh b/agent/agent.sh new file mode 100644 index 0000000..72dd85d --- /dev/null +++ b/agent/agent.sh @@ -0,0 +1,125 @@ +#!/bin/sh +set -eu + +SERVICE_NAME="cf-monitor-agent" +INSTALL_DIR="/opt/cf-monitor" +AGENT_FILE="$INSTALL_DIR/agent" +CONFIG_FILE="$INSTALL_DIR/agent.env" +SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service" +OPENRC_FILE="/etc/init.d/$SERVICE_NAME" + +usage() { + echo "Usage: agent.sh install --server-id ID --token TOKEN --url WORKER_URL [--interval 60] [--collect-interval 5]" + echo " agent.sh uninstall" +} + +arg() { + key="$1"; shift + while [ "$#" -gt 1 ]; do + [ "$1" = "$key" ] && { printf '%s' "$2"; return 0; } + shift + done + return 1 +} + +safe_value() { + case "$1" in *[!A-Za-z0-9_./:@-]*|'') return 1 ;; esac +} + +install_agent() { + [ "$(id -u)" -eq 0 ] || { echo "Run install as root" >&2; exit 1; } + command -v curl >/dev/null || { echo "curl is required" >&2; exit 1; } + server_id=$(arg --server-id "$@") || { usage; exit 1; } + token=$(arg --token "$@") || { usage; exit 1; } + worker_url=$(arg --url "$@") || { usage; exit 1; } + interval=$(arg --interval "$@" || printf '60') + collect_interval=$(arg --collect-interval "$@" || printf '5') + worker_url=${worker_url%/} + safe_value "$server_id" && safe_value "$token" && safe_value "$worker_url" || { echo "Invalid install argument" >&2; exit 1; } + case "$interval" in ''|*[!0-9]*) echo "Invalid interval" >&2; exit 1 ;; esac + [ "$interval" -ge 15 ] && [ "$interval" -le 3600 ] || { echo "Interval must be between 15 and 3600 seconds" >&2; exit 1; } + case "$collect_interval" in ''|*[!0-9]*) echo "Invalid collect interval" >&2; exit 1 ;; esac + [ "$collect_interval" -ge 2 ] && [ "$collect_interval" -le 60 ] && [ "$collect_interval" -le "$interval" ] || { echo "Collect interval must be between 2 and 60 seconds and no greater than report interval" >&2; exit 1; } + case "$(uname -m)" in x86_64|amd64) arch="x86_64" ;; aarch64|arm64) arch="aarch64" ;; *) echo "Unsupported architecture" >&2; exit 1 ;; esac + + mkdir -p "$INSTALL_DIR" + temporary="$INSTALL_DIR/agent.download" + if ! curl --fail --silent --show-error --max-time 120 "$worker_url/agent-linux-$arch" -o "$temporary"; then + curl --fail --location --silent --show-error --max-time 120 \ + "https://github.com/imengying/CF-Monitor/releases/latest/download/agent-linux-$arch" \ + -o "$temporary" + fi + chmod 755 "$temporary" + "$temporary" version >/dev/null + mv "$temporary" "$AGENT_FILE" + { + printf 'SERVER_ID=%s\n' "$server_id" + printf 'AGENT_TOKEN=%s\n' "$token" + printf 'WORKER_URL=%s\n' "$worker_url" + printf 'REPORT_INTERVAL=%s\n' "$interval" + printf 'COLLECT_INTERVAL=%s\n' "$collect_interval" + printf 'NETWORK_INTERFACE=\n' + } > "$CONFIG_FILE" + chmod 600 "$CONFIG_FILE" + + if command -v systemctl >/dev/null 2>&1; then + printf '%s\n' \ + '[Unit]' \ + 'Description=CF Monitor Rust Agent' \ + 'After=network-online.target' \ + 'Wants=network-online.target' \ + '' \ + '[Service]' \ + 'Type=simple' \ + "EnvironmentFile=$CONFIG_FILE" \ + "ExecStart=$AGENT_FILE run" \ + 'Restart=always' \ + 'RestartSec=10' \ + 'NoNewPrivileges=true' \ + 'PrivateTmp=true' \ + 'ProtectHome=true' \ + 'ProtectSystem=strict' \ + "ReadWritePaths=$INSTALL_DIR" \ + '' \ + '[Install]' \ + 'WantedBy=multi-user.target' > "$SERVICE_FILE" + systemctl daemon-reload + systemctl enable --now "$SERVICE_NAME" + elif command -v rc-service >/dev/null 2>&1; then + printf '%s\n' \ + '#!/sbin/openrc-run' \ + "name=\"$SERVICE_NAME\"" \ + "command=\"$AGENT_FILE\"" \ + 'command_args="run"' \ + "command_user=\"root\"" \ + "supervisor=\"supervise-daemon\"" \ + "respawn_delay=10" \ + "export \$(cat $CONFIG_FILE | xargs)" \ + 'depend() { need net; }' > "$OPENRC_FILE" + chmod 755 "$OPENRC_FILE" + rc-update add "$SERVICE_NAME" default + rc-service "$SERVICE_NAME" restart + else + echo "systemd or OpenRC is required" >&2 + exit 1 + fi + echo "CF Monitor Rust agent installed." +} + +uninstall_agent() { + [ "$(id -u)" -eq 0 ] || { echo "Run uninstall as root" >&2; exit 1; } + systemctl disable --now "$SERVICE_NAME" 2>/dev/null || true + rc-service "$SERVICE_NAME" stop 2>/dev/null || true + rc-update del "$SERVICE_NAME" default 2>/dev/null || true + rm -f "$SERVICE_FILE" + rm -f "$OPENRC_FILE" + systemctl daemon-reload 2>/dev/null || true + rm -rf "$INSTALL_DIR" + echo "CF Monitor agent removed." +} + +case "${1:-}" in + install) shift; install_agent "$@" ;; + uninstall) uninstall_agent ;; + *) usage; exit 1 ;; +esac diff --git a/agent/install-macos.sh b/agent/install-macos.sh new file mode 100644 index 0000000..5488f0e --- /dev/null +++ b/agent/install-macos.sh @@ -0,0 +1,69 @@ +#!/bin/sh +set -eu + +LABEL="com.cfmonitor.agent" +INSTALL_DIR="/usr/local/libexec/cf-monitor" +AGENT_FILE="$INSTALL_DIR/agent" +PLIST_FILE="/Library/LaunchDaemons/$LABEL.plist" + +arg() { + key="$1"; shift + while [ "$#" -gt 1 ]; do + [ "$1" = "$key" ] && { printf '%s' "$2"; return 0; } + shift + done + return 1 +} + +safe_value() { + case "$1" in *[!A-Za-z0-9_./:@-]*|'') return 1 ;; esac +} + +if [ "${1:-}" = "uninstall" ]; then + [ "$(id -u)" -eq 0 ] || { echo "Run as root" >&2; exit 1; } + launchctl bootout system "$PLIST_FILE" 2>/dev/null || true + rm -f "$PLIST_FILE" "$AGENT_FILE" + rmdir "$INSTALL_DIR" 2>/dev/null || true + echo "CF Monitor Agent removed." + exit 0 +fi + +[ "${1:-}" = "install" ] || { echo "Usage: install-macos.sh install --server-id ID --token TOKEN --url URL" >&2; exit 1; } +shift +[ "$(id -u)" -eq 0 ] || { echo "Run as root" >&2; exit 1; } +server_id=$(arg --server-id "$@") || exit 1 +token=$(arg --token "$@") || exit 1 +worker_url=$(arg --url "$@") || exit 1 +interval=$(arg --interval "$@" || printf '60') +collect_interval=$(arg --collect-interval "$@" || printf '5') +worker_url=${worker_url%/} +safe_value "$server_id" && safe_value "$token" && safe_value "$worker_url" || { echo "Invalid argument" >&2; exit 1; } + +mkdir -p "$INSTALL_DIR" +curl -fL "https://github.com/imengying/CF-Monitor/releases/latest/download/agent-macos-aarch64" -o "$AGENT_FILE.download" +chmod 755 "$AGENT_FILE.download" +"$AGENT_FILE.download" version >/dev/null +mv "$AGENT_FILE.download" "$AGENT_FILE" + +cat > "$PLIST_FILE" < + + +Label$LABEL +ProgramArguments$AGENT_FILErun +EnvironmentVariables +SERVER_ID$server_id +AGENT_TOKEN$token +WORKER_URL$worker_url +REPORT_INTERVAL$interval +COLLECT_INTERVAL$collect_interval + +KeepAliveRunAtLoad +StandardOutPath/var/log/cf-monitor-agent.log +StandardErrorPath/var/log/cf-monitor-agent.log + +EOF +chmod 600 "$PLIST_FILE" +launchctl bootout system "$PLIST_FILE" 2>/dev/null || true +launchctl bootstrap system "$PLIST_FILE" +echo "CF Monitor Agent installed and started." diff --git a/agent/install.ps1 b/agent/install.ps1 new file mode 100644 index 0000000..f62d35e --- /dev/null +++ b/agent/install.ps1 @@ -0,0 +1,65 @@ +param( + [Parameter(Position = 0)][ValidateSet("install", "uninstall", "status")][string]$Action = "install", + [string]$ServerId, + [string]$Token, + [string]$Url, + [ValidateRange(15, 3600)][int]$Interval = 60, + [ValidateRange(2, 60)][int]$CollectInterval = 5 +) + +$ErrorActionPreference = "Stop" +$TaskName = "CF Monitor Agent" +$InstallDir = Join-Path $env:ProgramData "CFMonitor" +$AgentFile = Join-Path $InstallDir "cf-monitor-agent.exe" +$RunnerFile = Join-Path $InstallDir "run.ps1" + +function Assert-Safe([string]$Name, [string]$Value) { + if ([string]::IsNullOrWhiteSpace($Value) -or $Value -notmatch '^[A-Za-z0-9_./:@-]+$') { + throw "Invalid $Name" + } +} + +if ($Action -eq "uninstall") { + Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $InstallDir -Recurse -Force -ErrorAction SilentlyContinue + Write-Host "CF Monitor Agent removed." + exit 0 +} + +if ($Action -eq "status") { + Get-ScheduledTask -TaskName $TaskName + exit 0 +} + +if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + throw "Run PowerShell as Administrator" +} +Assert-Safe "ServerId" $ServerId +Assert-Safe "Token" $Token +Assert-Safe "Url" $Url +$Url = $Url.TrimEnd('/') +$CollectInterval = [Math]::Min($CollectInterval, $Interval) + +New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null +& icacls.exe $InstallDir /inheritance:r /grant:r 'SYSTEM:(OI)(CI)F' 'Administrators:(OI)(CI)F' | Out-Null +$Download = "https://github.com/imengying/CF-Monitor/releases/latest/download/agent-windows-x86_64.exe" +Invoke-WebRequest -Uri $Download -OutFile "$AgentFile.download" +& "$AgentFile.download" version | Out-Null +Move-Item -LiteralPath "$AgentFile.download" -Destination $AgentFile -Force + +@" +`$env:SERVER_ID='$ServerId' +`$env:AGENT_TOKEN='$Token' +`$env:WORKER_URL='$Url' +`$env:REPORT_INTERVAL='$Interval' +`$env:COLLECT_INTERVAL='$CollectInterval' +& '$AgentFile' run +"@ | Set-Content -LiteralPath $RunnerFile -Encoding UTF8 + +$TaskAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$RunnerFile`"" +$Trigger = New-ScheduledTaskTrigger -AtStartup +$Settings = New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit (New-TimeSpan -Days 3650) +$Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest +Register-ScheduledTask -TaskName $TaskName -Action $TaskAction -Trigger $Trigger -Settings $Settings -Principal $Principal -Force | Out-Null +Start-ScheduledTask -TaskName $TaskName +Write-Host "CF Monitor Agent installed and started." diff --git a/agent/src/main.rs b/agent/src/main.rs new file mode 100644 index 0000000..ff95a60 --- /dev/null +++ b/agent/src/main.rs @@ -0,0 +1,1226 @@ +use std::collections::HashMap; +use std::env; +use std::fs; +use std::io::{self, Read, Write}; +#[cfg(any(target_os = "windows", target_os = "macos"))] +use std::net::UdpSocket; +use std::net::{TcpStream, ToSocketAddrs}; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +#[cfg(unix)] +use std::os::unix::process::CommandExt; +use std::path::Path; +use std::process::Command; +#[cfg(target_os = "windows")] +use std::process::Stdio; +use std::thread; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use serde::{Deserialize, Serialize}; +#[cfg(any(target_os = "windows", target_os = "macos"))] +use sysinfo::{Disks, Networks, System}; + +#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))] +compile_error!("cf-monitor-agent supports Linux, Windows, and macOS"); + +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const RELEASE_DOWNLOAD_BASE: &str = + "https://github.com/imengying/CF-Monitor/releases/latest/download"; +const PROBE_ATTEMPTS: usize = 4; +const PROBE_TIMEOUT: Duration = Duration::from_secs(1); +const MAX_AGENT_BINARY_BYTES: u64 = 64 * 1024 * 1024; + +type Error = Box; +type Result = std::result::Result; + +#[derive(Debug, Clone)] +struct RuntimeConfig { + server_id: String, + token: String, + worker_url: String, + report_interval: u64, + collect_interval: u64, + network_interface: String, + latency_tasks: Vec, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +struct LatencyTask { + id: String, + name: String, + task_type: String, + target: String, + interval_seconds: u64, +} + +#[derive(Debug, Clone, Serialize)] +struct LatencyResult { + task_id: String, + timestamp: i64, + latency_ms: f64, + packet_loss: f64, +} + +#[derive(Debug, Default, Deserialize)] +struct RemoteConfig { + #[serde(default)] + report_interval: u64, + #[serde(default)] + collect_interval: u64, + #[serde(default)] + network_interface: String, + auto_update: i64, + #[serde(default)] + latest_agent_version: String, + #[serde(default)] + latency_tasks: Vec, +} + +#[derive(Debug, Deserialize)] +struct ReportResponse { + config: Option, +} + +#[derive(Debug, Serialize)] +struct ReportBatch<'a> { + server_id: &'a str, + samples: &'a [Report], +} + +#[derive(Debug, Default, Clone, Serialize)] +struct DiskMetric { + name: String, + mount_point: String, + used: i64, + total: i64, + read_bps: f64, + write_bps: f64, + read_iops: f64, + write_iops: f64, + await_ms: f64, + utilization: f64, +} + +#[derive(Debug, Default, Clone, Serialize)] +struct GpuMetric { + model: String, + usage: f64, + memory_used: i64, + memory_total: i64, +} + +#[derive(Debug, Default, Serialize)] +struct Report { + timestamp: i64, + cpu: f64, + load1: f64, + load5: f64, + load15: f64, + mem_used: i64, + mem_total: i64, + swap_used: i64, + swap_total: i64, + disk_used: i64, + disk_total: i64, + net_in: f64, + net_out: f64, + net_rx_total: i64, + net_tx_total: i64, + uptime: i64, + processes: i64, + tcp_connections: i64, + udp_connections: i64, + cpu_cores: i64, + cpu_model: String, + os: String, + kernel: String, + arch: String, + virtualization: String, + ipv4: String, + ipv6: String, + gpu_usage: f64, + gpu_model: String, + agent_version: String, + disk_read_bps: f64, + disk_write_bps: f64, + disk_read_iops: f64, + disk_write_iops: f64, + disk_await_ms: f64, + disk_utilization: f64, + disks: Vec, + gpus: Vec, + message: String, + latency_results: Vec, +} + +#[cfg(target_os = "linux")] +#[derive(Debug, Default, Clone, Copy)] +struct CpuSample { + total: u64, + idle: u64, +} + +#[cfg(target_os = "linux")] +#[derive(Debug, Default, Clone, Copy)] +struct IoSample { + rx: u64, + tx: u64, + read_ops: u64, + read_sectors: u64, + write_ops: u64, + write_sectors: u64, + read_millis: u64, + write_millis: u64, + io_millis: u64, +} + +#[cfg(target_os = "linux")] +fn text(path: impl AsRef) -> String { + fs::read_to_string(path).unwrap_or_default() +} + +fn command(name: &str, args: &[&str]) -> String { + Command::new(name) + .args(args) + .output() + .ok() + .filter(|output| output.status.success()) + .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string()) + .unwrap_or_default() +} + +#[cfg(target_os = "linux")] +fn cpu_sample() -> CpuSample { + let line = text("/proc/stat").lines().next().unwrap_or("").to_string(); + let values = line + .split_whitespace() + .skip(1) + .filter_map(|value| value.parse::().ok()) + .collect::>(); + CpuSample { + total: values.iter().sum(), + idle: values.get(3).copied().unwrap_or(0) + values.get(4).copied().unwrap_or(0), + } +} + +fn selected_interface(name: &str, filter: &str) -> bool { + if filter.trim().is_empty() { + name != "lo" && !name.to_ascii_lowercase().contains("loopback") + } else { + filter.split(',').any(|value| value.trim() == name) + } +} + +#[cfg(target_os = "linux")] +fn disk_device(name: &str) -> bool { + ((name.starts_with("sd") || name.starts_with("vd") || name.starts_with("xvd")) + && name + .chars() + .last() + .is_some_and(|value| value.is_ascii_alphabetic())) + || (name.starts_with("nvme") && name.contains('n') && !name.contains('p')) + || (name.starts_with("mmcblk") && !name.contains('p')) +} + +#[cfg(target_os = "linux")] +fn io_sample(filter: &str) -> IoSample { + let mut sample = IoSample::default(); + for line in text("/proc/net/dev") + .lines() + .filter(|line| line.contains(':')) + { + let Some((name, values)) = line.split_once(':') else { + continue; + }; + if !selected_interface(name.trim(), filter) { + continue; + } + let fields = values.split_whitespace().collect::>(); + sample.rx = sample + .rx + .saturating_add(fields.first().and_then(|v| v.parse().ok()).unwrap_or(0)); + sample.tx = sample + .tx + .saturating_add(fields.get(8).and_then(|v| v.parse().ok()).unwrap_or(0)); + } + for line in text("/proc/diskstats").lines() { + let fields = line.split_whitespace().collect::>(); + if fields.len() < 13 || !disk_device(fields[2]) { + continue; + } + sample.read_ops = sample + .read_ops + .saturating_add(fields[3].parse().unwrap_or(0)); + sample.read_sectors = sample + .read_sectors + .saturating_add(fields[5].parse().unwrap_or(0)); + sample.write_ops = sample + .write_ops + .saturating_add(fields[7].parse().unwrap_or(0)); + sample.write_sectors = sample + .write_sectors + .saturating_add(fields[9].parse().unwrap_or(0)); + sample.read_millis = sample + .read_millis + .saturating_add(fields[6].parse().unwrap_or(0)); + sample.write_millis = sample + .write_millis + .saturating_add(fields[10].parse().unwrap_or(0)); + sample.io_millis = sample + .io_millis + .saturating_add(fields[12].parse().unwrap_or(0)); + } + sample +} + +#[cfg(target_os = "linux")] +fn mem_value(contents: &str, key: &str) -> i64 { + contents + .lines() + .find(|line| line.starts_with(key)) + .and_then(|line| line.split_whitespace().nth(1)) + .and_then(|value| value.parse::().ok()) + .unwrap_or(0) + .saturating_mul(1024) +} + +#[cfg(target_os = "linux")] +fn file_line_count(path: &str) -> i64 { + text(path).lines().skip(1).count() as i64 +} + +#[cfg(target_os = "linux")] +fn disk_usage() -> Vec { + let output = command( + "df", + &[ + "-B1", + "-l", + "--output=source,target,size,used", + "-x", + "tmpfs", + "-x", + "devtmpfs", + ], + ); + output + .lines() + .skip(1) + .filter_map(|line| { + let fields = line.split_whitespace().collect::>(); + if fields.len() < 4 { + return None; + } + let total = fields[2].parse::().ok()?; + let used = fields[3].parse::().ok()?; + (total > 0).then(|| DiskMetric { + name: fields[0].to_string(), + mount_point: fields[1].to_string(), + used, + total, + ..DiskMetric::default() + }) + }) + .collect() +} + +#[cfg(target_os = "linux")] +fn os_name() -> String { + text("/etc/os-release") + .lines() + .find_map(|line| line.strip_prefix("PRETTY_NAME=")) + .map(|value| value.trim_matches('"').to_string()) + .unwrap_or_else(|| command("uname", &["-s"])) +} + +#[cfg(target_os = "linux")] +fn cpu_model() -> String { + text("/proc/cpuinfo") + .lines() + .find_map(|line| { + let (key, value) = line.split_once(':')?; + matches!(key.trim(), "model name" | "Hardware").then(|| value.trim().to_string()) + }) + .unwrap_or_default() +} + +#[cfg(target_os = "linux")] +fn ip_address(family: &str) -> String { + command("ip", &["-o", family, "addr", "show", "scope", "global"]) + .lines() + .next() + .and_then(|line| line.split_whitespace().find(|value| value.contains('/'))) + .and_then(|value| value.split('/').next()) + .unwrap_or("") + .to_string() +} + +fn valid_probe_host(host: &str) -> bool { + if host.is_empty() || host.len() > 50 || host.starts_with('.') || host.ends_with('.') { + return false; + } + let labels = host.split('.').collect::>(); + let ipv4_like = labels.len() == 4 + && labels + .iter() + .all(|label| !label.is_empty() && label.chars().all(|c| c.is_ascii_digit())); + if ipv4_like { + return labels.iter().all(|label| label.parse::().is_ok()); + } + labels.iter().all(|label| { + !label.is_empty() + && label.chars().all(|character| { + character.is_ascii_alphanumeric() || matches!(character, '_' | '-') + }) + && label + .chars() + .next() + .is_some_and(|character| character.is_ascii_alphanumeric() || character == '_') + && label + .chars() + .last() + .is_some_and(|character| character.is_ascii_alphanumeric() || character == '_') + }) +} + +fn parse_probe_target(value: &str) -> Option<(String, u16)> { + let raw = value.trim(); + if raw.is_empty() + || raw.len() > 60 + || raw.contains("://") + || raw + .chars() + .any(|character| character.is_whitespace() || "/@?#\\[]".contains(character)) + || raw.matches(':').count() > 1 + { + return None; + } + let (host, port) = raw.split_once(':').map_or((raw, 443), |(host, port)| { + (host, port.parse::().unwrap_or(0)) + }); + (port > 0 && valid_probe_host(host)).then(|| (host.to_ascii_lowercase(), port)) +} + +fn median(values: &mut [f64]) -> f64 { + values.sort_by(f64::total_cmp); + let middle = values.len() / 2; + if values.len().is_multiple_of(2) { + (values[middle - 1] + values[middle]) / 2.0 + } else { + values[middle] + } +} + +fn tcp_latency_probe(target: &str) -> (f64, f64) { + if target.trim().is_empty() { + return (-1.0, -1.0); + } + let Some((host, port)) = parse_probe_target(target) else { + return (-1.0, 100.0); + }; + let Ok(addresses) = (host.as_str(), port).to_socket_addrs() else { + return (-1.0, 100.0); + }; + let addresses = addresses.collect::>(); + let Some(address) = addresses + .iter() + .find(|address| address.is_ipv4()) + .or_else(|| addresses.first()) + .copied() + else { + return (-1.0, 100.0); + }; + + let mut latencies = Vec::with_capacity(PROBE_ATTEMPTS); + for _ in 0..PROBE_ATTEMPTS { + let started = Instant::now(); + if TcpStream::connect_timeout(&address, PROBE_TIMEOUT).is_ok() { + latencies.push(started.elapsed().as_secs_f64() * 1000.0); + } + } + let loss = (PROBE_ATTEMPTS - latencies.len()) as f64 * 100.0 / PROBE_ATTEMPTS as f64; + if latencies.is_empty() { + (-1.0, loss) + } else { + (median(&mut latencies), loss) + } +} + +fn ping_latency(output: &str) -> Option { + let marker = output + .find("time=") + .map(|index| (index + 5, false)) + .or_else(|| output.find("time<").map(|index| (index + 5, true)))?; + if marker.1 { + return Some(0.5); + } + let value = output[marker.0..] + .chars() + .take_while(|character| character.is_ascii_digit() || *character == '.') + .collect::(); + value.parse().ok() +} + +fn icmp_latency_probe(target: &str) -> (f64, f64) { + let host = target.trim(); + if host.contains(':') || !valid_probe_host(host) { + return (-1.0, 100.0); + } + let mut latencies = Vec::with_capacity(PROBE_ATTEMPTS); + for _ in 0..PROBE_ATTEMPTS { + let started = Instant::now(); + let mut ping = Command::new("ping"); + #[cfg(target_os = "linux")] + ping.args(["-n", "-c", "1", "-W", "1", host]); + #[cfg(target_os = "macos")] + ping.args(["-n", "-c", "1", "-W", "1000", host]); + #[cfg(target_os = "windows")] + ping.args(["-n", "1", "-w", "1000", host]); + let output = ping.output(); + if let Ok(output) = output { + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + latencies.push( + ping_latency(&stdout) + .unwrap_or_else(|| started.elapsed().as_secs_f64() * 1000.0), + ); + } + } + } + let loss = (PROBE_ATTEMPTS - latencies.len()) as f64 * 100.0 / PROBE_ATTEMPTS as f64; + if latencies.is_empty() { + (-1.0, loss) + } else { + (median(&mut latencies), loss) + } +} + +fn unix_timestamp() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64 +} + +fn execute_latency_tasks(tasks: &[LatencyTask]) -> Vec { + let mut results = Vec::with_capacity(tasks.len()); + thread::scope(|scope| { + let handles = tasks + .iter() + .cloned() + .map(|task| { + scope.spawn(move || { + let (latency_ms, packet_loss) = match task.task_type.as_str() { + "tcp" => tcp_latency_probe(&task.target), + "icmp" => icmp_latency_probe(&task.target), + _ => (-1.0, 100.0), + }; + LatencyResult { + task_id: task.id, + timestamp: unix_timestamp(), + latency_ms, + packet_loss, + } + }) + }) + .collect::>(); + for handle in handles { + if let Ok(result) = handle.join() { + results.push(result); + } + } + }); + results +} + +fn gpu_info() -> Vec { + let gpu = command( + "nvidia-smi", + &[ + "--query-gpu=utilization.gpu,name,memory.used,memory.total", + "--format=csv,noheader,nounits", + ], + ); + gpu.lines() + .filter_map(|line| { + let fields = line.split(',').map(str::trim).collect::>(); + if fields.len() < 4 { + return None; + } + Some(GpuMetric { + usage: fields[0].parse().unwrap_or(0.0), + model: fields[1].to_string(), + memory_used: fields[2] + .parse::() + .unwrap_or(0) + .saturating_mul(1024 * 1024), + memory_total: fields[3] + .parse::() + .unwrap_or(0) + .saturating_mul(1024 * 1024), + }) + }) + .collect() +} + +#[cfg(any(target_os = "windows", target_os = "macos"))] +fn local_ip(bind: &str, destination: &str) -> String { + UdpSocket::bind(bind) + .and_then(|socket| { + socket.connect(destination)?; + socket.local_addr() + }) + .map(|address| address.ip().to_string()) + .unwrap_or_default() +} + +#[cfg(any(target_os = "windows", target_os = "macos"))] +fn u64_to_i64(value: u64) -> i64 { + value.min(i64::MAX as u64) as i64 +} + +#[cfg(target_os = "linux")] +fn collect(config: &RuntimeConfig, latency_results: Vec) -> Report { + let cpu_before = cpu_sample(); + let io_before = io_sample(&config.network_interface); + thread::sleep(Duration::from_secs(1)); + let cpu_after = cpu_sample(); + let io_after = io_sample(&config.network_interface); + let total = cpu_after.total.saturating_sub(cpu_before.total); + let idle = cpu_after.idle.saturating_sub(cpu_before.idle); + let cpu = if total == 0 { + 0.0 + } else { + (total.saturating_sub(idle)) as f64 * 100.0 / total as f64 + }; + + let mem = text("/proc/meminfo"); + let mem_total = mem_value(&mem, "MemTotal:"); + let mem_used = mem_total.saturating_sub(mem_value(&mem, "MemAvailable:")); + let swap_total = mem_value(&mem, "SwapTotal:"); + let swap_used = swap_total.saturating_sub(mem_value(&mem, "SwapFree:")); + let loads = text("/proc/loadavg") + .split_whitespace() + .take(3) + .filter_map(|value| value.parse::().ok()) + .collect::>(); + let uptime = text("/proc/uptime") + .split_whitespace() + .next() + .and_then(|value| value.parse::().ok()) + .unwrap_or(0.0) as i64; + let processes = fs::read_dir("/proc") + .map(|items| { + items + .filter_map(|item| item.ok()) + .filter(|item| { + item.file_name() + .to_string_lossy() + .chars() + .all(|ch| ch.is_ascii_digit()) + }) + .count() as i64 + }) + .unwrap_or(0); + let disks = disk_usage(); + let disk_used = disks.iter().map(|disk| disk.used).sum(); + let disk_total = disks.iter().map(|disk| disk.total).sum(); + let gpus = gpu_info(); + let gpu_usage = if gpus.is_empty() { + 0.0 + } else { + gpus.iter().map(|gpu| gpu.usage).sum::() / gpus.len() as f64 + }; + let gpu_model = gpus + .iter() + .map(|gpu| gpu.model.as_str()) + .collect::>() + .join(" · "); + let read_ops = io_after.read_ops.saturating_sub(io_before.read_ops); + let write_ops = io_after.write_ops.saturating_sub(io_before.write_ops); + let total_ops = read_ops.saturating_add(write_ops); + let io_wait = io_after + .read_millis + .saturating_sub(io_before.read_millis) + .saturating_add(io_after.write_millis.saturating_sub(io_before.write_millis)); + + Report { + timestamp: unix_timestamp(), + cpu, + load1: loads.first().copied().unwrap_or(0.0), + load5: loads.get(1).copied().unwrap_or(0.0), + load15: loads.get(2).copied().unwrap_or(0.0), + mem_used, + mem_total, + swap_used, + swap_total, + disk_used, + disk_total, + net_in: io_after.rx.saturating_sub(io_before.rx) as f64, + net_out: io_after.tx.saturating_sub(io_before.tx) as f64, + net_rx_total: io_after.rx.min(i64::MAX as u64) as i64, + net_tx_total: io_after.tx.min(i64::MAX as u64) as i64, + uptime, + processes, + tcp_connections: file_line_count("/proc/net/tcp") + file_line_count("/proc/net/tcp6"), + udp_connections: file_line_count("/proc/net/udp") + file_line_count("/proc/net/udp6"), + cpu_cores: thread::available_parallelism() + .map(|value| value.get() as i64) + .unwrap_or(1), + cpu_model: cpu_model(), + os: os_name(), + kernel: command("uname", &["-r"]), + arch: env::consts::ARCH.to_string(), + virtualization: command("systemd-detect-virt", &[]), + ipv4: ip_address("-4"), + ipv6: ip_address("-6"), + gpu_usage, + gpu_model, + agent_version: VERSION.to_string(), + message: env::var("AGENT_MESSAGE").unwrap_or_default(), + disk_read_bps: io_after + .read_sectors + .saturating_sub(io_before.read_sectors) + .saturating_mul(512) as f64, + disk_write_bps: io_after + .write_sectors + .saturating_sub(io_before.write_sectors) + .saturating_mul(512) as f64, + disk_read_iops: io_after.read_ops.saturating_sub(io_before.read_ops) as f64, + disk_write_iops: io_after.write_ops.saturating_sub(io_before.write_ops) as f64, + disk_await_ms: if total_ops == 0 { + 0.0 + } else { + io_wait as f64 / total_ops as f64 + }, + disk_utilization: (io_after.io_millis.saturating_sub(io_before.io_millis) as f64 / 10.0) + .clamp(0.0, 100.0), + disks, + gpus, + latency_results, + } +} + +#[cfg(any(target_os = "windows", target_os = "macos"))] +fn collect(config: &RuntimeConfig, latency_results: Vec) -> Report { + let mut system = System::new_all(); + let mut networks = Networks::new_with_refreshed_list(); + thread::sleep(Duration::from_secs(1)); + system.refresh_all(); + networks.refresh(true); + + let mut net_in = 0_u64; + let mut net_out = 0_u64; + let mut net_rx_total = 0_u64; + let mut net_tx_total = 0_u64; + for (name, network) in &networks { + if !selected_interface(name, &config.network_interface) { + continue; + } + net_in = net_in.saturating_add(network.received()); + net_out = net_out.saturating_add(network.transmitted()); + net_rx_total = net_rx_total.saturating_add(network.total_received()); + net_tx_total = net_tx_total.saturating_add(network.total_transmitted()); + } + + let disks = Disks::new_with_refreshed_list(); + #[cfg(target_os = "windows")] + let root = format!( + "{}\\", + env::var("SystemDrive").unwrap_or_else(|_| "C:".to_string()) + ); + #[cfg(target_os = "macos")] + let root = "/".to_string(); + let mut disk_metrics = disks + .iter() + .filter(|disk| !disk.is_removable()) + .map(|disk| { + let total = disk.total_space(); + DiskMetric { + name: disk.name().to_string_lossy().to_string(), + mount_point: disk.mount_point().to_string_lossy().to_string(), + used: u64_to_i64(total.saturating_sub(disk.available_space())), + total: u64_to_i64(total), + ..DiskMetric::default() + } + }) + .collect::>(); + disk_metrics.sort_by_key(|disk| (disk.mount_point != root, disk.mount_point.clone())); + let disk_used = disk_metrics.iter().map(|disk| disk.used).sum(); + let disk_total = disk_metrics.iter().map(|disk| disk.total).sum(); + let load = System::load_average(); + let gpus = gpu_info(); + let gpu_usage = if gpus.is_empty() { + 0.0 + } else { + gpus.iter().map(|gpu| gpu.usage).sum::() / gpus.len() as f64 + }; + let gpu_model = gpus + .iter() + .map(|gpu| gpu.model.as_str()) + .collect::>() + .join(" · "); + + Report { + timestamp: unix_timestamp(), + cpu: system.global_cpu_usage() as f64, + load1: load.one, + load5: load.five, + load15: load.fifteen, + mem_used: u64_to_i64(system.used_memory()), + mem_total: u64_to_i64(system.total_memory()), + swap_used: u64_to_i64(system.used_swap()), + swap_total: u64_to_i64(system.total_swap()), + disk_used, + disk_total, + net_in: net_in as f64, + net_out: net_out as f64, + net_rx_total: u64_to_i64(net_rx_total), + net_tx_total: u64_to_i64(net_tx_total), + uptime: u64_to_i64(System::uptime()), + processes: system.processes().len() as i64, + tcp_connections: 0, + udp_connections: 0, + cpu_cores: system.cpus().len().max(1) as i64, + cpu_model: system + .cpus() + .first() + .map(|cpu| cpu.brand().to_string()) + .unwrap_or_default(), + os: System::long_os_version().unwrap_or_else(|| env::consts::OS.to_string()), + kernel: System::kernel_version().unwrap_or_default(), + arch: env::consts::ARCH.to_string(), + virtualization: String::new(), + ipv4: local_ip("0.0.0.0:0", "1.1.1.1:80"), + ipv6: local_ip("[::]:0", "[2606:4700:4700::1111]:80"), + gpu_usage, + gpu_model, + agent_version: VERSION.to_string(), + disk_read_bps: 0.0, + disk_write_bps: 0.0, + disk_read_iops: 0.0, + disk_write_iops: 0.0, + disk_await_ms: 0.0, + disk_utilization: 0.0, + disks: disk_metrics, + gpus, + message: env::var("AGENT_MESSAGE").unwrap_or_default(), + latency_results, + } +} + +fn runtime_config() -> Result { + let required = |key: &str| -> Result { + env::var(key).map_err(|_| format!("missing environment variable {key}").into()) + }; + let interval = env::var("REPORT_INTERVAL") + .ok() + .and_then(|value| value.parse().ok()) + .unwrap_or(60) + .clamp(15, 3600); + let collect_interval = env::var("COLLECT_INTERVAL") + .ok() + .and_then(|value| value.parse().ok()) + .unwrap_or(5) + .clamp(2, 60) + .min(interval); + Ok(RuntimeConfig { + server_id: required("SERVER_ID")?, + token: required("AGENT_TOKEN")?, + worker_url: required("WORKER_URL")?.trim_end_matches('/').to_string(), + report_interval: interval, + collect_interval, + network_interface: env::var("NETWORK_INTERFACE").unwrap_or_default(), + latency_tasks: Vec::new(), + }) +} + +fn submit( + agent: &ureq::Agent, + config: &RuntimeConfig, + reports: &[Report], +) -> Result> { + let batch = ReportBatch { + server_id: &config.server_id, + samples: reports, + }; + let response = agent + .post(&format!("{}/api/agent/report", config.worker_url)) + .set("Authorization", &format!("Bearer {}", config.token)) + .send_json(batch)?; + Ok(response.into_json::()?.config) +} + +fn apply_remote(config: &mut RuntimeConfig, remote: &RemoteConfig) -> bool { + if (15..=3600).contains(&remote.report_interval) { + config.report_interval = remote.report_interval; + } + if (2..=60).contains(&remote.collect_interval) { + config.collect_interval = remote.collect_interval.min(config.report_interval); + } + config + .network_interface + .clone_from(&remote.network_interface); + let changed = config.latency_tasks != remote.latency_tasks; + config.latency_tasks.clone_from(&remote.latency_tasks); + changed +} + +fn normalized_version(value: &str) -> &str { + value.trim().strip_prefix('v').unwrap_or(value.trim()) +} + +fn version_triplet(value: &str) -> Option<(u64, u64, u64)> { + let mut parts = normalized_version(value).split('.'); + let version = ( + parts.next()?.parse().ok()?, + parts.next()?.parse().ok()?, + parts.next()?.parse().ok()?, + ); + parts.next().is_none().then_some(version) +} + +fn agent_artifact_name() -> Option<&'static str> { + match (env::consts::OS, env::consts::ARCH) { + ("linux", "x86_64") => Some("agent-linux-x86_64"), + ("linux", "aarch64") => Some("agent-linux-aarch64"), + ("windows", "x86_64") => Some("agent-windows-x86_64.exe"), + ("macos", "aarch64") => Some("agent-macos-aarch64"), + _ => None, + } +} + +fn executable_format_valid(path: &Path) -> bool { + let mut magic = [0_u8; 4]; + if fs::File::open(path) + .and_then(|mut file| file.read_exact(&mut magic)) + .is_err() + { + return false; + } + match env::consts::OS { + "linux" => magic == *b"\x7fELF", + "windows" => magic.starts_with(b"MZ"), + "macos" => matches!( + magic, + [0xcf, 0xfa, 0xed, 0xfe] + | [0xfe, 0xed, 0xfa, 0xcf] + | [0xca, 0xfe, 0xba, 0xbe] + | [0xbe, 0xba, 0xfe, 0xca] + ), + _ => false, + } +} + +fn download_agent( + agent: &ureq::Agent, + url: &str, + destination: &Path, + expected_version: &str, +) -> Result { + let Ok(response) = agent.get(url).call() else { + return Ok(false); + }; + let response = response.into_reader(); + let mut file = fs::File::create(destination)?; + let copied = io::copy(&mut response.take(MAX_AGENT_BINARY_BYTES + 1), &mut file)?; + file.flush()?; + drop(file); + if copied > MAX_AGENT_BINARY_BYTES || !executable_format_valid(destination) { + let _ = fs::remove_file(destination); + return Ok(false); + } + #[cfg(unix)] + fs::set_permissions(destination, fs::Permissions::from_mode(0o755))?; + + let downloaded_version = Command::new(destination) + .arg("version") + .output() + .ok() + .filter(|output| output.status.success()) + .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string()); + let valid = downloaded_version.as_deref().map(normalized_version) + == Some(normalized_version(expected_version)); + if !valid { + let _ = fs::remove_file(destination); + } + Ok(valid) +} + +fn update(agent: &ureq::Agent, config: &RuntimeConfig, remote: &RemoteConfig) -> Result { + let Some(remote_version) = version_triplet(&remote.latest_agent_version) else { + return Ok(false); + }; + let Some(current_version) = version_triplet(VERSION) else { + return Ok(false); + }; + if remote.auto_update != 1 || remote_version <= current_version { + return Ok(false); + } + let Some(artifact) = agent_artifact_name() else { + return Ok(false); + }; + let current = env::current_exe()?; + #[cfg(target_os = "windows")] + let temporary = current.with_extension("update.exe"); + #[cfg(not(target_os = "windows"))] + let temporary = current.with_extension("update"); + let primary = format!("{}/{}", config.worker_url, artifact); + let fallback = format!("{RELEASE_DOWNLOAD_BASE}/{artifact}"); + if !download_agent(agent, &primary, &temporary, &remote.latest_agent_version)? + && !download_agent(agent, &fallback, &temporary, &remote.latest_agent_version)? + { + return Err("downloaded agent version does not match the Worker version".into()); + } + + #[cfg(unix)] + { + fs::rename(&temporary, ¤t)?; + let error = Command::new(¤t).arg("run").exec(); + Err(error.into()) + } + + #[cfg(target_os = "windows")] + { + let script = concat!( + "$ErrorActionPreference='Stop'; ", + "$targetPid=[int]$env:CF_MONITOR_UPDATE_PID; ", + "Wait-Process -Id $targetPid; ", + "Move-Item -LiteralPath $env:CF_MONITOR_UPDATE_NEW ", + "-Destination $env:CF_MONITOR_UPDATE_CURRENT -Force; ", + "Start-Process -FilePath $env:CF_MONITOR_UPDATE_CURRENT -ArgumentList 'run'" + ); + Command::new("powershell.exe") + .args([ + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-Command", + script, + ]) + .env("CF_MONITOR_UPDATE_PID", std::process::id().to_string()) + .env("CF_MONITOR_UPDATE_NEW", &temporary) + .env("CF_MONITOR_UPDATE_CURRENT", ¤t) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + Ok(true) + } +} + +fn run(once: bool, print_only: bool) -> Result<()> { + let mut config = runtime_config()?; + let agent = ureq::AgentBuilder::new() + .timeout(Duration::from_secs(20)) + .build(); + let mut next_report = Instant::now(); + let mut next_collect = Instant::now(); + let mut next_latency: HashMap = HashMap::new(); + let mut pending_results = Vec::new(); + let mut pending_samples = Vec::new(); + loop { + let current = Instant::now(); + let due = config + .latency_tasks + .iter() + .filter(|task| { + next_latency + .get(&task.id) + .is_none_or(|deadline| *deadline <= current) + }) + .cloned() + .collect::>(); + if !due.is_empty() { + pending_results.extend(execute_latency_tasks(&due)); + let scheduled_at = Instant::now(); + for task in due { + next_latency.insert( + task.id, + scheduled_at + Duration::from_secs(task.interval_seconds.clamp(30, 3600)), + ); + } + } + + if Instant::now() >= next_collect { + let mut latest_results = HashMap::new(); + for result in std::mem::take(&mut pending_results) { + latest_results.insert(result.task_id.clone(), result); + } + let report = collect(&config, latest_results.into_values().collect()); + if print_only { + println!("{}", serde_json::to_string_pretty(&report)?); + return Ok(()); + } + pending_samples.push(report); + if pending_samples.len() > 720 { + let overflow = pending_samples.len() - 720; + pending_samples.drain(..overflow); + } + next_collect = Instant::now() + Duration::from_secs(config.collect_interval); + } + + if Instant::now() >= next_report && !pending_samples.is_empty() { + match submit(&agent, &config, &pending_samples) { + Ok(remote) => { + pending_samples.clear(); + if let Some(remote) = remote { + if update(&agent, &config, &remote)? { + return Ok(()); + } + if apply_remote(&mut config, &remote) { + next_latency.clear(); + } + } + } + Err(error) => { + eprintln!("report failed: {error}"); + } + } + next_report = Instant::now() + Duration::from_secs(config.report_interval); + if once { + return Ok(()); + } + } + + let wake_at = next_latency + .values() + .copied() + .min() + .map_or(next_report.min(next_collect), |deadline| { + deadline.min(next_report).min(next_collect) + }); + let wait = wake_at + .saturating_duration_since(Instant::now()) + .min(Duration::from_secs(1)); + if !wait.is_zero() { + thread::sleep(wait); + } + } +} + +fn main() { + let result = match env::args().nth(1).as_deref() { + Some("run") => run(false, false), + Some("once") => run(true, false), + Some("collect") => run(true, true), + Some("version") | Some("--version") => { + println!("{VERSION}"); + Ok(()) + } + _ => Err("usage: cf-monitor-agent ".into()), + }; + if let Err(error) = result { + eprintln!("cf-monitor-agent: {error}"); + std::process::exit(1); + } +} + +#[cfg(test)] +mod tests { + use std::net::TcpListener; + use std::thread; + + use super::{ + agent_artifact_name, executable_format_valid, median, normalized_version, + parse_probe_target, ping_latency, selected_interface, tcp_latency_probe, version_triplet, + PROBE_ATTEMPTS, + }; + + #[cfg(target_os = "linux")] + use super::disk_device; + + #[test] + fn selects_network_interfaces() { + assert!(selected_interface("eth0", "")); + assert!(!selected_interface("lo", "")); + assert!(selected_interface("ens3", "eth0, ens3")); + } + + #[cfg(target_os = "linux")] + #[test] + fn selects_whole_disk_devices() { + assert!(disk_device("sda")); + assert!(disk_device("nvme0n1")); + assert!(!disk_device("sda1")); + assert!(!disk_device("nvme0n1p1")); + } + + #[test] + fn parses_release_versions() { + assert_eq!(normalized_version("v1.2.3"), "1.2.3"); + assert_eq!(version_triplet("v1.2.3"), Some((1, 2, 3))); + assert_eq!(version_triplet("1.2"), None); + assert_eq!(version_triplet("1.2.3.4"), None); + assert_eq!(version_triplet("rust-3"), None); + } + + #[test] + fn selects_current_platform_artifact() { + #[cfg(target_os = "linux")] + assert!(agent_artifact_name().is_some_and(|name| name.starts_with("agent-linux-"))); + #[cfg(target_os = "windows")] + assert_eq!(agent_artifact_name(), Some("agent-windows-x86_64.exe")); + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + assert_eq!(agent_artifact_name(), Some("agent-macos-aarch64")); + } + + #[test] + fn recognizes_current_executable_format() { + assert!(executable_format_valid( + &std::env::current_exe().expect("current executable") + )); + } + + #[test] + fn parses_probe_targets() { + assert_eq!( + parse_probe_target("Example.COM"), + Some(("example.com".to_string(), 443)) + ); + assert_eq!( + parse_probe_target("127.0.0.1:8080"), + Some(("127.0.0.1".to_string(), 8080)) + ); + for target in [ + "", + "https://example.com", + "example.com:0", + "example.com:65536", + "999.1.1.1", + "[::1]:443", + "bad host", + ] { + assert_eq!(parse_probe_target(target), None, "target: {target}"); + } + } + + #[test] + fn calculates_median() { + assert_eq!(median(&mut [4.0, 1.0, 3.0]), 3.0); + assert_eq!(median(&mut [4.0, 1.0, 3.0, 2.0]), 2.5); + } + + #[test] + fn measures_local_tcp_latency() { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind test listener"); + let address = listener.local_addr().expect("listener address"); + let accepter = thread::spawn(move || { + for _ in 0..PROBE_ATTEMPTS { + listener.accept().expect("accept probe connection"); + } + }); + let (latency, loss) = tcp_latency_probe(&address.to_string()); + accepter.join().expect("join listener"); + assert!(latency >= 0.0); + assert_eq!(loss, 0.0); + assert_eq!(tcp_latency_probe(""), (-1.0, -1.0)); + } + + #[test] + fn parses_ping_latency() { + assert_eq!(ping_latency("64 bytes time=12.34 ms"), Some(12.34)); + assert_eq!(ping_latency("64 bytes time<1 ms"), Some(0.5)); + assert_eq!(ping_latency("unreachable"), None); + } +} diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..49ba222 --- /dev/null +++ b/build.rs @@ -0,0 +1,40 @@ +use std::{env, fs, path::PathBuf}; + +fn main() { + let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("manifest directory")); + let dist = root.join("frontend/admin-dist"); + let out = PathBuf::from(env::var("OUT_DIR").expect("build output directory")); + let release = env::var("PROFILE").as_deref() == Ok("release"); + + println!("cargo:rerun-if-changed=frontend/admin-dist/admin.html"); + println!("cargo:rerun-if-changed=frontend/admin-dist/admin.js"); + println!("cargo:rerun-if-changed=frontend/admin-dist/admin.css"); + + let files = [ + ( + "admin.html", + b"Admin unavailable

Run the frontend admin build first.

".as_slice(), + ), + ( + "admin.js", + b"console.error('Run the frontend admin build first.');".as_slice(), + ), + ( + "admin.css", + b"body{font-family:system-ui,sans-serif}".as_slice(), + ), + ]; + + for (name, fallback) in files { + let source = dist.join(name); + let body = match fs::read(&source) { + Ok(body) => body, + Err(error) if release => panic!( + "missing embedded admin asset {} ({error}); run `bun run build:frontend` before the release Worker build", + source.display() + ), + Err(_) => fallback.to_vec(), + }; + fs::write(out.join(name), body).expect("write embedded admin asset"); + } +} diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..cb9377e --- /dev/null +++ b/bun.lock @@ -0,0 +1,476 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "cf-monitor", + "devDependencies": { + "wrangler": "^4.120.0", + }, + }, + "frontend": { + "name": "cf-monitor-ui", + "version": "0.1.0", + "dependencies": { + "lucide-react": "^0.525.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "recharts": "^2.15.4", + }, + "devDependencies": { + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.6.0", + "typescript": "^5.8.3", + "vite": "^7.0.6", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], + + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], + + "@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="], + + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + + "@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], + + "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], + + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], + + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260801.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-wuJWbXpKvncJi1P0GKS+iYpN5tHdb7JPJJ/+6ZQe8zzovHHVMkLJPNBsgWpqeUhpM3g9qTwEKd2rglNKejuh5A=="], + + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260801.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kwoZiTpnhNrF3+APx84Q/oAqvJ3sU9yefGagwm/ASaH/2W19x0vghkW/r4qCoHCK0WW7EPugZ+aXjgPMRtlq1Q=="], + + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260801.1", "", { "os": "linux", "cpu": "x64" }, "sha512-r0vAxCZH+Jih9Unm1yoyiByPNWNgawcKciOHDm5Q37ZVGOkKLsT9AtLe3yLSaul76WrKqtf+JP2n0WW32VBLJg=="], + + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260801.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-zWgpdZtSozvIgzQNmQiDSF8yEOQJUkRAWNsDXXzAAoy+fCn8YUoSibj3mpFSbZRvbUldeBEaW6SCdC2VEMkhNQ=="], + + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260801.1", "", { "os": "win32", "cpu": "x64" }, "sha512-2oQz+Ksu4ji6e/+ZoYX+tWQEcxAii2p7l+iR8kx48W1llMalaufAsmVxTlk+3/vrM7D3/2c0iK448e0UQTcIMg=="], + + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.1" }, "os": "darwin", "cpu": "arm64" }, "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.1" }, "os": "darwin", "cpu": "x64" }, "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw=="], + + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "os": "freebsd" }, "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.1", "", { "os": "linux", "cpu": "none" }, "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.1" }, "os": "linux", "cpu": "arm" }, "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.1" }, "os": "linux", "cpu": "ppc64" }, "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.1" }, "os": "linux", "cpu": "none" }, "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.1" }, "os": "linux", "cpu": "s390x" }, "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.2", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw=="], + + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "cpu": "none" }, "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.2", "", { "os": "win32", "cpu": "x64" }, "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + + "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], + + "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], + + "@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="], + + "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.4", "", { "os": "android", "cpu": "arm" }, "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.4", "", { "os": "android", "cpu": "arm64" }, "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.4", "", { "os": "none", "cpu": "arm64" }, "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q=="], + + "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], + + "@speed-highlight/core": ["@speed-highlight/core@1.2.23", "", {}, "sha512-iRoq6i6JDJP6Mt2A5JaPvzw0pgYHH6k92ij+yXiTrB7T2y9N789aWE3EHWj/5ztlJBokcCBja3iYLVdu5wgnkg=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="], + + "@types/react-dom": ["@types/react-dom@19.2.4", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.12", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA=="], + + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], + + "browserslist": ["browserslist@4.28.7", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001809", "", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="], + + "cf-monitor-ui": ["cf-monitor-ui@workspace:frontend"], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.402", "", {}, "sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA=="], + + "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], + + "esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + + "fast-equals": ["fast-equals@5.4.1", "", {}, "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "lucide-react": ["lucide-react@0.525.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ=="], + + "miniflare": ["miniflare@5.20260801.1-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.29.0", "workerd": "1.20260801.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-BHPVzIDA6mbx7LefxpvkXW7DHx9FKB9GorZatbnrrFTt3CVMU8zuUpbgyCuebwDKcTTOZos43ta8GQ0eMVEpxA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.17", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g=="], + + "node-releases": ["node-releases@2.0.53", "", {}, "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], + + "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], + + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + + "react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "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" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="], + + "react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="], + + "recharts": ["recharts@2.15.4", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "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" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="], + + "recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="], + + "rollup": ["rollup@4.62.4", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.4", "@rollup/rollup-android-arm64": "4.62.4", "@rollup/rollup-darwin-arm64": "4.62.4", "@rollup/rollup-darwin-x64": "4.62.4", "@rollup/rollup-freebsd-arm64": "4.62.4", "@rollup/rollup-freebsd-x64": "4.62.4", "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", "@rollup/rollup-linux-arm-musleabihf": "4.62.4", "@rollup/rollup-linux-arm64-gnu": "4.62.4", "@rollup/rollup-linux-arm64-musl": "4.62.4", "@rollup/rollup-linux-loong64-gnu": "4.62.4", "@rollup/rollup-linux-loong64-musl": "4.62.4", "@rollup/rollup-linux-ppc64-gnu": "4.62.4", "@rollup/rollup-linux-ppc64-musl": "4.62.4", "@rollup/rollup-linux-riscv64-gnu": "4.62.4", "@rollup/rollup-linux-riscv64-musl": "4.62.4", "@rollup/rollup-linux-s390x-gnu": "4.62.4", "@rollup/rollup-linux-x64-gnu": "4.62.4", "@rollup/rollup-linux-x64-musl": "4.62.4", "@rollup/rollup-openbsd-x64": "4.62.4", "@rollup/rollup-openharmony-arm64": "4.62.4", "@rollup/rollup-win32-arm64-msvc": "4.62.4", "@rollup/rollup-win32-ia32-msvc": "4.62.4", "@rollup/rollup-win32-x64-gnu": "4.62.4", "@rollup/rollup-win32-x64-msvc": "4.62.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "sharp": ["sharp@0.35.2", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.4" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.2", "@img/sharp-darwin-x64": "0.35.2", "@img/sharp-freebsd-wasm32": "0.35.2", "@img/sharp-libvips-darwin-arm64": "1.3.1", "@img/sharp-libvips-darwin-x64": "1.3.1", "@img/sharp-libvips-linux-arm": "1.3.1", "@img/sharp-libvips-linux-arm64": "1.3.1", "@img/sharp-libvips-linux-ppc64": "1.3.1", "@img/sharp-libvips-linux-riscv64": "1.3.1", "@img/sharp-libvips-linux-s390x": "1.3.1", "@img/sharp-libvips-linux-x64": "1.3.1", "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", "@img/sharp-libvips-linuxmusl-x64": "1.3.1", "@img/sharp-linux-arm": "0.35.2", "@img/sharp-linux-arm64": "0.35.2", "@img/sharp-linux-ppc64": "0.35.2", "@img/sharp-linux-riscv64": "0.35.2", "@img/sharp-linux-s390x": "0.35.2", "@img/sharp-linux-x64": "0.35.2", "@img/sharp-linuxmusl-arm64": "0.35.2", "@img/sharp-linuxmusl-x64": "0.35.2", "@img/sharp-webcontainers-wasm32": "0.35.2", "@img/sharp-win32-arm64": "0.35.2", "@img/sharp-win32-ia32": "0.35.2", "@img/sharp-win32-x64": "0.35.2" } }, "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], + + "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + + "update-browserslist-db": ["update-browserslist-db@1.3.0", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-x/M6q3w4Ybp91CNaS4S69UnliqR3BzRpOT6LWbksjth0S/+jhfaPJsWjt/TewpT8j9eLIojUf5jr29WextHroA=="], + + "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], + + "vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="], + + "workerd": ["workerd@1.20260801.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260801.1", "@cloudflare/workerd-darwin-arm64": "1.20260801.1", "@cloudflare/workerd-linux-64": "1.20260801.1", "@cloudflare/workerd-linux-arm64": "1.20260801.1", "@cloudflare/workerd-windows-64": "1.20260801.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-/g9JGTyqnHtoIscpBHqKD8swE2V4StBs2i69PmLiOhH45OP95jCFICl4F1hKlgN57rqfni5LiCitttoX5OOkVA=="], + + "wrangler": ["wrangler@4.120.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "5.20260801.1-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260801.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260801.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-cBmu/MeaB/fPacC0JpATs4duTOCagBxrZo+vBzuTX06tLzwSyAHE1drlHUZ8rP0VqVz1fy3ReGYTiHdKkoHltg=="], + + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], + + "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], + + "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + } +} diff --git a/frontend/admin.html b/frontend/admin.html new file mode 100644 index 0000000..3b6acdb --- /dev/null +++ b/frontend/admin.html @@ -0,0 +1,15 @@ + + + + + + + + + 管理面板 + + +
+ + + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..c4134ef --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + + + CF Monitor + + +
+ + + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..b3e7bfb --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,27 @@ +{ + "name": "cf-monitor-ui", + "version": "0.1.0", + "private": true, + "repository": "https://github.com/imengying/CF-Monitor", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build && vite build --config vite.admin.config.ts", + "build:public": "tsc -b && vite build", + "build:admin": "vite build --config vite.admin.config.ts", + "preview": "vite preview" + }, + "dependencies": { + "lucide-react": "^0.525.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "recharts": "^2.15.4" + }, + "devDependencies": { + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.6.0", + "typescript": "^5.8.3", + "vite": "^7.0.6" + } +} diff --git a/frontend/public/_headers b/frontend/public/_headers new file mode 100644 index 0000000..b367a23 --- /dev/null +++ b/frontend/public/_headers @@ -0,0 +1,6 @@ +/* + X-Content-Type-Options: nosniff + X-Frame-Options: DENY + Referrer-Policy: strict-origin-when-cross-origin + Permissions-Policy: camera=(), microphone=(), geolocation=() + Content-Security-Policy: default-src 'self'; script-src 'self' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' ws: wss:; frame-src https://challenges.cloudflare.com; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none' diff --git a/frontend/public/logo.svg b/frontend/public/logo.svg new file mode 100644 index 0000000..98fde3f --- /dev/null +++ b/frontend/public/logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/public/os-icons/os-alibaba.svg b/frontend/public/os-icons/os-alibaba.svg new file mode 100644 index 0000000..3d67027 --- /dev/null +++ b/frontend/public/os-icons/os-alibaba.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/os-icons/os-alma.svg b/frontend/public/os-icons/os-alma.svg new file mode 100644 index 0000000..c0528fc --- /dev/null +++ b/frontend/public/os-icons/os-alma.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/frontend/public/os-icons/os-alpine.webp b/frontend/public/os-icons/os-alpine.webp new file mode 100644 index 0000000..6ad3e4f Binary files /dev/null and b/frontend/public/os-icons/os-alpine.webp differ diff --git a/frontend/public/os-icons/os-arch.svg b/frontend/public/os-icons/os-arch.svg new file mode 100644 index 0000000..debdd89 --- /dev/null +++ b/frontend/public/os-icons/os-arch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/os-icons/os-armbian.png b/frontend/public/os-icons/os-armbian.png new file mode 100644 index 0000000..b29c1c5 Binary files /dev/null and b/frontend/public/os-icons/os-armbian.png differ diff --git a/frontend/public/os-icons/os-centos.svg b/frontend/public/os-icons/os-centos.svg new file mode 100644 index 0000000..1b7ef70 --- /dev/null +++ b/frontend/public/os-icons/os-centos.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/public/os-icons/os-debian.svg b/frontend/public/os-icons/os-debian.svg new file mode 100644 index 0000000..c9b7d60 --- /dev/null +++ b/frontend/public/os-icons/os-debian.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/public/os-icons/os-fedora.svg b/frontend/public/os-icons/os-fedora.svg new file mode 100644 index 0000000..e227311 --- /dev/null +++ b/frontend/public/os-icons/os-fedora.svg @@ -0,0 +1,16 @@ + + + + + + + diff --git a/frontend/public/os-icons/os-gentoo.svg b/frontend/public/os-icons/os-gentoo.svg new file mode 100644 index 0000000..31953c0 --- /dev/null +++ b/frontend/public/os-icons/os-gentoo.svg @@ -0,0 +1,24 @@ + + +Gentoo Linux logo (matte) + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/os-icons/os-istore.png b/frontend/public/os-icons/os-istore.png new file mode 100644 index 0000000..eda0048 Binary files /dev/null and b/frontend/public/os-icons/os-istore.png differ diff --git a/frontend/public/os-icons/os-kali.svg b/frontend/public/os-icons/os-kali.svg new file mode 100644 index 0000000..fba691c --- /dev/null +++ b/frontend/public/os-icons/os-kali.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/os-icons/os-macos.svg b/frontend/public/os-icons/os-macos.svg new file mode 100644 index 0000000..c5afe02 --- /dev/null +++ b/frontend/public/os-icons/os-macos.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/public/os-icons/os-manjaro.svg b/frontend/public/os-icons/os-manjaro.svg new file mode 100644 index 0000000..829cbaa --- /dev/null +++ b/frontend/public/os-icons/os-manjaro.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/public/os-icons/os-mint.svg b/frontend/public/os-icons/os-mint.svg new file mode 100644 index 0000000..2256f4b --- /dev/null +++ b/frontend/public/os-icons/os-mint.svg @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/frontend/public/os-icons/os-nix.svg b/frontend/public/os-icons/os-nix.svg new file mode 100644 index 0000000..1cceefe --- /dev/null +++ b/frontend/public/os-icons/os-nix.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/os-icons/os-opencloud.svg b/frontend/public/os-icons/os-opencloud.svg new file mode 100644 index 0000000..261c98e --- /dev/null +++ b/frontend/public/os-icons/os-opencloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/os-icons/os-opensuse.svg b/frontend/public/os-icons/os-opensuse.svg new file mode 100644 index 0000000..f458fd9 --- /dev/null +++ b/frontend/public/os-icons/os-opensuse.svg @@ -0,0 +1,44 @@ + + + + + + + + + diff --git a/frontend/public/os-icons/os-openwrt.svg b/frontend/public/os-icons/os-openwrt.svg new file mode 100644 index 0000000..d504300 --- /dev/null +++ b/frontend/public/os-icons/os-openwrt.svg @@ -0,0 +1 @@ +OpenWrt \ No newline at end of file diff --git a/frontend/public/os-icons/os-oracle.svg b/frontend/public/os-icons/os-oracle.svg new file mode 100644 index 0000000..5bfbe01 --- /dev/null +++ b/frontend/public/os-icons/os-oracle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/os-icons/os-proxmox.ico b/frontend/public/os-icons/os-proxmox.ico new file mode 100644 index 0000000..ba0c9af Binary files /dev/null and b/frontend/public/os-icons/os-proxmox.ico differ diff --git a/frontend/public/os-icons/os-redhat.svg b/frontend/public/os-icons/os-redhat.svg new file mode 100644 index 0000000..078b338 --- /dev/null +++ b/frontend/public/os-icons/os-redhat.svg @@ -0,0 +1,69 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/frontend/public/os-icons/os-rocky.svg b/frontend/public/os-icons/os-rocky.svg new file mode 100644 index 0000000..8c3d766 --- /dev/null +++ b/frontend/public/os-icons/os-rocky.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/os-icons/os-synology.ico b/frontend/public/os-icons/os-synology.ico new file mode 100644 index 0000000..154f6b4 Binary files /dev/null and b/frontend/public/os-icons/os-synology.ico differ diff --git a/frontend/public/os-icons/os-ubuntu.svg b/frontend/public/os-icons/os-ubuntu.svg new file mode 100644 index 0000000..77adbbc --- /dev/null +++ b/frontend/public/os-icons/os-ubuntu.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/public/os-icons/os-unknown.svg b/frontend/public/os-icons/os-unknown.svg new file mode 100644 index 0000000..d03c07c --- /dev/null +++ b/frontend/public/os-icons/os-unknown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/os-icons/os-windows.svg b/frontend/public/os-icons/os-windows.svg new file mode 100644 index 0000000..d0332d4 --- /dev/null +++ b/frontend/public/os-icons/os-windows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..4b3d20f --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,260 @@ +import { Megaphone, Moon, Search, Sun, UserCircle } from "lucide-react"; +import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { api, ApiError, getToken, getTurnstileProof, setTurnstileProof } from "./api"; +import { NodeCard } from "./components/NodeCard"; +import { StatsBar } from "./components/StatsBar"; +import { TurnstileWidget } from "./components/TurnstileWidget"; +import { demoConfig, demoExchangeRates, demoServers } from "./demo"; +import type { Config, ExchangeRates, Server } from "./types"; +import { resolveBackground, themeToggle } from "./theme"; +import { ui } from "./locale"; + +const defaultConfig: Config = { ...demoConfig, site_description: "", site_name: "CF Monitor" }; +const demoMode = import.meta.env.DEV && new URLSearchParams(window.location.search).has("demo"); +const NodeDetails = lazy(() => import("./components/NodeDetails").then((module) => ({ default: module.NodeDetails }))); + +function routeServerId() { + const match = window.location.pathname.match(/^\/instance\/([^/]+)\/?$/); + if (!match) return null; + try { return decodeURIComponent(match[1]); } catch { return null; } +} + +export default function App() { + const [config, setConfig] = useState(defaultConfig); + const [servers, setServers] = useState([]); + const [exchangeRates, setExchangeRates] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [query, setQuery] = useState(""); + const [group, setGroup] = useState("__all__"); + const [selectedId, setSelectedId] = useState(routeServerId); + const [needsVerification, setNeedsVerification] = useState(false); + const [verificationBusy, setVerificationBusy] = useState(false); + const [appearance, setAppearance] = useState<"light" | "dark" | null>(() => { + const stored = localStorage.getItem("cf-monitor-theme"); + return stored === "light" || stored === "dark" ? stored : null; + }); + const [systemDark, setSystemDark] = useState(() => matchMedia("(prefers-color-scheme: dark)").matches); + const wsRef = useRef([]); + const dark = appearance ? appearance === "dark" : config.default_theme === "system" ? systemDark : config.default_theme === "dark"; + const background = resolveBackground(config.background_url, dark); + const blur = themeToggle(config, "enableBlur"); + + const load = useCallback(async (quiet = false) => { + if (!quiet) setLoading(true); + if (demoMode) { + setConfig(demoConfig); + setServers(demoServers); + setExchangeRates(demoExchangeRates); + setError(""); + setLoading(false); + return; + } + try { + const nextConfig = await api.config(); + setConfig(nextConfig); + if (nextConfig.turnstile_enabled && !getTurnstileProof() && !getToken()) { + setNeedsVerification(true); + setServers([]); + setError(""); + return; + } + const [serverResult, ratesResult] = await Promise.all([api.servers(), api.exchangeRates()]); + const localServers = serverResult.servers.map((server) => ({ ...server, source_id: server.id, source_url: "", source_name: nextConfig.site_name })); + const remoteResults = await Promise.allSettled((nextConfig.federation_sites || []).map(async (site, index) => { + const result = await api.servers(false, site.url); + return result.servers.map((server) => ({ ...server, id: `remote-${index}-${server.id}`, source_id: server.id, source_url: site.url.replace(/\/$/, ""), source_name: site.name })); + })); + const remoteServers = remoteResults.flatMap((result) => result.status === "fulfilled" ? result.value : []); + setServers([...localServers, ...remoteServers]); + setExchangeRates(ratesResult); + setNeedsVerification(false); + const failedSites = remoteResults.filter((result) => result.status === "rejected").length; + setError(failedSites ? ui(nextConfig.locale, `${failedSites} 个聚合站点暂时不可用`, `${failedSites} federated sites are unavailable`) : ""); + } catch (reason) { + if (reason instanceof ApiError && reason.status === 403) { + setTurnstileProof(""); + setNeedsVerification(true); + } + setError(reason instanceof Error ? reason.message : ui(config.locale, "无法加载节点状态", "Unable to load server status")); + } finally { + setLoading(false); + } + }, []); + + const verifyDashboard = useCallback(async (token: string) => { + if (!token || verificationBusy) return; + setVerificationBusy(true); + setError(""); + try { + const result = await api.verifyTurnstile(token); + setTurnstileProof(result.verification); + setNeedsVerification(false); + await load(); + } catch (reason) { + setError(reason instanceof Error ? reason.message : ui(config.locale, "Cloudflare 验证失败", "Cloudflare verification failed")); + } finally { + setVerificationBusy(false); + } + }, [load, verificationBusy]); + + useEffect(() => { + void load(); + const timer = window.setInterval(() => void load(true), 60_000); + return () => clearInterval(timer); + }, [load]); + + useEffect(() => { + document.documentElement.classList.toggle("dark", dark); + document.documentElement.style.colorScheme = dark ? "dark" : "light"; + document.documentElement.dataset.blur = blur ? "on" : "off"; + document.documentElement.lang = config.locale || "zh-CN"; + }, [blur, config.locale, dark]); + + useEffect(() => { + let link = document.querySelector('link[rel="icon"]'); + if (!link) { + link = document.createElement("link"); + link.rel = "icon"; + document.head.append(link); + } + link.href = config.favicon_url || "/logo.svg"; + }, [config.favicon_url]); + + useEffect(() => { + const media = matchMedia("(prefers-color-scheme: dark)"); + const update = () => setSystemDark(media.matches); + media.addEventListener("change", update); + return () => media.removeEventListener("change", update); + }, []); + + useEffect(() => { + const onPop = () => setSelectedId(routeServerId()); + window.addEventListener("popstate", onPop); + return () => window.removeEventListener("popstate", onPop); + }, []); + + useEffect(() => { + const selected = servers.find((server) => server.id === selectedId); + document.title = selected ? `${selected.name} · ${config.site_name}` : config.site_name; + }, [config.site_name, selectedId, servers]); + + useEffect(() => { + if (loading || !config.websocket || !config.public_dashboard || demoMode) return; + if (config.turnstile_enabled && !getTurnstileProof()) return; + const reconnects: number[] = []; + let cancelled = false; + const sources = [{ url: "", name: config.site_name }, ...(config.federation_sites || []).map((site) => ({ ...site, url: site.url.replace(/\/$/, "") }))]; + const connect = (source: typeof sources[number]) => { + if (cancelled) return; + const endpoint = source.url ? new URL(source.url) : new URL(location.origin); + endpoint.protocol = endpoint.protocol === "https:" ? "wss:" : "ws:"; + endpoint.pathname = "/api/ws"; + endpoint.search = ""; + const selectedServer = servers.find((server) => server.id === selectedId && (server.source_url || "") === source.url); + if (selectedServer?.source_id) endpoint.searchParams.set("server_id", selectedServer.source_id); + if (!source.url) { + const proof = getTurnstileProof(); + if (proof) endpoint.searchParams.set("turnstile_verified", proof); + } + const socket = new WebSocket(endpoint); + wsRef.current.push(socket); + socket.onclose = () => { + if (!cancelled) reconnects.push(window.setTimeout(() => connect(source), 3000)); + }; + socket.onmessage = (event) => { + if (event.data === "pong") return; + try { + const message = JSON.parse(event.data); + if (message.type !== "metrics") return; + setServers((current) => current.map((server) => (server.source_url || "") === source.url && (server.source_id || server.id) === message.server_id + ? { ...server, ...message.metrics, timestamp: message.timestamp } + : server)); + } catch { /* Ignore non-protocol messages. */ } + }; + }; + sources.forEach(connect); + const heartbeat = window.setInterval(() => wsRef.current.forEach((socket) => socket.readyState === WebSocket.OPEN && socket.send("ping")), 30_000); + return () => { + cancelled = true; + clearInterval(heartbeat); + reconnects.forEach(clearTimeout); + wsRef.current.forEach((socket) => socket.close()); + wsRef.current = []; + }; + }, [config.websocket, config.public_dashboard, config.turnstile_enabled, config.site_name, config.federation_sites, loading, needsVerification, selectedId]); + + const groups = useMemo(() => ["__all__", ...Array.from(new Set(servers.map((server) => server.group_name || "默认")))], [servers]); + const visible = useMemo(() => servers.filter((server) => { + const text = `${server.name} ${server.region} ${server.tags} ${server.group_name}`.toLowerCase(); + const groupMatches = !config.show_groups || group === "__all__" || (server.group_name || "默认") === group; + const queryMatches = !config.show_search || text.includes(query.trim().toLowerCase()); + return groupMatches && queryMatches; + }), [servers, query, group, config.show_groups, config.show_search]); + const selected = servers.find((server) => server.id === selectedId) ?? null; + + function goHome() { + window.history.pushState({}, "", demoMode ? "/?demo=1" : "/"); + setSelectedId(null); + window.scrollTo({ top: 0, behavior: "auto" }); + } + + function openServer(server: Server) { + const suffix = demoMode ? "?demo=1" : ""; + window.history.pushState({}, "", `/instance/${encodeURIComponent(server.id)}${suffix}`); + setSelectedId(server.id); + window.scrollTo({ top: 0, behavior: "auto" }); + } + + function openAdmin() { + window.location.assign("/admin"); + } + + function toggleTheme() { + const next = dark ? "light" : "dark"; + localStorage.setItem("cf-monitor-theme", next); + setAppearance(next); + } + + return ( +
+
+
+
+
{config.site_name}
+
+ + +
+
+
+ +
+ {config.site_announcement ?
{config.site_announcement}
: null} + {needsVerification ? ( +
+ +

{ui(config.locale, "访问验证", "Access verification")}

{config.site_name}

+ {config.turnstile_site_key ? void verifyDashboard(token)} onError={setError} /> :

{ui(config.locale, "Turnstile 尚未正确配置", "Turnstile is not configured")}

} + {verificationBusy ? {ui(config.locale, "正在验证", "Verifying")} : null} + {error ?

{error}

: null} +
+ ) : selectedId ? selected ? ( + {ui(config.locale, "正在加载节点", "Loading server")}
}> + ) : loading ?
{ui(config.locale, "正在加载节点", "Loading server")}
:
{ui(config.locale, "节点不存在或已隐藏", "Server not found or hidden")}
:
+ + {config.show_search || config.show_groups ?
+ {config.show_search ?
setQuery(event.target.value)} />
: null} + {config.show_groups ?
{groups.map((item) => )}
: null} + {ui(config.locale, `${visible.length} 个节点`, `${visible.length} servers`)} +
: null} + {error ?
{error}
: null} + {loading && !servers.length ?
{Array.from({ length: 8 }).map((_, index) => )}
: visible.length ? ( +
{visible.map((server) => openServer(server)} />)}
+ ) : !error ?
{servers.length ? ui(config.locale, "没有匹配的节点", "No matching servers") : ui(config.locale, "尚未添加节点", "No servers added")}
: null} +
} + + +
+ ); +} diff --git a/frontend/src/admin-main.tsx b/frontend/src/admin-main.tsx new file mode 100644 index 0000000..91f423c --- /dev/null +++ b/frontend/src/admin-main.tsx @@ -0,0 +1,56 @@ +import { StrictMode, useEffect, useState } from "react"; +import { createRoot } from "react-dom/client"; +import { api } from "./api"; +import { AdminPanel } from "./components/AdminPanel"; +import type { Config } from "./types"; +import "./styles.css"; + +const THEME_KEY = "cf-monitor-admin-theme"; + +function initialAppearance(): "light" | "dark" | null { + const stored = localStorage.getItem(THEME_KEY); + return stored === "light" || stored === "dark" ? stored : null; +} + +function AdminApp() { + const [config, setConfig] = useState(null); + const [error, setError] = useState(""); + const [appearance, setAppearance] = useState<"light" | "dark" | null>(initialAppearance); + const [systemDark, setSystemDark] = useState(() => matchMedia("(prefers-color-scheme: dark)").matches); + const dark = appearance ? appearance === "dark" : config?.default_theme === "system" || !config ? systemDark : config.default_theme === "dark"; + + async function loadConfig() { + try { + const next = await api.config(); + setConfig(next); + setError(""); + } catch (reason) { + setError(reason instanceof Error ? reason.message : "无法加载管理面板"); + } + } + + useEffect(() => { void loadConfig(); }, []); + useEffect(() => { + document.documentElement.classList.toggle("dark", dark); + document.documentElement.style.colorScheme = dark ? "dark" : "light"; + }, [dark]); + useEffect(() => { + const media = matchMedia("(prefers-color-scheme: dark)"); + const update = () => setSystemDark(media.matches); + media.addEventListener("change", update); + return () => media.removeEventListener("change", update); + }, []); + useEffect(() => { + document.title = config ? `管理面板 · ${config.site_name}` : "管理面板"; + }, [config]); + + if (error) return
{error}
; + if (!config) return
正在加载管理面板
; + return { + const next = dark ? "light" : "dark"; + localStorage.setItem(THEME_KEY, next); + setAppearance(next); + }} onClose={() => window.location.assign("/")} onChanged={() => void loadConfig()} />; +} + +createRoot(document.getElementById("root")!).render(); diff --git a/frontend/src/api.ts b/frontend/src/api.ts new file mode 100644 index 0000000..e510aff --- /dev/null +++ b/frontend/src/api.ts @@ -0,0 +1,125 @@ +import type { AlertRule, AlertRuleInput, CloudflareUsage, Config, DatabaseStats, ExchangeRates, HistoryPoint, LatencySample, LatencyTask, LatencyTaskInput, LatencyTestPoint, Server, ServerInput, Settings, ThemeSettingsSchema, ThemeStore } from "./types"; + +const TOKEN_KEY = "cf-monitor-admin-token"; +const TURNSTILE_KEY = "cf-monitor-turnstile-verified"; + +export class ApiError extends Error { + constructor(message: string, public status: number) { + super(message); + } +} + +export function getToken() { + return sessionStorage.getItem(TOKEN_KEY) ?? ""; +} + +export function setToken(token: string) { + if (token) sessionStorage.setItem(TOKEN_KEY, token); + else sessionStorage.removeItem(TOKEN_KEY); +} + +export function getTurnstileProof() { + return localStorage.getItem(TURNSTILE_KEY) ?? ""; +} + +export function setTurnstileProof(value: string) { + if (value) localStorage.setItem(TURNSTILE_KEY, value); + else localStorage.removeItem(TURNSTILE_KEY); +} + +async function request(path: string, init: RequestInit = {}, admin = false, baseUrl = ""): Promise { + const headers = new Headers(init.headers); + if (init.body) headers.set("Content-Type", "application/json"); + if (!baseUrl && getToken()) headers.set("Authorization", `Bearer ${getToken()}`); + if (!baseUrl && getTurnstileProof()) headers.set("X-Turnstile-Verified", getTurnstileProof()); + const response = await fetch(`${baseUrl.replace(/\/$/, "")}${path}`, { ...init, headers }); + if (!response.ok) { + const payload = await response.json().catch(() => ({ error: response.statusText })); + if (response.status === 401 && admin) setToken(""); + throw new ApiError(payload.error ?? "请求失败", response.status); + } + return response.json() as Promise; +} + +export const api = { + config: (baseUrl = "") => request("/api/config", {}, false, baseUrl), + exchangeRates: (baseUrl = "") => request("/api/exchange-rates", {}, false, baseUrl), + refreshExchangeRates: () => request("/api/admin/exchange-rates/refresh", { method: "POST" }, true), + servers: (admin = false, baseUrl = "") => + request<{ servers: Server[]; server_time?: number }>(admin ? "/api/admin/servers" : "/api/servers", {}, admin, baseUrl), + server: (id: string, baseUrl = "") => request(`/api/servers/${encodeURIComponent(id)}`, {}, false, baseUrl), + history: (id: string, hours: number, baseUrl = "") => + request<{ points: HistoryPoint[] }>(`/api/history/${encodeURIComponent(id)}?hours=${hours}`, {}, false, baseUrl), + latencyHistory: (id: string, hours: number, baseUrl = "") => + request<{ tasks: LatencyTestPoint[]; points: LatencySample[] }>(`/api/latency/${encodeURIComponent(id)}?hours=${hours}`, {}, false, baseUrl), + verifyTurnstile: (token: string) => + request<{ verification: string }>("/api/turnstile/verify", { method: "POST", body: JSON.stringify({ token }) }), + login: (username: string, password: string, turnstileToken: string) => + request<{ token: string }>("/api/admin/login", { + method: "POST", + body: JSON.stringify({ username, password, turnstile_token: turnstileToken }), + }), + settings: () => request("/api/admin/settings", {}, true), + latencyTasks: () => request<{ tasks: LatencyTask[] }>("/api/admin/latency-tasks", {}, true), + createLatencyTask: (input: LatencyTaskInput) => + request<{ id: string }>("/api/admin/latency-tasks", { method: "POST", body: JSON.stringify(input) }, true), + updateLatencyTask: (id: string, input: LatencyTaskInput) => + request<{ success: boolean }>(`/api/admin/latency-tasks/${encodeURIComponent(id)}`, { method: "PATCH", body: JSON.stringify(input) }, true), + deleteLatencyTask: (id: string) => + request<{ success: boolean }>(`/api/admin/latency-tasks/${encodeURIComponent(id)}`, { method: "DELETE" }, true), + alertRules: () => request<{ rules: AlertRule[] }>("/api/admin/alert-rules", {}, true), + createAlertRule: (input: AlertRuleInput) => + request<{ id: string }>("/api/admin/alert-rules", { method: "POST", body: JSON.stringify(input) }, true), + updateAlertRule: (id: string, input: AlertRuleInput) => + request<{ success: boolean }>(`/api/admin/alert-rules/${encodeURIComponent(id)}`, { method: "PATCH", body: JSON.stringify(input) }, true), + deleteAlertRule: (id: string) => + request<{ success: boolean }>(`/api/admin/alert-rules/${encodeURIComponent(id)}`, { method: "DELETE" }, true), + themes: () => request("/api/themes"), + themeSettings: () => request("/api/admin/theme-settings", {}, true), + previewTheme: (themeUrl: string) => + request<{ theme_url: string; proof: string }>("/api/admin/themes/preview", { + method: "POST", + body: JSON.stringify({ theme_url: themeUrl }), + }, true), + saveSettings: (input: Partial) => + request<{ settings: Settings; token: string | null }>("/api/admin/settings", { method: "PATCH", body: JSON.stringify(input) }, true), + createServer: (input: ServerInput) => + request<{ id: string; agent_token: string }>( + "/api/admin/servers", + { method: "POST", body: JSON.stringify(input) }, + true, + ), + updateServer: (id: string, input: ServerInput) => + request<{ success: boolean }>( + `/api/admin/servers/${encodeURIComponent(id)}`, + { method: "PATCH", body: JSON.stringify(input) }, + true, + ), + deleteServer: (id: string) => + request<{ success: boolean }>( + `/api/admin/servers/${encodeURIComponent(id)}`, + { method: "DELETE" }, + true, + ), + deleteServers: (ids: string[]) => + request<{ success: boolean }>("/api/admin/servers", { + method: "DELETE", + body: JSON.stringify({ ids }), + }, true), + rotateToken: (id: string) => + request<{ agent_token: string }>( + `/api/admin/servers/${encodeURIComponent(id)}/token`, + { method: "POST" }, + true, + ), + reorderServers: (ids: string[]) => + request<{ success: boolean }>( + "/api/admin/servers/order", + { method: "PATCH", body: JSON.stringify({ ids }) }, + true, + ), + databaseStats: () => request("/api/admin/database", {}, true), + cloudflareUsage: () => request("/api/admin/cloudflare-usage", {}, true), + clearHistory: () => request<{ success: boolean }>("/api/admin/history", { method: "DELETE" }, true), + testNotification: () => request<{ success: boolean }>("/api/admin/notifications/test", { method: "POST" }, true), +}; diff --git a/frontend/src/components/AdminPanel.tsx b/frontend/src/components/AdminPanel.tsx new file mode 100644 index 0000000..90d4864 --- /dev/null +++ b/frontend/src/components/AdminPanel.tsx @@ -0,0 +1,662 @@ +import { + AlertTriangle, + ArrowLeft, + ChevronDown, + ChevronUp, + Cloud, + Coins, + Copy, + Database, + Eye, + GripVertical, + KeyRound, + LogOut, + Moon, + Palette, + Pencil, + Plus, + RadioTower, + RotateCw, + Save, + ServerCog, + Settings2, + ShieldCheck, + SlidersHorizontal, + Sun, + Trash2, + Undo2, +} from "lucide-react"; +import { DragEvent, FormEvent, useCallback, useEffect, useMemo, useState } from "react"; +import { api, ApiError, getToken, setToken } from "../api"; +import { formatBytes, isOnline } from "../format"; +import { ASSET_CURRENCIES, type AgentInstallTarget, type CloudflareUsage, type Config, type DatabaseStats, type ExchangeRates, type FederationSite, type Server, type ServerInput, type Settings, type ThirdPartyTheme, type ThemeSettingField, type ThemeSettingsSchema, type ThemeSettingValue, type ThemeStore } from "../types"; +import { Checkbox } from "./Checkbox"; +import { TurnstileWidget } from "./TurnstileWidget"; +import { LatencyManager } from "./LatencyManager"; +import { AlertRuleManager } from "./AlertRuleManager"; + +type AdminTab = "servers" | "latency" | "appearance" | "themes" | "themeSettings" | "alerts" | "security" | "data"; +type AgentPlatform = "linux" | "windows" | "macos"; + +const adminPages: Record = { + servers: { title: "服务器", description: "管理监控节点、Agent 密钥和运行参数" }, + latency: { title: "延迟监测", description: "配置分配给各服务器的 TCP 与 ICMP 延迟任务" }, + appearance: { title: "站点设置", description: "调整站点信息、公开内容和前台显示项目" }, + themes: { title: "主题管理", description: "选择内置或第三方前端主题" }, + themeSettings: { title: "主题设置", description: "调整当前前端主题提供的显示选项" }, + alerts: { title: "通知", description: "配置 Telegram 通知和资源告警阈值" }, + security: { title: "登录与安全", description: "管理管理员账号和 Cloudflare Turnstile 防护" }, + data: { title: "监控数据库", description: "查看 D1 用量、汇率状态并维护历史数据" }, +}; + + +const emptyServer: ServerInput = { + name: "", + region: "", + group_name: "默认", + tags: "", + note: "", + hidden: false, + expires_at: null, + traffic_limit: 0, + traffic_limit_type: "sum", + price: -2, + billing_cycle: 30, + currency: "CNY", + auto_renewal: false, + public_remark: "", + network_interface: "", + reset_day: 1, + report_interval: 60, + collect_interval: 5, + rx_correction: 0, + tx_correction: 0, + offline_notify_disabled: false, + auto_update: false, +}; + +function toInput(server: Server): ServerInput { + return { + ...emptyServer, + name: server.name, + region: server.region, + group_name: server.group_name, + tags: server.tags, + note: server.note, + hidden: !!server.hidden, + expires_at: server.expires_at, + traffic_limit: server.traffic_limit, + traffic_limit_type: server.traffic_limit_type || "sum", + price: server.price, + billing_cycle: server.billing_cycle, + currency: server.currency, + auto_renewal: !!server.auto_renewal, + public_remark: server.public_remark, + network_interface: server.network_interface || "", + reset_day: server.reset_day || 1, + report_interval: server.report_interval || 60, + collect_interval: server.collect_interval || 5, + rx_correction: server.rx_correction || 0, + tx_correction: server.tx_correction || 0, + offline_notify_disabled: !!server.offline_notify_disabled, + auto_update: !!server.auto_update, + }; +} + +function formatDate(value: number | null) { + return value ? new Date(value * 1000).toISOString().slice(0, 10) : ""; +} + +function settingPatch(settings: Settings, key: keyof Settings, value: unknown) { + return { ...settings, [key]: value } as Settings; +} + +function shellLiteral(value: string) { + return `'${value.replaceAll("'", `'\\''`)}'`; +} + +function powershellLiteral(value: string) { + return `'${value.replaceAll("'", "''")}'`; +} + +function federationText(sites: FederationSite[]) { + return sites.map((site) => `${site.name}|${site.url}`).join("\n"); +} + +function parseFederationSites(value: string): FederationSite[] { + return value.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => { + const separator = line.indexOf("|"); + if (separator < 1) return []; + return [{ name: line.slice(0, separator).trim(), url: line.slice(separator + 1).trim() }]; + }); +} + +export function AdminPanel({ + config, + dark, + onToggleTheme, + onClose, + onChanged, +}: { + config: Config; + dark: boolean; + onToggleTheme: () => void; + onClose: () => void; + onChanged: () => void; +}) { + const [authenticated, setAuthenticated] = useState(!!getToken()); + const [username, setUsername] = useState("admin"); + const [password, setPassword] = useState(""); + const [turnstileToken, setTurnstileToken] = useState(""); + const [turnstileReset, setTurnstileReset] = useState(0); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + const [notice, setNotice] = useState(""); + const [tab, setTab] = useState("servers"); + const [servers, setServers] = useState([]); + const [selectedIds, setSelectedIds] = useState([]); + const [draggingId, setDraggingId] = useState(""); + const [settings, setSettings] = useState(null); + const [database, setDatabase] = useState(null); + const [cloudflareUsage, setCloudflareUsage] = useState(null); + const [exchangeRates, setExchangeRates] = useState(null); + const [themeStore, setThemeStore] = useState(null); + const [themeSettingsSchema, setThemeSettingsSchema] = useState(null); + const [themeVersions, setThemeVersions] = useState>({}); + const [customThemeUrl, setCustomThemeUrl] = useState(""); + const [editing, setEditing] = useState(null); + const [form, setForm] = useState(emptyServer); + const [install, setInstall] = useState([]); + const [installPlatform, setInstallPlatform] = useState("linux"); + + const load = useCallback(async () => { + if (!getToken()) return; + setBusy(true); + setError(""); + try { + const [serverResult, settingsResult] = await Promise.all([api.servers(true), api.settings()]); + setServers(serverResult.servers); + setSettings({ + ...settingsResult, + site_announcement: settingsResult.site_announcement || "", + theme_options: settingsResult.theme_options || {}, + cloudflare_account_id: settingsResult.cloudflare_account_id || "", + cloudflare_api_token: settingsResult.cloudflare_api_token || "", + favicon_url: settingsResult.favicon_url || "", + locale: settingsResult.locale || "zh-CN", + federation_sites: settingsResult.federation_sites || [], + cors_allowed_origins: settingsResult.cors_allowed_origins || "", + csp_asset_origins: settingsResult.csp_asset_origins || "", + }); + setAuthenticated(true); + } catch (reason) { + setError(reason instanceof Error ? reason.message : "加载失败"); + if (reason instanceof ApiError && reason.status === 401) { + setToken(""); + setAuthenticated(false); + } + } finally { + setBusy(false); + } + }, []); + + useEffect(() => { void load(); }, [load]); + + useEffect(() => { + if (!notice) return; + const timer = window.setTimeout(() => setNotice(""), 3200); + return () => window.clearTimeout(timer); + }, [notice]); + + async function login(event: FormEvent) { + event.preventDefault(); + setBusy(true); + setError(""); + try { + const result = await api.login(username.trim(), password, turnstileToken); + setToken(result.token); + setPassword(""); + setTurnstileToken(""); + setAuthenticated(true); + await load(); + } catch (reason) { + setError(reason instanceof Error ? reason.message : "登录失败"); + setTurnstileToken(""); + setTurnstileReset((value) => value + 1); + } finally { setBusy(false); } + } + + function openEditor(server?: Server) { + setEditing(server ?? "new"); + setForm(server ? toInput(server) : { ...emptyServer }); + setError(""); + } + + async function saveServer(event: FormEvent) { + event.preventDefault(); + setBusy(true); + setError(""); + try { + if (editing === "new") { + const result = await api.createServer(form); + setInstall([{ id: result.id, name: form.name, agent_token: result.agent_token }]); + } else if (editing) { + await api.updateServer(editing.id, form); + } + setEditing(null); + await load(); + onChanged(); + } catch (reason) { + setError(reason instanceof Error ? reason.message : "保存节点失败"); + } finally { setBusy(false); } + } + + async function removeSelected() { + if (!selectedIds.length || !window.confirm(`确认删除选中的 ${selectedIds.length} 个节点及其全部历史数据?`)) return; + setBusy(true); + try { + await api.deleteServers(selectedIds); + setSelectedIds([]); + await load(); + onChanged(); + } catch (reason) { setError(reason instanceof Error ? reason.message : "批量删除失败"); } + finally { setBusy(false); } + } + + async function remove(server: Server) { + if (!window.confirm(`确认删除“${server.name}”及其全部历史数据?`)) return; + try { await api.deleteServer(server.id); await load(); onChanged(); } + catch (reason) { setError(reason instanceof Error ? reason.message : "删除失败"); } + } + + async function rotate(server: Server) { + if (!window.confirm(`重置“${server.name}”的 Agent 密钥?旧 Agent 将立即失效。`)) return; + try { + const result = await api.rotateToken(server.id); + setInstall([{ id: server.id, name: server.name, agent_token: result.agent_token }]); + } catch (reason) { setError(reason instanceof Error ? reason.message : "重置失败"); } + } + + async function move(index: number, offset: number) { + const target = index + offset; + if (target < 0 || target >= servers.length) return; + const next = [...servers]; + [next[index], next[target]] = [next[target], next[index]]; + setServers(next); + try { await api.reorderServers(next.map((server) => server.id)); onChanged(); } + catch (reason) { setServers(servers); setError(reason instanceof Error ? reason.message : "排序失败"); } + } + + function startDrag(event: DragEvent, id: string) { + setDraggingId(id); + event.dataTransfer.effectAllowed = "move"; + event.dataTransfer.setData("text/plain", id); + } + + async function dropServer(event: DragEvent, targetId: string) { + event.preventDefault(); + const sourceId = draggingId || event.dataTransfer.getData("text/plain"); + setDraggingId(""); + if (!sourceId || sourceId === targetId) return; + const sourceIndex = servers.findIndex((server) => server.id === sourceId); + const targetIndex = servers.findIndex((server) => server.id === targetId); + if (sourceIndex < 0 || targetIndex < 0) return; + const previous = [...servers]; + const next = [...servers]; + const [moved] = next.splice(sourceIndex, 1); + next.splice(targetIndex, 0, moved); + setServers(next); + try { await api.reorderServers(next.map((server) => server.id)); onChanged(); } + catch (reason) { setServers(previous); setError(reason instanceof Error ? reason.message : "排序失败"); } + } + + async function saveSite(event: FormEvent) { + event.preventDefault(); + if (!settings) return; + setBusy(true); + setError(""); + setNotice(""); + const payload = { ...settings }; + if (!payload.new_password) delete payload.new_password; + try { + const result = await api.saveSettings(payload); + if (result.token) setToken(result.token); + setSettings(result.settings); + setNotice("设置已保存"); + onChanged(); + } catch (reason) { setError(reason instanceof Error ? reason.message : "保存设置失败"); } + finally { setBusy(false); } + } + + async function testNotification() { + setBusy(true); setError(""); setNotice(""); + try { await api.testNotification(); setNotice("测试通知已发送"); } + catch (reason) { setError(reason instanceof Error ? reason.message : "测试通知失败"); } + finally { setBusy(false); } + } + + async function loadDatabase() { + setBusy(true); setError(""); + try { + const [stats, rates] = await Promise.all([api.databaseStats(), api.exchangeRates()]); + setDatabase(stats); + setExchangeRates(rates); + } + catch (reason) { setError(reason instanceof Error ? reason.message : "读取数据库统计失败"); } + finally { setBusy(false); } + } + + async function refreshExchangeRates() { + setBusy(true); setError(""); setNotice(""); + try { + const rates = await api.refreshExchangeRates(); + setExchangeRates(rates); + setNotice(`汇率已更新 · ${rates.source} · ${rates.date}`); + } catch (reason) { setError(reason instanceof Error ? reason.message : "汇率更新失败"); } + finally { setBusy(false); } + } + + async function loadCloudflareUsage() { + setBusy(true); setError(""); + try { setCloudflareUsage(await api.cloudflareUsage()); } + catch (reason) { setError(reason instanceof Error ? reason.message : "读取 Cloudflare 用量失败"); } + finally { setBusy(false); } + } + + async function loadThemes() { + if (themeStore) return; + setBusy(true); setError(""); + try { + const result = await api.themes(); + const themes = Array.isArray(result.themes) ? result.themes : []; + setThemeStore({ ...result, themes }); + setThemeVersions(Object.fromEntries(themes.flatMap((theme) => { + const version = theme.versions?.[0]; + const reference = theme.branch || version?.commitid || version?.commitId || version?.theme_url || ""; + return reference ? [[theme.id, reference]] : []; + }))); + } catch (reason) { setError(reason instanceof Error ? reason.message : "读取主题商店失败"); } + finally { setBusy(false); } + } + + async function loadThemeSettings() { + setBusy(true); setError(""); + try { setThemeSettingsSchema(await api.themeSettings()); } + catch (reason) { setError(reason instanceof Error ? reason.message : "读取主题设置失败"); } + finally { setBusy(false); } + } + + function themeUrl(theme: ThirdPartyTheme) { + const selected = themeVersions[theme.id]; + if (selected?.startsWith("https://github.com/")) return selected; + const first = theme.versions?.[0]; + const reference = selected || first?.commitid || first?.commitId || theme.branch; + return reference ? `${theme.url.replace(/\/$/, "")}/tree/${reference}` : first?.theme_url || ""; + } + + async function previewTheme(themeUrl: string) { + if (!themeUrl.trim()) return; + setBusy(true); setError(""); setNotice(""); + try { + const result = await api.previewTheme(themeUrl.trim()); + const query = new URLSearchParams({ theme_url: result.theme_url, theme_proof: result.proof }); + window.open(`${window.location.origin}/?${query}`, "_blank", "noopener,noreferrer"); + } catch (reason) { setError(reason instanceof Error ? reason.message : "主题预览失败"); } + finally { setBusy(false); } + } + + async function applyTheme(themeUrl: string) { + setBusy(true); setError(""); setNotice(""); + try { + const result = await api.saveSettings({ theme_url: themeUrl.trim(), theme_options: {} }); + if (result.token) setToken(result.token); + setSettings(result.settings); + setThemeSettingsSchema(null); + setNotice(themeUrl ? "第三方主题已应用" : "已恢复内置主题"); + onChanged(); + } catch (reason) { setError(reason instanceof Error ? reason.message : "应用主题失败"); } + finally { setBusy(false); } + } + + async function clearHistory() { + if (!window.confirm("确认清空全部历史指标?节点配置和最新状态不会删除。")) return; + setBusy(true); setError(""); + try { await api.clearHistory(); setNotice("历史指标已清空"); await loadDatabase(); } + catch (reason) { setError(reason instanceof Error ? reason.message : "清理历史失败"); } + finally { setBusy(false); } + } + + function logout() { + setToken(""); setAuthenticated(false); setServers([]); setSettings(null); setSelectedIds([]); + } + + const commands = useMemo(() => install.map((server) => { + const origin = window.location.origin; + if (installPlatform === "windows") { + return { + ...server, + command: `Invoke-WebRequest -Uri "${origin}/agent.ps1" -OutFile "$env:TEMP\\cf-monitor-install.ps1"\n& "$env:TEMP\\cf-monitor-install.ps1" install -ServerId ${powershellLiteral(server.id)} -Token ${powershellLiteral(server.agent_token)} -Url ${powershellLiteral(origin)}`, + }; + } + if (installPlatform === "macos") { + return { + ...server, + command: `curl -fsSL ${origin}/agent-macos.sh | sudo sh -s -- install --server-id ${shellLiteral(server.id)} --token ${shellLiteral(server.agent_token)} --url ${shellLiteral(origin)}`, + }; + } + return { + ...server, + command: `curl -fsSL ${origin}/agent.sh | sudo bash -s -- install --server-id ${server.id} --token ${server.agent_token} --url ${origin}`, + }; + }), [install, installPlatform]); + + const toggleSelected = (id: string) => setSelectedIds((current) => current.includes(id) ? current.filter((value) => value !== id) : [...current, id]); + const allSelected = servers.length > 0 && selectedIds.length === servers.length; + const updateForm = (key: K, value: ServerInput[K]) => setForm((current) => ({ ...current, [key]: value })); + const updateSettings = (key: K, value: Settings[K]) => setSettings((current) => current ? settingPatch(current, key, value) : current); + const updateThemeOption = (key: string, value: ThemeSettingValue) => setSettings((current) => current ? { + ...current, + theme_options: { ...current.theme_options, [key]: value }, + } : current); + const selectTab = (next: AdminTab) => { + setTab(next); + setNotice(""); + setError(""); + }; + + return ( +
+ {!authenticated ?
+ + +
+ +

管理员登录

{config.site_name} 控制台

+ + + {config.turnstile_login_enabled || config.turnstile_enabled ?
: null} + {error ?

{error}

: null} + +
+
:
+
+
+ + {config.site_name} +
+
+ + + +
+
+ +
+ +
+

{adminPages[tab].title}

{adminPages[tab].description}

+ {error ?

{error}

: null} + {notice ?

{notice}

: null} + {tab === "servers" ? ( +
+

监控节点

{servers.length} 个节点 · 可拖动上下排序
+
{selectedIds.length ? : 批量操作}
+
+ {servers.map((server, index) => ( +
{ event.preventDefault(); event.dataTransfer.dropEffect = "move"; }} onDrop={(event) => void dropServer(event, server.id)}> + + toggleSelected(server.id)} ariaLabel={`选择 ${server.name}`} /> + +
{server.name}{server.group_name} · {server.region || "未设置地区"} · {server.report_interval}s
+ {server.traffic_limit > 0 ? formatBytes(server.traffic_limit) : "不限流量"} +
+
+ ))} + {!servers.length && !busy ?
暂无节点
: null} +
+
+ ) : tab === "latency" ? ( + + ) : tab === "themes" && settings ? ( +
+

第三方主题

{settings.theme_url ? "已启用" : "当前使用内置主题"}
{settings.theme_url ? : null}
+
+ {themeStore?.themes.map((theme) => { + const url = themeUrl(theme); + const description = typeof theme.description === "string" ? theme.description : theme.description?.["zh-CN"] || theme.description?.en || ""; + return
+ {`${theme.title} +
{theme.title}{theme.author || "第三方作者"}
{settings.theme_url === url ? 使用中 : null}

{description}

+ {theme.versions?.length ? : {theme.branch || "main"}} +
+
+
; + })} + {themeStore && !themeStore.themes.length ?
主题商店暂时不可用
: null} + {!themeStore ?
正在读取主题商店
: null} +
+
自定义主题
setCustomThemeUrl(event.target.value)} placeholder="https://github.com/owner/repo/tree/build" />
+
+ ) : settings ? ( +
+ {tab === "appearance" ? <> +
外观与展示
+
+