diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 893f73e..3a441b6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,6 +20,7 @@ jobs:
with:
toolchain: "1.88.0"
- uses: Swatinem/rust-cache@v2
+ - run: mkdir -p dist
- run: cargo check
fmt:
@@ -43,6 +44,7 @@ jobs:
toolchain: "1.88.0"
components: clippy
- uses: Swatinem/rust-cache@v2
+ - run: mkdir -p dist
- run: cargo clippy -- -D warnings
test:
@@ -71,6 +73,7 @@ jobs:
with:
toolchain: "1.88.0"
- uses: Swatinem/rust-cache@v2
+ - run: mkdir -p dist
- run: cargo test
api:
@@ -101,6 +104,7 @@ jobs:
with:
toolchain: "1.88.0"
- uses: Swatinem/rust-cache@v2
+ - run: mkdir -p dist
- run: cargo install rapina-cli --locked
- run: cargo build --release
- name: Start server
diff --git a/.gitignore b/.gitignore
index 60625ff..f29d515 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,5 @@
.env
*.db
*.db-journal
+dist/
+node_modules/
diff --git a/Cargo.lock b/Cargo.lock
index 199ef23..dda07d1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8,6 +8,12 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+[[package]]
+name = "adler32"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
+
[[package]]
name = "ahash"
version = "0.7.8"
@@ -99,6 +105,17 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "any_spawner"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41058deaa38c9d9dd933d6d238d825227cffa668e2839b52879f6619c63eee3b"
+dependencies = [
+ "futures",
+ "thiserror 2.0.18",
+ "wasm-bindgen-futures",
+]
+
[[package]]
name = "anyhow"
version = "1.0.101"
@@ -111,6 +128,17 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
[[package]]
name = "async-stream"
version = "0.3.6"
@@ -159,6 +187,36 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+[[package]]
+name = "attribute-derive"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77"
+dependencies = [
+ "attribute-derive-macro",
+ "derive-where",
+ "manyhow",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.116",
+]
+
+[[package]]
+name = "attribute-derive-macro"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61"
+dependencies = [
+ "collection_literals",
+ "interpolator",
+ "manyhow",
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
+ "quote-use",
+ "syn 2.0.116",
+]
+
[[package]]
name = "autocfg"
version = "1.5.0"
@@ -313,6 +371,12 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+[[package]]
+name = "camino"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
+
[[package]]
name = "cc"
version = "1.2.56"
@@ -320,6 +384,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [
"find-msvc-tools",
+ "jobserver",
+ "libc",
"shlex",
]
@@ -399,6 +465,23 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
+[[package]]
+name = "codee"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9dbbdc4b4d349732bc6690de10a9de952bd39ba6a065c586e26600b6b0b91f5"
+dependencies = [
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "collection_literals"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084"
+
[[package]]
name = "colorchoice"
version = "1.0.4"
@@ -414,18 +497,84 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "config"
+version = "0.15.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6"
+dependencies = [
+ "convert_case 0.6.0",
+ "pathdiff",
+ "serde_core",
+ "toml",
+ "winnow",
+]
+
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+[[package]]
+name = "const_format"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad"
+dependencies = [
+ "const_format_proc_macros",
+]
+
+[[package]]
+name = "const_format_proc_macros"
+version = "0.2.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "const_str_slice_concat"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b"
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
+dependencies = [
+ "unicode-segmentation",
+]
+
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+[[package]]
+name = "core2"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -558,6 +707,26 @@ dependencies = [
"syn 2.0.116",
]
+[[package]]
+name = "dary_heap"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06d2e3287df1c007e74221c49ca10a95d557349e54b3a75dc2fb14712c751f04"
+
+[[package]]
+name = "dashboard"
+version = "0.1.0"
+dependencies = [
+ "gloo-net",
+ "gloo-storage",
+ "leptos",
+ "leptos_router",
+ "serde",
+ "serde_json",
+ "wasm-bindgen",
+ "web-sys",
+]
+
[[package]]
name = "dashmap"
version = "6.1.0"
@@ -593,6 +762,17 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "derive-where"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.116",
+]
+
[[package]]
name = "derive_more"
version = "2.1.1"
@@ -644,6 +824,12 @@ version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+[[package]]
+name = "drain_filter_polyfill"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408"
+
[[package]]
name = "dyn-clone"
version = "1.0.20"
@@ -697,6 +883,16 @@ dependencies = [
"serde",
]
+[[package]]
+name = "either_of"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "216d23e0ec69759a17f05e1c553f3a6870e5ec73420fbb07807a6f34d5d1d5a4"
+dependencies = [
+ "paste",
+ "pin-project-lite",
+]
+
[[package]]
name = "elliptic-curve"
version = "0.13.8"
@@ -756,6 +952,16 @@ dependencies = [
"pin-project-lite",
]
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
[[package]]
name = "ff"
version = "0.13.1"
@@ -811,6 +1017,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -834,6 +1046,7 @@ checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
dependencies = [
"futures-channel",
"futures-core",
+ "futures-executor",
"futures-io",
"futures-sink",
"futures-task",
@@ -884,6 +1097,17 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.116",
+]
+
[[package]]
name = "futures-sink"
version = "0.3.32"
@@ -902,8 +1126,10 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
+ "futures-channel",
"futures-core",
"futures-io",
+ "futures-macro",
"futures-sink",
"futures-task",
"memchr",
@@ -933,6 +1159,18 @@ dependencies = [
"wasi",
]
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+]
+
[[package]]
name = "getrandom"
version = "0.4.1"
@@ -952,6 +1190,55 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+[[package]]
+name = "gloo-net"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-sink",
+ "gloo-utils",
+ "http",
+ "js-sys",
+ "pin-project",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-storage"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a"
+dependencies = [
+ "gloo-utils",
+ "js-sys",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-utils"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
+dependencies = [
+ "js-sys",
+ "serde",
+ "serde_json",
+ "wasm-bindgen",
+ "web-sys",
+]
+
[[package]]
name = "group"
version = "0.13.0"
@@ -963,6 +1250,12 @@ dependencies = [
"subtle",
]
+[[package]]
+name = "guardian"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f"
+
[[package]]
name = "h2"
version = "0.4.13"
@@ -1005,7 +1298,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
- "foldhash",
+ "foldhash 0.1.5",
]
[[package]]
@@ -1013,6 +1306,11 @@ name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash 0.2.0",
+]
[[package]]
name = "hashlink"
@@ -1068,6 +1366,15 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "html-escape"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
+dependencies = [
+ "utf8-width",
+]
+
[[package]]
name = "http"
version = "1.4.0"
@@ -1113,6 +1420,20 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+[[package]]
+name = "hydration_context"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d35485b3dcbf7e044b8f28c73f04f13e7b509c2466fd10cb2a8a447e38f8a93a"
+dependencies = [
+ "futures",
+ "once_cell",
+ "or_poisoned",
+ "pin-project-lite",
+ "serde",
+ "throw_error",
+]
+
[[package]]
name = "hyper"
version = "1.8.1"
@@ -1294,6 +1615,43 @@ dependencies = [
"icu_properties",
]
+[[package]]
+name = "include-flate"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01b7cb6ca682a621e7cda1c358c9724b53a7b4409be9be1dd443b7f3a26f998"
+dependencies = [
+ "include-flate-codegen",
+ "include-flate-compress",
+ "libflate",
+ "zstd",
+]
+
+[[package]]
+name = "include-flate-codegen"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f49bf5274aebe468d6e6eba14a977eaf1efa481dc173f361020de70c1c48050"
+dependencies = [
+ "include-flate-compress",
+ "libflate",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.116",
+ "zstd",
+]
+
+[[package]]
+name = "include-flate-compress"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eae6a40e716bcd5931f5dbb79cd921512a4f647e2e9413fded3171fca3824dbc"
+dependencies = [
+ "libflate",
+ "zstd",
+]
+
[[package]]
name = "indexmap"
version = "2.13.0"
@@ -1326,18 +1684,43 @@ dependencies = [
"generic-array",
]
+[[package]]
+name = "interpolator"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8"
+
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
+[[package]]
+name = "jobserver"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+dependencies = [
+ "getrandom 0.3.4",
+ "libc",
+]
+
[[package]]
name = "js-sys"
version = "0.3.85"
@@ -1386,12 +1769,195 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+[[package]]
+name = "leptos"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b8731cb00f3f0894058155410b95c8955b17273181d2bc72600ab84edd24f1"
+dependencies = [
+ "any_spawner",
+ "cfg-if",
+ "either_of",
+ "futures",
+ "hydration_context",
+ "leptos_config",
+ "leptos_dom",
+ "leptos_hot_reload",
+ "leptos_macro",
+ "leptos_server",
+ "oco_ref",
+ "or_poisoned",
+ "paste",
+ "reactive_graph",
+ "rustc-hash",
+ "send_wrapper",
+ "serde",
+ "serde_qs",
+ "server_fn",
+ "slotmap",
+ "tachys",
+ "thiserror 2.0.18",
+ "throw_error",
+ "typed-builder",
+ "typed-builder-macro",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "leptos_config"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bae3e0ead5a7a814c8340eef7cb8b6cba364125bd8174b15dc9fe1b3cab7e03"
+dependencies = [
+ "config",
+ "regex",
+ "serde",
+ "thiserror 2.0.18",
+ "typed-builder",
+]
+
+[[package]]
+name = "leptos_dom"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f89d4eb263bd5a9e7c49f780f17063f15aca56fd638c90b9dfd5f4739152e87d"
+dependencies = [
+ "js-sys",
+ "or_poisoned",
+ "reactive_graph",
+ "send_wrapper",
+ "tachys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "leptos_hot_reload"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e80219388501d99b246f43b6e7d08a28f327cdd34ba630a35654d917f3e1788e"
+dependencies = [
+ "anyhow",
+ "camino",
+ "indexmap",
+ "parking_lot",
+ "proc-macro2",
+ "quote",
+ "rstml",
+ "serde",
+ "syn 2.0.116",
+ "walkdir",
+]
+
+[[package]]
+name = "leptos_macro"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e621f8f5342b9bdc93bb263b839cee7405027a74560425a2dabea9de7952b1fd"
+dependencies = [
+ "attribute-derive",
+ "cfg-if",
+ "convert_case 0.7.1",
+ "html-escape",
+ "itertools",
+ "leptos_hot_reload",
+ "prettyplease",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "rstml",
+ "server_fn_macro",
+ "syn 2.0.116",
+ "uuid",
+]
+
+[[package]]
+name = "leptos_router"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4168ead6a9715daba953aa842795cb2ad81b6e011a15745bd3d1baf86f76de95"
+dependencies = [
+ "any_spawner",
+ "either_of",
+ "futures",
+ "gloo-net",
+ "js-sys",
+ "leptos",
+ "leptos_router_macro",
+ "once_cell",
+ "or_poisoned",
+ "reactive_graph",
+ "send_wrapper",
+ "tachys",
+ "thiserror 2.0.18",
+ "url",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "leptos_router_macro"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31197af38d209ffc5d9f89715381c415a1570176f8d23455fbe00d148e79640"
+dependencies = [
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.116",
+]
+
+[[package]]
+name = "leptos_server"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66985242812ec95e224fb48effe651ba02728beca92c461a9464c811a71aab11"
+dependencies = [
+ "any_spawner",
+ "base64",
+ "codee",
+ "futures",
+ "hydration_context",
+ "or_poisoned",
+ "reactive_graph",
+ "send_wrapper",
+ "serde",
+ "serde_json",
+ "server_fn",
+ "tachys",
+]
+
[[package]]
name = "libc"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
+[[package]]
+name = "libflate"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3248b8d211bd23a104a42d81b4fa8bb8ac4a3b75e7a43d85d2c9ccb6179cd74"
+dependencies = [
+ "adler32",
+ "core2",
+ "crc32fast",
+ "dary_heap",
+ "libflate_lz77",
+]
+
+[[package]]
+name = "libflate_lz77"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a599cb10a9cd92b1300debcef28da8f70b935ec937f44fcd1b70a7c986a11c5c"
+dependencies = [
+ "core2",
+ "hashbrown 0.16.1",
+ "rle-decode-fast",
+]
+
[[package]]
name = "libm"
version = "0.2.16"
@@ -1419,6 +1985,12 @@ dependencies = [
"vcpkg",
]
+[[package]]
+name = "linear-map"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee"
+
[[package]]
name = "litemap"
version = "0.8.1"
@@ -1440,6 +2012,29 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+[[package]]
+name = "manyhow"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587"
+dependencies = [
+ "manyhow-macros",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.116",
+]
+
+[[package]]
+name = "manyhow-macros"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495"
+dependencies = [
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
+]
+
[[package]]
name = "matchers"
version = "0.2.0"
@@ -1465,6 +2060,22 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -1486,6 +2097,12 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "next_tuple"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28"
+
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
@@ -1557,6 +2174,16 @@ dependencies = [
"libm",
]
+[[package]]
+name = "oco_ref"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed0423ff9973dea4d6bd075934fdda86ebb8c05bdf9d6b0507067d4a1226371d"
+dependencies = [
+ "serde",
+ "thiserror 2.0.18",
+]
+
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -1569,6 +2196,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+[[package]]
+name = "or_poisoned"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd"
+
[[package]]
name = "ordered-float"
version = "4.6.0"
@@ -1655,6 +2288,18 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
[[package]]
name = "pem"
version = "3.0.6"
@@ -1689,6 +2334,26 @@ dependencies = [
"serde",
]
+[[package]]
+name = "pin-project"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.116",
+]
+
[[package]]
name = "pin-project-lite"
version = "0.2.16"
@@ -1780,6 +2445,30 @@ dependencies = [
"toml_edit",
]
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
@@ -1802,6 +2491,17 @@ dependencies = [
"syn 2.0.116",
]
+[[package]]
+name = "proc-macro-utils"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "smallvec",
+]
+
[[package]]
name = "proc-macro2"
version = "1.0.106"
@@ -1853,6 +2553,28 @@ dependencies = [
"proc-macro2",
]
+[[package]]
+name = "quote-use"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e"
+dependencies = [
+ "quote",
+ "quote-use-macros",
+]
+
+[[package]]
+name = "quote-use-macros"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35"
+dependencies = [
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.116",
+]
+
[[package]]
name = "r-efi"
version = "5.3.0"
@@ -1926,12 +2648,61 @@ dependencies = [
]
[[package]]
-name = "rapina-macros"
-version = "0.5.0"
+name = "rapina-macros"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6775674a6acfc640c1623cd6075afdb3986cf9cfcf35cdb2348744d8ccfb32e"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.116",
+]
+
+[[package]]
+name = "reactive_graph"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a0ccddbc11a648bd09761801dac9e3f246ef7641130987d6120fced22515e6"
+dependencies = [
+ "any_spawner",
+ "async-lock",
+ "futures",
+ "guardian",
+ "hydration_context",
+ "or_poisoned",
+ "pin-project-lite",
+ "rustc-hash",
+ "send_wrapper",
+ "serde",
+ "slotmap",
+ "thiserror 2.0.18",
+ "web-sys",
+]
+
+[[package]]
+name = "reactive_stores"
+version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6775674a6acfc640c1623cd6075afdb3986cf9cfcf35cdb2348744d8ccfb32e"
+checksum = "aadc7c19e3a360bf19cd595d2dc8b58ce67b9240b95a103fbc1317a8ff194237"
dependencies = [
- "heck 0.5.0",
+ "guardian",
+ "itertools",
+ "or_poisoned",
+ "paste",
+ "reactive_graph",
+ "reactive_stores_macro",
+ "rustc-hash",
+]
+
+[[package]]
+name = "reactive_stores_macro"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "221095cb028dc51fbc2833743ea8b1a585da1a2af19b440b3528027495bf1f2d"
+dependencies = [
+ "convert_case 0.7.1",
+ "proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.116",
@@ -1962,7 +2733,9 @@ dependencies = [
"bcrypt",
"chrono",
"dotenvy",
+ "mime_guess",
"rapina",
+ "rust-embed",
"serde",
"serde_json",
"tokio",
@@ -2080,6 +2853,12 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "rle-decode-fast"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
+
[[package]]
name = "rsa"
version = "0.9.10"
@@ -2100,6 +2879,56 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "rstml"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61cf4616de7499fc5164570d40ca4e1b24d231c6833a88bff0fe00725080fd56"
+dependencies = [
+ "derive-where",
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "syn 2.0.116",
+ "syn_derive",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "rust-embed"
+version = "8.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27"
+dependencies = [
+ "include-flate",
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-impl"
+version = "8.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "syn 2.0.116",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-utils"
+version = "8.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
+dependencies = [
+ "sha2",
+ "walkdir",
+]
+
[[package]]
name = "rust_decimal"
version = "1.40.0"
@@ -2116,6 +2945,12 @@ dependencies = [
"serde_json",
]
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -2171,6 +3006,15 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
[[package]]
name = "schemars"
version = "1.2.1"
@@ -2239,7 +3083,7 @@ dependencies = [
"serde_json",
"sqlx",
"strum",
- "thiserror",
+ "thiserror 2.0.18",
"time",
"tracing",
"url",
@@ -2339,7 +3183,7 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.116",
- "thiserror",
+ "thiserror 2.0.18",
]
[[package]]
@@ -2393,6 +3237,15 @@ version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+[[package]]
+name = "send_wrapper"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
+dependencies = [
+ "futures-core",
+]
+
[[package]]
name = "serde"
version = "1.0.228"
@@ -2447,6 +3300,26 @@ dependencies = [
"zmij",
]
+[[package]]
+name = "serde_qs"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6"
+dependencies = [
+ "percent-encoding",
+ "serde",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
+dependencies = [
+ "serde_core",
+]
+
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -2459,6 +3332,60 @@ dependencies = [
"serde",
]
+[[package]]
+name = "server_fn"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d05a9e3fd8d7404985418db38c6617cc793a1a27f398d4fbc9dfe8e41b804e6"
+dependencies = [
+ "bytes",
+ "const_format",
+ "dashmap",
+ "futures",
+ "gloo-net",
+ "http",
+ "js-sys",
+ "once_cell",
+ "pin-project-lite",
+ "send_wrapper",
+ "serde",
+ "serde_json",
+ "serde_qs",
+ "server_fn_macro_default",
+ "thiserror 2.0.18",
+ "throw_error",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "xxhash-rust",
+]
+
+[[package]]
+name = "server_fn_macro"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "504b35e883267b3206317b46d02952ed7b8bf0e11b2e209e2eb453b609a5e052"
+dependencies = [
+ "const_format",
+ "convert_case 0.6.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.116",
+ "xxhash-rust",
+]
+
+[[package]]
+name = "server_fn_macro_default"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb8b274f568c94226a8045668554aace8142a59b8bca5414ac5a79627c825568"
+dependencies = [
+ "server_fn_macro",
+ "syn 2.0.116",
+]
+
[[package]]
name = "sha1"
version = "0.10.6"
@@ -2536,7 +3463,7 @@ checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d"
dependencies = [
"num-bigint",
"num-traits",
- "thiserror",
+ "thiserror 2.0.18",
"time",
]
@@ -2546,6 +3473,15 @@ version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+[[package]]
+name = "slotmap"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
+dependencies = [
+ "version_check",
+]
+
[[package]]
name = "smallvec"
version = "1.15.1"
@@ -2628,7 +3564,7 @@ dependencies = [
"serde_json",
"sha2",
"smallvec",
- "thiserror",
+ "thiserror 2.0.18",
"time",
"tokio",
"tokio-stream",
@@ -2716,7 +3652,7 @@ dependencies = [
"smallvec",
"sqlx-core",
"stringprep",
- "thiserror",
+ "thiserror 2.0.18",
"time",
"tracing",
"uuid",
@@ -2759,7 +3695,7 @@ dependencies = [
"smallvec",
"sqlx-core",
"stringprep",
- "thiserror",
+ "thiserror 2.0.18",
"time",
"tracing",
"uuid",
@@ -2786,7 +3722,7 @@ dependencies = [
"serde",
"serde_urlencoded",
"sqlx-core",
- "thiserror",
+ "thiserror 2.0.18",
"time",
"tracing",
"url",
@@ -2856,6 +3792,18 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "syn_derive"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb066a04799e45f5d582e8fc6ec8e6d6896040d00898eb4e6a835196815b219"
+dependencies = [
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.116",
+]
+
[[package]]
name = "synstructure"
version = "0.13.2"
@@ -2867,19 +3815,73 @@ dependencies = [
"syn 2.0.116",
]
+[[package]]
+name = "tachys"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f66c3b70c32844a6f1e2943c72a33ebb777ad6acbeb20d1329d62e3a7806d6ec"
+dependencies = [
+ "any_spawner",
+ "async-trait",
+ "const_str_slice_concat",
+ "drain_filter_polyfill",
+ "dyn-clone",
+ "either_of",
+ "futures",
+ "html-escape",
+ "indexmap",
+ "itertools",
+ "js-sys",
+ "linear-map",
+ "next_tuple",
+ "oco_ref",
+ "once_cell",
+ "or_poisoned",
+ "parking_lot",
+ "paste",
+ "reactive_graph",
+ "reactive_stores",
+ "rustc-hash",
+ "send_wrapper",
+ "slotmap",
+ "throw_error",
+ "wasm-bindgen",
+ "web-sys",
+]
+
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
- "thiserror-impl",
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.116",
]
[[package]]
@@ -2902,6 +3904,15 @@ dependencies = [
"cfg-if",
]
+[[package]]
+name = "throw_error"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4ef8bf264c6ae02a065a4a16553283f0656bd6266fc1fcb09fd2e6b5e91427b"
+dependencies = [
+ "pin-project-lite",
+]
+
[[package]]
name = "time"
version = "0.3.47"
@@ -3010,6 +4021,19 @@ dependencies = [
"tokio",
]
+[[package]]
+name = "toml"
+version = "0.9.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
+dependencies = [
+ "serde_core",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
[[package]]
name = "toml_datetime"
version = "0.7.5+spec-1.1.0"
@@ -3127,12 +4151,38 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+[[package]]
+name = "typed-builder"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd9d30e3a08026c78f246b173243cf07b3696d274debd26680773b6773c2afc7"
+dependencies = [
+ "typed-builder-macro",
+]
+
+[[package]]
+name = "typed-builder-macro"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c36781cc0e46a83726d9879608e4cf6c2505237e263a8eb8c24502989cfdb28"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.116",
+]
+
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+[[package]]
+name = "unicase"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
+
[[package]]
name = "unicode-bidi"
version = "0.3.18"
@@ -3160,6 +4210,12 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
[[package]]
name = "unicode-xid"
version = "0.2.6"
@@ -3184,6 +4240,12 @@ dependencies = [
"serde",
]
+[[package]]
+name = "utf8-width"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
+
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -3256,6 +4318,16 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
[[package]]
name = "want"
version = "0.3.1"
@@ -3308,6 +4380,20 @@ dependencies = [
"wasm-bindgen-shared",
]
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.108"
@@ -3362,6 +4448,19 @@ dependencies = [
"wasmparser",
]
+[[package]]
+name = "wasm-streams"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
[[package]]
name = "wasmparser"
version = "0.244.0"
@@ -3374,6 +4473,16 @@ dependencies = [
"semver",
]
+[[package]]
+name = "web-sys"
+version = "0.3.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
[[package]]
name = "webpki-roots"
version = "0.26.11"
@@ -3402,6 +4511,15 @@ dependencies = [
"wasite",
]
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "windows-core"
version = "0.62.2"
@@ -3795,6 +4913,12 @@ dependencies = [
"tap",
]
+[[package]]
+name = "xxhash-rust"
+version = "0.8.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
+
[[package]]
name = "yansi"
version = "1.0.1"
@@ -3909,3 +5033,31 @@ name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[package]]
+name = "zstd"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.16+zstd.1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
diff --git a/Cargo.toml b/Cargo.toml
index ab3d57c..61851bf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,3 +1,6 @@
+[workspace]
+members = [".", "crates/dashboard"]
+
[package]
name = "reeverb"
version = "0.1.0"
@@ -19,6 +22,8 @@ uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
dotenvy = "0.15"
bcrypt = "0.16"
+rust-embed = { version = "8", features = ["compression"] }
+mime_guess = "2"
[profile.release]
lto = true
diff --git a/crates/dashboard/Cargo.toml b/crates/dashboard/Cargo.toml
new file mode 100644
index 0000000..10024aa
--- /dev/null
+++ b/crates/dashboard/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "dashboard"
+version = "0.1.0"
+edition = "2024"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+leptos = { version = "0.7", features = ["csr"] }
+leptos_router = "0.7"
+gloo-net = { version = "0.6", features = ["http"] }
+gloo-storage = "0.3"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+wasm-bindgen = "0.2"
+web-sys = { version = "0.3", features = ["Window", "Location"] }
diff --git a/crates/dashboard/Trunk.toml b/crates/dashboard/Trunk.toml
new file mode 100644
index 0000000..481eb70
--- /dev/null
+++ b/crates/dashboard/Trunk.toml
@@ -0,0 +1,9 @@
+[build]
+target = "index.html"
+dist = "../../dist"
+
+[serve]
+port = 8080
+
+[[proxy]]
+backend = "http://127.0.0.1:3000/api"
diff --git a/crates/dashboard/index.html b/crates/dashboard/index.html
new file mode 100644
index 0000000..177f19b
--- /dev/null
+++ b/crates/dashboard/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Reeverb
+
+
+
+
+
diff --git a/crates/dashboard/src/api.rs b/crates/dashboard/src/api.rs
new file mode 100644
index 0000000..24c5adf
--- /dev/null
+++ b/crates/dashboard/src/api.rs
@@ -0,0 +1,65 @@
+use gloo_net::http::{Request, RequestBuilder};
+use gloo_storage::Storage;
+use serde::de::DeserializeOwned;
+
+fn get_token() -> Option {
+ gloo_storage::LocalStorage::raw()
+ .get_item("token")
+ .ok()
+ .flatten()
+}
+
+fn clear_token_and_redirect() {
+ let _ = gloo_storage::LocalStorage::raw().remove_item("token");
+ if let Some(window) = web_sys::window() {
+ let _ = window.location().set_href("/login");
+ }
+}
+
+fn with_auth(builder: RequestBuilder) -> RequestBuilder {
+ match get_token() {
+ Some(token) => builder.header("Authorization", &format!("Bearer {}", token)),
+ None => builder,
+ }
+}
+
+pub async fn get(path: &str) -> Result {
+ let resp = with_auth(Request::get(path))
+ .send()
+ .await
+ .map_err(|e| e.to_string())?;
+
+ if resp.status() == 401 {
+ clear_token_and_redirect();
+ return Err("Unauthorized".into());
+ }
+
+ if !resp.ok() {
+ return Err(format!("Request failed: {}", resp.status()));
+ }
+
+ resp.json::().await.map_err(|e| e.to_string())
+}
+
+pub async fn post(
+ path: &str,
+ body: &B,
+) -> Result {
+ let resp = with_auth(Request::post(path))
+ .json(body)
+ .map_err(|e| e.to_string())?
+ .send()
+ .await
+ .map_err(|e| e.to_string())?;
+
+ if resp.status() == 401 {
+ clear_token_and_redirect();
+ return Err("Unauthorized".into());
+ }
+
+ if !resp.ok() {
+ return Err(format!("Request failed: {}", resp.status()));
+ }
+
+ resp.json::().await.map_err(|e| e.to_string())
+}
diff --git a/crates/dashboard/src/app.rs b/crates/dashboard/src/app.rs
new file mode 100644
index 0000000..59037db
--- /dev/null
+++ b/crates/dashboard/src/app.rs
@@ -0,0 +1,19 @@
+use leptos::prelude::*;
+use leptos_router::components::{Route, Router, Routes};
+use leptos_router::path;
+
+use crate::pages::{DashboardPage, HomePage, LoginPage, NotFoundPage, SignupPage};
+
+#[component]
+pub fn App() -> impl IntoView {
+ view! {
+
+
+
+
+
+
+
+
+ }
+}
diff --git a/crates/dashboard/src/lib.rs b/crates/dashboard/src/lib.rs
new file mode 100644
index 0000000..836d804
--- /dev/null
+++ b/crates/dashboard/src/lib.rs
@@ -0,0 +1,10 @@
+mod api;
+mod app;
+mod pages;
+
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen(start)]
+pub fn main() {
+ leptos::mount::mount_to_body(app::App);
+}
diff --git a/crates/dashboard/src/pages/dashboard.rs b/crates/dashboard/src/pages/dashboard.rs
new file mode 100644
index 0000000..c8fab13
--- /dev/null
+++ b/crates/dashboard/src/pages/dashboard.rs
@@ -0,0 +1,177 @@
+use gloo_storage::Storage;
+use leptos::prelude::*;
+use serde::{Deserialize, Serialize};
+
+use crate::api;
+
+#[derive(Clone, Serialize, Deserialize)]
+struct Project {
+ pid: String,
+ name: String,
+ slug: String,
+}
+
+fn logout() {
+ let _ = gloo_storage::LocalStorage::raw().remove_item("token");
+ if let Some(window) = web_sys::window() {
+ let _ = window.location().set_href("/login");
+ }
+}
+
+#[component]
+pub fn DashboardPage() -> impl IntoView {
+ let token_exists = gloo_storage::LocalStorage::raw()
+ .get_item("token")
+ .ok()
+ .flatten()
+ .is_some();
+
+ if !token_exists {
+ if let Some(window) = web_sys::window() {
+ let _ = window.location().set_href("/login");
+ }
+ return view! { }.into_any();
+ }
+
+ let (projects, set_projects) = signal(None::, String>>);
+ let (loading, set_loading) = signal(true);
+
+ let fetch_projects = move || {
+ set_loading.set(true);
+ leptos::task::spawn_local(async move {
+ let result = api::get::>("/api/v1/projects").await;
+ set_projects.set(Some(result));
+ set_loading.set(false);
+ });
+ };
+
+ fetch_projects();
+
+ let (show_form, set_show_form) = signal(false);
+ let (new_name, set_new_name) = signal(String::new());
+ let (new_slug, set_new_slug) = signal(String::new());
+ let (form_error, set_form_error) = signal(Option::::None);
+
+ let on_create = move |ev: web_sys::SubmitEvent| {
+ ev.prevent_default();
+ set_form_error.set(None);
+
+ let name = new_name.get_untracked();
+ let slug = new_slug.get_untracked();
+
+ leptos::task::spawn_local(async move {
+ #[derive(serde::Serialize)]
+ struct CreateProject {
+ name: String,
+ slug: String,
+ }
+
+ match api::post::("/api/v1/projects", &CreateProject { name, slug }).await {
+ Ok(_) => {
+ set_show_form.set(false);
+ set_new_name.set(String::new());
+ set_new_slug.set(String::new());
+ // Refetch projects
+ let result = api::get::>("/api/v1/projects").await;
+ set_projects.set(Some(result));
+ }
+ Err(e) => set_form_error.set(Some(e)),
+ }
+ });
+ };
+
+ view! {
+
+
+
+
+
+
"Projects"
+
+
+
+ {move || show_form.get().then(|| view! {
+
+ {move || form_error.get().map(|e| view! {
+
{e}
+ })}
+
+
+ })}
+
+ {move || {
+ if loading.get() && projects.get().is_none() {
+ return view! {
+ "Loading projects..."
+ }.into_any();
+ }
+
+ match projects.get() {
+ Some(Ok(list)) if list.is_empty() => view! {
+
+
"No projects yet. Create your first one."
+
+ }.into_any(),
+ Some(Ok(list)) => view! {
+
+ {list.into_iter().map(|project| view! {
+
+
{project.name}
+
+ {project.slug}
+
+
+ }).collect::
>()}
+
+ }.into_any(),
+ Some(Err(e)) => view! {
+ {e}
+ }.into_any(),
+ None => view! {
+ "Loading projects..."
+ }.into_any(),
+ }
+ }}
+
+
+ }
+ .into_any()
+}
diff --git a/crates/dashboard/src/pages/home.rs b/crates/dashboard/src/pages/home.rs
new file mode 100644
index 0000000..09ac9f1
--- /dev/null
+++ b/crates/dashboard/src/pages/home.rs
@@ -0,0 +1,37 @@
+use leptos::prelude::*;
+
+#[component]
+pub fn HomePage() -> impl IntoView {
+ view! {
+
+
+
+
+
+ "Social proof that "
+ "resonates"
+
+
+ "Collect, manage, and display testimonials. One platform for all your social proof."
+
+
+
+
+
+
+ }
+}
diff --git a/crates/dashboard/src/pages/login.rs b/crates/dashboard/src/pages/login.rs
new file mode 100644
index 0000000..a4fddef
--- /dev/null
+++ b/crates/dashboard/src/pages/login.rs
@@ -0,0 +1,110 @@
+use gloo_storage::Storage;
+use leptos::prelude::*;
+use serde::{Deserialize, Serialize};
+
+use crate::api;
+
+#[derive(Serialize)]
+struct LoginRequest {
+ email: String,
+ password: String,
+}
+
+#[derive(Deserialize)]
+struct LoginResponse {
+ token: String,
+}
+
+#[component]
+pub fn LoginPage() -> impl IntoView {
+ let (email, set_email) = signal(String::new());
+ let (password, set_password) = signal(String::new());
+ let (error, set_error) = signal(Option::::None);
+ let (loading, set_loading) = signal(false);
+
+ let on_submit = move |ev: web_sys::SubmitEvent| {
+ ev.prevent_default();
+ set_error.set(None);
+ set_loading.set(true);
+
+ let email_val = email.get_untracked();
+ let password_val = password.get_untracked();
+
+ leptos::task::spawn_local(async move {
+ let body = LoginRequest {
+ email: email_val,
+ password: password_val,
+ };
+
+ match api::post::("/api/v1/auth/login", &body).await {
+ Ok(resp) => {
+ let _ = gloo_storage::LocalStorage::raw().set_item("token", &resp.token);
+ if let Some(window) = web_sys::window() {
+ let _ = window.location().set_href("/dashboard");
+ }
+ }
+ Err(e) => {
+ set_error.set(Some(e));
+ set_loading.set(false);
+ }
+ }
+ });
+ };
+
+ view! {
+
+
+
+
"Reeverb"
+
"Sign in to your account"
+
+
+ {move || error.get().map(|e| view! {
+
{e}
+ })}
+
+
+
+
+ "Don't have an account? "
+ "Sign up"
+
+
+
+ }
+}
diff --git a/crates/dashboard/src/pages/mod.rs b/crates/dashboard/src/pages/mod.rs
new file mode 100644
index 0000000..dbde449
--- /dev/null
+++ b/crates/dashboard/src/pages/mod.rs
@@ -0,0 +1,24 @@
+mod dashboard;
+mod home;
+mod login;
+mod signup;
+
+pub use dashboard::DashboardPage;
+pub use home::HomePage;
+pub use login::LoginPage;
+pub use signup::SignupPage;
+
+use leptos::prelude::*;
+
+#[component]
+pub fn NotFoundPage() -> impl IntoView {
+ view! {
+
+ }
+}
diff --git a/crates/dashboard/src/pages/signup.rs b/crates/dashboard/src/pages/signup.rs
new file mode 100644
index 0000000..7e79b9b
--- /dev/null
+++ b/crates/dashboard/src/pages/signup.rs
@@ -0,0 +1,130 @@
+use gloo_storage::Storage;
+use leptos::prelude::*;
+use serde::{Deserialize, Serialize};
+
+use crate::api;
+
+#[derive(Serialize)]
+struct RegisterRequest {
+ email: String,
+ password: String,
+ name: Option,
+}
+
+#[derive(Deserialize)]
+struct AuthResponse {
+ token: String,
+}
+
+#[component]
+pub fn SignupPage() -> impl IntoView {
+ let (name, set_name) = signal(String::new());
+ let (email, set_email) = signal(String::new());
+ let (password, set_password) = signal(String::new());
+ let (error, set_error) = signal(Option::::None);
+ let (loading, set_loading) = signal(false);
+
+ let on_submit = move |ev: web_sys::SubmitEvent| {
+ ev.prevent_default();
+ set_error.set(None);
+ set_loading.set(true);
+
+ let name_val = name.get_untracked();
+ let email_val = email.get_untracked();
+ let password_val = password.get_untracked();
+
+ leptos::task::spawn_local(async move {
+ let body = RegisterRequest {
+ email: email_val,
+ password: password_val,
+ name: if name_val.is_empty() {
+ None
+ } else {
+ Some(name_val)
+ },
+ };
+
+ match api::post::("/api/v1/auth/register", &body).await {
+ Ok(resp) => {
+ let _ = gloo_storage::LocalStorage::raw().set_item("token", &resp.token);
+ if let Some(window) = web_sys::window() {
+ let _ = window.location().set_href("/dashboard");
+ }
+ }
+ Err(e) => {
+ set_error.set(Some(e));
+ set_loading.set(false);
+ }
+ }
+ });
+ };
+
+ view! {
+
+
+
+
"Reeverb"
+
"Create your account"
+
+
+ {move || error.get().map(|e| view! {
+
{e}
+ })}
+
+
+
+
+ "Already have an account? "
+ "Sign in"
+
+
+
+ }
+}
diff --git a/crates/dashboard/style.css b/crates/dashboard/style.css
new file mode 100644
index 0000000..28dfd6c
--- /dev/null
+++ b/crates/dashboard/style.css
@@ -0,0 +1,115 @@
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+:root {
+ --color-bg: #fafafa;
+ --color-surface: #ffffff;
+ --color-text: #1a1a1a;
+ --color-text-secondary: #6b7280;
+ --color-primary: #ef4444;
+ --color-primary-hover: #dc2626;
+ --color-border: #e5e7eb;
+ --color-error: #dc2626;
+ --color-error-bg: #fef2f2;
+ --radius: 8px;
+ --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ --font-mono: "JetBrains Mono", "Fira Code", monospace;
+}
+
+html {
+ font-family: var(--font-sans);
+ font-size: 16px;
+ line-height: 1.6;
+ color: var(--color-text);
+ background-color: var(--color-bg);
+ -webkit-font-smoothing: antialiased;
+}
+
+body {
+ min-height: 100vh;
+}
+
+a {
+ color: var(--color-primary);
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+input {
+ font-family: inherit;
+ font-size: inherit;
+}
+
+button {
+ font-family: inherit;
+ font-size: inherit;
+ cursor: pointer;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 24px;
+}
+
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 10px 20px;
+ border-radius: var(--radius);
+ font-weight: 500;
+ border: none;
+ transition: background-color 0.15s ease;
+}
+
+.btn-primary {
+ background-color: var(--color-primary);
+ color: white;
+}
+
+.btn-primary:hover {
+ background-color: var(--color-primary-hover);
+}
+
+.btn-primary:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.input {
+ width: 100%;
+ padding: 10px 14px;
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius);
+ outline: none;
+ transition: border-color 0.15s ease;
+ background: var(--color-surface);
+}
+
+.input:focus {
+ border-color: var(--color-primary);
+}
+
+.card {
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius);
+ padding: 24px;
+}
+
+.error-message {
+ background: var(--color-error-bg);
+ color: var(--color-error);
+ padding: 12px 16px;
+ border-radius: var(--radius);
+ font-size: 0.875rem;
+}
diff --git a/src/lib.rs b/src/lib.rs
index 0377820..679f415 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,2 +1,3 @@
pub mod api;
pub mod db;
+pub mod static_files;
diff --git a/src/main.rs b/src/main.rs
index b376a5a..a9b2adf 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7,6 +7,7 @@ use reeverb::api::v1::auth;
use reeverb::api::v1::projects;
use reeverb::api::v1::tags;
use reeverb::api::v1::testimonials;
+use reeverb::static_files::DashboardMiddleware;
#[derive(Clone, Config)]
struct AppConfig {
@@ -66,6 +67,7 @@ async fn main() -> std::io::Result<()> {
let addr = format!("{}:{}", config.host, config.port);
app.openapi("Reeverb API", env!("CARGO_PKG_VERSION"))
+ .middleware(DashboardMiddleware)
.middleware(RequestLogMiddleware::new())
.state(auth_config)
.with_database(DatabaseConfig::new(&config.database_url))
diff --git a/src/static_files.rs b/src/static_files.rs
new file mode 100644
index 0000000..eabe401
--- /dev/null
+++ b/src/static_files.rs
@@ -0,0 +1,86 @@
+use rapina::context::RequestContext;
+use rapina::http::{Response, StatusCode};
+use rapina::hyper::Request;
+use rapina::hyper::body::Incoming;
+use rapina::middleware::{BoxFuture, Middleware, Next};
+use rapina::response::BoxBody;
+
+use rust_embed::Embed;
+
+#[derive(Embed)]
+#[folder = "dist/"]
+struct DashboardAssets;
+
+fn mime_for(path: &str) -> &'static str {
+ match path.rsplit('.').next() {
+ Some("html") => "text/html; charset=utf-8",
+ Some("js") => "application/javascript",
+ Some("wasm") => "application/wasm",
+ Some("css") => "text/css; charset=utf-8",
+ Some("json") => "application/json",
+ Some("png") => "image/png",
+ Some("svg") => "image/svg+xml",
+ Some("ico") => "image/x-icon",
+ _ => "application/octet-stream",
+ }
+}
+
+fn cache_header(path: &str) -> &'static str {
+ if path.ends_with(".wasm") || path.ends_with(".js") || path.ends_with(".css") {
+ "public, max-age=31536000, immutable"
+ } else {
+ "no-cache"
+ }
+}
+
+fn serve_file(path: &str) -> Response {
+ use rapina::response::IntoResponse;
+
+ let path = path.trim_start_matches('/');
+
+ if let Some(file) = DashboardAssets::get(path) {
+ return Response::builder()
+ .status(StatusCode::OK)
+ .header("content-type", mime_for(path))
+ .header("cache-control", cache_header(path))
+ .body(BoxBody::new(file.data.into_owned().into()))
+ .unwrap();
+ }
+
+ // SPA fallback: serve index.html
+ if let Some(index) = DashboardAssets::get("index.html") {
+ return Response::builder()
+ .status(StatusCode::OK)
+ .header("content-type", "text/html; charset=utf-8")
+ .header("cache-control", "no-cache")
+ .body(BoxBody::new(index.data.into_owned().into()))
+ .unwrap();
+ }
+
+ StatusCode::NOT_FOUND.into_response()
+}
+
+fn is_api_path(path: &str) -> bool {
+ path.starts_with("/api/") || path == "/health" || path.starts_with("/__rapina")
+}
+
+pub struct DashboardMiddleware;
+
+impl Middleware for DashboardMiddleware {
+ fn handle<'a>(
+ &'a self,
+ req: Request,
+ _ctx: &'a RequestContext,
+ next: Next<'a>,
+ ) -> BoxFuture<'a, Response> {
+ Box::pin(async move {
+ let path = req.uri().path().to_string();
+
+ if is_api_path(&path) {
+ return next.run(req).await;
+ }
+
+ serve_file(&path)
+ })
+ }
+}