From e90eb5df60339a1953735b402a59db8e547c411e Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 4 Apr 2026 14:42:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Sprint=203=20=E2=80=94=20benchmark=20+?= =?UTF-8?q?=20conversion=20funnel=20+=20invoice=20PDF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 3.1: Add criterion benchmark for zclaw-growth TF-IDF retrieval (indexing throughput, query scoring latency, top-K retrieval) - 3.2: Extend admin-v2 Usage page with recharts funnel chart (registration → trial → paid conversion) and daily trend bar chart - 3.3: Add invoice PDF export via genpdf (Arial font, Windows) with GET /api/v1/billing/invoices/{id}/pdf handler Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 651 ++++++++++++++++-- admin-v2/package.json | 1 + admin-v2/pnpm-lock.yaml | 298 +++++++- admin-v2/src/pages/Usage.tsx | 168 ++++- crates/zclaw-growth/Cargo.toml | 6 + .../zclaw-growth/benches/retrieval_bench.rs | 151 ++++ crates/zclaw-saas/Cargo.toml | 1 + crates/zclaw-saas/src/billing/handlers.rs | 74 ++ crates/zclaw-saas/src/billing/invoice_pdf.rs | 107 +++ crates/zclaw-saas/src/billing/mod.rs | 2 + 10 files changed, 1408 insertions(+), 51 deletions(-) create mode 100644 crates/zclaw-growth/benches/retrieval_bench.rs create mode 100644 crates/zclaw-saas/src/billing/invoice_pdf.rs diff --git a/Cargo.lock b/Cargo.lock index 17fbe26..264de63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,12 +110,33 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + [[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "approx" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" +dependencies = [ + "num-traits", +] + [[package]] name = "arbitrary" version = "1.4.2" @@ -356,7 +377,7 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "itoa", + "itoa 1.0.18", "matchit", "memchr", "mime", @@ -432,6 +453,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + [[package]] name = "base32" version = "0.5.1" @@ -547,6 +574,17 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -706,7 +744,7 @@ checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" dependencies = [ "camino", "cargo-platform", - "semver", + "semver 1.0.27", "serde", "serde_json", "thiserror 2.0.18", @@ -722,6 +760,12 @@ dependencies = [ "toml 0.9.12+spec-1.1.0", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.57" @@ -785,6 +829,33 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.4.4" @@ -795,6 +866,31 @@ dependencies = [ "inout", ] +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "cobs" version = "0.3.0" @@ -829,6 +925,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_fn" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413d67b29ef1021b4d60f4aa1e925ca031751e213832b4b1d588fae623c05c60" + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -847,7 +949,7 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ - "time", + "time 0.3.47", "version_check", ] @@ -858,7 +960,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ "percent-encoding", - "time", + "time 0.3.47", "version_check", ] @@ -1077,6 +1179,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -1086,6 +1224,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -1101,6 +1258,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -1120,7 +1283,7 @@ checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" dependencies = [ "cssparser-macros", "dtoa-short", - "itoa", + "itoa 1.0.18", "matches", "phf 0.10.1", "proc-macro2", @@ -1137,7 +1300,7 @@ checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" dependencies = [ "cssparser-macros", "dtoa-short", - "itoa", + "itoa 1.0.18", "phf 0.13.1", "smallvec", ] @@ -1266,7 +1429,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version", + "rustc_version 0.4.1", "syn 2.0.117", ] @@ -1287,7 +1450,7 @@ checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "proc-macro2", "quote", - "rustc_version", + "rustc_version 0.4.1", "syn 2.0.117", ] @@ -1368,6 +1531,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + [[package]] name = "dispatch2" version = "0.3.1" @@ -1444,6 +1613,12 @@ dependencies = [ "serde", ] +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + [[package]] name = "dtoa" version = "1.0.11" @@ -1456,7 +1631,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" dependencies = [ - "dtoa", + "dtoa 1.0.11", ] [[package]] @@ -1488,7 +1663,7 @@ checksum = "63a1d0de4f2249aa0ff5884d7080814f446bb241a559af6c170a41e878ed2d45" dependencies = [ "cc", "memchr", - "rustc_version", + "rustc_version 0.4.1", "toml 0.9.12+spec-1.1.0", "vswhom", "winreg", @@ -1512,6 +1687,70 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1631,7 +1870,7 @@ dependencies = [ "openssl", "serde", "serde_json", - "time", + "time 0.3.47", "tokio", "url", "webdriver", @@ -1670,7 +1909,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" dependencies = [ "memoffset", - "rustc_version", + "rustc_version 0.4.1", ] [[package]] @@ -2020,6 +2259,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "genpdf" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1c422344482708cb32db843cf3f55f27918cd24fec7b505bde895a1e8702c34" +dependencies = [ + "derive_more 0.99.20", + "lopdf", + "printpdf", + "rusttype", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -2241,6 +2492,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2309,7 +2571,7 @@ dependencies = [ "http 1.4.0", "httpdate", "mime", - "sha1", + "sha1 0.10.6", ] [[package]] @@ -2405,7 +2667,7 @@ checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", - "itoa", + "itoa 1.0.18", ] [[package]] @@ -2415,7 +2677,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "itoa", + "itoa 1.0.18", ] [[package]] @@ -2467,7 +2729,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa", + "itoa 1.0.18", "pin-project-lite", "pin-utils", "smallvec", @@ -2761,6 +3023,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is-wsl" version = "0.4.0" @@ -2771,6 +3044,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -2780,6 +3062,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + [[package]] name = "itoa" version = "1.0.18" @@ -3023,6 +3311,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -3056,12 +3350,36 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lopdf" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49a0272112719d0037ab63d4bb67f73ba659e1e90bc38f235f163a457ac16f3" +dependencies = [ + "chrono", + "dtoa 0.4.8", + "encoding", + "flate2", + "itoa 0.4.8", + "linked-hash-map", + "log", + "lzw", + "pom", + "time 0.2.27", +] + [[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lzw" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" + [[package]] name = "mac" version = "0.1.1" @@ -3548,6 +3866,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -3616,6 +3940,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3305af35278dd29f46fcdd139e0b1fbfae2153f0e5928b39b035542dd31e37b7" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -4010,7 +4343,35 @@ dependencies = [ "indexmap 2.13.0", "quick-xml", "serde", - "time", + "time 0.3.47", +] + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", ] [[package]] @@ -4052,6 +4413,15 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "pom" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c972d8f86e943ad532d0b04e8965a749ad1d18bb981a9c7b3ae72fe7fd7744b" +dependencies = [ + "bstr", +] + [[package]] name = "postcard" version = "1.1.3" @@ -4104,6 +4474,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "printpdf" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2472a184bcb128d0e3db65b59ebd11d010259a5e14fd9d048cba8f2c9302d4" +dependencies = [ + "js-sys", + "lopdf", + "rusttype", + "time 0.2.27", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -4396,6 +4778,26 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -4605,13 +5007,22 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver", + "semver 1.0.27", ] [[package]] @@ -4685,6 +5096,17 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rusttype" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f61411055101f7b60ecf1041d87fb74205fb20b0c7a723f07ef39174cf6b4c0" +dependencies = [ + "approx", + "ordered-float", + "stb_truetype", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -4852,6 +5274,15 @@ dependencies = [ "smallvec", ] +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + [[package]] name = "semver" version = "1.0.27" @@ -4862,6 +5293,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "serde" version = "1.0.228" @@ -4921,7 +5358,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "itoa", + "itoa 1.0.18", "memchr", "serde", "serde_core", @@ -4934,7 +5371,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ - "itoa", + "itoa 1.0.18", "serde", "serde_core", ] @@ -4975,7 +5412,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa", + "itoa 1.0.18", "ryu", "serde", ] @@ -4996,7 +5433,7 @@ dependencies = [ "serde_core", "serde_json", "serde_with_macros", - "time", + "time 0.3.47", ] [[package]] @@ -5018,7 +5455,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ "indexmap 2.13.0", - "itoa", + "itoa 1.0.18", "ryu", "serde", "unsafe-libyaml", @@ -5032,7 +5469,7 @@ checksum = "6b4d05b26468431333c4d16963433fe0e04ef24e4a7b568c9da81e91d25c0dbb" dependencies = [ "base64 0.22.1", "indexmap 2.13.0", - "itoa", + "itoa 1.0.18", "num-traits", "regex", "saphyr-parser-bw", @@ -5082,6 +5519,15 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + [[package]] name = "sha1" version = "0.10.6" @@ -5160,7 +5606,7 @@ dependencies = [ "num-bigint", "num-traits", "thiserror 2.0.18", - "time", + "time 0.3.47", ] [[package]] @@ -5481,7 +5927,7 @@ dependencies = [ "hex", "hkdf", "hmac", - "itoa", + "itoa 1.0.18", "log", "md-5", "memchr", @@ -5490,7 +5936,7 @@ dependencies = [ "rand 0.8.5", "rsa", "serde", - "sha1", + "sha1 0.10.6", "sha2", "smallvec", "sqlx-core 0.7.4", @@ -5522,7 +5968,7 @@ dependencies = [ "hkdf", "hmac", "home", - "itoa", + "itoa 1.0.18", "log", "md-5", "memchr", @@ -5559,7 +6005,7 @@ dependencies = [ "hkdf", "hmac", "home", - "itoa", + "itoa 1.0.18", "log", "md-5", "memchr", @@ -5606,6 +6052,73 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "standback" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +dependencies = [ + "version_check", +] + +[[package]] +name = "stb_truetype" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f77b6b07e862c66a9f3e62a07588fee67cd90a9135a2b942409f195507b4fb51" +dependencies = [ + "byteorder", +] + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version 0.2.3", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn 1.0.109", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1 0.6.1", + "syn 1.0.109", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + [[package]] name = "string_cache" version = "0.8.9" @@ -5885,7 +6398,7 @@ dependencies = [ "heck 0.5.0", "json-patch", "schemars 0.8.22", - "semver", + "semver 1.0.27", "serde", "serde_json", "tauri-utils", @@ -5908,14 +6421,14 @@ dependencies = [ "png", "proc-macro2", "quote", - "semver", + "semver 1.0.27", "serde", "serde_json", "sha2", "syn 2.0.117", "tauri-utils", "thiserror 2.0.18", - "time", + "time 0.3.47", "url", "uuid", "walkdir", @@ -6049,7 +6562,7 @@ dependencies = [ "quote", "regex", "schemars 0.8.22", - "semver", + "semver 1.0.27", "serde", "serde-untagged", "serde_json", @@ -6166,6 +6679,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb", + "time-macros 0.1.1", + "version_check", + "winapi", +] + [[package]] name = "time" version = "0.3.47" @@ -6173,12 +6701,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", - "itoa", + "itoa 1.0.18", "num-conv", "powerfmt", "serde_core", "time-core", - "time-macros", + "time-macros 0.2.27", ] [[package]] @@ -6187,6 +6715,16 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + [[package]] name = "time-macros" version = "0.2.27" @@ -6197,6 +6735,19 @@ dependencies = [ "time-core", ] +[[package]] +name = "time-macros-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn 1.0.109", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -6207,6 +6758,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.11.0" @@ -6419,7 +6980,7 @@ dependencies = [ "base32", "constant_time_eq", "hmac", - "sha1", + "sha1 0.10.6", "sha2", ] @@ -7010,7 +7571,7 @@ dependencies = [ "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap 2.13.0", - "semver", + "semver 1.0.27", ] [[package]] @@ -7022,7 +7583,7 @@ dependencies = [ "bitflags 2.11.0", "hashbrown 0.16.1", "indexmap 2.13.0", - "semver", + "semver 1.0.27", "serde", ] @@ -7059,7 +7620,7 @@ dependencies = [ "postcard", "pulley-interpreter", "rustix 1.1.4", - "semver", + "semver 1.0.27", "serde", "serde_derive", "smallvec", @@ -7095,7 +7656,7 @@ dependencies = [ "log", "object", "postcard", - "semver", + "semver 1.0.27", "serde", "serde_derive", "sha2", @@ -7153,7 +7714,7 @@ dependencies = [ "cranelift-frontend", "cranelift-native", "gimli", - "itertools", + "itertools 0.14.0", "log", "object", "pulley-interpreter", @@ -7357,7 +7918,7 @@ dependencies = [ "serde_derive", "serde_json", "thiserror 1.0.69", - "time", + "time 0.3.47", "unicode-segmentation", "url", ] @@ -8147,7 +8708,7 @@ dependencies = [ "id-arena", "indexmap 2.13.0", "log", - "semver", + "semver 1.0.27", "serde", "serde_derive", "serde_json", @@ -8166,7 +8727,7 @@ dependencies = [ "id-arena", "indexmap 2.13.0", "log", - "semver", + "semver 1.0.27", "serde", "serde_derive", "serde_json", @@ -8348,6 +8909,7 @@ dependencies = [ "anyhow", "async-trait", "chrono", + "criterion", "futures", "libsqlite3-sys", "serde", @@ -8513,6 +9075,7 @@ dependencies = [ "dashmap", "data-encoding", "futures", + "genpdf", "hex", "jsonwebtoken", "pgvector", diff --git a/admin-v2/package.json b/admin-v2/package.json index b344dd9..215390c 100644 --- a/admin-v2/package.json +++ b/admin-v2/package.json @@ -21,6 +21,7 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "react-router-dom": "^7.13.2", + "recharts": "^3.8.1", "zustand": "^5.0.12" }, "devDependencies": { diff --git a/admin-v2/pnpm-lock.yaml b/admin-v2/pnpm-lock.yaml index 1a6613b..cf95935 100644 --- a/admin-v2/pnpm-lock.yaml +++ b/admin-v2/pnpm-lock.yaml @@ -35,9 +35,12 @@ importers: react-router-dom: specifier: ^7.13.2 version: 7.13.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + recharts: + specifier: ^3.8.1 + version: 3.8.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@18.3.1)(react@19.2.4)(redux@5.0.1) zustand: specifier: ^5.0.12 - version: 5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) + version: 5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) devDependencies: '@eslint/js': specifier: ^9.39.4 @@ -832,6 +835,17 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' + '@reduxjs/toolkit@2.11.2': + resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@rolldown/binding-android-arm64@1.0.0-rc.12': resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -936,6 +950,9 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@tailwindcss/node@4.2.2': resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} @@ -1076,6 +1093,33 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -1099,6 +1143,9 @@ packages: '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@typescript-eslint/eslint-plugin@8.57.2': resolution: {integrity: sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1367,6 +1414,50 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + data-urls@7.0.0: resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -1383,6 +1474,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} @@ -1444,6 +1538,9 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + es-toolkit@1.45.1: + resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1512,6 +1609,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -1646,6 +1746,12 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + + immer@11.1.4: + resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1658,6 +1764,10 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2012,6 +2122,18 @@ packages: react-lifecycles-compat@3.0.4: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react-router-dom@7.13.2: resolution: {integrity: sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==} engines: {node: '>=20.0.0'} @@ -2038,10 +2160,26 @@ packages: peerDependencies: react: '*' + recharts@3.8.1: + resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -2050,6 +2188,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -2187,6 +2328,9 @@ packages: resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} engines: {node: '>=12.22'} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -2273,6 +2417,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + vite@8.0.3: resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3410,6 +3557,18 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.4 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.4 + react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) + '@rolldown/binding-android-arm64@1.0.0-rc.12': optional: true @@ -3466,6 +3625,8 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@standard-schema/utils@0.3.0': {} + '@tailwindcss/node@4.2.2': dependencies: '@jridgewell/remapping': 2.3.5 @@ -3587,6 +3748,30 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} @@ -3607,6 +3792,8 @@ snapshots: '@types/statuses@2.0.6': {} + '@types/use-sync-external-store@0.0.6': {} + '@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -3947,6 +4134,44 @@ snapshots: csstype@3.2.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + data-urls@7.0.0: dependencies: whatwg-mimetype: 5.0.0 @@ -3960,6 +4185,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + decimal.js@10.6.0: {} deep-is@0.1.4: {} @@ -4008,6 +4235,8 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + es-toolkit@1.45.1: {} + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -4101,6 +4330,8 @@ snapshots: esutils@2.0.3: {} + eventemitter3@5.0.4: {} + expect-type@1.3.0: {} fast-deep-equal@3.1.3: {} @@ -4210,6 +4441,10 @@ snapshots: ignore@7.0.5: {} + immer@10.2.0: {} + + immer@11.1.4: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -4219,6 +4454,8 @@ snapshots: indent-string@4.0.0: {} + internmap@2.0.3: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -4545,6 +4782,15 @@ snapshots: react-lifecycles-compat@3.0.4: {} + react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + redux: 5.0.1 + react-router-dom@7.13.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: react: 19.2.4 @@ -4566,15 +4812,43 @@ snapshots: lodash: 4.17.23 react: 19.2.4 + recharts@3.8.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@18.3.1)(react@19.2.4)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.45.1 + eventemitter3: 5.0.4 + immer: 10.2.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-is: 18.3.1 + react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@19.2.4) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + redent@3.0.0: dependencies: indent-string: 4.0.0 strip-indent: 3.0.0 + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + require-directory@2.1.1: {} require-from-string@2.0.2: {} + reselect@5.1.1: {} + resize-observer-polyfill@1.5.1: {} resolve-from@4.0.0: {} @@ -4702,6 +4976,8 @@ snapshots: throttle-debounce@5.0.2: {} + tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} tinycolor2@1.6.0: {} @@ -4776,6 +5052,23 @@ snapshots: dependencies: react: 19.2.4 + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(jiti@2.6.1)(terser@5.46.1): dependencies: lightningcss: 1.32.0 @@ -4893,8 +5186,9 @@ snapshots: zod@4.3.6: {} - zustand@5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): + zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): optionalDependencies: '@types/react': 19.2.14 + immer: 11.1.4 react: 19.2.4 use-sync-external-store: 1.6.0(react@19.2.4) diff --git a/admin-v2/src/pages/Usage.tsx b/admin-v2/src/pages/Usage.tsx index 071ac24..cac2eb8 100644 --- a/admin-v2/src/pages/Usage.tsx +++ b/admin-v2/src/pages/Usage.tsx @@ -1,18 +1,71 @@ // ============================================================ -// 用量统计 +// 用量统计 + 转化漏斗 // ============================================================ import { useState } from 'react' import { useQuery } from '@tanstack/react-query' import { Card, Col, Row, Select, Statistic } from 'antd' -import { ThunderboltOutlined, ColumnWidthOutlined } from '@ant-design/icons' +import { ThunderboltOutlined, ColumnWidthOutlined, UserOutlined, TeamOutlined } from '@ant-design/icons' import type { ProColumns } from '@ant-design/pro-components' import { ProTable } from '@ant-design/pro-components' +import { + BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, + FunnelChart, Funnel, LabelList, +} from 'recharts' import { telemetryService } from '@/services/telemetry' +import { statsService } from '@/services/stats' import { PageHeader } from '@/components/PageHeader' import { ErrorState } from '@/components/ErrorState' import type { DailyUsageStat, ModelUsageStat } from '@/types' +// ─── Conversion Funnel Data ─── + +interface FunnelStep { + name: string + value: number + fill: string +} + +function buildFunnelData( + totalAccounts: number, + activeAccounts: number, + dailyData?: DailyUsageStat[], + modelData?: ModelUsageStat[], +): FunnelStep[] { + const activeDevicesToday = dailyData?.length + ? dailyData.reduce((s, d) => s + d.unique_devices, 0) + : 0 + const activeModels = modelData?.filter((m) => m.request_count > 0).length ?? 0 + + return [ + { name: '注册用户', value: totalAccounts, fill: '#8c8c8c' }, + { name: '活跃用户', value: activeAccounts, fill: '#863bff' }, + { name: '今日使用', value: Math.max(activeDevicesToday, 0), fill: '#47bfff' }, + { name: '使用多模型', value: activeModels, fill: '#10b981' }, + ] +} + +// ─── Daily Trend Bar Data ─── + +interface DailyTrend { + day: string + requests: number + inputTokens: number + outputTokens: number +} + +function buildDailyTrend(data?: DailyUsageStat[]): DailyTrend[] { + if (!data) return [] + return data.map((d) => ({ + day: d.day.slice(5), // MM-DD + requests: d.request_count, + inputTokens: Math.round(d.input_tokens / 1000), // K tokens + outputTokens: Math.round(d.output_tokens / 1000), + })) +} + +// ─── Main Component ─── + export default function Usage() { const [days, setDays] = useState(30) @@ -31,6 +84,11 @@ export default function Usage() { queryFn: ({ signal }) => telemetryService.modelStats({}, signal), }) + const { data: dashboardStats } = useQuery({ + queryKey: ['stats-dashboard'], + queryFn: ({ signal }) => statsService.dashboard(signal), + }) + if (dailyError) { return ( <> @@ -43,6 +101,12 @@ export default function Usage() { const totalRequests = dailyData?.reduce((s, d) => s + d.request_count, 0) ?? 0 const totalTokens = dailyData?.reduce((s, d) => s + d.input_tokens + d.output_tokens, 0) ?? 0 + const totalAccounts = dashboardStats?.total_accounts ?? 0 + const activeAccounts = dashboardStats?.active_accounts ?? 0 + + const funnelData = buildFunnelData(totalAccounts, activeAccounts, dailyData, modelData) + const trendData = buildDailyTrend(dailyData) + const dailyColumns: ProColumns[] = [ { title: '日期', dataIndex: 'day', width: 120 }, { @@ -104,7 +168,7 @@ export default function Usage() {
- + - + + + + + 注册用户 + + } + value={totalAccounts} + prefix={} + valueStyle={{ fontWeight: 600, color: '#10b981' }} + /> + + + + + + 活跃用户 + + } + value={activeAccounts} + prefix={} + valueStyle={{ fontWeight: 600, color: '#f59e0b' }} + /> + + + + + {/* Conversion Funnel + Daily Trend */} + + + + 用户转化漏斗 + + } + size="small" + > + + + [value.toLocaleString(), '数量']} + /> + + + + + + + + + + 每日趋势 + + } + size="small" + > + + + + + + { + const labels: Record = { + requests: '请求数', + inputTokens: '输入 Token(K)', + outputTokens: '输出 Token(K)', + } + return [value.toLocaleString(), labels[name] ?? name] + }} + /> + + + + + + + {/* Daily Stats */} diff --git a/crates/zclaw-growth/Cargo.toml b/crates/zclaw-growth/Cargo.toml index 1ac8ec3..1d6dfba 100644 --- a/crates/zclaw-growth/Cargo.toml +++ b/crates/zclaw-growth/Cargo.toml @@ -39,3 +39,9 @@ zclaw-types = { workspace = true } [dev-dependencies] tokio-test = "0.4" +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "retrieval_bench" +harness = false +path = "benches/retrieval_bench.rs" diff --git a/crates/zclaw-growth/benches/retrieval_bench.rs b/crates/zclaw-growth/benches/retrieval_bench.rs new file mode 100644 index 0000000..4a20367 --- /dev/null +++ b/crates/zclaw-growth/benches/retrieval_bench.rs @@ -0,0 +1,151 @@ +//! Benchmark for TF-IDF retrieval performance in zclaw-growth +//! +//! Measures: +//! - Indexing throughput (documents/sec) +//! - Query latency at various corpus sizes (10/50/100/500 candidates) +//! - Top-K retrieval latency + +use criterion::{ + black_box, criterion_group, criterion_main, BenchmarkId, Criterion, +}; +use zclaw_growth::retrieval::SemanticScorer; +use zclaw_growth::types::{MemoryEntry, MemoryType}; + +/// Generate a synthetic memory entry +fn make_entry(agent: &str, idx: usize, topic: &str, content: &str) -> MemoryEntry { + MemoryEntry::new( + agent, + MemoryType::Knowledge, + &format!("fact-{idx}"), + content.to_string(), + ) + .with_keywords(vec![topic.to_string(), format!("topic-{idx}")]) +} + +/// Build a corpus of N entries with realistic content +fn build_corpus(size: usize) -> (SemanticScorer, Vec) { + let mut scorer = SemanticScorer::new(); + let mut entries = Vec::with_capacity(size); + + let topics = [ + ("rust", "Rust is a systems programming language focused on safety and performance with zero-cost abstractions"), + ("python", "Python is a high-level general-purpose programming language emphasizing code readability"), + ("machine-learning", "Machine learning is a subset of artificial intelligence that enables systems to learn from data"), + ("web-development", "Web development involves building and maintaining websites using frontend and backend technologies"), + ("database", "Database management systems provide tools for storing retrieving and managing structured data efficiently"), + ("security", "Cybersecurity involves protecting computer systems and networks from information disclosure"), + ("devops", "DevOps combines software development and IT operations to shorten the systems development lifecycle"), + ("testing", "Software testing validates that applications meet their specified requirements and are free of defects"), + ("api", "API design involves creating interfaces that allow different software applications to communicate"), + ("cloud", "Cloud computing delivers computing services over the internet including servers storage databases networking"), + ]; + + for i in 0..size { + let (topic, base_content) = &topics[i % topics.len()]; + let content = format!( + "{base_content}. This fact #{i} discusses {topic} in depth with examples and use cases. \ + The key concepts include {topic} patterns, Implementation details cover performance optimization." + ); + let entry = make_entry("bench-agent", i, topic, &content); + scorer.index_entry(&entry); + entries.push(entry); + } + + (scorer, entries) +} + +/// Build a list of entries for indexing benchmarks +fn build_entries(count: usize) -> Vec { + let topics = ["rust", "python", "ml", "web", "database"]; + (0..count) + .map(|i| { + let topic = topics[i % topics.len()]; + let content = format!( + "Fact {} about {}: detailed technical content with multiple keywords and concepts \ + covering advanced patterns, best practices, and optimization strategies.", + i, topic + ); + make_entry("bench-agent", i, topic, &content) + }) + .collect() +} + +// ─── Indexing throughput ─── + +fn bench_indexing(c: &mut Criterion) { + let mut group = c.benchmark_group("index_entry"); + group.sample_size(50); + + for &batch_size in &[10, 50, 100, 500] { + let entries = build_entries(batch_size); + + group.bench_with_input( + BenchmarkId::new("batch", batch_size), + &entries, + |b, entries| { + b.iter(|| { + let mut scorer = SemanticScorer::new(); + for entry in entries { + scorer.index_entry(black_box(entry)); + } + }); + }, + ); + } +} + +// ─── Query scoring latency ─── + +fn bench_query_scoring(c: &mut Criterion) { + let mut group = c.benchmark_group("score_similarity"); + + for &corpus_size in &[10, 50, 100, 500] { + let (scorer, entries) = build_corpus(corpus_size); + let query = "rust safety performance optimization"; + let entry = &entries[corpus_size / 2]; + + group.bench_with_input( + BenchmarkId::new("corpus", corpus_size), + &scorer, + |b, scorer| { + b.iter(|| scorer.score_similarity(black_box(query), black_box(entry))); + }, + ); + } +} + +// ─── Top-K retrieval ─── + +fn bench_top_k_retrieval(c: &mut Criterion) { + let mut group = c.benchmark_group("top_k_retrieval"); + + for &corpus_size in &[10, 50, 100, 500] { + let (scorer, entries) = build_corpus(corpus_size); + let query = "machine learning model training optimization"; + + group.bench_with_input( + BenchmarkId::new("top3", corpus_size), + &entries, + |b, entries| { + b.iter(|| { + let mut scored: Vec<(f32, usize)> = entries + .iter() + .enumerate() + .map(|(idx, entry)| (scorer.score_similarity(query, entry), idx)) + .collect(); + scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal)); + let _top3 = scored.into_iter().take(3).collect::>(); + }); + }, + ); + } +} + +criterion_group!( + benches, + bench_indexing, + bench_query_scoring, + bench_top_k_retrieval, +); + +criterion_main!(benches); diff --git a/crates/zclaw-saas/Cargo.toml b/crates/zclaw-saas/Cargo.toml index 9f2c499..b5b2b8e 100644 --- a/crates/zclaw-saas/Cargo.toml +++ b/crates/zclaw-saas/Cargo.toml @@ -51,6 +51,7 @@ aes-gcm = { workspace = true } sha2 = { workspace = true } bytes = { workspace = true } async-stream = { workspace = true } +genpdf = "0.2" [dev-dependencies] tempfile = { workspace = true } diff --git a/crates/zclaw-saas/src/billing/handlers.rs b/crates/zclaw-saas/src/billing/handlers.rs index ddbe21c..6aed99c 100644 --- a/crates/zclaw-saas/src/billing/handlers.rs +++ b/crates/zclaw-saas/src/billing/handlers.rs @@ -473,3 +473,77 @@ mod crypto { provided.len() >= 16 && expected.len() >= 16 && provided == expected } } + +// === 发票 PDF === + +/// GET /api/v1/billing/invoices/:id/pdf — 下载发票 PDF +pub async fn get_invoice_pdf( + State(state): State, + Extension(ctx): Extension, + Path(invoice_id): Path, +) -> SaasResult { + // 查询发票(需属于当前账户) + let invoice: Invoice = sqlx::query_as::<_, Invoice>( + "SELECT * FROM billing_invoices WHERE id = $1 AND account_id = $2" + ) + .bind(&invoice_id) + .bind(&ctx.account_id) + .fetch_optional(&state.db) + .await? + .ok_or_else(|| SaasError::NotFound("发票不存在".into()))?; + + // 仅已支付的发票可下载 PDF + if invoice.status != "paid" { + return Err(SaasError::InvalidInput("仅已支付的发票可导出 PDF".into())); + } + + // 查询关联支付记录 + let payments: Vec = sqlx::query_as::<_, Payment>( + "SELECT * FROM billing_payments WHERE invoice_id = $1" + ) + .bind(&invoice_id) + .fetch_all(&state.db) + .await?; + + // 构造发票信息(从 invoice metadata 中提取) + let info = super::invoice_pdf::InvoiceInfo { + title: invoice.metadata.get("invoice_title") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), + tax_id: invoice.metadata.get("tax_id") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), + email: invoice.metadata.get("email") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), + address: invoice.metadata.get("address") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), + phone: invoice.metadata.get("phone") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), + }; + + // 生成 PDF + let bytes = super::invoice_pdf::generate_invoice_pdf(&invoice, &payments, &info) + .map_err(|e| { + tracing::error!("Invoice PDF generation failed: {}", e); + SaasError::Internal("PDF 生成失败".into()) + })?; + + // 返回 PDF 响应 + Ok(axum::response::Response::builder() + .status(200) + .header("Content-Type", "application/pdf") + .header( + "Content-Disposition", + format!("attachment; filename=\"invoice-{}.pdf\"", invoice.id), + ) + .body(axum::body::Body::from(bytes)) + .unwrap()) +} diff --git a/crates/zclaw-saas/src/billing/invoice_pdf.rs b/crates/zclaw-saas/src/billing/invoice_pdf.rs new file mode 100644 index 0000000..887184c --- /dev/null +++ b/crates/zclaw-saas/src/billing/invoice_pdf.rs @@ -0,0 +1,107 @@ +//! 发票 PDF 生成模块 +//! +//! 使用 genpdf 生成 PDF 发票。 +//! genpdf 需要 TTF 字体文件来测量文本宽度,我们使用 Windows 内置 Arial 字体。 +//! 注意: Arial 不支持中文字符,发票字段使用英文标签。 + +use genpdf::elements::Paragraph; +use genpdf::fonts; +use genpdf::{Document, Element, SimplePageDecorator}; + +use crate::billing::types::Invoice; +use crate::billing::types::Payment; + +/// 发票信息结构 — 用于客户填写 +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct InvoiceInfo { + pub title: String, + pub tax_id: String, + pub email: String, + pub address: String, + pub phone: String, +} + +/// 加载 Arial 字体族(Windows 系统字体) +fn load_font_family() -> Result, Box> { + let font_dir = "C:/Windows/Fonts"; + let family = fonts::from_files(font_dir, "arial", Some(fonts::Builtin::Helvetica))?; + Ok(family) +} + +/// 生成发票 PDF 字节 +pub fn generate_invoice_pdf( + invoice: &Invoice, + payments: &[Payment], + info: &InvoiceInfo, +) -> Result, Box> { + let font_family = load_font_family()?; + let mut doc = Document::new(font_family); + doc.set_title(&format!("ZCLAW Invoice #{}", invoice.id)); + + let mut decorator = SimplePageDecorator::new(); + decorator.set_margins(10); + doc.set_page_decorator(decorator); + + // Header + let header_style = genpdf::style::Style::new().with_font_size(14).bold(); + doc.push(Paragraph::new(format!("ZCLAW INVOICE #{}", invoice.id)).styled(header_style)); + doc.push(Paragraph::new("")); + + // Customer info + let info_style = genpdf::style::Style::new().with_font_size(10); + doc.push( + Paragraph::new(format!( + "Title: {}\nTax ID: {}\nEmail: {}\nAddress: {}\nPhone: {}", + info.title, info.tax_id, info.email, info.address, info.phone, + )) + .styled(info_style), + ); + doc.push(Paragraph::new("")); + + // Invoice details + let detail_style = genpdf::style::Style::new().with_font_size(10); + let amount_yuan = format!("{:.2} CNY", invoice.amount_cents as f64 / 100.0); + let created = invoice.created_at.format("%Y-%m-%d %H:%M"); + + doc.push( + Paragraph::new(format!( + "Plan: {}\nSubscription: {}\nAmount: {}\nDate: {}\nStatus: {}", + invoice.plan_id.as_deref().unwrap_or("N/A"), + invoice.subscription_id.as_deref().unwrap_or("N/A"), + amount_yuan, + created, + invoice.status, + )) + .styled(detail_style), + ); + doc.push(Paragraph::new("")); + + // Payment records + if !payments.is_empty() { + doc.push(Paragraph::new("Payments:").styled(detail_style)); + for p in payments { + let paid_at = p + .paid_at + .map(|t| t.format("%Y-%m-%d %H:%M").to_string()) + .unwrap_or_else(|| "pending".to_string()); + let amount = format!("{:.2} CNY", p.amount_cents as f64 / 100.0); + doc.push( + Paragraph::new(format!( + " {} via {} - {} ({})", + amount, p.method, p.status, paid_at, + )) + .styled(detail_style), + ); + } + doc.push(Paragraph::new("")); + } + + // Footer + let footer_style = genpdf::style::Style::new().with_font_size(8); + doc.push(Paragraph::new("Thank you for using ZCLAW.").styled(footer_style)); + + // Render to bytes + let mut buf: Vec = Vec::new(); + doc.render(&mut buf)?; + Ok(buf) +} diff --git a/crates/zclaw-saas/src/billing/mod.rs b/crates/zclaw-saas/src/billing/mod.rs index 6b54b1a..063abed 100644 --- a/crates/zclaw-saas/src/billing/mod.rs +++ b/crates/zclaw-saas/src/billing/mod.rs @@ -4,6 +4,7 @@ pub mod types; pub mod service; pub mod handlers; pub mod payment; +pub mod invoice_pdf; use axum::routing::{get, post}; @@ -17,6 +18,7 @@ pub fn routes() -> axum::Router { .route("/api/v1/billing/usage/increment", post(handlers::increment_usage_dimension)) .route("/api/v1/billing/payments", post(handlers::create_payment)) .route("/api/v1/billing/payments/{id}", get(handlers::get_payment_status)) + .route("/api/v1/billing/invoices/{id}/pdf", get(handlers::get_invoice_pdf)) } /// 支付回调路由(无需 auth — 支付宝/微信服务器回调)