From cb5fb93c970ad54946a6c4ba033aa8657bb77a1e Mon Sep 17 00:00:00 2001 From: luisdralves Date: Thu, 2 May 2024 19:41:50 +0100 Subject: [PATCH] Initial implementation --- .gitignore | 5 + Cargo.lock | 1311 +++++++++++++++++ Cargo.toml | 17 + README.md | 30 + biome.json | 30 + bun.lockb | Bin 0 -> 96634 bytes index.html | 13 + package.json | 28 + public/favicon.svg | 21 + src/client/App.css | 26 + src/client/App.tsx | 45 + src/client/api.ts | 17 + src/client/components/chart-cards/cpu.tsx | 44 + src/client/components/chart-cards/disks.tsx | 37 + src/client/components/chart-cards/index.tsx | 66 + src/client/components/chart-cards/memory.tsx | 37 + src/client/components/chart-cards/network.tsx | 37 + src/client/components/chart-cards/static.tsx | 63 + src/client/components/chart-cards/temps.tsx | 39 + src/client/components/legend/index.css | 19 + src/client/components/legend/index.tsx | 21 + src/client/hooks/use-animation-frame.ts | 24 + src/client/index.css | 33 + src/client/main.tsx | 10 + src/client/types.ts | 31 + src/client/utils/colors.ts | 3 + src/client/utils/format.ts | 20 + src/client/vite-env.d.ts | 1 + src/stats-server/disks.rs | 56 + src/stats-server/main.rs | 149 ++ tsconfig.json | 28 + tsconfig.node.json | 11 + vite.config.ts | 13 + 33 files changed, 2285 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 biome.json create mode 100755 bun.lockb create mode 100644 index.html create mode 100644 package.json create mode 100644 public/favicon.svg create mode 100644 src/client/App.css create mode 100644 src/client/App.tsx create mode 100644 src/client/api.ts create mode 100644 src/client/components/chart-cards/cpu.tsx create mode 100644 src/client/components/chart-cards/disks.tsx create mode 100644 src/client/components/chart-cards/index.tsx create mode 100644 src/client/components/chart-cards/memory.tsx create mode 100644 src/client/components/chart-cards/network.tsx create mode 100644 src/client/components/chart-cards/static.tsx create mode 100644 src/client/components/chart-cards/temps.tsx create mode 100644 src/client/components/legend/index.css create mode 100644 src/client/components/legend/index.tsx create mode 100644 src/client/hooks/use-animation-frame.ts create mode 100644 src/client/index.css create mode 100644 src/client/main.tsx create mode 100644 src/client/types.ts create mode 100644 src/client/utils/colors.ts create mode 100644 src/client/utils/format.ts create mode 100644 src/client/vite-env.d.ts create mode 100644 src/stats-server/disks.rs create mode 100644 src/stats-server/main.rs create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d849ce --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +dist-ssr +*.local +target \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c257fbb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1311 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "async-trait" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6137c6234afb339e75e764c866e3594900f0211e1315d33779f269bbe2ec6967" +dependencies = [ + "async-trait", + "axum-core", + "axum-macros", + "base64 0.21.0", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-http", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cae3e661676ffbacb30f1a824089a8c9150e71017f7e1e38f2aa32009188d34" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-macros" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fbf955307ff8addb48d2399393c9e2740dd491537ec562b66ab364fc4a38841" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" + +[[package]] +name = "futures-sink" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" + +[[package]] +name = "futures-task" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" + +[[package]] +name = "futures-util" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.45.0", +] + +[[package]] +name = "ntapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys 0.45.0", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "rustversion" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "serde_json" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b04f22b563c91331a10074bda3dd5492e3cc39d56bd557e91c0af42b6c7341" +dependencies = [ + "serde", +] + +[[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 = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "stats_server" +version = "0.1.0" +dependencies = [ + "axum", + "serde_json", + "sysinfo", + "tokio", + "tracing-subscriber", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sysinfo" +version = "0.30.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87341a165d73787554941cd5ef55ad728011566fe714e987d1b976c15dbc3a83" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.42.0", +] + +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" +dependencies = [ + "base64 0.13.1", + "byteorder", + "bytes", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-bidi" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58" + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[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.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.1", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm 0.42.1", + "windows_x86_64_msvc 0.42.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.1", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm 0.42.1", + "windows_x86_64_msvc 0.42.1", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ee8f2f5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "stats_server" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +axum = { version = "0.6.9", features = ["macros", "ws"] } +serde_json = "1.0.93" +sysinfo = "0.30.11" +tokio = { version = "1.25.0", features = ["full"] } +tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } + +[[bin]] +name = "stats_server" +path = "src/stats-server/main.rs" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d6babe --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +} +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..29794af --- /dev/null +++ b/biome.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.7.1/schema.json", + "organizeImports": { + "enabled": true + }, + "files": { + "ignore": ["src/stats-server", "dist"], + "ignoreUnknown": true + }, + "formatter": { + "indentStyle": "space", + "lineWidth": 120 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "noNonNullAssertion": "off" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "jsxQuoteStyle": "single", + "arrowParentheses": "asNeeded" + } + } +} diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..3f166ebcbcf0c87b07f85815080d7d5d8fb1223a GIT binary patch literal 96634 zcmeFac{o*F|37}{m`P-ak~x{DCS{6@nP(yMlzB*G$P_AbWJriID`W^oNEss$nG?zo zMHCg^wK)5J&hxoG&wZ26AHUyqJ@4z@UT3YnUa$9iz4uycuf5N??pT@mFS@z%o7p<@ zTRM6$nO$@w1_!UblewLht-U3$m6NN3sXMQS05L8Kh59*CWj6V1Y}|-jqvONzd(N81 zgy>?1+A5n9hYo!>?tfz169=?Hp?+`tMWG1)2l`{m*H$qfHh>s~@^`f~w=s2fcY6qG zh&IZm=I-EgI1nTSWj5d*)+d58#6tjg1GEt4b+>h}bj2VFpsA~?sTU!7vmGeA*_b+8 z0(3TYw}I!~%uVerY4A4rTD$(zF?X_ea{c))o~5aqB}xXICkK9xW7-|Yl*tJ;d32aE z8Xz@jHxBAy`(+R>jPD~rSl)+;ml%V`KqvNq`Zdr;*uQyD-pm8&H?OUmCMb}B`gfq7 z2B0_z3I+c2H+OS$_j0yG34j9B{|Rs}z*nFhJa6x0Vd`dsLVX3gkZ*49<_SKclEDGv z3kQ`{06lEoEn&Tto27@Pqr01{lfC^#XA~+91jPW(djdQF&;V1G0!RnSYyb}egu$}` z90i?Y0$2|a)~5kv1?UTq1>kvri~uDt$P5s+!^M;*K_{S}R)G5f)&PWYJ3G0#gTA7O zXgAC2APF!(j;4-IwiYOqrJLDBTYC#sCkToX)IS9X^NJ+m}f1_d2!5nE=+kJ zK*)~R7tj+d8_VPii>+}7IB$rX%X7Ep$MU?Wp|H*t?X^BZQLzEI~b>(rI!cH8}X6N zvX#B5ySt?Y>IcVWUM$@%y4!-dE!|u$TDp3nP95F!Yw70fY6(0{fOc?Pm4SNLPg8Rz zS9l@bfqIy~!DE~Cji3y#w}q3ryQ{6ErG=&0MQhN`!3}yxp{l@n7|$geTXP#wwso`g zgdLDRfw?{+n0W^lNnQ{%SdH!N!1;bq53hrbsT(hd-MRymp-#rh&Gugb!aSK?*ytwe zqN^?M1>n>9qNA6&lZB=Ksm-{o0K##d3F3g`^d>-IBsX>30Kz=+p58P0=Fn@I3}EAHhKGGH{(4I5XNEVYHDr?^45c?-!HeBcL#v*JTjgw zY+W}d#3A|3dJCWjuZI$dlL;V5#|AnoY{owX5canfASFO`fN=c)(_|x`V6EQB&#|*8 z6a}aUecsS(1Z9{vkkgIp|66I3HwF;qy#*l5PbomS{@n%$^B4>eUSE66c^%Aos*6s( zamOy^2q?Yi^I)~G_TG1D)izYiD*9vJFkSTXINV+r|Af;*+@Er;lfQk7@2v45%P!9B zp-w_#v90z2Rrjd9F9gTFPxyHq+fyBTHCo*~|lTrgzfjyI% zlV3X;=s?=5b78}3_5 zAA2px^Q-k2!(~;R37;N+2?BL1*RW;_t>N0cq2B`NV*?K8`*|!KKF(2iLg;6w?Br>7 z!X@c5&wrdx@;j2!vVJ4#cS;2-#q^0U-m1IzguZ^njy4kbbagMjdzXd%t3_P($3~Q^eybwR5X-EQAvT~=W(BO&l8Rqn!ak8RU!=F?#Xn0ZC7IGc1?b*^gf%9 zu__}QOa0p$dsxM!FA*$O98+p5zZ+Y6nUF%9`!P;jThp>Xqt89i((~gj_l3I>%hhHyzlg{gsg~_cGS=V5RG#LdJS>cJdzSKyQ5rtTN0dNXByZr|#vphn9vOAYZ(?gI=I*-Zp$39_FY>W7DG@8k97RW*qrvS3MxixMSzl=jNRCYv=G%hCd%}j_}z{i`(O=tKO|_w9x$C zaYf{Z@&%3kI`s0d8NWVij(m56d?m907ngjlN2h-H1=WrIG3~a-` zzCW1YgD^iip&4V$qv>gl7tMW^Sr?w`3w$RIFOTA@dCVszbhxXI^3;;CQfzB#NVRm% z@7-CuCvA7rm#MqS`Ru)GcK&^~j(V$#>7LS;y{w`HSG__*mW0)HMY`?n4{NMk!ft5F7q>1a%n1(N=o!JC4Qdr z?iGiR$4`pnY1(}w-PY9idFp4zI=k^c#vgM`d_cPPiRIgOcj_bu&Vy!oab~Ko(?_}= zlr!ehHN_AOT`kJ@Y&FU^v%i_vGdId5(k(^aEz$n%+o643n^TszvtceW z2zBH=eank`rO$iHdZXmk{Oq3$7C$OzbBO$&sk$JW!=u};NjcpsNj;KyvX;=7cuJQ+ zB0YSy%dK{;M?;jT<3oUe5{+S>OoO?3JIA8y%|V+gGQEKo75@4Rx|RkiGZ!{7NfPoC zul4de6~3iSEUCurmK;<(%|Yh>!jSuV2&cH&wd1?-jNXMES`#-2BMO%2-mlo%Bv(Sm zc(!gYD?u&K?fz5R`xHIK6yFG_L>Q5?$!JN$^BA_1DtDhNo97HA;7nD{EXh8_D1ZKP ztm4@l7IDQ-`kO~i3ZB)X4;U#BQ*riUxLPkN9;TkLVr=h2gQw4+!VuJK={I%BRsVgq zYT@fkpL^+!4wvVXn`_)ppZ#GrWW1N~NItV(#_H8|wi!|;I+8$kq4!|}?!_uCgl@nf5jaP@kzWU>=-kkfK&3?Bs7;{sHvw!X89Gh{ONn&E_|X^n9w1MW&7~uxvz6d zZ`+%Ngrc|fKFkaMbG%3PnOtFTT<`vThlq;PGUC;@EXowzSgUA3iF@omK@{{e4$p7v zeN=DNHkn;tjW%6=M%9O3)$ox~Z1Guf%bue)XgwjVcKp0XE`Ir6KMbfAkn!yHym6p4 ze%|K6VD&7s8xQ@f@u$v-kxL_v2N!F5`q)I8+Fni5tDWZ~Q1QOx%sD5O>hXlvwhPa- zY3fM9#iHTan2Lr5TNc5vuTvV2pmZL-*MqNq@5W^`U${?xresh{3inJdn2f>1a z`%aFU>_#**d67Z4h zwp~6Ac<})958nN3e2cJMz6s#N`48S1!S^5Ajz1ajr7-bB-`n*+2>4nU{-5Mu0ld@! zufF_Y?%_4quK!TLN7gUM-R}5#it!Jo5q!^sLarnD__qdQg|-`P+EhXO8=t|S?Z$5e z_(=bu#&-F)0U!1s#tq(U!FTdoG9>=jfR9{%NQ8OViXr^HV9*Hw|F8^W-){WUfUf}f z@cJ`>W2=VvPX~Ot9q<mihnCy*aqQe0R9oc zN7ml$-oM@h{&B#EnqVmYr3=e{XNdnX@MX}M9qP-ySKPt%KgPJT{`&#`PWs=nL;sXaJG=g7fG-O2 z2k)C`aBTPfnF{!7fDi9KTYdKgn*Md4`40HU03Y7Jp?|O)-jX5rA88g8N^b}J+dJeh z0zT~jAIHy%Lh0;)p9J{lcfkL?L;uQbo9{pVwEuSjUtU0{;kqyZ4V@fDhMy7&Fw^ zZvKV1H}?Nst{)^qjjb5M zHw64M82^xm)c=zP(k>nFB>^Ai9gbb3eyjXZz<0&+zuovvc>neOcB@<%8xrR&z(?Lc z!uqXhApBQ=5A%nty9h$9zcYkS$@j1LVf*d!H2@#ZU#JIT-){VofG-aCfV#nj*KsR` z#NP(^@cjXtx5ymW4j%=6V2aHDt@?)gi2sv-53e714ZZRG!&ZD_^SuEdxqtr2^M5(u z!~7xZHWJrA#euZ@2KaFO+UmN8=puYc@Z|yYzn$D+_umi0N8(5H{|OQQPXHh0|4-xp z2Kcc5+Zj99@gEiZ_woBraU-hYw)Z+HGQ0zRC7Fn&Z6@$q+vw8atn*ZViv z9;yF3E_S=4fDcmW5BF}6yWRD}AMj!Sk$xk%RsT&G|B$zxF^Kf@2d4jsU!;Dk{&|G| zef{2Q42T~K3?K22jDxNEPXT;5{}BE^@sRU~|9-%S*AK=G=g@ZJrv{5J^p9Nkt;T`) z*93fI{%vOtBK&KBFN^UHeIs!o{rpde@H;U6k@9x=c;Mj!#t(gOXAMI9$pAj`{$i`X zVV@9w0N{fz_`|(F96Q_1e+}TPVDbmA{Z?Z@{F8#t!}}-fJLDqu;N#yK(#{s}!4TYd zeuI0@?fOpvd`ZAZ)*nRoZ=HjaNV^G4{M+d})JFJ>;Ne*f_}}WhLE=LAPJj>ZUt8_> z|Kw)_K3Kvw{6mec#)0^M2l!wM><`Ou>>%}spZ|nNJ3J6Poc}QY(6-tSgl`Y{Fn*+r z;HKJt{)@Cr{U7}Q%=q^+2)`5X;rfA;|Gm|JE+Oq%!Al1zz~5@W5k-V=3HTs{4L;2O zcIWRMz(=nCpIpCBfPV(~N9GR_-#;M|KM$DvF#fHMJ46@ZTL3FR_;CG(e&Mm*_?rM9<{!ob^S50- z>^xk*0QHajs{y_t#{X7*LtjY#3IJaf@F5qegXjOrkhVVoUjmCy_)kR4d4#Vlwt4@7 z{^7g(t@Z)o`vX3>g>1wRrMEkN$^l;n@Sz^`z12Mg;-3~w9!aeJ1Bope!nXo^xc(ve z-|8Gh_yvFu^N*Cb8~+U8Yh(Pwz_*)!N#I-^@F5rOA9lkJTQDTvLJS`%Z@2$zfDg7% zfBgPgSaS3FLI1Gt+Z{i#fRF6oAb-308wGsu-^TrCt9=JOcQQ_|E|!Ea89MKmRl1-_IcYHNaN@d>B9Rzc=~Y62g}Q!E0dn+sOgK zF9m!}z=!u=cx<=-1k#)9$Di_*0N-c_{>uPg67XT%e=>e>&urfRVEkLXcY>yWjUNfX zhu42QTx`A<;KTKMJJ$}I{}AIJK7T>mst$7frvM+Wzestz*PkEUJmC5R*S@WCVVsCR zPr!%!$L(A@Y;7!d!%fRD^y7z0udef*sv?N~taz!tzC$vfWPk+9Ao zd~3jm{$UyV-){VO0U!1s$seMNp162elfrh zBj1F7{EP7I03YuE|1^J9fG-93l)xi02mjGIXn^?t1^96MApGrKKe@A;`G@_6eSpt^ zTQS6cIN*avaDOU5g52%q|25#B27Gw!=)r-k1K{J|8sdMy(!YLx4CCK!|LK)Ce?JX* zf5LYId}ROsC;TSBN1h-5gii}L&me{Ve;U6T;KS#SKjl9He9(nI=EKhKr2ndb{3rc~ zgNqDv5CfzI2b_O!48mOO$KU~g&>+Hk25{^K2RAsNL4@@@7~};A4I-@Pg9Ts^;n)W6 ziZ|-PJ$D1a-e<$Mf>00a^)?J5EQ8md8wl<}8}@ey+k?5fVGv+ zjI#+Guw64ap#2@fb}it5yf$z^`#Xf~+c#SMJHq-`nEJm%*scT94k9dff&=>R0tYmR zu>K7OyD``U5E?{y{w+A*_HR@3=y86!jvIGKQrKf^|RoB1`(D&V{i^2G>A~|D>&f!1#m!v2+NDG01O+U z{t`Ig`DJiGg9yvtG57-@G>CBT{0kiL{BLkT`#XgC>)?RxH~(xRtj7hFuzuIZ$^RW; zfABHqv5^SWlVj>3LOlvh86x~hi78_vY)1v^A<|&ZLxg*&g8*R_BLJ!M5JT7T-^In%xXM5re71v)c;xYta_{@aw?q4;{ml7_qa9^`R6Qo&`oR84z9Q%9ow8W2KsvENgp6S#{-FRbhTu#2B6C#>yuy?CJaIM-p^ zLuk>tn8?0y;{_>Jqu9tAU&`Sc!=ndx37y?NGGnC09C_r+Sr-{D;vp(k|M#aR=FxZujL-7^133?{)gQ0YbP%x5|widrgLj9RS2HEg;O5A9L;)C z;mPRep$ElI9h^W2i5K3B5JRht-!1wjJlo%JBl#L#zon}4Oa2o8O0@O5m;+t=@r@x(ZPpvRB5o#$!F`R?2E%ybzG)I0^P zL#GHS-$p(_du4PCnRt;2#VvljAf^p|7xR}cd^SN0t&@@2MmZc&8m=11tnbZg>0Mb^6bjD|hMd*?czLoEPxj z8Di+NZq#k2qYHdQ^Tc-duRkHy6}0BKrM^th+Dx#1qiWs!`4bg`m-{=`I{U5kQ8xx-uU zmz-{8Ea0g9BL4P)oxf~G)m>JCX+b(yYO8h`+VLxg7cW_|?K1;Hh%PB23J|T3QCE$V zHP{e;jlLX3cigi|ur7w=p5LPoqegpTqR;wefuSug@3MD?taLcEtI5lJBf(F2QgcaY zc%0`Liui>RmM(nuK@6Sk8a3{m_PhE69^3Uw&pT7e8BqtnFfZ!-NSn0zDSiWeX(Hc? z?NF<_7)hIvVd}AZ2Ad1*=%(Xlw%#r3UL|O1AcVxb8xaMFri*_(6LwVIuRqA+)K$-s zuk=)papdz9RB#L1WY6!mwYsgOq4#a^%3d9|Thy(gAEJjA#hQ7uPfF&Tow#A0%ioBl zOOCDUo}!9xSU)?EPGH`^Pb?-~Z>G%9HZ&(;xXYq&ELM}kr)H@o&q2@c!JdhCK8_zP ztK7BYW$4M|@hU3(zlcZhVd?I{)*UM6dHX16#5?C}-Zh`h@9gH!u00m`npFOy)(L%= zfWyLM^~X)QDiI;Om81j~&ylM#A^swkEAr8MjdPER+5d#^K9D?6VC%jjAG}(&;JIH( zT4R8GDfX^I&^_L!t_xOTJvA-vno++zNbwR2LQdsQ${*yg>A>?Cb7Z-&x-_Zj%!>O} z*1R|vOP3N`*PNa36AqvGQf@{4Ek1m?qN}1)`BvA@=A1N1%djo%EGM{zj*47b#;G$L zY3S`_vVCYF)tqSV%@}cZq2uIQVh)z>UTobu#$Imc??&}a*2D%+N%u@F)O@|h@JKIr zmh5KZXQ3CDXchX)_y^dW9S7oH-ie-AvuqOFM|8tj-R_LlGjh_gH(0vxdltmd;_Yv~ zs4`d_%bHpeI>yR&S*pV5EWMiiy;TmC@5`*>y&wAX=4e}G@Z77{rfLW_Q4n)tY5 zh<%EW+IT=ij2{Rg`GsQ+F?4@wPQ0vVgSYsbstOf{JC3Y$t;tek%L1iky$lLH?Vfw$ z(_S_G)auB*G149oJrfdFHtj1>l(&x8VE1cX{vcc%5nUQY6d-y$xse6$} zUQixgF$w!k;~Tr@sW#~aKlf+eB>UB$jwZQK=-1yzQTU#6YZ*xFO8YH#@tu?FsX#79 zCM;d}Y=#(G!rD@EwX$brx7*jWgp2IU@+rENeuWP&jit*NGMg6O+|8fu*du%-%R%$% zjpxUvgiiXIF0Ypeq{*bkD!wNcE(AhIy!#MQfapM}GHtDyNmetH)OkH2@#fPqx7ywx zY1O`b>DsjR-ornp9<;XdL|rMz!N2CJlBz+*Mvm7+E*h}XS>&rAvpcn~d8* zA!2u5`=TkOeBiksd&N%$l?z51Sw@u;m$2eGIrgw!(fQJ&^oi#8rDnE!PIEZuN?faF zA6m+ph}jS4!GB`ouXy426^Nn3=G}{1O_xmeCzJflAm;k`=&4z378T88x+3lY0gva; z^xn+3=v4XJFf9-&U%q&j({rZjxw<;*XCBJ_pnTrjqd*AB1N{C2G4#>Ki-&|O(?vgV zJLU)j^eBUbdbvBrq=Djq7h4q+u{tDi>KGr{m?|Tx%V&T_%oxYFg^8?&n-{V3#_J?a!lAX88 zCgZp>Z$NQ0rQqq9c7@(q-l=GdkUOoXZQpq`}^b(#Eeyr?3#?(W3X)c2m| zy@?U$#T&oD$Gqr9xSB70$dp;?+BvhF1x+?t!UqZD?dpUzSh`Hux+heaZ@ z>`Yd*qIz;?`c6$$*1Y|Kcb=N}>h&X@y(InT-%1Ns3DCV6lA#sWrU!G{-zDAf$fEvXt;RK2D@@JKU34yDgB(HKf9=Jd2(UjH z`D+(R2MnUM^@&)12`<_y;*RG>9{*zI@~oeb?H&Gy6yMvYy3Y7GVCllWFkKy4aJmmOR8&Yt91mJ02YZGG=TqQf7% zlvFb(nV?lWOqy3Ft9rdwR~nr=Ix8G}RtDnKK56%&{F)W!uCHECr%GeMB{ZXv#L|U( zD#Xyj2GP;YK83ioj|9H)^f}~HlJg~I(L3>+I6yV0@sT+0w(6nd%@aqpUrmhOTWJ^P zS!XRi;HNUz#rOD8U`o+0IUt1Ofddf*i1tu2p8sUMV0!vl&g@ckT^#Wuuc5XBKVNZX zH`%c5yN3#dwGIRg$s(E;uRr^-;&X4zTlD*{^Czu3YX}r)1F7AybPr?eUcC1HAOpvt zAjVW#`%ku6!TKC13p;+1SJg>_-|E-)q{!Ahxy)F7zH(+-C;fp~L(BT_;oVgqq-gIN z8Tz_<;a|eih0g?tq4kV(ZZv$UJ9KJ2qqu7!p@N*Eemo?--QdZwYmRem&qdVVuJT(6 zp|~m+TT0*i6Hb1Lep4;i6JXg8S67e~YJ3+6A$j0JL;<2}ZFf=3dB=a;Tcs1k@V5l9@*zxFm>(z2?x{s2F)@iK`oT6f!6tQ&S z_sWQ&7f3?!qKmj(Oif?U3~=+wm~yKU+8nq1WhnLfR;hEA^{~0khe~PBvU{Y(QZ4G| zj&l3YTN#&T5iDeK6TDb?2=^jLygY~~K(yDj($gjm8bvymrlpszoIGJ7(3|D^{pOg+ zxnl(7AM_8s^m=`N;xnCoUe%;@NWhth%XNgiC4HF>y$M=STq`@qsN{-NX_<>6YHj;8wZ(Vi=pQB9lB~)2FK_VNMZUxc5N}ElN77eyd~7j$dl^j=V&0 zf%pY__p4#%Ip-|BnsDRxM6h}68(_?B(>F|#cY6;y%{m|NBUj9SJL<^`f)`111+QO7dzB5zMe135@^XgU{N*BJO6}X-@(UtKN$3q z?)>;*pcIv~I;?!G*Rnl5Mfeu&h%^vF;uS@3k{HN+irLP3rR17zK#yK98}|7S4=?a=eRs)~x6dv^Eak&EkO9PH?X- zgsm$gZ=ukyG#T0xuYK&Mf052)Up)_B&jVId^Qp=w?Vk<%GoQXl|6=p(60Yw(4eA3+ zifzq6@vrwO3^ZVd)BE>(*vxTyR=yaOw6{eHSIP>uzthYYw+!!Njo-4mWUq zifYK2avk*+`u1B&IEnZEeHNMSapen1x}JCYlC2#U4SF`815kwiK=TN;?$i08g(2HQ zs+bg`*x{6*vZD>RmdeCE_jlA3M&je0b}(<$Dk-tRwOsr}N_;|Z-9I%-*z3B_cr~gp zTE5qGqz-7oJizOA6kB)L)#bHBN!Fm!rJ3KuMe2>LZ*bHeF0{@4{+dLZc-^>?UuNEZ zPv3GN-Uq@fE!>9slx8YdGi_dX*zaq?zqrD`Ip0vYFu%vJb!pzDSMlWuq_R4&zj>}; z-?~=HBUd}5%za)#xX)3FqkrJQ7yj_prk|Iuc9JnZD;2rwRBOQ(<>+3{`Az3r5)*vJ z#6kJPe@|-TcOQr}de+#t^{D1dkvFrnQh3pCNoHvh=J6&LrL(%Ob;&OR)DzE$m~(%N2^`wgIuu zXEOGlyWHuw|H5v83`$};Dz1h|67vvci<3A31+hpF@lxY8E(J(0fLL_fBn zO9&$V*Y8fCIQqS9SHOK14a+8eoC{~nE;ee_CM23Qi z=0qz+3T2;6ulDoYYf9q1SKi@pcj=RNe814S3Q zH7a^T*O+`>$uaFw%=r0p;0!UzW{Yw?(A!VTT32$0Bf5l31-|>OueMX~h zJk@7hl0x43C^3#R_5BWEjc$L)rWcbG8u{YPB6sf0!BdUm)^+Tp&ywOzjNo1x2YmYt z;}yf!ePl1$^o}T3&Ot_xj5|nibX~2yyKq$Gd zYu#ttf6;wd4w{@K7OQUV-`r1b>WX9Qw$<374HSNw;-@^fdUx`AMBgY)u1y)cxJ3-v z2zdPZ5i}SX(kiDF>RG2ZB%r8Et0hQe>ez=jU)wK6wmda3fQi>1paiz=wYr>8E#4yR@6(-%B`t{7c_JprCl2MmMy@+(_aYJ+! zQZLo=ALGi(xszUarAy}9+QZ5R4j?Nz<_PbJpVaP&G(Wuz6arHp*s(A|80 zCWWoLCQI?$q}4$16+u-^eRrIcx?R~zEteMK)`_)cR-AjTPb5p;zqBV5yAeBnj90EF z`S&$_oRY(}eAC*U7jYllXvNZn_YTC+=ZI`Ql#krt$VuvM*S+s-?{i%my4x;}I@cK+n9g{oK;vBuF@;P>>NpTW?n7q~QGn>{T{;T} zUAxZoQYM%j z38c;3gRR^1EB!%mYn*r0%!%@;b9;BM3&|3dQolUt+RB^xfw}n3u3zAv z4r_k0_Zh3fBdrN-P1#}D#)_oLbjL%rp-ts%5m>si*t%Y9wpqu?ne)||o|S3d=kQ;r zrEdErh!=8t;G&>_+U#5ZlSxIq6Rl^+aklUQNSu%3umB`FVrE5nn6IaC^&}A3pTRAr*FZlhcTMXGU z%gH;R<>ff|R{VQ{1m*6zmR{R@4%ocE$s?iw(Z{27$24>YYVR)}YvtrhnktEiI>OIz z?-x^mR_i2To@J-UFW{>3msi?^>t&-If;9p{?LSHG3`a}d5Gf%jucfM*d= zfarHhSIhQPxwtM|KE7_EU^MEHnOR1_R8iQb>{LvP&wnYoFotHn#HB#*<*3qf?!7UJ zMJ5~{W1Q%$o}X>MoIY(n$D#M0|l0MXnX z4Z#NtwTXmHUmb6XZ#a8+g8F?Y+rFG$(cIZud)P*33wimz};-qD4=bT7(mD=Zm-~pWn^Ap>k|2 zQYHAEd4yTaZ%gH@W8Hr7cLK&{xmRbn=qHS+B^-oaKg9f|bz@(mimhAM7$!fxU&h>O z=;fo^F|RCN2*0h*IZ)xF_0%ZAS8Vs;;zo5F>iJJ!yuLgR=a_Ds@$Mk2mx!;6=VxC- z(b5TF^009qQp46Q@i~OfO5w8KD`ayvLE^iOuiBCX|A$*GCZ@-u9B&q9#+M7x2ZfXP zT4Y#r(zbgCzm=`uJKHR4&$)7|=^*FF&G#^y`yKEb<-ZJFAl6cxK2>m>@xqbo9=??s z;VT?zBYFq!+uk3qx^bG+wk<_?Nn=f#XV;Yv$!T|fH~?>@TG!`>3__4p#=g zV4=|4FN5#CKkLkF#`O)|&HKDIw(jz+)q;I5o(d{oFE6Jmp~(@EsLcM+|Gb1pmGy09 zch>Ce34sf<22;;G=o-nJD%SU}?27H7Va&t1dZbi-HTOc_hVJG*3SL9R&^nYIUW^1v z4TCP#xAg0%$kl{ro=@8z6N<_m*&B@09J_}CPY2D&UzJ9F|KLIoPU|3-A#H)tgym_? z^;q=T7jQ1Xc>yP!E+PsLeQ9(ooBO5gbg<_ux82+=x*8A8U(xZRUah&8$8}3 zQ*0^bxM|EDGQ*M8osDm)RWqo=Znj@Jy>DNkHGlLpeNL0J5i7SP-Q{n@FN@54lR^!g zfEJ7w4NxCjx0IGPyDK7#uANcNIAopJOx84_BXr2wf;PIPyp70F+nY=4_nwHFh*8nq zDD&(gQN6Q?x6bF%PEeLfrcoxi7H#OFH-7hc4qJCLHB3gLtn*Gko9{#~&-0+ciuyAR z{vxN8nHr5}iD}p~b1z&8<7j0Lf6<&TxNJm;(tOkHLrKI~+VJBd1%Df6{1U>T4X||` z2cN&d^IOxzCp}j7-9JU0sB-v{VEd?@UIK^nsLNx%Y}pE;XAGm3TB0*^Kat&B zKh&JASl69M{)n!pzP3{hKOH0zk8E6bT{|aM%cO)BNTRd&hK=B zLcses^W0FgUMGp??9Hu-886T&=(>yRY`c604YS^si6%&C={*RI4taZ`Ggcwrc--i0b9XtGgyZ&Wp^7l9=VtU=*8uG-Qj-?P3l32ASIKp9D(Ce#iJPRWN>^)}PRbGYM+9VW!5b zW1x`_XsIx=<|y^o(II1*kIu?2F=fAPyuVR_uJp1qtQizeK?_Y zSh^py@^BSP*A!c~{9)EbyHD=Vl+iQ>&ooGt)e{-!mFG`Ax8tDJ zIX=IOq4cKqr#6q5SNE?i$52xxevzXbI&bGhop9-N%xl!N4Sd!{_77&*x*e~wXXRBI zc)r#<1@WjTr&kIRiKOQ(z#$9pk8$iSAt_SNE_zK6Z&af@MC% zEtNOI`zJ;h-Z#y$b@M2G1lF41YFm7H=Fsl3sPwyzgX=cV(rz5Vd(CHULKsDSmO9O5 zd*X`)Z!k1kx__Z^GjliX`@)~&`jT*Nv>$50c;P&@z}98Z_cI6_esw(*jnC8UwG`{qkS zYnYx@Ht2W3`R7OZ0Oo6N4(L%ff}UQu~#p6u#m3(D67q=Gm zvAd;wiLQCnxHWu}V zZsL#1yRc=q$-WJJj>F`deY~{CK{Kykv|=`AnuK#^P?#v%@`bMblT|0`_{#C*QrRnU zDHvUUfETcJ!}WKkSkNc;NaH)S(Z=Sz)uUPXdrVn->%yhM z5_0#^8&P9!ugE8yC*)gB?~Xp}SUW`cV^VQQjZKbiK`vhk-&vLzkA{EG zt4n_)&U_LTt@$7jOVxiveW4?&~P}mWL_bD1*n}6K$ z+6hXT{>+?^0*T`bEf*Wy)FgRn7M|4RKRC*%LSEqb>~JUJ$4nI$M*Nd8zY<=nZrD*)6vw0-BKnPjaoe@!h=;QNL*Pqdn+KP&5cZ@mE) z>!>{Qs7y|4981>~TQ_e^?Yvf(n%OeX6%F;vqPise##-o8_Z|N9NaOY^ba#>A-qz!% zlsG-u+?pBPe&13M(xj>Nn?5o5`+k?D;A!H`_i~&0g>w%v^r!1;b7pakv~)*Gb~`XI zcT~C*=V{1?kXfYAidtXovgpY%yUOgjL`Fb*SFHT3yNdIZ#I+~i#H?;vzb!dub!Qm} zA^C;h6(NQuWuN;L`aZXr=kqZ|bC<8#K2>>%dJ`=A$z|cYOHwGPB8@B$UDrQ zEHwPQzbqWq!az3K&TG$Sdbdv$`}?7bh$ukxO6l@rKlFHU@EqgEp_#$6KljAN*WbH- zxy5O>Ug~+>hgGwuxDponZN;bA_VD&U8M*s#MUo;QbkEpJU8M@!J)A%Ti5Jdk#L(w; z>^A6X(%~{85F>TK-yZM*e+#e|q+@Nlfm?ue58St=v@m8*!DUQk_f14P5ax#zR94$WQ`oP4r zWvpzzvBJ}i7F(ix&t%O!jmF%jfkgk$J#t)q3_QKq`w~w?6d)QsR8EkPEKo0|)@oj0 z?G=;|XtF9LdQ_-WL)YO3y=qF|3qcjvGnGSZyY^nzi8)7e?_!UWvz=)j*?Up?kI`Om zE+FxGVe6&`EygzA42o)da5&k-ye31YLG{V!v|q<5Z#%7jqGW!XJW3G?}d%+a)xHC=1D_+#XwFc3>jIAqTF8%dWh0YtP<%2ylifLEe;-} zB^0cDEOa`6Gsww3w9DObHkj~@f;S!B?c2LAobcLjbXms#sM`xA_|5^*g}Fuy%~5rE zn92-S^ym>mfd=jB8-8m9EZidIl}2b(`<)(<`fs9xx9bxobr;<~3QBgM#ngF%UHl4% zq-(fu<>BoLgWvTdx;}^~Ky(;))B!=0f{Q;H?Vhu)xnzr-YGg-lCYvo` zGZpfEzgSnH0H{oj$IME)7=oRL?CV(oa#i^c$rK6kW1$HyDVe%*w;k z^~2U34o+43yjSpib?}g6QrRqbcyg}F=Sb!WCn{-+X}Qk(Pk(SEyf2ls^+l!acduXw ze-LMDfTowgWoKq4x_VUvzVk)$a0OepGtWHNUAv}Sh`4d-5&DXeQ0dbL@lt0~Z)_TGU87);WsX}Ql}*et!sBETr(!&Jh$l5MN2y*(EvWGQdojgB zc=!k7rbUI-U2PNjK5@$mS>pG=|K-YGd4Ts&#L$cLI=Pbrv?q1Hjr-|8h#YuZz!G-w zOxqxRC*je4r2{_xK}L*kZpBio88|D3Tv_aGP-n$S%NtmJj!)K5Gu<)-gpmBgy(MC3 z>R~1n&02Gve_|fr*AI-P+_{QeEAa;|TnPzU`|j>6mgg4G`sPt4(M+gNEk&k(j$n4+ zVFCAFk^v&xnW}jFVn7Jd4Ms!(q8q0)?3iDUWSQ(O*9p<9zVGlO_-%VaLuPGke6p1E z*d8NtZ_}^G$CsDsiynz5U5vD%un0&17ccuE+V1_7h9Z{kRcu{_lG~_vWHZV=H^QD< zt%>nYCRPeg8nxg1Iuy!R@zDgnpf?r;44fo#rJ!ACn|NJYYRycXtO$rXhW z5S8b>Aq#K%A)P0$sevx%RgT*HzGd_N7K*JqZdll{z-`ksolP(_a_!(i!yB!pci}PR z0xK&OkAmM%;HvfhLgQCE#mAi;>8z>AdNxiu+kEor0BLHZsrUFVW2|`LoJI^y*;SN2 zckemNZ@oE}>xrStNg8c*)q16?1Etd>KjUSF2)HghIqH+@=%)AlB+iW|ykE!P7Am$F zvc}t-d-_tJ3GPji>vjzh1&GGoTX|ZJbwS_G-a+h|jjE=J$D6>5FWiRtoA)H0<5p~| zQ9Q9!qRPa2__B-Q0eaIP{4#vOxUQ>|N@sQ-^51<3`#ygiTleUE>fY<$*4VS}kxDP# zc{E5K(YfF2%*gsNA!nJPcq*4e4|xQy+@7`4^OVaqdvQMCC+Ygvku2LI{?S7+EKGaAv$-?4(zVwu#?bV`gH^8= ze%+6i?#7Ui3u|~tUMgW(uMCh5JS&*bQp;_$CWe$h&DW`X3g(&>p0s>KKt?K z6QO%@kA2+*iLI@2zZUHICYa0=D;d!ruI<}Xrctu!P!MNkkozJP2qF26KtuteFQ}@u z#+`}?SvDXz*cxM7Lv}r#K*A#Ito)-`d<(0VU9O|!@JnH$|>Jnugp+JmM4)NhEqt`!A<-L|4#C2*r zl@VAsIx4pK{j%0#d0^CFzO|N*W}3Mi9#ao|CyB%xg{{k*Xtr;x zu5wUvO8b}nyJU-Rdbnuyi4uSLkSch`b0yF6YESWDoLAy5s-F^y<3p<$F8`S7)|B&j zKRB4B4tD~@nhA` zYv%De2}R-`%92^8l-yZl(Jh1(#r#1BQQGGM2TmO{*MEnl8;h+wd%^zq#ge3w@prg= z+(B;l8uHDAncc&6AKzGbhqC8UxgR;r!1dkYT*qKGi^3qgyiUxwle}^@WHwJ93G==_ zjgO^!6I+)k6n*sJ)v`unJ{!|~4&M9C_W7lFBQ1&%F|+&nVkOVpjT+b-KALQ=HZgpl zpJErks>aK`+yaw>Onj?)rZxPV?-4iG9r!GV7@9qaNH7VtPhC~Qipwaz!@gMwpG2@t zm5}chyTH};{s@vKN#}3#eZ!GOwB%z;f&KRzi42Hu-|%919yCN!c#{bTA^D9*L;<2x zwA|aOwBGqA%N;mOc)NA#Y(1lyOp~^JExNTRbh(=(G_I9xm!7HeJ8?c8qS7h7ih)&+ zpTguOva;!CSJ{Y{uyk)>>*DBq8ItnwemgyLqsx|G^0wRuZ_WONTQ72t%qCsxXJO?` zt&xs9VazsjBdR;?`z=*10jkSIBb2J50xMD8Ww=sUx(V32ac{5rN0m#*Q%KSsaFz;H z-W6qgN$Z7&KBd-P%Lr0xNj@9J)Na*?mLCiq%VAA@Rn~8WPbD~YU*Y)J@nTQarOo$7 zoB2(|)=db`{dL6t^7Y@(YJ#5{)u@bf%v@56{_$?D#rM2CvyyDjw_Me;hFZmU=H?gn zoC+5#t2_PvF{k7=LpgWcr?VaKnGMNr61MKmpD%3vB+PUw3`E0;zjcau9AxsIK9O&o zL+jWsIQ1%yu8oB4aFc%D!{C&CvBHBd6a(=*TXeZ;9$ySiVkS1E-f7@RoznVMU+&5e7J>;O z+#4rbJ9ZZbvH>9^-rI;MK=g^voQ$@LQ-#b{&I}jkYPo3lD|-@_341;o zP@Of%O`lB(B$C7LJavZNY}vKvaQC9-bzfBtPQQoik00LT)Irty`C3G_c_$xv5S|x? z{d{}}5e0~TU*?7P%_BLxQz@xN_qM87@0!)^yJ0upJVtp1H6JK+OS*q#U7D0y#>BPG z*XC8teaa6*_CC%wNmkz+vsmpI6@Uhk2e`%~hBki5B~UWvT~csVvg^|Eh%Y+C%f-B- zr!LRV;Qtbqy%XF;O7Zxy8W9y;_hoj!Hg?Hc9M_K-til5XAN6`{i64& z`bUbmBXpWY&&3U$mo*Ww?xFfs_M5y|N72$(9$#=8OBe2`5JQ{d_G%z1+`zi)MeBQyj74OTwO>((cT^Ipnm zIb863<3p zgpU3`rwr`<*?nx?2M_Ik+%&x+g&T58+L+Fg_FM4N=pv=m zF;Dx}%^lv5?u&290)&w9kb{T7yGo%2|0*sG(elx&&&xA^ z{dzjZnWwcY;EA*Lxpbp0&HvZldq738bX~)Mm;;C?W~=H6|GQ)kzyQ@Ogj z8JASf#V9pq!noYc^`)YjJFW@+OV;SPpRU{Op>`{8lxj1!_Kp^V>YkPMU*GwH#Q=SW zk>%^j2WH-yu6HTwV0h`$rHma{FMq8+b$G-Vr$GsZ!*(b1Sduz#(aJ~=arSQINO9CX zYdJ4d^sw~LQ)Rk@HkH?Y{h*hiW?MoA9T{#*X z((%OML-uVtPZQd^UC29K|5BrkdLA7*yuNa&b?TFV=a)7azmG~jx!3=Bn0Nb0mNnXr zx}mIo?p^Cfot~IIdy#Te_Onmy?THsg-Iy;6xp?ZOz}^HQukLO~w_*K;D6^LwHp^(9 zqRhfU(*6wyG73|JD zB5U8AA9e_NJ4W=hSS?wx_Ef!E4?by&JDS!RS+;uI_`|z>rYg2)WNh<}^6c+cGk5E{ zJ3SAKdTnm{w!vzX${qKclrfcd-_xkRsepHg9;fA=~Tu z)JcB#wR|&k_x)jEJBFWc9MntPYh-_M&!^kttZsZ?wkgh{@|I6nD!mr1e{M%&9;9g>t7oP5q|M;*6YkEhgB zC9WORb4k*|wyAZ$hj*!SUBJ6r$h*#Im1Fmn-$bT+tVL_*wAs<&e*BUkt8V3DOcS10 z8S!|~hy~}3_QwxQsM*wQLDrdRWtYngr*0env{l^A=~t8YcYH43-6P~(v*pg@q4Q0` zJr2D39$l^Rna=xH?$0%TVsp@_?Qu&V@1yA+IabEmo~080b;+(e@6FH4vaeke-TrCe zTp`Bp<-q-9OvdF~qL6p%yvW0~#_hf{r+&_i-Q!QMJm!Anrz(A2?VxR!W3qo*RvMb~ zy7?WQ_eUc$j%3(6RI;)xuT4dqBuLcWjzP#cS(V9vT&~^vuGky(SK+^D=8?8Ov(j4#Z09 zrr8`>t=}xpb*9zCkJ453WS;FEq83c8b!4G+OG(cSWhNOv67U`r^1j&}b54}gVd#pS z)IQg(8e7~oy3xDSYnujxJ@<-lw;4RlYo^Dl`b}0%Yj-*#XXdRBM&{c-?m4}p`K1xt z%x5iJoBGE&K{rqTiuw@V)CILt#VusWtZA%`6y;cM~|^(zg1cp+`Lz{ zK&^46D-ZmfHNWcM$wxYQ@BOsaEvs4oPJ6%pTSH*)ze3)V`;IkO(q8P=?`@Lq)H-Wl zDI(s6ycrU{&2)_2`OHquK6k8_+CMs^ZIilBUdr1goLc_VyJ5YGUOA=ozFlourVFwd3WKV+I{cDt~7q%;=Esd!zKDQ zqZ;Uci#-vc*JEPr7N28Qi$2tO+dyFN5h3rzr^9OODL>=Eq;?&2E|i({Y~AO#XR@}g zj~E`*Ji7bgHn)yWHoex>@8wX_)yJIuk54=)|JEzjtM{?vLp&efy-{@(=_9y$d{oH0 zX5{#OwMGs)qrae1)3<)Qb;`!AwGO=7x5}ru+X1y=>y$pT#$Z{l&-<$jKYdtyzjl># ziIGS>1#07XEXjw)5!0eS$L5b^Ax^ zBp)|&>Ne+VnbRG6Sz9SmfNgDaN-NDxfChU$q?tkHifR~H`xu0&oTcdGy>y58vp^hxIG#ReET|=+__a-$oN_ru%gefEFZ zP2s}ftg&X?`))9>asFZI}= zLhV8AgLK|EX!vlxPiae|r?od!s+#=fh)K9}>PLaSCxyJ7yY_ZjxhG*wva8J(z$6Z-52c_IHq2+XzJ+R-^g5!`{~|mTXEj=HIL_cKIy9?AC7N9*tna; zcSyF08gT6O{G>a!v5UjwXADZbamuq|<@%!*y*Zr{SZdshw;$(sKj$6x`mwP5p5aJw z)Xj;wsY)^UaiPP@yUU{2q|EI!=F!*L=Nmj(Gwfi~&#{qZ@7?{C8`VvGx6QW9Xf5-H z)-$i$Tyibt!3*&+Q@mwJJSQx@0(ZUEaP*HB$_Ttvvhu zX!#?$a=VBXC!4nQ)$VldZq}x_$EO44%>5ZKxuRjWglX4zwwaI#F3u0BLf$(&24!3` zcv-sJzEr~{!z#Y)v~q!gr|R9lhgIrJtaDZchc8}!wR_bH8M>yGCv@vH($M9@!&j!8 z_kZ>HIOBzMzMS;I9Pc?H@AD6LztnFyt-{=A8Mhn$Se;&~?E8L;_WG1vw`TvDgw0*; zJ=dO#(>Z-?tCP{H%}Z@+%}TM%)K@%zJz(wPdoPx3={Q5cdtS(UIJ4%IGDq}0w9TEe zr?h(~KYjbu;J1Av0z$3o=&W*j)yt~I&CR#=ENC8j6aM)U9T~qpcw5Ta_PxD^4RSJS zsc$Rby&&Y>c=WuNT)lv*-0X6(40$YCxwK+RuNzeBc|e zQ*-f_qn4HL>Fr;?E9gl4T(h&cUe~Y@_HP%3ytOJ%9#p%}fJ@JEE4A^8A0b(oVPAcj zSLa8n-aeTJB|r6(j$Aoi`d!@3X^&oPT=XTiRPFB9o6WPXrg)aLtnRK_;;#aGNgKrd zblWC->zvG%SS~WQFLz^Lm-5<{*;A{%+-Ep*K{vy+=*@?dGLwC0k9Uc0(DUGzfK2oE zb{&Q$)o*oD=kmP`Z>k+?Aq5eaZ(!QD*Y^?dUJ>%%_@?#n zuK9U=o3QTsFJ=v%tccsb{MCw`YaSZC8#1O-P3gS(rhVG<>oMSRjJ}87jRqaX!PnNu zhM&AS$hP0`&#RM#_4ulgH_{--VEcjnT5qo>I?o?G;=WCwHdMsNIeA7I1)Fa1g zE*slda6eTgDBQhM^;;&xOFwAYOq?FL`^Bx$?@Wj=NjaDG8n?iOYNR-|O}({P_X1fA-=KW>{>`iIE$>;;*Y5M)a93l;eR_ILz7BY`W_y}T zn1DB3$ZHT`H1$Bx(6i2w@dti<3C=wCYt;CEbUI370w4BF_;<)I_evQXa-T-cP3W0@ zcw8&jHn$Whr;>~=Z6dcP+=SyD(l>KI-NtiPHgVHg*7VKAi1OV;&)uKKjeYlV z+{dT$Lms3oJrcg%qr#|p$xX+H9;)dSxzjr%WTtrVwCE7s4mzi2ES&87iac}U{BV;a z#Zfn-Z0G6eiui>o*De~h=y@?B%(nx@|6e{ITX=d$Z;yF+y8rggZ z&pdH8B7XGZrpBgso1OK3w568Wu>%kElwA`uJIOubd#7jgy$>SJzjrxO9CasMx1X|U z`ML3Px|s*br@t;Ip6VZ8+q3EAm#0%sO|ERfbva6VFH+mQH_qKP^$cpi4l8Gr?s^-zIJVBhcd(D+8wtV9UZi+scFls zU#e4j72V3u98gE!vGuv$7nj(UGk$RN@#nTH60a<9YbD?%wsSw-wd3OVYxha{bupoB zo?BHQCU_dMhNTMP|7 z1iTN0yz+B#xrg1)Ur4XqB73TAjj6L@oI^@dT(w0h6?DEe-rRVQQI%GgwjW$H@WUipf3!98=2 zEq~&^@r`knhw&HNe_Pr~^5f>s?yu*BRGvF}oP*Rcj8Cuz};4_yE#U_I-r^s=r`l|;YTl~CUtB3=+XCx@T7)` zcAbCT9JzbtgISGDwrS86$ktbKF#lCc{1(;eJBebbfs4>o2r^|ftzVBvm=S6Zhj6Dm4pANUb` zG3;mh*D5b|8*LrhVP2i+;1*NXJomoyb?t`uTS1eiUx~9Vt271?$D75G;;0*Gv;19L z&&m&e?On9(y+tx{q3|7t&X)Ya*i4@ z_=2bNjr~{5KG-Icxgf{;Ovqa|VAq-_H~SgSdU4L3B>}mD_U6HV|>VFE~;hbDF0DmS+Dz~I*5VLg}5+;C^@?G{T?ofbQG`Lwck z>rIt*j1%d`S{*#tvAWp3+t~Mezjf$e^X-k9CN}M7WRWor$4k~gxu0&k9w{f@)h<0Y zH*MaKQBArVH?3}&@@RGU@VLZMhvM#pr)|!-+&$1MxO0kmYun1b2j4jCB*|TMZ0f#D zE3@;T4_P{ah~p)F68FI!(@2zh;bloti28y#$&L&Jzuj|S_>i?df8F~KeQn)`xBCucj@~DHE}bppHK?l<_axJ*){_1G)-5osXk5L? zi-#jWc?;7T>$|q`ko2 zw?f_l-7IQW&-ogABiF9&o5Ndo|LC>rMpn54*CwjEr7v!Bx|#N5Z`-e%f68WsEl#+x z^NsJ`<=R`9zD;R;G`pg$^9OCEfcKq{H>>r-VKd$BYsuF1eyp>qO#c%r4|v+zdu?!v zJyxmu(nPmA(^u?Fxx7z1#dcDbfz>YeaRcjX4c-!wzC1ejUG*EDWUR;K+j}9eeJ}Gy zyZv_8=&$PiF(;{Bxf<_>3=Eyz)$UZRr6N*)WLU(QV4F=JTw9s6%v^pq*C?b)#=af* zEpI)XvQa0r%YGN(^MDUR-o|}qwa!|rYF}Zid+@?_4f;nK_V>4aeX{lWwhupzQN=rM z-er>6XOY{5MwW*sFC7$CdZ=Y|#O?(qIS-HasP6kBoIG>m>?Lak+)p>`m4Q3&-yize zfB2a=Plf98^(^gg5y{ldUMB428o1ZyS9Cgo{c1X+( zu&i!>XF=(@LDu7A%a&PIDJ#C^(oXkVA1>p2v({+KXXy>f_4zb$(9c0{bk-=N-$qRe z>F4QuGVaX%8lF0om$5fT$ZM^Yk)>@ucVN_!0oD~wW3PlS8<|m~@u4mK%UoEs*)Q?+ zdk@_QZv%!M@NAOy_UfqV%2lpccDr1<6!IqRrM6MUj9ALcynjK~PPm_LM)`?5{dDfe zg>L-#WMy#1nd4?dKbS;)O4Cj`PolTy`tQ#@!&3xfVan#-N{-tqb*t=WC)8~HLr+sjoag=%BT*-#$32{$TGTZHx zt+Q$RF;i^O_`Ygo_8u2yC;M*O-`{t%vwY#C%87Po*Q1#C>d2gy`{}my+wG82che%j zX1m7yu<~xx=(JsjxHnPvBYWJxw%4)QvOQbA2Hng`nt3^G$hq(%xu1GXeVO8`v*_M| z2ICv)I<#iy7tDLj-#Ahnb>AD58r813@3@99Uw?leUb9)c%J-mU@ABE!y)SS2T7KQ@ zGOs$Mh5WO{>2=dnjSNDnsfHwdEpPdL*4w2A-1k(H#gjQEDG!l2eh0Xp?mfBB{fA$x zCM;?-&mgB(BOQ-Kkz-b^a+xNstD;sLRJpp(K_?_Kd#kEj8MDJ?dpFB=dR$1W)a{1b zJ=>iIrLGzpa76#ZBPnCEU@YbWk=_APi&3NzD*2Uopu_|J*#o3*jghJ(gea>r{X0mJ zPLW7h6StBjp^Uzi7c*eNx0&BfcSZo zQWPc*4AAN;V%nzSYnO!lFCHMiS4aaDVqYySFR83#{QO^>sYaLhS?n!W$Z#C#pruv* z|5#s3?CaZ7OABiOkwbCB8bc&*mHbLPP~ri2fYcAAByfyGfwi{|C1Zh-Lh--x04Y~q zQh6YmWE=GSzt=DPFH(u+xsqRr2TD9p;(-zmlz5=T10^0P@j!_ON<2{Fff5gtc%Z}s zB_1g8K#2!RJW%3+5)YJkpu__u9w_lZi3dtNP~w3S50rSI!~-QBDDgmv2TD9p;(-zm zlz5=T10^0P@j!_ON<2{Fff5gtc%Z}s|9|$tW%i3H#_ShPDzs55ye*_MrAizSU=bkq z9_=d)kXU#qBoY^68!Ka_G+ZM0ZDDL@EDn(R$>hG|>o&~q=P%|*uE_UY$a@Kk$OV4f zHF+PS4g^F-VkXB@fEhW#k61xY`>^J?l8;^DSD{Uat&Iy}RTDqaBm9JqypvrTCA*G{y00d7EVpgT|-s0rY+4_f%7gO(mZzB^PEAm4GR1{eYz!PN=q z3{(eR<6Z+Cm*cnsAb;nEd^c`2um)HQtOM2q8-X}r6R-u?3d953fbBp6umji$>;V#i zy}&*o3D^%D01g8G0_2mfQf(<7zK<51^|PA!N3q; zC@>5d4vYXs0%E`m@CJMU34qsgwfq2opbp>!v;tZKcwJMg9#9`>02l)%KtsS3Xatx6 zje#aWQ=l2p95B{Kc>}5drGOu}_Y?R6dxd0u_L2 zfId(Ks05S;NIOMj6@kj^xdDzO9Fgm>N77Ch0fetM&;V!#kg{k32yHXPHL;(RO;S#Y zZNf0*J}JAT+*$xcw+qk#um-FEwrMZ)(4Gy`4o6p@E#Lw;18o3Ohe%x_b&Av_Qtli8 zQuau>vjuE`&Oj%iBQP8o1`Gv;0E2-+z(8OC&>!#wJb->cU%(yc1M~*mfL=gPpa;+$ zAZ4*DAO=W&2Lb`WXkZi|1^fX&z!#7JK7cpi1(3uOo#y~4zt@4QKs+D^t^k*TRNyRd z8aM?U1&#m*fIYx=faq)mwg8)e4ZwO}EwBby4J-g=0W*Q=Kr}E7mHefff6G#Ad0K0&F zKq9agNCNf)#9xPje}RL*A>bI045R=jf#bjl;0!?gPwXRkLe7criI0hY&jZA-M1Bsq z2wVUz0mN34*ChYRIXQA=;113w1Gj*5APpD++yHI@w*lhcXFw)!A0YAF1MULE#-qSf zfZWRf9s&=5N5Es?2|(h=0=T$InR^br0bT-}JzRMq<$<$DhI3*+DXXu6R{+r^T%d+DG*lOb5Cc3Q-(Xk~aC?sNU6{PdBZr-mo z??enFG(A?d29$@+KQ&u_l}_{UPq_MbZ7f9?ZtcbG%FYg|(H1%}=nAT$mn}5ZX1i%~O^Q@l-(elRVv!o%Hmd1;P6v zv9hqUu!A>QNvd|$+r8tTeI;xbw#0(QtYpd3T~g1wy;YFdI#@VZwEWFG`r2fyQ4jn( zV1dv(yoCFLq0Wuq$R+J;%xOH<#0N{EOLY1^jvF20wWB|^&Bhj4S1=d!X)X}sie_ts zGD+B;ZJwc*o?a$MN81p0^l_(Xoh*dVLkJ7UdGy~25&K<*vX_@OY-?9?;trjGNZR1YAYg2VH+e1R~6eY1KD|r@?nci^8HUp|d zec%WQiDyo51M$SlOGEg0Iv^T-iMz$dIjjzB^I+o<9o<{c&pgtWmq179Ee?=049#xmG&eH^64q)W@fQb4 zMD=>y{IV#bCL0fY0EtS9_xi&e^bakVvc{8T<0Xo@)lOMI%6U3IUpxh@wlNkN!`1ZT zp(N)X2AJL4*gY&&N81__xQ)aUBv$!r*=AHo{679HB*-7?H%JPwz0+mdfp0hxwi;6$ z%B^iwY81mp;`!YNx}}-0zo(}%E2)7a$)6ha2GqFl@#!KRZDKX5Pa!@aR+D(Z<}DA9 zD^5=M&}UZIKoi~qy|N6O{nr{YZ<|UFb+nr!Jo)`z4mimZ=#WxXX=L@=<G#_B1z`)X^_-;8I)`$Fwh)n!map&h^o05Y zyH}tVSQFcdXy1_IR!9uZg`W*O9o=zl4s;yR`on4$NJvU&^&7BZ>5gSbAz@Qmv;~Ey z)S=LJ$Hf^Ks@9i5WDb%)>7LFLZDwV3RAc>aKXH}(&VGHNOHqyGFV1I+j)nIj{ zt(Pyi(p}*X337+HXBJ#z2#MR$K_lZbYkt$wW;>=ra)I1Aw46;_HFoxWM^XH=cZ(t`#Sc2Xq%Etn$4qya)w3zpc*$^9((>&;y{qU?yTVJxLDCRxGw1$Tkz`u+1ut0* zi76zx!Tao&UmmGtHSdiJytlXy#9Z&F*2ap&;;@D(+OPg<(JM$9Kv^0WbK~&vA z5=KaEI>GDB!<{ z`ZIB3qX3nZi+?dx*cyl%a!n|O%_t5fsqSL?p14iaAC4+$A{uBu~o@Y-|l z!MtP~B%~f$wT~TZNFF6PaNsshE zo6UA*0yBBZc~X4B*f}y-EICOsJltMn!nBs1xBl8vcUOBo=%5)VJ%$q` zq+H*(cMls@du1C|f@Z7(B*fQSoE8Q3t@VDjF4{NJT4AjQ5@cTFyKZj7wpMC8iI>PB zAvt&JMf4KW1)DGMlBulDs_pj!zK-dgIaHdc?1h};(o>I> zBrZRC<8J2qSYF}?3F&3d^h*~u(E6i^C$+u^kJ@5&3R7v)^2zFdIGT2|k&}+L zBRks-fQ0yM{+({xyRVF##!DtcLfXVF_3oWX+ui9iFWC$UY5jHXPK{VP-B!j+d?a2W ze&}uHOwzJ@SUdF;at=K(nSn`^%=cFhctrlQLw|EF%0WvDdki+T$~R$JSmnQ-@d-M5 z@2C!KtC~PUa^Ye09n~(k)N)jUNg!7LAVGr}DH;EKtYXl^O^{#~O5*7Q2`M`@N)M{% zR&UZ-9c@HId$YlikbchDEhwzslw)kE!eGoU1QOE5x3ccxQ9CMQ8`wxYig*^X@f>Zj z{B~>4n%$|y29rar^^lNKwYye>9x2sK%d!&Wf_T&zcmOLCy<@7l-tFEF64;EvsupI> z0iz+g^f=0@XN{pZAtAF5=o|za8QJw2AMo$pU3-WHu$k0_B0NeYa&MJFDwE9cY}Q$H z`S=^?kP#QyPJ)fpk~wR4d;S=AiOg8ocy2*L%Fgm-jgItMxalK}hprzyg@m*&SqWkD z2RsU9W>d&Jl5;_LCWna<@r7*? zWr#`|pwtR&{-91o@(Gj>u7re)dy!Hj=n%iHntG_i<{wq!SRGj47a;c%!vc#V z`z_krG`$K5Mts&z7FG?RL)xkngRF8Y%-T1XNSM(RdCaIlN?-Inb@rwt4znr5LrbDL z8;_-4Ij;uu_mK9Dv{fi?Ql*GQr8Rc4Z>B}>5PL{iUpqht1E@&DnYSN#oy-xY_Mr4~B{4uUMn+1NZ8@`E-Fgn00SQ^# zfOn+OA$cnM;*{HKRNp$3jSTR$LLeb2ZJ_gLxci$oSS6sohGaY>Bp0gd?>ZDdSGJVK zLr0LaAtCk0cue=53d8QnG#)3CKTB9iy@n4>lgl?e012BvYgvist%v3Y4SIxA9qKpJ zW(?b4?G<-A=d30currp8tYpvG2@Bf$Z8L)eZ9hqmk3Fi69xd8$$DpGRw$KBg980(AMoJVf_P6?Yf6*_D^g+v-CRcU=1)@RlaWomOs*x6xo*hk{A z+~ks+ImL^tJ&~G)I1WKVd~mwih{&-IeaZNM=%CK|28dNEiI3KmH5u0v_BGf_B{mjL zc)}tHqmA#+XAM8*sH!i61oM4Tdbw{SZ^;c0_%x@-2$qd)DM-!o9WeV%?LlsxAi+W~ z(UCw(Y%86$$U(L=p%NtQ_*yFS#ij&*RrQT6j=g+znXFq9326^x{t|^$rK}qJ!l-7( zb+lq2p}!((BT^}sd85_A^Mal2mn{tySD+ zmJ^|9T~!|anbx;Wd00UDELhM1BLUKmx>nCv+UjUIQhG^kgk-G0)Y~7r=L6^DtndB4 zGb=$#rAkRC$?nZ92Hfsv@ZtuqQv)TK%-$~8n z4Ix3>Pb9Y?A-zne5O@T1)7JD5N6t7k-*ljyyHgdJXAx;}Vsb~7e_P}H=1#Y$B$j$n&o9I}Rbu6;V7NV5;hH2?F z>g3UOQ43}sOzY`4u<66LnLaVQ{=Iz`D<#AQgsq|r)1OW&+iX>@^}9DwHfz$l41k2> zcIyURhleld-h)c0)${R~hs^6;qTSbh84(actyVXFBR)W{Ny>G;-kF+E^a!%((O?my zH29$CkxP)F&|yabVc4}NRjRb!FWH%1JHq5WBy9c^GTvcZm!iiz zKAo8M?c$TeK}+X+LT#jD7nEKfse%r$l_yT#Ikza=9}<$=C_8fKkaA$%c7s{Oq^h;4 zgpLA=F4ut)g&*nFKgJoX&{^O+4?1M*4>n~0eGsYjdS!XTW|G+#*?3^V?=1ywEx}U6 z>=5}=$h?H~n$RiK0-6goJ2ScPGVsLmfm!Zkltw&?JoQqDy(MTZlMQc{Ie0v)0c#uB zhC_$=z;ooQUti~mF-M1Os9SiJ&aydwNZysIZ%_P2W|WZB>B96Fx&v)IE|l4N5)yVy z9ta6xYi`-~d+rcJlBYxmJ}6?G2)4#u8J&c?hhCW|Taa8}%fa8&l0s+vc<5`TW(C8& z8?z!aKk4{!X+!I1el-Nvspk{CIjFMaR_?vqAhx1073q{Yf{-&OeKs~L6 z^l&={u!h3s&+qM<+SL{05Z3+S9RskPd2Y8YWas@?TnVw)x(5jvhpZa&uSj~eG&82a zBIo`)VT(&-&QeFgpx$ zv*rj(enmeWI|&`q&y|_7gse*GeOR)%fy<>eykt2fr0g8t*0pWj0WImuDYM@tk&Wl+7u(TK zOiN)so{J}yji;?>OSw_E>u2+lN05-Uw1csdnq6W>9p@!KAR+6co^xF%CUk#f%1dst zyK`zb)22SjzX6;n3l^P1@o-Bf4(Lagw)|X-t@#Z+E%R;QQ7OGCOY%3vjf$9ghWMK| zmgJt^{q=3$)C)eSQsNOA{>sC!YsOK1eS75gH)i}ixuyMi?a(SeyRue13gxUJ`-QBF z+AgG}3{wWmWumbXFIW1R|4~XydUPdEJ(ZR+3|ZMz`eavlF7lBFGS}V;X^=|96fH~U zAPNcy@sr95k<)3u!j&wbBQhqKhq)UPDL2wVK0w`$}gNg7?ohsjtsG7qYaeHRsNhB zvy(z4m4%6})jc#T@d zo{R7Rn2Z`>T7X=xl!zpr1(3j@}?a}iZ1lxj5dQVPwya}g#PWMr;hHGj51KmQF8c04&TBy73m zcAW5+WUCE-g+HYxQ3T1c4kNfpch>M>=yn?ZlI)?$f2BYy0wOg@XSb{1G4-YS6;KTC*12Zo{b2Q|OW1Ch#&fSQaP_Qe$pj7%=3WqiSKZmFB3a+nA`C4M`;G zVwg-8DHt`nTv5z~(mZ2hBO9Y=NuWHLDe-?`26a;HdWSC2zli~$|6}gi&-R8s7f>Eqg6 z0N}`f1Dz6Cbq9x!#9JCD4!~a{!~+1Vm#eX>U{I1z1+>wGl3BDS+sJ?cgEb6QsAc>m zZQQ6nSuqqzftW-@R3JFc!^&1Cj33fqhN|m6bf<|x!pMXF^>X`lY zP-IR>mXpo(8np5a{h^%inpPr54GiR604S0JnNn!9v(PyV2$-TzXEaDJd1-+}%!g(q z_;Z{RH91mPaB%aw0%|giBr`6wSZWwa2r!TXQ#7er>=sqhQn)x{PaXcFjh+lMSrkb% zeB3rvaB`<`8`}>1VBUtgrp%Jr#;S&f2|(ONH@q^r&3cT^QYDxHN+hmqI5w1zSTR;A z!w&O62fG3yvCKz-zcgN`3_7+pj6@Wu9OGR;6}x>xLRqn&EJTeoFhm(pKrDzCd;Gnm z1;lyWA}|xVzi*Kt2z#MH82hDSB%(<36;h@57zaBNO`#D4$;V9I@|A&YKb)sM-0?}VZP@qck^aTWe zMb0-Ge3QV(P?+>TMuVcwG(3gHk|+GDND8)(d<+F8e?_0?4AKUnDdFi0O8zr_Alc%|2o+5-1%<*6|t}v=6Qv8i%H9@M>9P=h?o=1>c}2p z^+RFy1sW)^XCmhP8KM>>^_J7Au2LxvQFu#4SOLQh4mCYNzHs2ocMW?qFKY2|5|i=_ zplP9_8Zszi-j1;FR%;6r2rSGACI)zaf+UMDux}KpNeUYR`s^85b;bX5EXoPR(oj(l z84I}P-Qh=1h21M8B>LLCkix7lcvBe87b7|^B{xRn;^yuOL*`Z9Ji5F)f_RyGWL#7r z7}mgLe!m3_lm;ms>?8pN0_XPx!^w6J1wztZvA{i{)iQ~SBu{ut9wdQWOxCMGO1>pC?})gLl(V<)D@3a}snT-J&)SfT`e~0*s5u1ADT>P41@8;ItuFHm zO(8p{qnVcH7Iv=pr@TT3akdKY{VAW&9%gd*o0(*4$d|Q0OFf_YoUwTk@}fUwFHq(< z<^s3>l$9}z6#U=wi@W>xoWJ>&tzm!4$Rv{5!_2m6cIChOr<{EAz`2sY^9NkaOqgvj z(de>|t~D1J+HTRP=^0hk_#8>lEVCQDmOQxvj4tl~t~*AGC!2V7g*RA~%!C(3OQrq^2b%;k<0XNy5gaX8=sgd6_AqWVWB}mtbh%y@aC>ZDP8?TknXYmSYPnLPC9S7%vb7%CkOcG zPXSS&ob2=xNdtrA3evNXO?LrOFFZ4`7>hS+T|;Dk;vh>r;|TFqg(xIGBJTjXOyaGO zE0rQ;ka(<2f^7tFGeY2z4EE9y%UFYH2C(NshtVf)^p!;VxR`vS7CX@&1iJp48IK4*1Ni)Cb%6@OT+p^}N8r*nkaeqOnqy zzdS^R)fBl5*UXbT3%XA{P%6XYbCtKhOpFAH1MsPxKoXfeh&-t#4qHM)FM(B&^gWXM2^HL-$msu~tP5LoyN3}rMw&E%5}UH$^r zJ26jc@uG4-h>t`Rpd2e#_=w2+ZlpShkWu)&FnQ*W*Fow2JPF>@D|%;S!Jvq);4QkH zi*AKc!$V#^g)TY3NX-v>d1Il)Ur=3I`P9V4ryaWd1=>r^kIwVF1=YNBs;OzskbPMf zdh8i&C4DnmIW@KxD9+F>aEG??no^usJD{I;P9OTyl{xy>ga|d9lrp)x>@E-zbOr9f z9Q;QMIbJFk<8@6RwAHAWXsqzUD_IOxlUhCynE4BMN^_CVA04qQU;HqPR9*Gn7Pcs0 z5ME+U8E=Yf&@WcA1>$0bqZTus#M}q_g2KdsWPgwv|FMB+3^X`d2}iosrMyvZ0;-2FjP_ZJbac9Rs6Gy9lzumkuEyLK{r(skea*#89vo#o9_C=zWMX z-xn3DW-_l%5k>kf1hO-NY24LVo7eF{E$_ToHJG>oTd;^8b0)&3CTzQ*c}v?rUp&81 z6Lnq~P~@E>q1@`c8daJtOwY~6&-UZAL(^0z*iTYGlRcvy7`2;u2cGu(wC|UMVgrY| z@+}AjKEV~rIcX!*mvcdM&=y?LbZFA%UfCxRvv2S7DN*A+_Jw{%jl9}V+g%!47`{s) z7h!izh`LQVf>0n4T+w2x$?Sr|Fi;e{#g9wK=K`3~nwkXhfe2Tg29l+DWW;=10D8;` zZ6Y)kAX5CGK}#X|AS`VhFp$NLrvOp}XfZ%zA@)+r142~lTv;$EXbaxrTU|9SARi8Z zE;$sd_A;LnfCh6yGIlxbI$pgFB^lN|PP*El{FFzEMUk*VO0)l95TEk`B8xHEe=a z1}F)xuN4sU#lqsG&~}pM_Ms88Ad-h0u6ek z8k!tcz)t8ExWlt>0=kr<#_W9I1cX$tSh+`gG&ICe=1ysArl}>R-&}(dJtEi1beOaZ zWUYdHdlesZ#cqGBCn=fl7^_P}zJMeZ`EHOM;~Fai_fZyTbEm~B^a6XKD!8HrndT_- zG85*PXlm7sne&Aso{{78%}6}eqghLR%G{QT1J$+Ke4socqJhp;^CAGgazc_aY79nd z+)JmlzbbSd$=t+3PM}!Cbq;Fsiuu3{_?Z)UN%QIp_vsmEai?&H=5Dtz)4);KZ47QT z+flR=z*@8rwrZXg7qLbPVsz5^mQ9g|nu~M6C0Jr0vlvU&=?hxIFj#+;;fo$>GLED# zm`!IeDM##@xf&JrBR^1L&!9&9tiD?+#4N^o)<)`1y8M+dq)H4w$;ea@A3%|)+hr{b zMr;#k@xG0@ znwsbWq-crq9sqz*cuD4lC=xVl3uv*gHjx*j1I23Gs7?*B<)fi4(dbcT0Ey}jbu3l! zPfFBT!74Ea1=*#_ozikgQ^-8$5Mi%}(w|vxr%ehOK9FygtD!F>G-4>^Zn3&hp&g(R zUP7N805U5nA}0S~u{>Z5UhY+B_y7SP48%gMVs(o7;simyYrg2=z2*Qs0fQHdUh&Th z1Dd>Z>K#p^r6Qkr(G<~VUq}pjms(SU#FPL!JpBE^8rWDlea=QZG|E`ml)}YX(mWAaCI9GBjSCB*BujFnTF~QvA`w2~@ zE!1RE#(tPY?Fuy$1~kkGjA8fD1Smo@t~}+1fqve3u@>*xZ*W16Ju6nN$$L}+y}WbG znKgHr1)hpPzrY>ZU}|da$&!~B8OJjxBGRHWm4o@z0?Iym0J3mF0;tJC`pGWv&?D-q z<|@W~e2b);eg_O0UChxKu>@*)R?&8kP9$j8O`ip+v6K%);wGIv)u7o$yIiunT@y)O z7;xmB!{eFU2pB&jO)F^Ctw8Pj4ynT?aLm2!nw|c!?$g zA7`W4Lagt9GB8Vz)M`IzkdFT8YF}EcWPUd6dZvHJPbxb}+ zdgpKHU`{?VVqRWZC~k6vyK7lC;jO8+u literal 0 HcmV?d00001 diff --git a/index.html b/index.html new file mode 100644 index 0000000..c8d5e84 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + SysMon Web + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..43dae7a --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "sysmon-web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --host & cargo run", + "build": "tsc && vite build && cargo build --release", + "preview": "vite preview --host & cargo run --release" + }, + "dependencies": { + "@tanstack/react-query": "^5.32.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "recharts": "^2.12.6" + }, + "devDependencies": { + "@biomejs/biome": "1.7.1", + "@types/node": "^20.12.8", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "typescript": "^5.2.2", + "vite": "^5.2.0" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..c6398d9 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/client/App.css b/src/client/App.css new file mode 100644 index 0000000..e0a2584 --- /dev/null +++ b/src/client/App.css @@ -0,0 +1,26 @@ +#root { + background-color: var(--color-neutral1); + display: grid; + grid-auto-rows: calc(50svh - 16px); + padding: 8px; + gap: 8px; + + @media (min-width: 500px) { + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); + height: 100vh; + } + + > div { + border-radius: 16px; + background-color: var(--color-neutral0); + padding: 16px; + width: 100%; + overflow: hidden; + } +} + +.chart-card { + display: grid; + grid-template-rows: repeat(2, max-content) 1fr; +} \ No newline at end of file diff --git a/src/client/App.tsx b/src/client/App.tsx new file mode 100644 index 0000000..7263508 --- /dev/null +++ b/src/client/App.tsx @@ -0,0 +1,45 @@ +import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'; +import './App.css'; +import { fetchDynamicData, fetchStaticData } from './api'; +import { Cpu } from './components/chart-cards/cpu'; +import { Disks } from './components/chart-cards/disks'; +import { Memory } from './components/chart-cards/memory'; +import { Network } from './components/chart-cards/network'; +import { Static } from './components/chart-cards/static'; +import { Temps } from './components/chart-cards/temps'; + +const queryClient = new QueryClient(); + +const Main = () => { + const staticQuery = useQuery({ + queryKey: ['static'], + queryFn: fetchStaticData, + }); + + const dynamicQuery = useQuery({ + queryKey: ['dynamic'], + queryFn: fetchDynamicData, + refetchInterval: 500, + }); + + const isLoading = staticQuery.isLoading || dynamicQuery.isLoading; + + return ( + !isLoading && ( + <> + + + + + + + + ) + ); +}; + +export const App = () => ( + +
+ +); diff --git a/src/client/api.ts b/src/client/api.ts new file mode 100644 index 0000000..5f0daa4 --- /dev/null +++ b/src/client/api.ts @@ -0,0 +1,17 @@ +const getApiUrl = (path: string) => { + const url = new URL(window.location.href); + url.port = '3001'; + url.pathname = path; + + return url; +}; + +export const fetchStaticData = async (): Promise => { + const response = await fetch(getApiUrl('/api/static')); + return await response.json(); +}; + +export const fetchDynamicData = async (): Promise => { + const response = await fetch(getApiUrl('/api/dynamic')); + return await response.json(); +}; diff --git a/src/client/components/chart-cards/cpu.tsx b/src/client/components/chart-cards/cpu.tsx new file mode 100644 index 0000000..9bbbd47 --- /dev/null +++ b/src/client/components/chart-cards/cpu.tsx @@ -0,0 +1,44 @@ +import { useQuery } from '@tanstack/react-query'; +import { useEffect, useMemo, useState } from 'react'; +import { ChartCard } from './index'; + +export const Cpu = () => { + const { data: staticData } = useQuery({ queryKey: ['static'] }); + const { data: dynamicData } = useQuery({ queryKey: ['dynamic'] }); + const [history, setHistory] = useState(new Array(150).fill([])); + + useEffect(() => { + if (dynamicData) { + setHistory(history => { + const newHistory = history.slice(1); + newHistory.push(dynamicData.cpu_usage); + + return newHistory; + }); + } + }, [dynamicData]); + + const total_cpus = useMemo(() => history.at(-1)?.length ?? 0, [history]); + + if (!staticData || !dynamicData) { + return
; + } + + return ( + !!total_cpus && ( + + {staticData.cpu.brand} + {` (${total_cpus} threads)`} + + } + domain={[0, 100]} + formatOptions={{ prefix: false, units: '%' }} + data={history} + total={total_cpus} + /> + ) + ); +}; diff --git a/src/client/components/chart-cards/disks.tsx b/src/client/components/chart-cards/disks.tsx new file mode 100644 index 0000000..1689a51 --- /dev/null +++ b/src/client/components/chart-cards/disks.tsx @@ -0,0 +1,37 @@ +import { useQuery } from '@tanstack/react-query'; +import { useEffect, useState } from 'react'; +import { ChartCard } from './index'; + +export const Disks = () => { + const { data: dynamicData } = useQuery({ queryKey: ['dynamic'] }); + const [history, setHistory] = useState(new Array(150).fill([])); + + useEffect(() => { + if (dynamicData) { + setHistory(history => { + const newHistory = history.slice(1); + newHistory.push([dynamicData.disks.read, dynamicData.disks.write]); + + return newHistory; + }); + } + }, [dynamicData]); + + if (!dynamicData) { + return
; + } + + return ( + + ); +}; diff --git a/src/client/components/chart-cards/index.tsx b/src/client/components/chart-cards/index.tsx new file mode 100644 index 0000000..632eeea --- /dev/null +++ b/src/client/components/chart-cards/index.tsx @@ -0,0 +1,66 @@ +import { Legend, type LegendProps } from '@/components/legend'; +import { getFillColor, getStrokeColor } from '@/utils/colors'; +import { type FormatOptions, formatValue } from '@/utils/format'; +import type { ReactNode } from 'react'; +import { Area, AreaChart, CartesianGrid, ResponsiveContainer, XAxis, YAxis } from 'recharts'; +import type { AxisDomain } from 'recharts/types/util/types'; + +type Props = { + title: ReactNode; + subtitle?: ReactNode; + legend?: LegendProps; + formatOptions?: FormatOptions; + domain?: AxisDomain; + hueOffset?: number; + data: number[][]; + total: number; +}; + +export const ChartCard = ({ data, domain, legend, hueOffset = 0, title, subtitle, formatOptions, total }: Props) => { + return ( +
+

{title}

+ + {subtitle} + + {legend && } + + + + + + formatValue(value, formatOptions)} + /> + + {Array.from({ length: total }).map((_, index) => ( + // biome-ignore lint/correctness/useJsxKeyInIterable: order irrelevant + + ))} + + + + +
+ ); +}; diff --git a/src/client/components/chart-cards/memory.tsx b/src/client/components/chart-cards/memory.tsx new file mode 100644 index 0000000..87c0252 --- /dev/null +++ b/src/client/components/chart-cards/memory.tsx @@ -0,0 +1,37 @@ +import { useQuery } from '@tanstack/react-query'; +import { useEffect, useState } from 'react'; +import { ChartCard } from './index'; + +export const Memory = () => { + const { data: staticData } = useQuery({ queryKey: ['static'] }); + const { data: dynamicData } = useQuery({ queryKey: ['dynamic'] }); + const [history, setHistory] = useState(new Array(150).fill([])); + + useEffect(() => { + if (dynamicData) { + setHistory(history => { + const newHistory = history.slice(1); + newHistory.push([dynamicData.mem_usage, dynamicData.swap_usage]); + + return newHistory; + }); + } + }, [dynamicData]); + + if (!staticData || !dynamicData) { + return
; + } + + return ( + + ); +}; diff --git a/src/client/components/chart-cards/network.tsx b/src/client/components/chart-cards/network.tsx new file mode 100644 index 0000000..6731194 --- /dev/null +++ b/src/client/components/chart-cards/network.tsx @@ -0,0 +1,37 @@ +import { useQuery } from '@tanstack/react-query'; +import { useEffect, useState } from 'react'; +import { ChartCard } from './index'; + +export const Network = () => { + const { data: dynamicData } = useQuery({ queryKey: ['dynamic'] }); + const [history, setHistory] = useState(new Array(150).fill([])); + + useEffect(() => { + if (dynamicData) { + setHistory(history => { + const newHistory = history.slice(1); + newHistory.push([dynamicData.network.down, dynamicData.network.up]); + + return newHistory; + }); + } + }, [dynamicData]); + + if (!dynamicData) { + return
; + } + + return ( + + ); +}; diff --git a/src/client/components/chart-cards/static.tsx b/src/client/components/chart-cards/static.tsx new file mode 100644 index 0000000..a7f7875 --- /dev/null +++ b/src/client/components/chart-cards/static.tsx @@ -0,0 +1,63 @@ +import { useAnimationFrame } from '@/hooks/use-animation-frame'; +import { useQuery } from '@tanstack/react-query'; +import { useRef, useState } from 'react'; + +const formatUptime = (value: number) => { + const seconds = String(Math.floor(value % 60)).padStart(2, '0'); + const minutes = String(Math.floor((value / 60) % 60)).padStart(2, '0'); + const hours = String(Math.floor((value / (60 * 60)) % 24)).padStart(2, '0'); + const days = Math.floor((value / (60 * 60 * 24)) % 365); + const years = Math.floor(value / (60 * 60 * 24 * 365)); + + let formatted = ''; + + if (years >= 1) { + formatted += `${years}y `; + } + + if (days >= 1 || years >= 1) { + formatted += `${days}d `; + } + + return `${formatted}${hours}:${minutes}:${seconds}`; +}; + +const Uptime = ({ boot_time }: Pick) => { + const [uptime, setUptime] = useState(formatUptime(Date.now() / 1000 - boot_time)); + const lastUpdate = useRef(0); + + useAnimationFrame(dt => { + lastUpdate.current += dt; + if (lastUpdate.current > 1000) { + setUptime(formatUptime(Date.now() / 1000 - boot_time)); + } + }); + + return ( + <> + Uptime +

{uptime}

+ + ); +}; + +export const Static = () => { + const { data: staticData } = useQuery({ queryKey: ['static'] }); + + return ( + staticData && ( +
+

{staticData.host_name}

+ + OS +

+ {staticData.name} {staticData.os_version} +

+ + Kernel +

{staticData.kernel_version}

+ {staticData.boot_time && } +
+ ) + ); +}; diff --git a/src/client/components/chart-cards/temps.tsx b/src/client/components/chart-cards/temps.tsx new file mode 100644 index 0000000..3c40f3f --- /dev/null +++ b/src/client/components/chart-cards/temps.tsx @@ -0,0 +1,39 @@ +import { useQuery } from '@tanstack/react-query'; +import { useEffect, useMemo, useState } from 'react'; +import { ChartCard } from './index'; + +export const Temps = () => { + const { data: staticData } = useQuery({ queryKey: ['static'] }); + const { data: dynamicData } = useQuery({ queryKey: ['dynamic'] }); + const [history, setHistory] = useState(new Array(150).fill([])); + + useEffect(() => { + if (dynamicData) { + setHistory(history => { + const newHistory = history.slice(1); + newHistory.push(dynamicData.temps); + + return newHistory; + }); + } + }, [dynamicData]); + + const total_sensors = useMemo(() => history.at(-1)?.length ?? 0, [history]); + + if (!staticData || !dynamicData) { + return
; + } + + return ( + + ); +}; diff --git a/src/client/components/legend/index.css b/src/client/components/legend/index.css new file mode 100644 index 0000000..bf27718 --- /dev/null +++ b/src/client/components/legend/index.css @@ -0,0 +1,19 @@ +.legend-wrapper { + display: flex; + flex-wrap: wrap; + gap: 12; + width: 100%; + overflow: hidden; + + > div { + flex: 1; + } + + small { + display: inline-block; + max-width: 128px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} \ No newline at end of file diff --git a/src/client/components/legend/index.tsx b/src/client/components/legend/index.tsx new file mode 100644 index 0000000..cb70e2b --- /dev/null +++ b/src/client/components/legend/index.tsx @@ -0,0 +1,21 @@ +import { getTextColor } from '@/utils/colors'; +import { type FormatOptions, formatValue } from '@/utils/format'; +import './index.css'; + +export type LegendProps = { + labels: string[]; + values: number[]; + formatOptions?: FormatOptions; + hueOffset?: number; +}; + +export const Legend = ({ labels, values, formatOptions, hueOffset = 0 }: LegendProps) => ( +
+ {labels.map((label, index) => ( +
+ {label} +

{formatValue(values[index], formatOptions)}

+
+ ))} +
+); diff --git a/src/client/hooks/use-animation-frame.ts b/src/client/hooks/use-animation-frame.ts new file mode 100644 index 0000000..96f2211 --- /dev/null +++ b/src/client/hooks/use-animation-frame.ts @@ -0,0 +1,24 @@ +import { useEffect, useRef } from 'react'; + +export const useAnimationFrame = (callback: (dt: number) => void) => { + const requestRef = useRef(); + const previousTimeRef = useRef(); + + useEffect(() => { + const animate: FrameRequestCallback = time => { + if (previousTimeRef.current !== undefined) { + const deltaTime = time - previousTimeRef.current; + callback(deltaTime); + } + previousTimeRef.current = time; + requestRef.current = requestAnimationFrame(animate); + }; + + requestRef.current = requestAnimationFrame(animate); + return () => { + if (requestRef.current) { + cancelAnimationFrame(requestRef.current); + } + }; + }, [callback]); +}; diff --git a/src/client/index.css b/src/client/index.css new file mode 100644 index 0000000..69b7e80 --- /dev/null +++ b/src/client/index.css @@ -0,0 +1,33 @@ +:root { + --color-neutral0: #002b36; + --color-neutral1: #073642; + --color-neutral2: #586e75; + --color-neutral3: #657b83; + --color-neutral4: #839496; + --color-neutral5: #93a1a1; + --color-neutral6: #eee8d5; + --color-neutral7: #fdf6e3; + + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color: var(--color-neutral6); + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + margin: 0; +} + +* { + box-sizing: border-box; +} + +* { + margin-top: 0; +} \ No newline at end of file diff --git a/src/client/main.tsx b/src/client/main.tsx new file mode 100644 index 0000000..559a7c9 --- /dev/null +++ b/src/client/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App.tsx'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/src/client/types.ts b/src/client/types.ts new file mode 100644 index 0000000..01a19eb --- /dev/null +++ b/src/client/types.ts @@ -0,0 +1,31 @@ +type StaticData = { + boot_time: number; + components: string[]; + cpu: { + brand: string; + name: string; + vendor_id: string; + }; + host_name: string; + kernel_version: string; + name: string; + os_version: string; + total_memory: number; + total_swap: number; + uptime: number; +}; + +type DynamicData = { + cpu_usage: number[]; + disks: { + read: number; + write: number; + }; + mem_usage: number; + network: { + down: number; + up: number; + }; + swap_usage: number; + temps: number[]; +}; diff --git a/src/client/utils/colors.ts b/src/client/utils/colors.ts new file mode 100644 index 0000000..585211a --- /dev/null +++ b/src/client/utils/colors.ts @@ -0,0 +1,3 @@ +export const getTextColor = (hue: number) => `hsl(${hue} 50 50)`; +export const getFillColor = (hue: number) => `hsl(${hue} 50 50 / 10%)`; +export const getStrokeColor = (hue: number) => `hsl(${hue} 50 50 / 60%)`; diff --git a/src/client/utils/format.ts b/src/client/utils/format.ts new file mode 100644 index 0000000..9076487 --- /dev/null +++ b/src/client/utils/format.ts @@ -0,0 +1,20 @@ +const prefixes = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; + +export type FormatOptions = { decimals?: number; prefix?: boolean; si?: boolean; units?: string }; + +export const formatValue = (value: number, { decimals = 1, prefix = true, si, units = 'B' }: FormatOptions = {}) => { + if (!value) { + return `0\u2009${units}`; + } + + if (!prefix) { + return `${value}\u2009${units}`; + } + + const k = si ? 1000 : 1024; + const i = Math.floor(Math.log(value) / Math.log(k)); + + return `${Number((value / k ** i).toFixed(Math.max(0, decimals)))}\u2009${prefixes[i]}${ + i > 0 && !si ? 'i' : '' + }${units}`; +}; diff --git a/src/client/vite-env.d.ts b/src/client/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/client/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/stats-server/disks.rs b/src/stats-server/disks.rs new file mode 100644 index 0000000..1cb6235 --- /dev/null +++ b/src/stats-server/disks.rs @@ -0,0 +1,56 @@ +pub mod disks { + use serde_json::json; + use std::{ + fs::File, + io::{Read, Seek, SeekFrom}, + }; + + pub struct DiskUsage { + read: u64, + write: u64, + } + + impl DiskUsage { + fn new() -> Self { + DiskUsage { read: 0, write: 0 } + } + } + + pub struct DiskStats { + fd: File, + prev: DiskUsage, + } + + impl DiskStats { + pub fn new() -> Self { + let fd = File::open("/proc/diskstats").unwrap(); + DiskStats { + fd, + prev: DiskUsage::new(), + } + } + + pub fn refresh(&mut self) -> DiskUsage { + let mut curr = DiskUsage::new(); + let mut io_data = String::new(); + self.fd.read_to_string(&mut io_data).unwrap(); + for line in io_data.lines() { + let fields: Vec<_> = line.split_whitespace().collect(); + curr.read += fields[5].parse::().unwrap() * 512 * 8; + curr.write += fields[9].parse::().unwrap() * 512 * 8; + } + self.fd.seek(SeekFrom::Start(0)).unwrap(); + curr + } + + pub fn diff(&mut self) -> serde_json::Value { + let curr = self.refresh(); + let diff = json!({ + "read": curr.read - self.prev.read, + "write": curr.write - self.prev.write, + }); + self.prev = curr; + diff + } + } +} diff --git a/src/stats-server/main.rs b/src/stats-server/main.rs new file mode 100644 index 0000000..eff073e --- /dev/null +++ b/src/stats-server/main.rs @@ -0,0 +1,149 @@ +use axum::{extract::State, http::Response, response::IntoResponse, routing::get, Router, Server}; +use serde_json::{json, Value}; +use std::{ + sync::{Arc, Mutex}, + time::{Duration, Instant}, +}; +use sysinfo::{Components, CpuRefreshKind, MemoryRefreshKind, Networks, RefreshKind, System}; + +use crate::disks::disks::DiskStats; + +mod disks; + +#[derive(Clone)] +struct AppState { + latest: Arc>, + data: Arc>, +} + +impl Default for AppState { + fn default() -> Self { + AppState { + latest: Arc::new(Mutex::new(Instant::now())), + data: Arc::new(Mutex::new(json!({}))), + } + } +} + +#[tokio::main] +async fn main() { + let app_state = AppState::default(); + + let router = Router::new() + .route("/api/dynamic", get(dynamic_sysinfo_get)) + .route("/api/static", get(static_sysinfo_get)) + .with_state(app_state.clone()); + + // Update system usage in the background + tokio::task::spawn_blocking(move || { + let mut sys = System::new(); + let mut components = Components::new_with_refreshed_list(); + let mut networks = Networks::new_with_refreshed_list(); + let mut disk_stats = DiskStats::new(); + + loop { + let now = Instant::now(); + let latest = app_state.latest.lock().unwrap().clone(); + + if (now - latest).as_millis() < 10000 { + sys.refresh_cpu(); + sys.refresh_memory(); + components.refresh(); + networks.refresh(); + disk_stats.refresh(); + + let cpu_usage: Vec<_> = sys.cpus().iter().map(|cpu| cpu.cpu_usage()).collect(); + let mem_usage = sys.used_memory(); + let swap_usage = sys.used_swap(); + let temps: Vec<_> = components + .iter() + .map(|component| component.temperature()) + .collect(); + let (net_down, net_up) = + networks + .iter() + .fold((0, 0), |(down, up), (_name, network)| { + (down + network.received(), up + network.transmitted()) + }); + + { + let mut data = app_state.data.lock().unwrap(); + *data = json!({ + "cpu_usage": cpu_usage, + "mem_usage": mem_usage, + "swap_usage": swap_usage, + "temps": temps, + "network": { + "down": net_down, + "up": net_up + }, + "disks": disk_stats.diff() + }); + } + } + + std::thread::sleep(Duration::from_millis(200)); + } + }); + + let server = Server::bind(&"0.0.0.0:3001".parse().unwrap()).serve(router.into_make_service()); + let addr = server.local_addr(); + println!("Listening on {addr}"); + + server.await.unwrap(); +} + +#[axum::debug_handler] +async fn dynamic_sysinfo_get(State(state): State) -> impl IntoResponse { + let data = state.data.lock().unwrap().clone(); + + { + let mut latest = state.latest.lock().unwrap(); + *latest = Instant::now() + } + + Response::builder() + .header(axum::http::header::ORIGIN, "*") + .header(axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .header("content-type", "application/json") + .body(data.to_string()) + .unwrap() +} + +async fn static_sysinfo_get() -> impl IntoResponse { + let sys = System::new_with_specifics( + RefreshKind::new() + .with_cpu(CpuRefreshKind::everything()) + .with_memory(MemoryRefreshKind::everything()), + ); + + let components = Components::new_with_refreshed_list(); + + Response::builder() + .header(axum::http::header::ORIGIN, "*") + .header(axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .header("content-type", "application/json") + .body( + json!({ + "boot_time": System::boot_time(), + "uptime": System::uptime(), + "name": System::name(), + "kernel_version": System::kernel_version(), + "os_version": System::os_version(), + "host_name": System::host_name(), + "total_memory": sys.total_memory(), + "total_swap": sys.total_swap(), + "cpu": { + "name": sys.cpus()[0].name(), + "vendor_id": sys.cpus()[0].vendor_id(), + "brand": sys.cpus()[0].brand(), + }, + "components": components + .iter() + .map(|component| component.label()) + .collect::>() + }) + .to_string(), + ) + .unwrap() +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..74dcae1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": ["./src/client/*"] + } + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..97ede7e --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..235b204 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,13 @@ +import path from 'node:path'; +import react from '@vitejs/plugin-react-swc'; +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve('./src/client'), + }, + }, +});