diff --git a/.gitignore b/.gitignore index 355560c..2d46a7a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /target *.swp *.swo +.claude/ +khm-wasm/target \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index a01e4d6..950241d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1002,6 +1002,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.52.6", ] @@ -1121,6 +1122,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -1935,8 +1946,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -2678,11 +2691,13 @@ dependencies = [ "base64 0.21.7", "chrono", "clap", + "console_error_panic_hook", "dirs 5.0.1", "eframe", "egui", "env_logger", "futures", + "getrandom", "glib", "gtk", "hostname", @@ -2698,9 +2713,13 @@ dependencies = [ "tokio", "tokio-postgres", "tokio-util", + "tracing-wasm", "tray-icon", "trust-dns-resolver", "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", "winit", ] @@ -2747,6 +2766,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libappindicator" version = "0.9.0" @@ -4352,6 +4377,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -4631,6 +4665,15 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.36" @@ -4907,6 +4950,28 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + [[package]] name = "tray-icon" version = "0.21.0" diff --git a/Cargo.toml b/Cargo.toml index 6a94124..33b8103 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,9 @@ license = "WTFPL" keywords = ["ssh", "known-hosts", "security", "system-admin", "automation"] categories = ["command-line-utilities", "network-programming"] +[lib] +crate-type = ["cdylib", "rlib"] + [[bin]] name = "khm" path = "src/bin/cli.rs" @@ -20,28 +23,34 @@ path = "src/bin/desktop.rs" required-features = ["gui"] [dependencies] -actix-web = "4" +actix-web = { version = "4", optional = true } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" log = "0.4" -regex = "1.10.5" -base64 = "0.21" -tokio = { version = "1", features = ["full", "sync"] } -tokio-postgres = { version = "0.7", features = ["with-chrono-0_4"] } -tokio-util = { version = "0.7", features = ["codec"] } -clap = { version = "4", features = ["derive"] } -chrono = "0.4.38" -reqwest = { version = "0.12", features = ["json"] } -trust-dns-resolver = "0.23" -futures = "0.3" -hostname = "0.3" -rust-embed = "8.0" +regex = { version = "1.10.5", optional = true } +base64 = { version = "0.21", optional = true } +tokio = { version = "1", features = ["full", "sync"], optional = true } +tokio-postgres = { version = "0.7", features = ["with-chrono-0_4"], optional = true } +tokio-util = { version = "0.7", features = ["codec"], optional = true } +clap = { version = "4", features = ["derive"], optional = true } +chrono = { version = "0.4.38", features = ["serde"], optional = true } +reqwest = { version = "0.12", features = ["json"], optional = true } +trust-dns-resolver = { version = "0.23", optional = true } +futures = { version = "0.3", optional = true } +hostname = { version = "0.3", optional = true } +rust-embed = { version = "8.0", optional = true } tray-icon = { version = "0.21", optional = true } notify = { version = "6.1", optional = true } notify-debouncer-mini = { version = "0.4", optional = true } dirs = "5.0" eframe = { version = "0.29", optional = true } egui = { version = "0.29", optional = true } +wasm-bindgen-futures = { version = "0.4", optional = true } +web-sys = { version = "0.3", optional = true } +wasm-bindgen = { version = "0.2", optional = true } +console_error_panic_hook = { version = "0.1", optional = true } +tracing-wasm = { version = "0.2", optional = true } +getrandom = { version = "0.2", features = ["js"], optional = true } winit = { version = "0.30", optional = true } env_logger = "0.11" urlencoding = "2.1" @@ -53,13 +62,19 @@ glib = { version = "0.18", optional = true } [features] default = ["server", "web", "gui"] -cli = ["server", "web"] +cli = ["server", "web", "web-gui"] desktop = ["gui"] gui = ["tray-icon", "eframe", "egui", "winit", "notify", "notify-debouncer-mini", "gtk", "glib"] -server = [] -web = [] +web-gui = ["egui", "eframe", "wasm-bindgen-futures", "web-sys", "wasm-bindgen", "console_error_panic_hook", "tracing-wasm", "getrandom"] +web-gui-wasm = ["web-gui"] +server = ["actix-web", "tokio", "tokio-postgres", "tokio-util", "clap", "chrono", "regex", "base64", "futures", "hostname", "rust-embed", "trust-dns-resolver", "reqwest"] +web = ["server"] # Target-specific dependencies for cross-compilation [target.aarch64-unknown-linux-gnu.dependencies] openssl = { version = "0.10", features = ["vendored"] } +# WASM-specific dependencies +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2", features = ["js"] } + diff --git a/khm-wasm/Cargo.lock b/khm-wasm/Cargo.lock new file mode 100644 index 0000000..fc1d37f --- /dev/null +++ b/khm-wasm/Cargo.lock @@ -0,0 +1,2715 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e074464580a518d16a7126262fffaaa47af89d4099d4cb403f8ed938ba12ee7d" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2187590a23ab1e3df8681afdf0987c48504d80291f002fcdb651f0ef5e25169" + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.9.1", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "arboard" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55f533f8e0af236ffe5eb979b99381df3258853f00ba2e44b6e1955292c75227" +dependencies = [ + "clipboard-win", + "log", + "objc2 0.6.1", + "objc2-app-kit 0.3.1", + "objc2-foundation 0.3.1", + "parking_lot", + "percent-encoding", + "x11rb", +] + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytemuck" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "441473f2b4b0459a68628c744bc61d23e730fb00128b841d30fa4bb3972257e4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.9.1", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.1", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "ecolor" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775cfde491852059e386c4e1deb4aef381c617dc364184c6f6afee99b87c402b" +dependencies = [ + "bytemuck", + "emath", +] + +[[package]] +name = "eframe" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ac2645a9bf4826eb4e91488b1f17b8eaddeef09396706b2f14066461338e24f" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "egui-winit", + "egui_glow", + "glow", + "glutin", + "glutin-winit", + "image", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "winapi", + "windows-sys 0.52.0", + "winit", +] + +[[package]] +name = "egui" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53eafabcce0cb2325a59a98736efe0bf060585b437763f8c476957fb274bb974" +dependencies = [ + "ahash", + "emath", + "epaint", + "log", + "nohash-hasher", +] + +[[package]] +name = "egui-winit" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9c430f4f816340e8e8c1b20eec274186b1be6bc4c7dfc467ed50d57abc36c6" +dependencies = [ + "ahash", + "arboard", + "egui", + "log", + "raw-window-handle", + "smithay-clipboard", + "web-time", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_glow" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e39bccc683cd43adab530d8f21a13eb91e80de10bcc38c3f1c16601b6f62b26" +dependencies = [ + "ahash", + "bytemuck", + "egui", + "glow", + "log", + "memoffset", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "emath" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1fe0049ce51d0fb414d029e668dd72eb30bc2b739bf34296ed97bd33df544f3" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "epaint" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a32af8da821bd4f43f2c137e295459ee2e1661d87ca8779dfa0eaf45d870e20f" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "log", + "nohash-hasher", + "parking_lot", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483440db0b7993cf77a20314f08311dbe95675092405518c0677aa08c151a3ea" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glow" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51fa363f025f5c111e03f13eda21162faeacb6911fe8caa0c0349f9cf0c4483" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +dependencies = [ + "bitflags 2.9.1", + "cfg_aliases", + "cgl", + "dispatch2", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading", + "objc2 0.6.1", + "objc2-app-kit 0.3.1", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +dependencies = [ + "cfg_aliases", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "num-traits", + "png", +] + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "khm-wasm" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "eframe", + "egui", + "getrandom 0.2.16", + "log", + "serde", + "serde-wasm-bindgen", + "serde_json", + "tracing-wasm", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.2", +] + +[[package]] +name = "libredox" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" +dependencies = [ + "bitflags 2.9.1", + "libc", + "redox_syscall 0.5.15", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memmap2" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.9.1", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.9.1", + "block2", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.1", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.9.1", + "block2", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.9.1", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags 2.9.1", + "dispatch2", + "objc2 0.6.1", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" +dependencies = [ + "bitflags 2.9.1", + "dispatch2", + "objc2 0.6.1", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.9.1", + "block2", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.1", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.1", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.9.1", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.9.1", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.9.1", + "block2", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.9.1", + "block2", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "orbclient" +version = "0.3.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" +dependencies = [ + "libredox", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.15", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[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", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.0.8", + "windows-sys 0.60.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[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 = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.141" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.9.1", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" +dependencies = [ + "libc", + "smithay-client-toolkit", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[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", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121" +dependencies = [ + "cc", + "downcast-rs", + "rustix 0.38.44", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61" +dependencies = [ + "bitflags 2.9.1", + "rustix 0.38.44", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.9.1", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65317158dec28d00416cb16705934070aef4f8393353d41126c54264ae0f182" +dependencies = [ + "rustix 0.38.44", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd38cdad69b56ace413c6bcc1fbf5acc5e2ef4af9d5f8f1f9570c0c83eae175" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf4f3c0ba838e82b4e5ccc4157003fb8c324ee24c058470ffb82820becbde98" +dependencies = [ + "core-foundation 0.10.1", + "jni", + "log", + "ndk-context", + "objc2 0.6.1", + "objc2-foundation 0.3.1", + "url", + "web-sys", +] + +[[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-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[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-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winit" +version = "0.30.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.9.1", + "block2", + "bytemuck", + "calloop", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "smithay-client-toolkit", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 0.38.44", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.9.1", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/khm-wasm/Cargo.toml b/khm-wasm/Cargo.toml new file mode 100644 index 0000000..f7b4fce --- /dev/null +++ b/khm-wasm/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "khm-wasm" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +log = "0.4" +eframe = { version = "0.29", default-features = false, features = ["glow"] } +egui = "0.29" +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +web-sys = { version = "0.3", features = ["console", "Headers", "Request", "RequestInit", "RequestMode", "Response", "Window"] } +console_error_panic_hook = "0.1" +tracing-wasm = "0.2" +getrandom = { version = "0.2", features = ["js"] } +serde-wasm-bindgen = "0.6" + +[features] +default = [] \ No newline at end of file diff --git a/khm-wasm/src/lib.rs b/khm-wasm/src/lib.rs new file mode 100644 index 0000000..f01c0f7 --- /dev/null +++ b/khm-wasm/src/lib.rs @@ -0,0 +1,1482 @@ +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::JsFuture; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, BTreeMap, HashSet}; +use std::future::Future; +use web_sys::{window, Request, RequestInit, RequestMode, Response}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SshKey { + pub server: String, + pub public_key: String, + #[serde(default)] + pub deprecated: bool, +} + +#[derive(Debug, Clone)] +pub struct AdminSettings { + pub selected_flow: String, +} + +impl Default for AdminSettings { + fn default() -> Self { + Self { + selected_flow: String::new(), + } + } +} + +#[derive(Debug, Clone)] +pub struct AdminState { + pub keys: Vec, + pub filtered_keys: Vec, + pub search_term: String, + pub show_deprecated_only: bool, + pub show_active_only: bool, + pub selected_servers: HashMap, + pub expanded_servers: HashMap, + pub current_operation: String, +} + +#[derive(Debug, Clone)] +pub struct AdminStatistics { + pub total_keys: usize, + pub active_keys: usize, + pub deprecated_keys: usize, + pub unique_servers: usize, +} + +#[derive(Debug, Clone)] +pub enum KeyAction { + None, + DeprecateKey(String), + RestoreKey(String), + DeleteKey(String), + DeprecateServer(String), + RestoreServer(String), +} + +#[derive(Debug, Clone)] +pub enum BulkAction { + None, + DeprecateSelected, + RestoreSelected, + ClearSelection, +} + +impl Default for AdminState { + fn default() -> Self { + Self { + keys: Vec::new(), + filtered_keys: Vec::new(), + search_term: String::new(), + show_deprecated_only: false, + show_active_only: false, + selected_servers: HashMap::new(), + expanded_servers: HashMap::new(), + current_operation: String::new(), + } + } +} + +impl AdminState { + pub fn filter_keys(&mut self) { + let mut filtered = self.keys.clone(); + + // Apply status filter + if self.show_deprecated_only { + filtered.retain(|key| key.deprecated); + } else if self.show_active_only { + filtered.retain(|key| !key.deprecated); + } + // By default, show all keys (both active and deprecated) + + // Apply search filter + if !self.search_term.is_empty() { + let search_term = self.search_term.to_lowercase(); + filtered.retain(|key| { + key.server.to_lowercase().contains(&search_term) + || key.public_key.to_lowercase().contains(&search_term) + }); + } + + self.filtered_keys = filtered; + } + + pub fn get_statistics(&self) -> AdminStatistics { + let total_keys = self.keys.len(); + let active_keys = self.keys.iter().filter(|k| !k.deprecated).count(); + let deprecated_keys = total_keys - active_keys; + let unique_servers = self + .keys + .iter() + .map(|k| &k.server) + .collect::>() + .len(); + + AdminStatistics { + total_keys, + active_keys, + deprecated_keys, + unique_servers, + } + } + + pub fn get_selected_servers(&self) -> Vec { + self.selected_servers + .iter() + .filter_map(|(server, &selected)| { + if selected { Some(server.clone()) } else { None } + }) + .collect() + } + + pub fn clear_selection(&mut self) { + self.selected_servers.clear(); + } +} + +// Utility functions +pub fn get_key_type(public_key: &str) -> String { + if public_key.starts_with("ssh-rsa") { + "RSA".to_string() + } else if public_key.starts_with("ssh-ed25519") { + "ED25519".to_string() + } else if public_key.starts_with("ecdsa-sha2-nistp") { + "ECDSA".to_string() + } else if public_key.starts_with("ssh-dss") { + "DSA".to_string() + } else { + "Unknown".to_string() + } +} + +pub fn get_key_preview(public_key: &str) -> String { + let parts: Vec<&str> = public_key.split_whitespace().collect(); + if parts.len() >= 2 { + let key_part = parts[1]; + if key_part.len() > 12 { + format!("{}...", &key_part[..12]) + } else { + key_part.to_string() + } + } else { + format!("{}...", &public_key[..std::cmp::min(12, public_key.len())]) + } +} + +// API functions for WASM +async fn fetch_api(url: &str) -> Result { + let mut opts = RequestInit::new(); + opts.method("GET"); + opts.mode(RequestMode::Cors); + + let request = Request::new_with_str_and_init(url, &opts)?; + + let window = window().unwrap(); + let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?; + let resp: Response = resp_value.dyn_into()?; + + Ok(resp) +} + +async fn fetch_flows() -> Result, JsValue> { + let resp = fetch_api("/api/flows").await?; + let json = JsFuture::from(resp.json()?).await?; + let flows: Vec = serde_wasm_bindgen::from_value(json)?; + Ok(flows) +} + +async fn fetch_keys(flow: &str) -> Result, JsValue> { + let url = format!("/{}/keys", flow); + let resp = fetch_api(&url).await?; + let json = JsFuture::from(resp.json()?).await?; + let keys: Vec = serde_wasm_bindgen::from_value(json)?; + Ok(keys) +} + +pub struct WebAdminApp { + settings: AdminSettings, + admin_state: AdminState, + status_message: String, + available_flows: Vec, + loading: bool, + flows_promise: Option, + keys_promise: Option, + json_promise: Option, + operation_promise: Option, + pending_operation: String, + flows_loaded: bool, + auto_load_keys: bool, +} + +impl Default for WebAdminApp { + fn default() -> Self { + Self { + settings: AdminSettings::default(), + admin_state: AdminState::default(), + status_message: "Loading flows...".to_string(), + available_flows: Vec::new(), + loading: true, + flows_promise: None, + keys_promise: None, + json_promise: None, + operation_promise: None, + pending_operation: String::new(), + flows_loaded: false, + auto_load_keys: false, + } + } +} + +impl eframe::App for WebAdminApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + // Check if we're on mobile/small screen + let screen_width = ctx.screen_rect().width(); + let is_mobile = screen_width < 600.0; + + // Set mobile-friendly spacing + let base_spacing = if is_mobile { 8.0 } else { 10.0 }; + let button_height = if is_mobile { 44.0 } else { 32.0 }; // Touch-friendly size + // Auto-load flows on startup + if !self.flows_loaded && self.flows_promise.is_none() { + self.load_flows(); + } + + // Auto-load keys when flow changes + if self.auto_load_keys && !self.settings.selected_flow.is_empty() && self.keys_promise.is_none() && self.json_promise.is_none() { + self.load_keys(); + self.auto_load_keys = false; + } + + // Check for completed promises + if let Some(mut promise) = self.flows_promise.take() { + use std::task::{Context, Poll, Waker}; + use std::pin::Pin; + + struct DummyWaker; + impl std::task::Wake for DummyWaker { + fn wake(self: std::sync::Arc) {} + } + let waker = Waker::from(std::sync::Arc::new(DummyWaker)); + let mut cx = Context::from_waker(&waker); + + match Pin::new(&mut promise).poll(&mut cx) { + Poll::Ready(Ok(response_js)) => { + if let Ok(response) = response_js.dyn_into::() { + if response.ok() { + let json_promise = response.json().unwrap(); + self.json_promise = Some(wasm_bindgen_futures::JsFuture::from(json_promise)); + self.pending_operation = "flows".to_string(); + self.status_message = "Parsing flows response...".to_string(); + } else { + self.loading = false; + self.status_message = "Failed to load flows".to_string(); + } + } + } + Poll::Ready(Err(_)) => { + self.loading = false; + self.status_message = "Error loading flows".to_string(); + } + Poll::Pending => { + self.flows_promise = Some(promise); + ctx.request_repaint(); + } + } + } + + if let Some(mut promise) = self.keys_promise.take() { + use std::task::{Context, Poll, Waker}; + use std::pin::Pin; + + struct DummyWaker; + impl std::task::Wake for DummyWaker { + fn wake(self: std::sync::Arc) {} + } + let waker = Waker::from(std::sync::Arc::new(DummyWaker)); + let mut cx = Context::from_waker(&waker); + + match Pin::new(&mut promise).poll(&mut cx) { + Poll::Ready(Ok(response_js)) => { + if let Ok(response) = response_js.dyn_into::() { + if response.ok() { + let json_promise = response.json().unwrap(); + self.json_promise = Some(wasm_bindgen_futures::JsFuture::from(json_promise)); + self.pending_operation = "keys".to_string(); + self.status_message = "Parsing keys response...".to_string(); + } else { + self.loading = false; + self.status_message = "Failed to load keys".to_string(); + } + } + } + Poll::Ready(Err(_)) => { + self.loading = false; + self.status_message = "Error loading keys".to_string(); + } + Poll::Pending => { + self.keys_promise = Some(promise); + ctx.request_repaint(); + } + } + } + + // Check for completed operations + if let Some(mut promise) = self.operation_promise.take() { + use std::task::{Context, Poll, Waker}; + use std::pin::Pin; + + struct DummyWaker; + impl std::task::Wake for DummyWaker { + fn wake(self: std::sync::Arc) {} + } + let waker = Waker::from(std::sync::Arc::new(DummyWaker)); + let mut cx = Context::from_waker(&waker); + + match Pin::new(&mut promise).poll(&mut cx) { + Poll::Ready(Ok(response_js)) => { + self.loading = false; + if let Ok(response) = response_js.dyn_into::() { + if response.ok() { + let parts: Vec<&str> = self.pending_operation.split(':').collect(); + if parts.len() == 2 { + let operation = parts[0]; + let param = parts[1]; + match operation { + "deprecate" => { + self.status_message = format!("Key deprecated for {}", param); + self.load_keys(); // Reload to show changes + } + "restore" => { + self.status_message = format!("Key restored for {}", param); + self.load_keys(); // Reload to show changes + } + "delete" => { + self.status_message = format!("Key deleted for {}", param); + self.load_keys(); // Reload to show changes + } + "bulk-deprecate" => { + self.status_message = format!("Deprecated {} servers", param); + self.admin_state.clear_selection(); // Clear selection after bulk operation + self.load_keys(); // Reload to show changes + } + "bulk-restore" => { + self.status_message = format!("Restored {} servers", param); + self.admin_state.clear_selection(); // Clear selection after bulk operation + self.load_keys(); // Reload to show changes + } + _ => { + self.status_message = "Operation completed".to_string(); + } + } + } + } else { + self.status_message = "Operation failed".to_string(); + } + } + self.pending_operation.clear(); + } + Poll::Ready(Err(_)) => { + self.loading = false; + self.status_message = "Operation error".to_string(); + self.pending_operation.clear(); + } + Poll::Pending => { + self.operation_promise = Some(promise); + ctx.request_repaint(); + } + } + } + + // Check for completed JSON parsing + if let Some(mut promise) = self.json_promise.take() { + use std::task::{Context, Poll, Waker}; + use std::pin::Pin; + + struct DummyWaker; + impl std::task::Wake for DummyWaker { + fn wake(self: std::sync::Arc) {} + } + let waker = Waker::from(std::sync::Arc::new(DummyWaker)); + let mut cx = Context::from_waker(&waker); + + match Pin::new(&mut promise).poll(&mut cx) { + Poll::Ready(Ok(json_data)) => { + self.loading = false; + + match self.pending_operation.as_str() { + "flows" => { + if let Ok(flows) = serde_wasm_bindgen::from_value::>(json_data) { + self.available_flows = flows.clone(); + self.flows_loaded = true; + + // Auto-select first flow + if !flows.is_empty() && self.settings.selected_flow.is_empty() { + self.settings.selected_flow = flows[0].clone(); + self.auto_load_keys = true; + } + + self.status_message = format!("Loaded {} flows", flows.len()); + } else { + self.status_message = "Failed to parse flows data".to_string(); + } + } + "keys" => { + if let Ok(keys) = serde_wasm_bindgen::from_value::>(json_data) { + self.admin_state.keys = keys.clone(); + self.admin_state.filter_keys(); + self.status_message = format!("Loaded {} keys", keys.len()); + } else { + self.status_message = "Failed to parse keys data".to_string(); + } + } + _ => { + self.status_message = "Unknown operation completed".to_string(); + } + } + + self.pending_operation.clear(); + } + Poll::Ready(Err(_)) => { + self.loading = false; + self.status_message = "Error parsing JSON response".to_string(); + self.pending_operation.clear(); + } + Poll::Pending => { + self.json_promise = Some(promise); + ctx.request_repaint(); + } + } + } + + egui::CentralPanel::default().show(ctx, |ui| { + let title_size = if is_mobile { 22.0 } else { 28.0 }; + ui.add_space(base_spacing); + ui.heading(egui::RichText::new("🔑 KHM Web Admin Panel").size(title_size)); + ui.separator(); + ui.add_space(base_spacing * 1.5); + + // Flow Selection + ui.group(|ui| { + ui.set_min_width(ui.available_width()); + ui.vertical(|ui| { + let section_title_size = if is_mobile { 16.0 } else { 18.0 }; + ui.label(egui::RichText::new("📂 Flow Selection").size(section_title_size).strong()); + ui.add_space(base_spacing); + + // Use vertical layout on mobile for better space usage + if is_mobile { + ui.vertical(|ui| { + ui.label(egui::RichText::new("Current Flow:").size(14.0)); + ui.add_space(5.0); + + let mut flow_changed = false; + let old_flow = self.settings.selected_flow.clone(); + + egui::ComboBox::from_id_salt("flow_selector") + .selected_text(if self.settings.selected_flow.is_empty() { "Select flow..." } else { &self.settings.selected_flow }) + .width(ui.available_width() - 20.0) + .show_ui(ui, |ui| { + for flow in &self.available_flows { + if ui.selectable_value(&mut self.settings.selected_flow, flow.clone(), egui::RichText::new(flow).size(14.0)).clicked() { + flow_changed = true; + } + } + }); + + if flow_changed && old_flow != self.settings.selected_flow { + self.auto_load_keys = true; + } + + ui.add_space(base_spacing); + + if ui.add_sized([ui.available_width(), button_height], egui::Button::new(egui::RichText::new("🔄 Refresh").size(14.0))).clicked() && !self.loading { + if !self.settings.selected_flow.is_empty() { + self.load_keys(); + } + } + }); + } else { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Current Flow:").size(16.0)); + ui.add_space(10.0); + + let mut flow_changed = false; + let old_flow = self.settings.selected_flow.clone(); + + egui::ComboBox::from_id_salt("flow_selector") + .selected_text(if self.settings.selected_flow.is_empty() { "Select flow..." } else { &self.settings.selected_flow }) + .width(300.0) + .show_ui(ui, |ui| { + for flow in &self.available_flows { + if ui.selectable_value(&mut self.settings.selected_flow, flow.clone(), egui::RichText::new(flow).size(14.0)).clicked() { + flow_changed = true; + } + } + }); + + if flow_changed && old_flow != self.settings.selected_flow { + self.auto_load_keys = true; + } + + ui.add_space(20.0); + + if ui.add_sized([120.0, button_height], egui::Button::new(egui::RichText::new("🔄 Refresh").size(14.0))).clicked() && !self.loading { + if !self.settings.selected_flow.is_empty() { + self.load_keys(); + } + } + }); + } + }); + }); + + ui.add_space(base_spacing); + + // Statistics + if !self.admin_state.keys.is_empty() { + self.render_statistics(ui, is_mobile); + ui.add_space(base_spacing); + } + + // Search and filters + if !self.admin_state.keys.is_empty() { + self.render_search_controls(ui, is_mobile); + ui.add_space(base_spacing); + } + + // Bulk actions + let bulk_action = self.render_bulk_actions(ui, is_mobile, button_height); + if bulk_action != BulkAction::None { + self.handle_bulk_action(bulk_action); + } + + // Keys display + if !self.admin_state.filtered_keys.is_empty() { + let key_action = self.render_keys_table(ui, is_mobile, button_height); + if key_action != KeyAction::None { + self.handle_key_action(key_action); + } + } else if !self.admin_state.keys.is_empty() { + self.render_empty_state(ui); + } + + ui.add_space(10.0); + + // Status bar + ui.separator(); + ui.horizontal(|ui| { + ui.label("Status:"); + ui.colored_label(egui::Color32::LIGHT_BLUE, &self.status_message); + }); + }); + } +} + +impl WebAdminApp { + fn load_flows(&mut self) { + self.status_message = "Loading flows...".to_string(); + + let window = web_sys::window().unwrap(); + let opts = web_sys::RequestInit::new(); + opts.set_method("GET"); + opts.set_mode(web_sys::RequestMode::Cors); + + if let Ok(request) = web_sys::Request::new_with_str_and_init("/api/flows", &opts) { + let promise = window.fetch_with_request(&request); + self.flows_promise = Some(wasm_bindgen_futures::JsFuture::from(promise)); + self.loading = true; + } + } + + fn load_keys(&mut self) { + if self.settings.selected_flow.is_empty() { + return; + } + + self.status_message = format!("Loading keys for {}...", self.settings.selected_flow); + + // Add include_deprecated=true to show all keys (active and deprecated) + let url = format!("/{}/keys?include_deprecated=true", self.settings.selected_flow); + let window = web_sys::window().unwrap(); + let opts = web_sys::RequestInit::new(); + opts.set_method("GET"); + opts.set_mode(web_sys::RequestMode::Cors); + + if let Ok(request) = web_sys::Request::new_with_str_and_init(&url, &opts) { + let promise = window.fetch_with_request(&request); + self.keys_promise = Some(wasm_bindgen_futures::JsFuture::from(promise)); + self.loading = true; + } + } + + fn deprecate_key(&mut self, server: &str) { + if self.settings.selected_flow.is_empty() { + return; + } + + self.status_message = format!("Deprecating key for {}...", server); + + let url = format!("/{}/keys/{}", self.settings.selected_flow, server); + let window = web_sys::window().unwrap(); + let opts = web_sys::RequestInit::new(); + opts.set_method("DELETE"); // Правильный метод для deprecate + opts.set_mode(web_sys::RequestMode::Cors); + + if let Ok(request) = web_sys::Request::new_with_str_and_init(&url, &opts) { + let promise = window.fetch_with_request(&request); + self.operation_promise = Some(wasm_bindgen_futures::JsFuture::from(promise)); + self.pending_operation = format!("deprecate:{}", server); + self.loading = true; + } + } + + fn restore_key(&mut self, server: &str) { + if self.settings.selected_flow.is_empty() { + return; + } + + self.status_message = format!("Restoring key for {}...", server); + + let url = format!("/{}/keys/{}/restore", self.settings.selected_flow, server); + let window = web_sys::window().unwrap(); + let opts = web_sys::RequestInit::new(); + opts.set_method("POST"); + opts.set_mode(web_sys::RequestMode::Cors); + + if let Ok(request) = web_sys::Request::new_with_str_and_init(&url, &opts) { + let promise = window.fetch_with_request(&request); + self.operation_promise = Some(wasm_bindgen_futures::JsFuture::from(promise)); + self.pending_operation = format!("restore:{}", server); + self.loading = true; + } + } + + fn delete_key(&mut self, server: &str) { + if self.settings.selected_flow.is_empty() { + return; + } + + self.status_message = format!("Deleting key for {}...", server); + + let url = format!("/{}/keys/{}/delete", self.settings.selected_flow, server); + let window = web_sys::window().unwrap(); + let opts = web_sys::RequestInit::new(); + opts.set_method("DELETE"); // Правильный метод для delete + opts.set_mode(web_sys::RequestMode::Cors); + + if let Ok(request) = web_sys::Request::new_with_str_and_init(&url, &opts) { + let promise = window.fetch_with_request(&request); + self.operation_promise = Some(wasm_bindgen_futures::JsFuture::from(promise)); + self.pending_operation = format!("delete:{}", server); + self.loading = true; + } + } + + fn bulk_deprecate_servers(&mut self, servers: Vec) { + if self.settings.selected_flow.is_empty() { + return; + } + + self.status_message = format!("Deprecating {} servers...", servers.len()); + + let url = format!("/{}/bulk-deprecate", self.settings.selected_flow); + let window = web_sys::window().unwrap(); + let opts = web_sys::RequestInit::new(); + opts.set_method("POST"); + opts.set_mode(web_sys::RequestMode::Cors); + + // Create JSON body + let body = serde_json::json!({ + "servers": servers + }); + + if let Ok(body_str) = serde_json::to_string(&body) { + opts.set_body(&wasm_bindgen::JsValue::from_str(&body_str)); + opts.set_headers(&{ + let headers = web_sys::Headers::new().unwrap(); + headers.set("Content-Type", "application/json").unwrap(); + headers.into() + }); + + if let Ok(request) = web_sys::Request::new_with_str_and_init(&url, &opts) { + let promise = window.fetch_with_request(&request); + self.operation_promise = Some(wasm_bindgen_futures::JsFuture::from(promise)); + self.pending_operation = format!("bulk-deprecate:{}", servers.len()); + self.loading = true; + } + } + } + + fn bulk_restore_servers(&mut self, servers: Vec) { + if self.settings.selected_flow.is_empty() { + return; + } + + self.status_message = format!("Restoring {} servers...", servers.len()); + + let url = format!("/{}/bulk-restore", self.settings.selected_flow); + let window = web_sys::window().unwrap(); + let opts = web_sys::RequestInit::new(); + opts.set_method("POST"); + opts.set_mode(web_sys::RequestMode::Cors); + + // Create JSON body + let body = serde_json::json!({ + "servers": servers + }); + + if let Ok(body_str) = serde_json::to_string(&body) { + opts.set_body(&wasm_bindgen::JsValue::from_str(&body_str)); + opts.set_headers(&{ + let headers = web_sys::Headers::new().unwrap(); + headers.set("Content-Type", "application/json").unwrap(); + headers.into() + }); + + if let Ok(request) = web_sys::Request::new_with_str_and_init(&url, &opts) { + let promise = window.fetch_with_request(&request); + self.operation_promise = Some(wasm_bindgen_futures::JsFuture::from(promise)); + self.pending_operation = format!("bulk-restore:{}", servers.len()); + self.loading = true; + } + } + } + + fn render_statistics(&self, ui: &mut egui::Ui, is_mobile: bool) { + let stats = self.admin_state.get_statistics(); + + ui.group(|ui| { + ui.set_min_width(ui.available_width()); + ui.vertical(|ui| { + let title_size = if is_mobile { 16.0 } else { 20.0 }; + ui.label(egui::RichText::new("📊 Statistics").size(title_size).strong()); + ui.add_space(if is_mobile { 10.0 } else { 15.0 }); + + // Use 2x2 grid on mobile for better readability + if is_mobile { + ui.columns(2, |cols| { + // Total keys + cols[0].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("📊").size(24.0)); + ui.label( + egui::RichText::new(stats.total_keys.to_string()) + .size(28.0) + .strong(), + ); + ui.label( + egui::RichText::new("Total Keys") + .size(12.0) + .color(egui::Color32::GRAY), + ); + }); + + // Active keys - using original admin colors + cols[1].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("✅").size(24.0)); + ui.label( + egui::RichText::new(stats.active_keys.to_string()) + .size(28.0) + .strong() + .color(egui::Color32::from_rgb(46, 204, 113)), + ); + ui.label( + egui::RichText::new("Active") + .size(12.0) + .color(egui::Color32::GRAY), + ); + }); + }); + + ui.add_space(10.0); + + ui.columns(2, |cols| { + // Deprecated keys - using original admin colors + cols[0].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("❌").size(24.0)); + ui.label( + egui::RichText::new(stats.deprecated_keys.to_string()) + .size(28.0) + .strong() + .color(egui::Color32::from_rgb(231, 76, 60)), + ); + ui.label( + egui::RichText::new("Deprecated") + .size(12.0) + .color(egui::Color32::GRAY), + ); + }); + + // Servers - using original admin colors + cols[1].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("💻").size(24.0)); + ui.label( + egui::RichText::new(stats.unique_servers.to_string()) + .size(28.0) + .strong() + .color(egui::Color32::from_rgb(52, 152, 219)), + ); + ui.label( + egui::RichText::new("Servers") + .size(12.0) + .color(egui::Color32::GRAY), + ); + }); + }); + } else { + ui.horizontal(|ui| { + ui.columns(4, |cols| { + // Total keys + cols[0].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("📊").size(32.0)); + ui.add_space(5.0); + ui.label( + egui::RichText::new(stats.total_keys.to_string()) + .size(36.0) + .strong(), + ); + ui.label( + egui::RichText::new("Total Keys") + .size(14.0) + .color(egui::Color32::GRAY), + ); + }); + + // Active keys - using original admin colors + cols[1].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("✅").size(32.0)); + ui.add_space(5.0); + ui.label( + egui::RichText::new(stats.active_keys.to_string()) + .size(36.0) + .strong() + .color(egui::Color32::from_rgb(46, 204, 113)), + ); + ui.label( + egui::RichText::new("Active") + .size(14.0) + .color(egui::Color32::GRAY), + ); + }); + + // Deprecated keys - using original admin colors + cols[2].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("❌").size(32.0)); + ui.add_space(5.0); + ui.label( + egui::RichText::new(stats.deprecated_keys.to_string()) + .size(36.0) + .strong() + .color(egui::Color32::from_rgb(231, 76, 60)), + ); + ui.label( + egui::RichText::new("Deprecated") + .size(14.0) + .color(egui::Color32::GRAY), + ); + }); + + // Servers - using original admin colors + cols[3].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("💻").size(32.0)); + ui.add_space(5.0); + ui.label( + egui::RichText::new(stats.unique_servers.to_string()) + .size(36.0) + .strong() + .color(egui::Color32::from_rgb(52, 152, 219)), + ); + ui.label( + egui::RichText::new("Servers") + .size(14.0) + .color(egui::Color32::GRAY), + ); + }); + }); + }); + } + }); + }); + } + + fn render_search_controls(&mut self, ui: &mut egui::Ui, is_mobile: bool) { + ui.group(|ui| { + ui.set_min_width(ui.available_width()); + ui.vertical(|ui| { + let title_size = if is_mobile { 16.0 } else { 20.0 }; + ui.label(egui::RichText::new("🔍 Search & Filter").size(title_size).strong()); + ui.add_space(if is_mobile { 8.0 } else { 12.0 }); + + // Search field + if is_mobile { + ui.vertical(|ui| { + ui.label(egui::RichText::new("🔍 Search").size(14.0)); + let search_response = ui.add_sized( + [ui.available_width(), 36.0], // Larger touch target + egui::TextEdit::singleline(&mut self.admin_state.search_term) + .hint_text("Search servers or keys...") + .font(egui::FontId::proportional(16.0)), + ); + + ui.add_space(5.0); + + if self.admin_state.search_term.is_empty() { + ui.label( + egui::RichText::new("Type to search") + .size(12.0) + .color(egui::Color32::GRAY), + ); + } else { + ui.horizontal(|ui| { + ui.label( + egui::RichText::new(format!("{} results", self.admin_state.filtered_keys.len())) + .size(12.0), + ); + if ui.add_sized([60.0, 32.0], egui::Button::new(egui::RichText::new("❌ Clear").size(12.0))).clicked() { + self.admin_state.search_term.clear(); + self.admin_state.filter_keys(); + } + }); + } + + if search_response.changed() { + self.admin_state.filter_keys(); + } + }); + } else { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("🔍").size(18.0)); + let search_response = ui.add_sized( + [ui.available_width() * 0.6, 28.0], + egui::TextEdit::singleline(&mut self.admin_state.search_term) + .hint_text("Search servers or keys...") + .font(egui::FontId::proportional(16.0)), + ); + + if self.admin_state.search_term.is_empty() { + ui.label( + egui::RichText::new("Type to search") + .size(14.0) + .color(egui::Color32::GRAY), + ); + } else { + ui.label( + egui::RichText::new(format!("{} results", self.admin_state.filtered_keys.len())) + .size(14.0), + ); + if ui.add_sized([35.0, 28.0], egui::Button::new(egui::RichText::new("❌").size(14.0))).on_hover_text("Clear search").clicked() { + self.admin_state.search_term.clear(); + self.admin_state.filter_keys(); + } + } + + if search_response.changed() { + self.admin_state.filter_keys(); + } + }); + } + + ui.add_space(if is_mobile { 8.0 } else { 10.0 }); + + // Filter buttons - using original admin colors + let show_all = !self.admin_state.show_deprecated_only && !self.admin_state.show_active_only; + let show_active = self.admin_state.show_active_only; + let show_deprecated = self.admin_state.show_deprecated_only; + + if is_mobile { + ui.vertical(|ui| { + ui.label(egui::RichText::new("Filter:").size(14.0)); + ui.add_space(5.0); + + if ui.add_sized([ui.available_width(), 40.0], egui::Button::new(egui::RichText::new("📋 All Keys").size(14.0) + .color(if show_all { egui::Color32::WHITE } else { egui::Color32::BLACK })) + .fill(if show_all { egui::Color32::from_rgb(52, 152, 219) } else { egui::Color32::GRAY })).clicked() { + self.admin_state.show_deprecated_only = false; + self.admin_state.show_active_only = false; + self.admin_state.filter_keys(); + } + + ui.add_space(5.0); + + if ui.add_sized([ui.available_width(), 40.0], egui::Button::new(egui::RichText::new("✅ Active Only").size(14.0) + .color(if show_active { egui::Color32::WHITE } else { egui::Color32::BLACK })) + .fill(if show_active { egui::Color32::from_rgb(46, 204, 113) } else { egui::Color32::GRAY })).clicked() { + self.admin_state.show_deprecated_only = false; + self.admin_state.show_active_only = true; + self.admin_state.filter_keys(); + } + + ui.add_space(5.0); + + if ui.add_sized([ui.available_width(), 40.0], egui::Button::new(egui::RichText::new("❗ Deprecated Only").size(14.0) + .color(if show_deprecated { egui::Color32::WHITE } else { egui::Color32::BLACK })) + .fill(if show_deprecated { egui::Color32::from_rgb(231, 76, 60) } else { egui::Color32::GRAY })).clicked() { + self.admin_state.show_deprecated_only = true; + self.admin_state.show_active_only = false; + self.admin_state.filter_keys(); + } + }); + } else { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Filter:").size(16.0)); + ui.add_space(10.0); + + if ui.add_sized([80.0, 32.0], egui::Button::new(egui::RichText::new("📋 All").size(14.0) + .color(if show_all { egui::Color32::WHITE } else { egui::Color32::BLACK })) + .fill(if show_all { egui::Color32::from_rgb(52, 152, 219) } else { egui::Color32::GRAY })).clicked() { + self.admin_state.show_deprecated_only = false; + self.admin_state.show_active_only = false; + self.admin_state.filter_keys(); + } + if ui.add_sized([100.0, 32.0], egui::Button::new(egui::RichText::new("✅ Active").size(14.0) + .color(if show_active { egui::Color32::WHITE } else { egui::Color32::BLACK })) + .fill(if show_active { egui::Color32::from_rgb(46, 204, 113) } else { egui::Color32::GRAY })).clicked() { + self.admin_state.show_deprecated_only = false; + self.admin_state.show_active_only = true; + self.admin_state.filter_keys(); + } + if ui.add_sized([120.0, 32.0], egui::Button::new(egui::RichText::new("❗ Deprecated").size(14.0) + .color(if show_deprecated { egui::Color32::WHITE } else { egui::Color32::BLACK })) + .fill(if show_deprecated { egui::Color32::from_rgb(231, 76, 60) } else { egui::Color32::GRAY })).clicked() { + self.admin_state.show_deprecated_only = true; + self.admin_state.show_active_only = false; + self.admin_state.filter_keys(); + } + }); + } + }); + }); + } + + fn render_bulk_actions(&mut self, ui: &mut egui::Ui, is_mobile: bool, button_height: f32) -> BulkAction { + let selected_count = self.admin_state.selected_servers.values().filter(|&&v| v).count(); + + if selected_count == 0 { + return BulkAction::None; + } + + let mut action = BulkAction::None; + + ui.group(|ui| { + ui.set_min_width(ui.available_width()); + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("📋").size(14.0)); + ui.label( + egui::RichText::new(format!("Selected {} servers", selected_count)) + .size(14.0) + .strong() + .color(egui::Color32::LIGHT_BLUE), + ); + }); + + ui.add_space(5.0); + + // Use original admin colors for buttons + if is_mobile { + ui.vertical(|ui| { + if ui.add_sized([ui.available_width(), button_height], egui::Button::new(egui::RichText::new("❗ Deprecate Selected").size(14.0) + .color(egui::Color32::BLACK)) + .fill(egui::Color32::from_rgb(255, 200, 0))).clicked() { + action = BulkAction::DeprecateSelected; + } + + ui.add_space(5.0); + + if ui.add_sized([ui.available_width(), button_height], egui::Button::new(egui::RichText::new("✅ Restore Selected").size(14.0) + .color(egui::Color32::WHITE)) + .fill(egui::Color32::from_rgb(101, 199, 40))).clicked() { + action = BulkAction::RestoreSelected; + } + + ui.add_space(5.0); + + if ui.add_sized([ui.available_width(), button_height], egui::Button::new(egui::RichText::new("❌ Clear Selection").size(14.0) + .color(egui::Color32::WHITE)) + .fill(egui::Color32::from_rgb(170, 170, 170))).clicked() { + action = BulkAction::ClearSelection; + } + }); + } else { + ui.horizontal(|ui| { + if ui.add_sized([160.0, button_height], egui::Button::new(egui::RichText::new("❗ Deprecate Selected").size(14.0) + .color(egui::Color32::BLACK)) + .fill(egui::Color32::from_rgb(255, 200, 0))).clicked() { + action = BulkAction::DeprecateSelected; + } + + ui.add_space(10.0); + + if ui.add_sized([140.0, button_height], egui::Button::new(egui::RichText::new("✅ Restore Selected").size(14.0) + .color(egui::Color32::WHITE)) + .fill(egui::Color32::from_rgb(101, 199, 40))).clicked() { + action = BulkAction::RestoreSelected; + } + + ui.add_space(10.0); + + if ui.add_sized([120.0, button_height], egui::Button::new(egui::RichText::new("❌ Clear Selection").size(14.0) + .color(egui::Color32::WHITE)) + .fill(egui::Color32::from_rgb(170, 170, 170))).clicked() { + action = BulkAction::ClearSelection; + } + }); + } + }); + }); + + action + } + + fn render_keys_table(&mut self, ui: &mut egui::Ui, is_mobile: bool, button_height: f32) -> KeyAction { + let mut action = KeyAction::None; + + // Group keys by server + let mut servers: BTreeMap> = BTreeMap::new(); + for key in &self.admin_state.filtered_keys { + servers + .entry(key.server.clone()) + .or_insert_with(Vec::new) + .push(key.clone()); + } + + // Render each server group + for (server_name, server_keys) in servers { + let is_expanded = self.admin_state + .expanded_servers + .get(&server_name) + .copied() + .unwrap_or(false); + let active_count = server_keys.iter().filter(|k| !k.deprecated).count(); + let deprecated_count = server_keys.len() - active_count; + + // Server header + ui.group(|ui| { + ui.horizontal(|ui| { + // Server selection checkbox + let mut selected = self.admin_state + .selected_servers + .get(&server_name) + .copied() + .unwrap_or(false); + if ui.checkbox(&mut selected, "").changed() { + self.admin_state + .selected_servers + .insert(server_name.clone(), selected); + } + + // Expand/collapse button + let expand_icon = if is_expanded { "-" } else { "+" }; + if ui.small_button(expand_icon).clicked() { + self.admin_state + .expanded_servers + .insert(server_name.clone(), !is_expanded); + } + + // Server info + ui.label(egui::RichText::new("💻").size(16.0)); + ui.label( + egui::RichText::new(&server_name) + .size(15.0) + .strong() + .color(egui::Color32::WHITE), + ); + + ui.label(format!("{} keys", server_keys.len())); + + if deprecated_count > 0 { + ui.label( + egui::RichText::new(format!("{} depr", deprecated_count)) + .color(egui::Color32::LIGHT_RED), + ); + } + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let server_button_size = if is_mobile { egui::vec2(80.0, 32.0) } else { egui::vec2(70.0, 24.0) }; + + if deprecated_count > 0 { + if ui.add_sized(server_button_size, egui::Button::new( + egui::RichText::new("✅ Restore").color(egui::Color32::WHITE) + ).fill(egui::Color32::from_rgb(101, 199, 40)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(94, 105, 25)))) + .clicked() { + action = KeyAction::RestoreServer(server_name.clone()); + } + } + + if active_count > 0 { + if ui.add_sized(server_button_size, egui::Button::new( + egui::RichText::new("❗ Deprecate").color(egui::Color32::BLACK) + ).fill(egui::Color32::from_rgb(255, 200, 0)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(102, 94, 72)))) + .clicked() { + action = KeyAction::DeprecateServer(server_name.clone()); + } + } + }); + }); + }); + + // Expanded key details + if is_expanded { + ui.indent("server_keys", |ui| { + for key in &server_keys { + if let Some(key_action) = self.render_key_item(ui, key, &server_name, is_mobile, button_height) { + action = key_action; + } + } + }); + } + + ui.add_space(5.0); + } + + action + } + + fn render_key_item(&mut self, ui: &mut egui::Ui, key: &SshKey, server_name: &str, is_mobile: bool, _button_height: f32) -> Option { + let mut action = None; + + ui.group(|ui| { + ui.horizontal(|ui| { + // Key type badge + let key_type = get_key_type(&key.public_key); + ui.label( + egui::RichText::new(&key_type) + .size(10.0) + .color(egui::Color32::LIGHT_BLUE), + ); + + ui.add_space(5.0); + + // Status badge + if key.deprecated { + ui.label( + egui::RichText::new("❗ DEPR") + .size(10.0) + .color(egui::Color32::from_rgb(231, 76, 60)) + .strong(), + ); + } else { + ui.label( + egui::RichText::new("✅") + .size(10.0) + .color(egui::Color32::from_rgb(46, 204, 113)) + .strong(), + ); + } + + ui.add_space(5.0); + + // Key preview + ui.label( + egui::RichText::new(get_key_preview(&key.public_key)) + .font(egui::FontId::monospace(10.0)) + .color(egui::Color32::LIGHT_GRAY), + ); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + // Key action buttons with original admin colors + let button_size = if is_mobile { egui::vec2(50.0, 32.0) } else { egui::vec2(40.0, 24.0) }; + + if key.deprecated { + if ui.add_sized(button_size, egui::Button::new( + egui::RichText::new("R").color(egui::Color32::WHITE) + ).fill(egui::Color32::from_rgb(101, 199, 40)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(94, 105, 25)))) + .on_hover_text("Restore key").clicked() { + action = Some(KeyAction::RestoreKey(server_name.to_string())); + } + + if ui.add_sized(button_size, egui::Button::new( + egui::RichText::new("Del").color(egui::Color32::WHITE) + ).fill(egui::Color32::from_rgb(246, 36, 71)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(129, 18, 17)))) + .on_hover_text("Delete key").clicked() { + action = Some(KeyAction::DeleteKey(server_name.to_string())); + } + } else { + if ui.add_sized(button_size, egui::Button::new( + egui::RichText::new("❗").color(egui::Color32::BLACK) + ).fill(egui::Color32::from_rgb(255, 200, 0)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(102, 94, 72)))) + .on_hover_text("Deprecate key").clicked() { + action = Some(KeyAction::DeprecateKey(server_name.to_string())); + } + } + + if ui.add_sized(button_size, egui::Button::new( + egui::RichText::new("Copy").color(egui::Color32::WHITE) + ).fill(egui::Color32::from_rgb(0, 111, 230)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(35, 84, 97)))) + .on_hover_text("Copy to clipboard").clicked() { + ui.output_mut(|o| o.copied_text = key.public_key.clone()); + } + }); + }); + }); + + action + } + + fn render_empty_state(&self, ui: &mut egui::Ui) { + ui.vertical_centered(|ui| { + ui.add_space(60.0); + if self.admin_state.keys.is_empty() { + ui.label( + egui::RichText::new("🔑") + .size(48.0) + .color(egui::Color32::GRAY), + ); + ui.label( + egui::RichText::new("No SSH keys available") + .size(18.0) + .color(egui::Color32::GRAY), + ); + ui.label( + egui::RichText::new("Keys will appear here once loaded from the server") + .size(14.0) + .color(egui::Color32::DARK_GRAY), + ); + } else if !self.admin_state.search_term.is_empty() { + ui.label( + egui::RichText::new("🔍") + .size(48.0) + .color(egui::Color32::GRAY), + ); + ui.label( + egui::RichText::new("No results found") + .size(18.0) + .color(egui::Color32::GRAY), + ); + ui.label( + egui::RichText::new(format!( + "Try adjusting your search: '{}'", + self.admin_state.search_term + )) + .size(14.0) + .color(egui::Color32::DARK_GRAY), + ); + } else { + ui.label( + egui::RichText::new("❌") + .size(48.0) + .color(egui::Color32::GRAY), + ); + ui.label( + egui::RichText::new("No keys match current filters") + .size(18.0) + .color(egui::Color32::GRAY), + ); + ui.label( + egui::RichText::new("Try adjusting your search or filter settings") + .size(14.0) + .color(egui::Color32::DARK_GRAY), + ); + } + }); + } + + fn handle_bulk_action(&mut self, action: BulkAction) { + match action { + BulkAction::DeprecateSelected => { + let selected = self.admin_state.get_selected_servers(); + if !selected.is_empty() { + self.bulk_deprecate_servers(selected); + } + } + BulkAction::RestoreSelected => { + let selected = self.admin_state.get_selected_servers(); + if !selected.is_empty() { + self.bulk_restore_servers(selected); + } + } + BulkAction::ClearSelection => { + self.admin_state.clear_selection(); + self.status_message = "Selection cleared".to_string(); + } + BulkAction::None => {} + } + } + + fn handle_key_action(&mut self, action: KeyAction) { + match action { + KeyAction::DeprecateKey(server) => { + self.deprecate_key(&server); + } + KeyAction::RestoreKey(server) => { + self.restore_key(&server); + } + KeyAction::DeleteKey(server) => { + self.delete_key(&server); + } + KeyAction::DeprecateServer(server) => { + self.deprecate_key(&server); + } + KeyAction::RestoreServer(server) => { + self.restore_key(&server); + } + KeyAction::None => {} + } + } +} + +impl PartialEq for KeyAction { + fn eq(&self, other: &Self) -> bool { + matches!((self, other), (KeyAction::None, KeyAction::None)) + } +} + +impl PartialEq for BulkAction { + fn eq(&self, other: &Self) -> bool { + matches!((self, other), (BulkAction::None, BulkAction::None)) + } +} + +/// WASM entry point +#[wasm_bindgen] +pub fn start_web_admin(canvas_id: &str) -> Result<(), JsValue> { + console_error_panic_hook::set_once(); + tracing_wasm::set_as_global_default(); + + let web_options = eframe::WebOptions::default(); + let canvas_id = canvas_id.to_string(); + + wasm_bindgen_futures::spawn_local(async move { + let app = WebAdminApp::default(); + + // Get the canvas element + let document = web_sys::window() + .unwrap() + .document() + .unwrap(); + + let canvas = document + .get_element_by_id(&canvas_id) + .unwrap() + .dyn_into::() + .unwrap(); + + let result = eframe::WebRunner::new() + .start( + canvas, + web_options, + Box::new(|_cc| Ok(Box::new(app))), + ) + .await; + + match result { + Ok(_) => web_sys::console::log_1(&"KHM Web Admin started successfully".into()), + Err(e) => web_sys::console::error_1(&format!("Failed to start KHM Web Admin: {:?}", e).into()), + } + }); + + Ok(()) +} + +#[wasm_bindgen(start)] +pub fn wasm_main() { + console_error_panic_hook::set_once(); +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 1ecee44..845e7cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,8 @@ pub mod gui; pub mod server; #[cfg(feature = "web")] pub mod web; +#[cfg(feature = "web-gui")] +pub mod web_gui; use clap::Parser; @@ -110,4 +112,8 @@ pub struct Args { /// Basic auth string for client mode. Format: user:pass #[arg(long, default_value = "", help = "Client mode: Basic Auth credentials")] pub basic_auth: String, -} \ No newline at end of file +} + +// Re-export WASM functions for wasm-pack +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +pub use web_gui::wasm::*; \ No newline at end of file diff --git a/src/server.rs b/src/server.rs index d4ff17d..5b0a84c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -356,4 +356,13 @@ fn configure_web_routes(cfg: &mut web::ServiceConfig) { "/static/{filename:.*}", web::get().to(crate::web::serve_static_file), ); + + // Web GUI routes + cfg.route("/gui", web::get().to(crate::web_gui::serve_egui_interface)) + .route("/gui/", web::get().to(crate::web_gui::serve_egui_interface)) + .route("/gui/config", web::get().to(crate::web_gui::get_gui_config)) + .route("/gui/state", web::get().to(crate::web_gui::get_gui_state)) + .route("/gui/settings", web::post().to(crate::web_gui::update_gui_settings)) + .route("/wasm/{filename:.*}", web::get().to(crate::web_gui::serve_wasm_file)); } + diff --git a/src/wasm_lib.rs b/src/wasm_lib.rs new file mode 100644 index 0000000..dd33770 --- /dev/null +++ b/src/wasm_lib.rs @@ -0,0 +1,265 @@ +// Минимальная WASM библиотека только для egui интерфейса +use wasm_bindgen::prelude::*; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +// Основные структуры данных (копии из main lib) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SshKey { + pub server: String, + pub public_key: String, + #[serde(default)] + pub deprecated: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DnsResult { + pub server: String, + pub resolved: bool, + pub error: Option, +} + +#[derive(Debug, Clone)] +pub struct AdminSettings { + pub server_url: String, + pub basic_auth: String, + pub selected_flow: String, + pub auto_refresh: bool, + pub refresh_interval: u32, +} + +impl Default for AdminSettings { + fn default() -> Self { + let server_url = { + #[cfg(target_arch = "wasm32")] + { + web_sys::window() + .and_then(|w| w.location().origin().ok()) + .unwrap_or_else(|| "http://localhost:8080".to_string()) + } + #[cfg(not(target_arch = "wasm32"))] + { + "http://localhost:8080".to_string() + } + }; + + Self { + server_url, + basic_auth: String::new(), + selected_flow: String::new(), + auto_refresh: false, + refresh_interval: 30, + } + } +} + +#[derive(Debug, Clone)] +pub struct AdminState { + pub keys: Vec, + pub filtered_keys: Vec, + pub search_term: String, + pub show_deprecated_only: bool, + pub selected_servers: HashMap, + pub expanded_servers: HashMap, + pub current_operation: String, +} + +impl Default for AdminState { + fn default() -> Self { + Self { + keys: Vec::new(), + filtered_keys: Vec::new(), + search_term: String::new(), + show_deprecated_only: false, + selected_servers: HashMap::new(), + expanded_servers: HashMap::new(), + current_operation: String::new(), + } + } +} + +impl AdminState { + pub fn filter_keys(&mut self) { + self.filtered_keys = self.keys.iter() + .filter(|key| { + if self.show_deprecated_only && !key.deprecated { + return false; + } + if !self.show_deprecated_only && key.deprecated { + return false; + } + if !self.search_term.is_empty() { + let search_lower = self.search_term.to_lowercase(); + return key.server.to_lowercase().contains(&search_lower) || + key.public_key.to_lowercase().contains(&search_lower); + } + true + }) + .cloned() + .collect(); + } +} + +// Простое egui приложение +pub struct WebAdminApp { + settings: AdminSettings, + admin_state: AdminState, + status_message: String, +} + +impl Default for WebAdminApp { + fn default() -> Self { + Self { + settings: AdminSettings::default(), + admin_state: AdminState::default(), + status_message: "Ready".to_string(), + } + } +} + +impl eframe::App for WebAdminApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("🔑 KHM Web Admin Panel"); + ui.separator(); + + // Connection Settings + egui::CollapsingHeader::new("⚙️ Connection Settings") + .default_open(true) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label("Server URL:"); + ui.text_edit_singleline(&mut self.settings.server_url); + }); + + ui.horizontal(|ui| { + ui.label("Basic Auth:"); + ui.add(egui::TextEdit::singleline(&mut self.settings.basic_auth).password(true)); + }); + + ui.horizontal(|ui| { + ui.label("Flow:"); + ui.text_edit_singleline(&mut self.settings.selected_flow); + }); + + ui.horizontal(|ui| { + if ui.button("Test Connection").clicked() { + self.status_message = "Testing connection... (WASM demo mode)".to_string(); + } + if ui.button("Load Keys").clicked() { + // Add demo data + self.admin_state.keys = vec![ + SshKey { + server: "demo-server-1".to_string(), + public_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC demo key 1".to_string(), + deprecated: false, + }, + SshKey { + server: "demo-server-2".to_string(), + public_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5 demo key 2".to_string(), + deprecated: true, + }, + ]; + self.admin_state.filter_keys(); + self.status_message = format!("Loaded {} demo keys", self.admin_state.keys.len()); + } + }); + }); + + ui.add_space(10.0); + + // Keys display + if !self.admin_state.filtered_keys.is_empty() { + egui::CollapsingHeader::new("🔑 SSH Keys") + .default_open(true) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label("Search:"); + let search_response = ui.text_edit_singleline(&mut self.admin_state.search_term); + if search_response.changed() { + self.admin_state.filter_keys(); + } + }); + + ui.horizontal(|ui| { + if ui.selectable_label(!self.admin_state.show_deprecated_only, "✅ Active").clicked() { + self.admin_state.show_deprecated_only = false; + self.admin_state.filter_keys(); + } + if ui.selectable_label(self.admin_state.show_deprecated_only, "❗ Deprecated").clicked() { + self.admin_state.show_deprecated_only = true; + self.admin_state.filter_keys(); + } + }); + + ui.separator(); + + for key in &self.admin_state.filtered_keys { + ui.group(|ui| { + ui.horizontal(|ui| { + if key.deprecated { + ui.colored_label(egui::Color32::RED, "❗ DEPRECATED"); + } else { + ui.colored_label(egui::Color32::GREEN, "✅ ACTIVE"); + } + + ui.label(&key.server); + ui.monospace(&key.public_key[..50.min(key.public_key.len())]); + + if ui.small_button("Copy").clicked() { + ui.output_mut(|o| o.copied_text = key.public_key.clone()); + } + }); + }); + } + }); + } + + ui.add_space(10.0); + + // Status + ui.horizontal(|ui| { + ui.label("Status:"); + ui.colored_label(egui::Color32::LIGHT_BLUE, &self.status_message); + }); + + // Info + ui.separator(); + ui.label("ℹ️ This is a demo WASM version. For full functionality, the server API integration is needed."); + }); + } +} + +/// WASM entry point +#[wasm_bindgen] +pub fn start_web_admin(canvas_id: &str) -> Result<(), JsValue> { + console_error_panic_hook::set_once(); + tracing_wasm::set_as_global_default(); + + let web_options = eframe::WebOptions::default(); + let canvas_id = canvas_id.to_string(); + + wasm_bindgen_futures::spawn_local(async move { + let app = WebAdminApp::default(); + + let result = eframe::WebRunner::new() + .start( + &canvas_id, + web_options, + Box::new(|_cc| Ok(Box::new(app))), + ) + .await; + + match result { + Ok(_) => web_sys::console::log_1(&"eframe started successfully".into()), + Err(e) => web_sys::console::error_1(&format!("Failed to start eframe: {:?}", e).into()), + } + }); + + Ok(()) +} + +#[wasm_bindgen(start)] +pub fn wasm_main() { + console_error_panic_hook::set_once(); +} \ No newline at end of file diff --git a/src/web.rs b/src/web.rs index a08f530..998f604 100644 --- a/src/web.rs +++ b/src/web.rs @@ -17,7 +17,7 @@ use crate::server::Flows; #[folder = "static/"] struct StaticAssets; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct DnsResolutionResult { pub server: String, pub resolved: bool, diff --git a/src/web_gui.rs b/src/web_gui.rs new file mode 100644 index 0000000..bdbe54d --- /dev/null +++ b/src/web_gui.rs @@ -0,0 +1,288 @@ +use actix_web::{HttpResponse, Result, web}; +use serde_json::json; +use log::info; + +#[cfg(feature = "web-gui")] +pub mod app; +#[cfg(feature = "web-gui")] +pub mod state; +#[cfg(feature = "web-gui")] +pub mod ui; +#[cfg(all(feature = "web-gui", not(target_arch = "wasm32")))] +pub mod api; +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +pub mod wasm_api; +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +pub mod wasm; + + +/// Serve the egui web GUI interface +pub async fn serve_egui_interface() -> Result { + #[cfg(feature = "web-gui")] + { + let html = r#" + + + + + KHM Admin Panel + + + + +
+
+ Loading KHM Admin Panel... +
+ + + + + + + "#; + + Ok(HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(html)) + } + + #[cfg(not(feature = "web-gui"))] + { + let html = r#" + + + + + KHM Admin Panel - Not Available + + + +
+

⚠️ Web GUI Not Available

+

This server was compiled without web-gui support.

+

Please rebuild with --features web-gui to enable the admin interface.

+
+ + + "#; + + Ok(HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(html)) + } +} + +/// API endpoint to get GUI configuration +pub async fn get_gui_config( + flows: web::Data, + allowed_flows: web::Data>, +) -> Result { + info!("Web GUI config requested"); + + let flows_guard = flows.lock().unwrap(); + let available_flows: Vec = flows_guard.iter().map(|f| f.name.clone()).collect(); + + Ok(HttpResponse::Ok().json(json!({ + "version": env!("CARGO_PKG_VERSION"), + "gui_ready": cfg!(feature = "web-gui"), + "features": ["key_management", "bulk_operations", "real_time_updates"], + "available_flows": available_flows, + "allowed_flows": &**allowed_flows, + "api_endpoints": { + "flows": "/api/flows", + "keys": "/{flow}/keys", + "deprecate": "/{flow}/keys/{server}", + "restore": "/{flow}/keys/{server}/restore", + "delete": "/{flow}/keys/{server}/delete", + "bulk_deprecate": "/{flow}/bulk-deprecate", + "bulk_restore": "/{flow}/bulk-restore", + "dns_scan": "/{flow}/scan-dns" + } + }))) +} + +/// API endpoint for web GUI state management +pub async fn get_gui_state( + flows: web::Data, + allowed_flows: web::Data>, +) -> Result { + info!("Web GUI state requested"); + + let flows_guard = flows.lock().unwrap(); + let flow_data: Vec<_> = flows_guard.iter().map(|f| json!({ + "name": f.name, + "servers_count": f.servers.len(), + "active_keys": f.servers.iter().filter(|k| !k.deprecated).count(), + "deprecated_keys": f.servers.iter().filter(|k| k.deprecated).count() + })).collect(); + + Ok(HttpResponse::Ok().json(json!({ + "flows": flow_data, + "allowed_flows": &**allowed_flows, + "timestamp": chrono::Utc::now().to_rfc3339() + }))) +} + +/// API endpoint to update GUI settings +pub async fn update_gui_settings( + settings: web::Json, +) -> Result { + info!("Web GUI settings updated: {:?}", settings); + + Ok(HttpResponse::Ok().json(json!({ + "status": "success", + "message": "Settings updated successfully", + "timestamp": chrono::Utc::now().to_rfc3339() + }))) +} + +/// Serve WASM files for egui web application +pub async fn serve_wasm_file(path: web::Path) -> Result { + let filename = path.into_inner(); + info!("WASM file requested: {}", filename); + + // Try to read the actual WASM files from the wasm directory + let wasm_dir = std::path::Path::new("wasm"); + let file_path = wasm_dir.join(&filename); + + match std::fs::read(&file_path) { + Ok(content) => { + let content_type = if filename.ends_with(".js") { + "application/javascript; charset=utf-8" + } else if filename.ends_with(".wasm") { + "application/wasm" + } else { + "application/octet-stream" + }; + + info!("Serving WASM file: {} ({} bytes)", filename, content.len()); + Ok(HttpResponse::Ok() + .content_type(content_type) + .body(content)) + } + Err(_) => { + // Fallback to placeholder if files don't exist + let content = match filename.as_str() { + "khm_wasm.js" => { + r#" +// KHM WASM Module Not Found +// Build the WASM module first: +// cd khm-wasm && wasm-pack build --target web --out-dir ../wasm + +export default function init() { + return Promise.reject(new Error('WASM module not found. Run: cd khm-wasm && wasm-pack build --target web --out-dir ../wasm')); +} + +export function start_web_admin(canvas_id) { + throw new Error('WASM module not found. Run: cd khm-wasm && wasm-pack build --target web --out-dir ../wasm'); +} + "# + } + _ => { + return Ok(HttpResponse::NotFound().json(json!({ + "error": "WASM file not found", + "filename": filename, + "message": "Run: cd khm-wasm && wasm-pack build --target web --out-dir ../wasm" + }))); + } + }; + + Ok(HttpResponse::Ok() + .content_type("application/javascript; charset=utf-8") + .body(content)) + } + } +} \ No newline at end of file diff --git a/src/web_gui/api.rs b/src/web_gui/api.rs new file mode 100644 index 0000000..b69a1ba --- /dev/null +++ b/src/web_gui/api.rs @@ -0,0 +1,399 @@ +use super::state::{SshKey, DnsResult, AdminSettings}; +use log::info; +use reqwest::Client; +use std::time::Duration; + +/// Create HTTP client for API requests +fn create_http_client() -> Result { + Client::builder() + .timeout(Duration::from_secs(30)) + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(|e| format!("Failed to create HTTP client: {}", e)) +} + +/// Add basic auth to request if provided +fn add_auth_if_needed( + request: reqwest::RequestBuilder, + basic_auth: &str, +) -> Result { + if basic_auth.is_empty() { + return Ok(request); + } + + let auth_parts: Vec<&str> = basic_auth.splitn(2, ':').collect(); + if auth_parts.len() == 2 { + Ok(request.basic_auth(auth_parts[0], Some(auth_parts[1]))) + } else { + Err("Basic auth format should be 'username:password'".to_string()) + } +} + +/// Check response status for errors +fn check_response_status(response: &reqwest::Response) -> Result<(), String> { + let status = response.status().as_u16(); + + if status == 401 { + return Err("Authentication required. Please provide valid basic auth credentials.".to_string()); + } + + if status >= 300 && status < 400 { + return Err("Server redirects to login page. Authentication may be required.".to_string()); + } + + if !response.status().is_success() { + return Err(format!( + "Server returned error: {} {}", + status, + response.status().canonical_reason().unwrap_or("Unknown") + )); + } + + Ok(()) +} + +/// Check if response is HTML instead of JSON +fn check_html_response(body: &str) -> Result<(), String> { + if body.trim_start().starts_with(" Result { + if settings.server_url.is_empty() { + return Err("Server URL must be specified".to_string()); + } + + let url = format!("{}/api/version", settings.server_url.trim_end_matches('/')); + info!("Getting version from: {}", url); + + let client = create_http_client()?; + let mut request = client.get(&url); + + request = add_auth_if_needed(request, &settings.basic_auth)?; + + let response = request + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + + check_response_status(&response)?; + + let body = response + .text() + .await + .map_err(|e| format!("Failed to read response: {}", e))?; + + check_html_response(&body)?; + + let version_response: serde_json::Value = serde_json::from_str(&body) + .map_err(|e| format!("Failed to parse version: {}", e))?; + + let version = version_response + .get("version") + .and_then(|v| v.as_str()) + .unwrap_or("unknown") + .to_string(); + + info!("KHM server version: {}", version); + Ok(version) +} + +/// Test connection to KHM server using existing API endpoint +pub async fn test_connection(settings: &AdminSettings) -> Result { + if settings.server_url.is_empty() || settings.selected_flow.is_empty() { + return Err("Server URL and flow must be specified".to_string()); + } + + let url = format!( + "{}/{}/keys", + settings.server_url.trim_end_matches('/'), + settings.selected_flow + ); + info!("Testing connection to: {}", url); + + let client = create_http_client()?; + let mut request = client.get(&url); + + request = add_auth_if_needed(request, &settings.basic_auth)?; + + let response = request + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + + check_response_status(&response)?; + + let body = response + .text() + .await + .map_err(|e| format!("Failed to read response: {}", e))?; + + check_html_response(&body)?; + + let keys: Vec = serde_json::from_str(&body) + .map_err(|e| format!("Failed to parse response: {}", e))?; + + let message = format!("Connection successful! Found {} SSH keys from flow '{}'", keys.len(), settings.selected_flow); + info!("{}", message); + Ok(message) +} + +/// Load available flows from server +pub async fn load_flows(settings: &AdminSettings) -> Result, String> { + if settings.server_url.is_empty() { + return Err("Server URL must be specified".to_string()); + } + + let url = format!("{}/api/flows", settings.server_url.trim_end_matches('/')); + info!("Loading flows from: {}", url); + + let client = create_http_client()?; + let mut request = client.get(&url); + + request = add_auth_if_needed(request, &settings.basic_auth)?; + + let response = request + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + + check_response_status(&response)?; + + let body = response + .text() + .await + .map_err(|e| format!("Failed to read response: {}", e))?; + + check_html_response(&body)?; + + let flows: Vec = serde_json::from_str(&body) + .map_err(|e| format!("Failed to parse flows: {}", e))?; + + info!("Loaded {} flows", flows.len()); + Ok(flows) +} + +/// Fetch all SSH keys including deprecated ones using existing API endpoint +pub async fn fetch_keys(settings: &AdminSettings) -> Result, String> { + if settings.server_url.is_empty() || settings.selected_flow.is_empty() { + return Err("Server URL and flow must be specified".to_string()); + } + + let url = format!( + "{}/{}/keys", + settings.server_url.trim_end_matches('/'), + settings.selected_flow + ); + info!("Fetching keys from: {}", url); + + let client = create_http_client()?; + let mut request = client.get(&url); + + request = add_auth_if_needed(request, &settings.basic_auth)?; + + let response = request + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + + check_response_status(&response)?; + + let body = response + .text() + .await + .map_err(|e| format!("Failed to read response: {}", e))?; + + check_html_response(&body)?; + + let keys: Vec = serde_json::from_str(&body) + .map_err(|e| format!("Failed to parse keys: {}", e))?; + + info!("Fetched {} SSH keys", keys.len()); + Ok(keys) +} + +/// Deprecate a key for a specific server +pub async fn deprecate_key( + settings: &AdminSettings, + server: &str, +) -> Result { + let url = format!( + "{}/{}/keys/{}", + settings.server_url.trim_end_matches('/'), + settings.selected_flow, + urlencoding::encode(server) + ); + info!("Deprecating key for server '{}' at: {}", server, url); + + let client = create_http_client()?; + let mut request = client.delete(&url); + + request = add_auth_if_needed(request, &settings.basic_auth)?; + + let response = request + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + + check_response_status(&response)?; + + Ok(format!("Successfully deprecated key for server '{}'", server)) +} + +/// Restore a key for a specific server +pub async fn restore_key( + settings: &AdminSettings, + server: &str, +) -> Result { + let url = format!( + "{}/{}/keys/{}/restore", + settings.server_url.trim_end_matches('/'), + settings.selected_flow, + urlencoding::encode(server) + ); + info!("Restoring key for server '{}' at: {}", server, url); + + let client = create_http_client()?; + let mut request = client.post(&url); + + request = add_auth_if_needed(request, &settings.basic_auth)?; + + let response = request + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + + check_response_status(&response)?; + + Ok(format!("Successfully restored key for server '{}'", server)) +} + +/// Delete a key permanently for a specific server +pub async fn delete_key( + settings: &AdminSettings, + server: &str, +) -> Result { + let url = format!( + "{}/{}/keys/{}/delete", + settings.server_url.trim_end_matches('/'), + settings.selected_flow, + urlencoding::encode(server) + ); + info!("Permanently deleting key for server '{}' at: {}", server, url); + + let client = create_http_client()?; + let mut request = client.delete(&url); + + request = add_auth_if_needed(request, &settings.basic_auth)?; + + let response = request + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + + check_response_status(&response)?; + + Ok(format!("Successfully deleted key for server '{}'", server)) +} + +/// Bulk deprecate multiple servers +pub async fn bulk_deprecate_servers( + settings: &AdminSettings, + servers: Vec, +) -> Result { + let url = format!( + "{}/{}/bulk-deprecate", + settings.server_url.trim_end_matches('/'), + settings.selected_flow + ); + info!("Bulk deprecating {} servers at: {}", servers.len(), url); + + let client = create_http_client()?; + let mut request = client.post(&url).json(&serde_json::json!({ + "servers": servers + })); + + request = add_auth_if_needed(request, &settings.basic_auth)?; + + let response = request + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + + check_response_status(&response)?; + + Ok("Successfully deprecated selected servers".to_string()) +} + +/// Bulk restore multiple servers +pub async fn bulk_restore_servers( + settings: &AdminSettings, + servers: Vec, +) -> Result { + let url = format!( + "{}/{}/bulk-restore", + settings.server_url.trim_end_matches('/'), + settings.selected_flow + ); + info!("Bulk restoring {} servers at: {}", servers.len(), url); + + let client = create_http_client()?; + let mut request = client.post(&url).json(&serde_json::json!({ + "servers": servers + })); + + request = add_auth_if_needed(request, &settings.basic_auth)?; + + let response = request + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + + check_response_status(&response)?; + + Ok("Successfully restored selected servers".to_string()) +} + +/// Scan DNS resolution for servers using existing API endpoint +pub async fn scan_dns_resolution( + settings: &AdminSettings, +) -> Result, String> { + let url = format!( + "{}/{}/scan-dns", + settings.server_url.trim_end_matches('/'), + settings.selected_flow + ); + info!("Scanning DNS resolution at: {}", url); + + let client = create_http_client()?; + let mut request = client.post(&url); + + request = add_auth_if_needed(request, &settings.basic_auth)?; + + let response = request + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + + check_response_status(&response)?; + + let body = response + .text() + .await + .map_err(|e| format!("Failed to read response: {}", e))?; + + // Parse the response format from existing API: {"results": [...], "total": N, "unresolved": N} + let api_response: serde_json::Value = serde_json::from_str(&body) + .map_err(|e| format!("Failed to parse DNS response: {}", e))?; + + let results = api_response + .get("results") + .and_then(|r| serde_json::from_value(r.clone()).ok()) + .unwrap_or_else(Vec::new); + + info!("DNS scan completed for {} servers", results.len()); + Ok(results) +} \ No newline at end of file diff --git a/src/web_gui/app.rs b/src/web_gui/app.rs new file mode 100644 index 0000000..05c7016 --- /dev/null +++ b/src/web_gui/app.rs @@ -0,0 +1,590 @@ +use super::state::{AdminSettings, AdminState, ConnectionStatus, AdminOperation}; +use super::ui::{self, ConnectionAction, KeyAction, BulkAction}; +#[cfg(not(target_arch = "wasm32"))] +use super::api; +#[cfg(target_arch = "wasm32")] +use super::wasm_api as api; +use eframe::egui; +use std::sync::mpsc; + +pub struct WebAdminApp { + settings: AdminSettings, + admin_state: AdminState, + flows: Vec, + connection_status: ConnectionStatus, + operation_receiver: Option>, + last_operation: String, + server_version: Option, +} + +impl Default for WebAdminApp { + fn default() -> Self { + // Get server URL from current location if possible + let server_url = { + #[cfg(all(target_arch = "wasm32", feature = "web-gui"))] + { + web_sys::window() + .and_then(|w| w.location().origin().ok()) + .unwrap_or_else(|| "http://localhost:8080".to_string()) + } + #[cfg(not(all(target_arch = "wasm32", feature = "web-gui")))] + { + "http://localhost:8080".to_string() + } + }; + + Self { + settings: AdminSettings { + server_url, + ..Default::default() + }, + admin_state: AdminState::default(), + flows: Vec::new(), + connection_status: ConnectionStatus::Disconnected, + operation_receiver: None, + last_operation: "Application started".to_string(), + server_version: None, + } + } +} + +impl eframe::App for WebAdminApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + // Handle async operations + if let Some(receiver) = &self.operation_receiver { + if let Ok(operation) = receiver.try_recv() { + self.handle_operation_result(operation); + ctx.request_repaint(); + } + } + + // Use the same UI structure as desktop version + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("🔑 KHM Web Admin Panel"); + ui.separator(); + + // Connection Settings (always visible for web version) + egui::CollapsingHeader::new("⚙️ Connection Settings") + .default_open(matches!(self.connection_status, ConnectionStatus::Disconnected)) + .show(ui, |ui| { + let connection_action = ui::render_connection_settings( + ui, + &mut self.settings, + &self.connection_status, + &self.flows, + &self.server_version, + ); + + match connection_action { + ConnectionAction::LoadFlows => self.load_flows(ctx), + ConnectionAction::TestConnection => self.test_connection(ctx), + ConnectionAction::LoadKeys => self.load_keys(ctx), + ConnectionAction::LoadVersion => self.load_version(ctx), + ConnectionAction::None => {} + } + }); + + ui.add_space(10.0); + + // Statistics (from desktop version) + if !self.admin_state.keys.is_empty() { + ui::render_statistics(ui, &self.admin_state); + ui.add_space(10.0); + } + + // Key Management (from desktop version) + if !self.admin_state.keys.is_empty() { + egui::CollapsingHeader::new("🔑 Key Management") + .default_open(true) + .show(ui, |ui| { + // Search and filter controls (from desktop version) + ui::render_search_controls(ui, &mut self.admin_state); + ui.add_space(5.0); + + // Bulk actions (from desktop version) + let bulk_action = ui::render_bulk_actions(ui, &mut self.admin_state); + match bulk_action { + BulkAction::DeprecateSelected => self.bulk_deprecate(ctx), + BulkAction::RestoreSelected => self.bulk_restore(ctx), + BulkAction::ClearSelection => { + self.admin_state.clear_selection(); + } + BulkAction::None => {} + } + + ui.add_space(5.0); + + // Keys table (from desktop version) + let key_action = ui::render_keys_table(ui, &mut self.admin_state); + match key_action { + KeyAction::DeprecateKey(server) => self.deprecate_key(server, ctx), + KeyAction::RestoreKey(server) => self.restore_key(server, ctx), + KeyAction::DeleteKey(server) => self.delete_key(server, ctx), + KeyAction::DeprecateServer(server) => self.deprecate_server(server, ctx), + KeyAction::RestoreServer(server) => self.restore_server(server, ctx), + KeyAction::None => {} + } + }); + + ui.add_space(10.0); + } + + // Additional web-specific actions + if matches!(self.connection_status, ConnectionStatus::Connected) && !self.settings.selected_flow.is_empty() { + ui.horizontal(|ui| { + if ui.button("🔍 Scan DNS").clicked() { + self.scan_dns(ctx); + } + + if ui.button("🔄 Refresh Keys").clicked() { + self.load_keys(ctx); + } + + ui.checkbox(&mut self.settings.auto_refresh, "Auto-refresh"); + }); + + ui.add_space(10.0); + } + + // Status bar (from desktop version) + ui.horizontal(|ui| { + ui.label("Status:"); + match &self.connection_status { + ConnectionStatus::Connected => { + ui.colored_label(egui::Color32::GREEN, "● Connected"); + } + ConnectionStatus::Connecting => { + ui.colored_label(egui::Color32::YELLOW, "● Connecting..."); + } + ConnectionStatus::Disconnected => { + ui.colored_label(egui::Color32::GRAY, "● Disconnected"); + } + ConnectionStatus::Error(msg) => { + ui.colored_label(egui::Color32::RED, format!("● Error: {}", msg)); + } + } + + ui.separator(); + ui.label(&self.last_operation); + }); + }); + + // Auto-refresh like desktop version + if self.settings.auto_refresh && matches!(self.connection_status, ConnectionStatus::Connected) { + ctx.request_repaint_after(std::time::Duration::from_secs(self.settings.refresh_interval as u64)); + } + } +} + +impl WebAdminApp { + fn handle_operation_result(&mut self, operation: AdminOperation) { + match operation { + AdminOperation::LoadFlows(result) => { + match result { + Ok(flows) => { + self.flows = flows; + if !self.flows.is_empty() && self.settings.selected_flow.is_empty() { + self.settings.selected_flow = self.flows[0].clone(); + } + self.last_operation = format!("Loaded {} flows", self.flows.len()); + } + Err(err) => { + self.connection_status = ConnectionStatus::Error(err.clone()); + self.last_operation = format!("Failed to load flows: {}", err); + } + } + } + AdminOperation::LoadKeys(result) => { + match result { + Ok(keys) => { + self.admin_state.keys = keys; + self.admin_state.filter_keys(); + self.connection_status = ConnectionStatus::Connected; + self.last_operation = format!("Loaded {} keys", self.admin_state.keys.len()); + } + Err(err) => { + self.connection_status = ConnectionStatus::Error(err.clone()); + self.last_operation = format!("Failed to load keys: {}", err); + } + } + } + AdminOperation::TestConnection(result) => { + match result { + Ok(msg) => { + self.connection_status = ConnectionStatus::Connected; + self.last_operation = msg; + } + Err(err) => { + self.connection_status = ConnectionStatus::Error(err.clone()); + self.last_operation = format!("Connection failed: {}", err); + } + } + } + AdminOperation::DeprecateKey(server, result) => { + match result { + Ok(msg) => { + self.last_operation = msg; + self.load_keys_silent(); + } + Err(err) => { + self.last_operation = format!("Failed to deprecate key for {}: {}", server, err); + } + } + } + AdminOperation::RestoreKey(server, result) => { + match result { + Ok(msg) => { + self.last_operation = msg; + self.load_keys_silent(); + } + Err(err) => { + self.last_operation = format!("Failed to restore key for {}: {}", server, err); + } + } + } + AdminOperation::DeleteKey(server, result) => { + match result { + Ok(msg) => { + self.last_operation = msg; + self.load_keys_silent(); + } + Err(err) => { + self.last_operation = format!("Failed to delete key for {}: {}", server, err); + } + } + } + AdminOperation::BulkDeprecate(result) | AdminOperation::BulkRestore(result) => { + match result { + Ok(msg) => { + self.last_operation = msg; + self.admin_state.clear_selection(); + self.load_keys_silent(); + } + Err(err) => { + self.last_operation = format!("Bulk operation failed: {}", err); + } + } + } + AdminOperation::ScanDns(result) => { + match result { + Ok(results) => { + let resolved = results.iter().filter(|r| r.resolved).count(); + let total = results.len(); + self.last_operation = format!("DNS scan completed: {}/{} servers resolved", resolved, total); + } + Err(err) => { + self.last_operation = format!("DNS scan failed: {}", err); + } + } + } + AdminOperation::LoadVersion(result) => { + match result { + Ok(version) => { + self.server_version = Some(version.clone()); + self.last_operation = format!("Server version: {}", version); + } + Err(err) => { + self.last_operation = format!("Failed to get server version: {}", err); + } + } + } + } + } + + // Async operation methods - adapted from desktop version + fn load_flows(&mut self, _ctx: &egui::Context) { + self.last_operation = "Loading flows...".to_string(); + + let settings = self.settings.clone(); + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + #[cfg(not(target_arch = "wasm32"))] + { + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(api::load_flows(&settings)); + let _ = tx.send(AdminOperation::LoadFlows(result)); + }); + } + + #[cfg(all(target_arch = "wasm32", feature = "web-gui"))] + { + wasm_bindgen_futures::spawn_local(async move { + let result = api::load_flows(&settings).await; + let _ = tx.send(AdminOperation::LoadFlows(result)); + }); + } + } + + fn test_connection(&mut self, _ctx: &egui::Context) { + self.connection_status = ConnectionStatus::Connecting; + self.last_operation = "Testing connection...".to_string(); + + let settings = self.settings.clone(); + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + #[cfg(not(target_arch = "wasm32"))] + { + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(api::test_connection(&settings)); + let _ = tx.send(AdminOperation::TestConnection(result)); + }); + } + + #[cfg(all(target_arch = "wasm32", feature = "web-gui"))] + { + wasm_bindgen_futures::spawn_local(async move { + let result = api::test_connection(&settings).await; + let _ = tx.send(AdminOperation::TestConnection(result)); + }); + } + } + + fn load_keys(&mut self, _ctx: &egui::Context) { + self.admin_state.current_operation = "Loading keys...".to_string(); + self.last_operation = "Loading keys...".to_string(); + + let settings = self.settings.clone(); + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + #[cfg(not(target_arch = "wasm32"))] + { + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(api::fetch_keys(&settings)); + let _ = tx.send(AdminOperation::LoadKeys(result)); + }); + } + + #[cfg(all(target_arch = "wasm32", feature = "web-gui"))] + { + wasm_bindgen_futures::spawn_local(async move { + let result = api::fetch_keys(&settings).await; + let _ = tx.send(AdminOperation::LoadKeys(result)); + }); + } + } + + fn load_keys_silent(&mut self) { + let settings = self.settings.clone(); + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + #[cfg(not(target_arch = "wasm32"))] + { + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(api::fetch_keys(&settings)); + let _ = tx.send(AdminOperation::LoadKeys(result)); + }); + } + + #[cfg(all(target_arch = "wasm32", feature = "web-gui"))] + { + wasm_bindgen_futures::spawn_local(async move { + let result = api::fetch_keys(&settings).await; + let _ = tx.send(AdminOperation::LoadKeys(result)); + }); + } + } + + fn deprecate_key(&mut self, server: String, _ctx: &egui::Context) { + self.last_operation = format!("Deprecating key for {}...", server); + + let settings = self.settings.clone(); + let server_clone = server.clone(); + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + #[cfg(not(target_arch = "wasm32"))] + { + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(api::deprecate_key(&settings, &server)); + let _ = tx.send(AdminOperation::DeprecateKey(server_clone, result)); + }); + } + + #[cfg(all(target_arch = "wasm32", feature = "web-gui"))] + { + wasm_bindgen_futures::spawn_local(async move { + let result = api::deprecate_key(&settings, &server).await; + let _ = tx.send(AdminOperation::DeprecateKey(server_clone, result)); + }); + } + } + + fn restore_key(&mut self, server: String, _ctx: &egui::Context) { + self.last_operation = format!("Restoring key for {}...", server); + + let settings = self.settings.clone(); + let server_clone = server.clone(); + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + #[cfg(not(target_arch = "wasm32"))] + { + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(api::restore_key(&settings, &server)); + let _ = tx.send(AdminOperation::RestoreKey(server_clone, result)); + }); + } + + #[cfg(all(target_arch = "wasm32", feature = "web-gui"))] + { + wasm_bindgen_futures::spawn_local(async move { + let result = api::restore_key(&settings, &server).await; + let _ = tx.send(AdminOperation::RestoreKey(server_clone, result)); + }); + } + } + + fn delete_key(&mut self, server: String, _ctx: &egui::Context) { + self.last_operation = format!("Deleting key for {}...", server); + + let settings = self.settings.clone(); + let server_clone = server.clone(); + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + #[cfg(not(target_arch = "wasm32"))] + { + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(api::delete_key(&settings, &server)); + let _ = tx.send(AdminOperation::DeleteKey(server_clone, result)); + }); + } + + #[cfg(all(target_arch = "wasm32", feature = "web-gui"))] + { + wasm_bindgen_futures::spawn_local(async move { + let result = api::delete_key(&settings, &server).await; + let _ = tx.send(AdminOperation::DeleteKey(server_clone, result)); + }); + } + } + + fn deprecate_server(&mut self, server: String, ctx: &egui::Context) { + self.deprecate_key(server, ctx); + } + + fn restore_server(&mut self, server: String, ctx: &egui::Context) { + self.restore_key(server, ctx); + } + + fn bulk_deprecate(&mut self, _ctx: &egui::Context) { + let servers = self.admin_state.get_selected_servers(); + if servers.is_empty() { + return; + } + + self.last_operation = format!("Bulk deprecating {} servers...", servers.len()); + + let settings = self.settings.clone(); + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + #[cfg(not(target_arch = "wasm32"))] + { + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(api::bulk_deprecate_servers(&settings, servers)); + let _ = tx.send(AdminOperation::BulkDeprecate(result)); + }); + } + + #[cfg(all(target_arch = "wasm32", feature = "web-gui"))] + { + wasm_bindgen_futures::spawn_local(async move { + let result = api::bulk_deprecate_servers(&settings, servers).await; + let _ = tx.send(AdminOperation::BulkDeprecate(result)); + }); + } + } + + fn bulk_restore(&mut self, _ctx: &egui::Context) { + let servers = self.admin_state.get_selected_servers(); + if servers.is_empty() { + return; + } + + self.last_operation = format!("Bulk restoring {} servers...", servers.len()); + + let settings = self.settings.clone(); + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + #[cfg(not(target_arch = "wasm32"))] + { + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(api::bulk_restore_servers(&settings, servers)); + let _ = tx.send(AdminOperation::BulkRestore(result)); + }); + } + + #[cfg(all(target_arch = "wasm32", feature = "web-gui"))] + { + wasm_bindgen_futures::spawn_local(async move { + let result = api::bulk_restore_servers(&settings, servers).await; + let _ = tx.send(AdminOperation::BulkRestore(result)); + }); + } + } + + fn scan_dns(&mut self, _ctx: &egui::Context) { + self.last_operation = "Scanning DNS resolution...".to_string(); + + let settings = self.settings.clone(); + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + #[cfg(not(target_arch = "wasm32"))] + { + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(api::scan_dns_resolution(&settings)); + let _ = tx.send(AdminOperation::ScanDns(result)); + }); + } + + #[cfg(all(target_arch = "wasm32", feature = "web-gui"))] + { + wasm_bindgen_futures::spawn_local(async move { + let result = api::scan_dns_resolution(&settings).await; + let _ = tx.send(AdminOperation::ScanDns(result)); + }); + } + } + + fn load_version(&mut self, _ctx: &egui::Context) { + self.last_operation = "Loading server version...".to_string(); + + let settings = self.settings.clone(); + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + #[cfg(not(target_arch = "wasm32"))] + { + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(api::get_version(&settings)); + let _ = tx.send(AdminOperation::LoadVersion(result)); + }); + } + + #[cfg(all(target_arch = "wasm32", feature = "web-gui"))] + { + wasm_bindgen_futures::spawn_local(async move { + let result = api::get_version(&settings).await; + let _ = tx.send(AdminOperation::LoadVersion(result)); + }); + } + } +} \ No newline at end of file diff --git a/src/web_gui/state.rs b/src/web_gui/state.rs new file mode 100644 index 0000000..97f4273 --- /dev/null +++ b/src/web_gui/state.rs @@ -0,0 +1,182 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AdminSettings { + pub server_url: String, + pub basic_auth: String, + pub selected_flow: String, + pub auto_refresh: bool, + pub refresh_interval: u32, +} + +impl Default for AdminSettings { + fn default() -> Self { + Self { + server_url: String::new(), + basic_auth: String::new(), + selected_flow: String::new(), + auto_refresh: false, + refresh_interval: 30, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AdminState { + pub keys: Vec, + pub filtered_keys: Vec, + pub search_term: String, + pub show_deprecated_only: bool, + pub selected_servers: HashMap, + pub expanded_servers: HashMap, + pub current_operation: String, +} + +impl Default for AdminState { + fn default() -> Self { + Self { + keys: Vec::new(), + filtered_keys: Vec::new(), + search_term: String::new(), + show_deprecated_only: false, + selected_servers: HashMap::new(), + expanded_servers: HashMap::new(), + current_operation: "Ready".to_string(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SshKey { + pub server: String, + pub public_key: String, + #[serde(default)] + pub deprecated: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ConnectionStatus { + Disconnected, + Connecting, + Connected, + Error(String), +} + +impl PartialEq for ConnectionStatus { + fn eq(&self, other: &Self) -> bool { + std::mem::discriminant(self) == std::mem::discriminant(other) + } +} + +#[derive(Debug, Clone)] +pub enum AdminOperation { + LoadKeys(Result, String>), + LoadFlows(Result, String>), + DeprecateKey(String, Result), + RestoreKey(String, Result), + DeleteKey(String, Result), + BulkDeprecate(Result), + BulkRestore(Result), + TestConnection(Result), + ScanDns(Result, String>), + LoadVersion(Result), +} + +// Re-export DnsResolutionResult from web.rs for consistency +pub use crate::web::DnsResolutionResult as DnsResult; + +impl AdminState { + /// Filter keys based on search term and deprecated filter + pub fn filter_keys(&mut self) { + let mut filtered = self.keys.clone(); + + // Apply deprecated filter + if self.show_deprecated_only { + filtered.retain(|key| key.deprecated); + } + + // Apply search filter + if !self.search_term.is_empty() { + let search_term = self.search_term.to_lowercase(); + filtered.retain(|key| { + key.server.to_lowercase().contains(&search_term) + || key.public_key.to_lowercase().contains(&search_term) + }); + } + + self.filtered_keys = filtered; + } + + /// Get selected servers list + pub fn get_selected_servers(&self) -> Vec { + self.selected_servers + .iter() + .filter_map(|(server, &selected)| { + if selected { Some(server.clone()) } else { None } + }) + .collect() + } + + /// Clear selection + pub fn clear_selection(&mut self) { + self.selected_servers.clear(); + } + + /// Get statistics + pub fn get_statistics(&self) -> AdminStatistics { + let total_keys = self.keys.len(); + let active_keys = self.keys.iter().filter(|k| !k.deprecated).count(); + let deprecated_keys = total_keys - active_keys; + let unique_servers = self.keys + .iter() + .map(|k| &k.server) + .collect::>() + .len(); + + AdminStatistics { + total_keys, + active_keys, + deprecated_keys, + unique_servers, + } + } +} + +#[derive(Debug, Clone)] +pub struct AdminStatistics { + pub total_keys: usize, + pub active_keys: usize, + pub deprecated_keys: usize, + pub unique_servers: usize, +} + +/// Get SSH key type from public key string +pub fn get_key_type(public_key: &str) -> String { + if public_key.starts_with("ssh-rsa") { + "RSA".to_string() + } else if public_key.starts_with("ssh-ed25519") { + "ED25519".to_string() + } else if public_key.starts_with("ecdsa-sha2-nistp") { + "ECDSA".to_string() + } else if public_key.starts_with("ssh-dss") { + "DSA".to_string() + } else { + "Unknown".to_string() + } +} + +/// Get preview of SSH key (first 16 characters of key part) +pub fn get_key_preview(public_key: &str) -> String { + let parts: Vec<&str> = public_key.split_whitespace().collect(); + if parts.len() >= 2 { + let key_part = parts[1]; + if key_part.len() > 16 { + format!("{}...", &key_part[..16]) + } else { + key_part.to_string() + } + } else { + format!("{}...", &public_key[..std::cmp::min(16, public_key.len())]) + } +} \ No newline at end of file diff --git a/src/web_gui/ui.rs b/src/web_gui/ui.rs new file mode 100644 index 0000000..db1938b --- /dev/null +++ b/src/web_gui/ui.rs @@ -0,0 +1,532 @@ +use super::state::{AdminState, AdminSettings, ConnectionStatus, get_key_type, get_key_preview}; +use eframe::egui; +use std::collections::BTreeMap; + +#[derive(Debug, Clone)] +pub enum KeyAction { + None, + DeprecateKey(String), + RestoreKey(String), + DeleteKey(String), + DeprecateServer(String), + RestoreServer(String), +} + +#[derive(Debug, Clone)] +pub enum BulkAction { + None, + DeprecateSelected, + RestoreSelected, + ClearSelection, +} + +/// Render connection settings panel +pub fn render_connection_settings( + ui: &mut egui::Ui, + settings: &mut AdminSettings, + connection_status: &ConnectionStatus, + flows: &[String], + server_version: &Option, +) -> ConnectionAction { + let mut action = ConnectionAction::None; + + ui.group(|ui| { + ui.set_min_width(ui.available_width()); + ui.vertical(|ui| { + ui.label(egui::RichText::new("⚙️ Connection Settings").size(16.0).strong()); + ui.add_space(8.0); + + // Server URL + ui.horizontal(|ui| { + ui.label("Server URL:"); + ui.text_edit_singleline(&mut settings.server_url); + }); + + // Basic Auth + ui.horizontal(|ui| { + ui.label("Basic Auth:"); + ui.add(egui::TextEdit::singleline(&mut settings.basic_auth).password(true)); + }); + + // Flow selection + ui.horizontal(|ui| { + ui.label("Flow:"); + egui::ComboBox::from_id_salt("flow_select") + .selected_text(&settings.selected_flow) + .show_ui(ui, |ui| { + for flow in flows { + ui.selectable_value(&mut settings.selected_flow, flow.clone(), flow); + } + }); + }); + + // Connection status + ui.horizontal(|ui| { + ui.label("Status:"); + match connection_status { + ConnectionStatus::Connected => { + ui.colored_label(egui::Color32::GREEN, "● Connected"); + } + ConnectionStatus::Connecting => { + ui.colored_label(egui::Color32::YELLOW, "● Connecting..."); + } + ConnectionStatus::Disconnected => { + ui.colored_label(egui::Color32::GRAY, "● Disconnected"); + } + ConnectionStatus::Error(msg) => { + ui.colored_label(egui::Color32::RED, format!("● Error: {}", msg)); + } + } + }); + + // Server version display + if let Some(version) = server_version { + ui.horizontal(|ui| { + ui.label("Server Version:"); + ui.colored_label(egui::Color32::LIGHT_BLUE, version); + }); + } + + ui.add_space(8.0); + + // Action buttons + ui.horizontal(|ui| { + if ui.button("Load Flows").clicked() { + action = ConnectionAction::LoadFlows; + } + + if ui.button("Test Connection").clicked() { + action = ConnectionAction::TestConnection; + } + + if ui.button("Get Version").clicked() { + action = ConnectionAction::LoadVersion; + } + + if !settings.selected_flow.is_empty() && ui.button("Load Keys").clicked() { + action = ConnectionAction::LoadKeys; + } + }); + }); + }); + + action +} + +/// Render statistics cards +pub fn render_statistics(ui: &mut egui::Ui, admin_state: &AdminState) { + let stats = admin_state.get_statistics(); + + ui.group(|ui| { + ui.set_min_width(ui.available_width()); + ui.vertical(|ui| { + ui.label(egui::RichText::new("📊 Statistics").size(16.0).strong()); + ui.add_space(8.0); + + ui.horizontal(|ui| { + ui.columns(4, |cols| { + // Total keys + cols[0].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("📊").size(20.0)); + ui.label( + egui::RichText::new(stats.total_keys.to_string()) + .size(24.0) + .strong(), + ); + ui.label( + egui::RichText::new("Total Keys") + .size(11.0) + .color(egui::Color32::GRAY), + ); + }); + + // Active keys + cols[1].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("✅").size(20.0)); + ui.label( + egui::RichText::new(stats.active_keys.to_string()) + .size(24.0) + .strong() + .color(egui::Color32::LIGHT_GREEN), + ); + ui.label( + egui::RichText::new("Active") + .size(11.0) + .color(egui::Color32::GRAY), + ); + }); + + // Deprecated keys + cols[2].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("❌").size(20.0)); + ui.label( + egui::RichText::new(stats.deprecated_keys.to_string()) + .size(24.0) + .strong() + .color(egui::Color32::LIGHT_RED), + ); + ui.label( + egui::RichText::new("Deprecated") + .size(11.0) + .color(egui::Color32::GRAY), + ); + }); + + // Servers + cols[3].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("💻").size(20.0)); + ui.label( + egui::RichText::new(stats.unique_servers.to_string()) + .size(24.0) + .strong() + .color(egui::Color32::LIGHT_BLUE), + ); + ui.label( + egui::RichText::new("Servers") + .size(11.0) + .color(egui::Color32::GRAY), + ); + }); + }); + }); + }); + }); +} + +/// Render search and filter controls +pub fn render_search_controls(ui: &mut egui::Ui, admin_state: &mut AdminState) -> bool { + let mut changed = false; + + ui.group(|ui| { + ui.set_min_width(ui.available_width()); + ui.vertical(|ui| { + ui.label(egui::RichText::new("🔍 Search & Filter").size(16.0).strong()); + ui.add_space(8.0); + + // Search field + ui.horizontal(|ui| { + ui.label("Search:"); + let search_response = ui.add_sized( + [ui.available_width() * 0.6, 20.0], + egui::TextEdit::singleline(&mut admin_state.search_term) + .hint_text("Search servers or keys..."), + ); + + if search_response.changed() { + changed = true; + } + + if !admin_state.search_term.is_empty() { + if ui.small_button("Clear").clicked() { + admin_state.search_term.clear(); + changed = true; + } + } + }); + + ui.add_space(5.0); + + // Filter controls + ui.horizontal(|ui| { + ui.label("Filter:"); + let show_deprecated = admin_state.show_deprecated_only; + if ui.selectable_label(!show_deprecated, "✅ Active").clicked() { + admin_state.show_deprecated_only = false; + changed = true; + } + if ui.selectable_label(show_deprecated, "❗ Deprecated").clicked() { + admin_state.show_deprecated_only = true; + changed = true; + } + }); + }); + }); + + if changed { + admin_state.filter_keys(); + } + + changed +} + +/// Render bulk actions controls +pub fn render_bulk_actions(ui: &mut egui::Ui, admin_state: &mut AdminState) -> BulkAction { + let selected_count = admin_state + .selected_servers + .values() + .filter(|&&v| v) + .count(); + + if selected_count == 0 { + return BulkAction::None; + } + + let mut action = BulkAction::None; + + ui.group(|ui| { + ui.set_min_width(ui.available_width()); + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("📋").size(14.0)); + ui.label( + egui::RichText::new(format!("Selected {} servers", selected_count)) + .size(14.0) + .strong() + .color(egui::Color32::LIGHT_BLUE), + ); + }); + + ui.add_space(5.0); + + ui.horizontal(|ui| { + if ui.button("❗ Deprecate Selected").clicked() { + action = BulkAction::DeprecateSelected; + } + + if ui.button("✅ Restore Selected").clicked() { + action = BulkAction::RestoreSelected; + } + + if ui.button("Clear Selection").clicked() { + action = BulkAction::ClearSelection; + } + }); + }); + }); + + action +} + +/// Render keys table grouped by servers +pub fn render_keys_table(ui: &mut egui::Ui, admin_state: &mut AdminState) -> KeyAction { + if admin_state.filtered_keys.is_empty() { + render_empty_state(ui, admin_state); + return KeyAction::None; + } + + let mut action = KeyAction::None; + + // Group keys by server + let mut servers: BTreeMap> = BTreeMap::new(); + for key in &admin_state.filtered_keys { + servers + .entry(key.server.clone()) + .or_insert_with(Vec::new) + .push(key); + } + + // Render each server group + egui::ScrollArea::vertical().show(ui, |ui| { + for (server_name, server_keys) in servers { + let is_expanded = admin_state + .expanded_servers + .get(&server_name) + .copied() + .unwrap_or(false); + let active_count = server_keys.iter().filter(|k| !k.deprecated).count(); + let deprecated_count = server_keys.len() - active_count; + + // Server header + ui.group(|ui| { + ui.horizontal(|ui| { + // Server selection checkbox + let mut selected = admin_state + .selected_servers + .get(&server_name) + .copied() + .unwrap_or(false); + if ui.checkbox(&mut selected, "").changed() { + admin_state + .selected_servers + .insert(server_name.clone(), selected); + } + + // Expand/collapse button + let expand_icon = if is_expanded { "▼" } else { "▶" }; + if ui.small_button(expand_icon).clicked() { + admin_state + .expanded_servers + .insert(server_name.clone(), !is_expanded); + } + + // Server icon and name + ui.label(egui::RichText::new("💻").size(16.0)); + ui.label( + egui::RichText::new(&server_name) + .size(15.0) + .strong(), + ); + + // Keys count badge + ui.label(format!("({} keys)", server_keys.len())); + + // Deprecated count badge + if deprecated_count > 0 { + ui.colored_label( + egui::Color32::RED, + format!("{} deprecated", deprecated_count) + ); + } + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + // Server action buttons + if deprecated_count > 0 { + if ui.small_button("✅ Restore").clicked() { + action = KeyAction::RestoreServer(server_name.clone()); + } + } + + if active_count > 0 { + if ui.small_button("❗ Deprecate").clicked() { + action = KeyAction::DeprecateServer(server_name.clone()); + } + } + }); + }); + }); + + // Expanded key details + if is_expanded { + ui.indent(&server_name, |ui| { + for key in &server_keys { + if let Some(key_action) = render_key_item(ui, key, &server_name) { + action = key_action; + } + } + }); + } + + ui.add_space(5.0); + } + }); + + action +} + +/// Render empty state when no keys are available +fn render_empty_state(ui: &mut egui::Ui, admin_state: &AdminState) { + ui.vertical_centered(|ui| { + ui.add_space(60.0); + if admin_state.keys.is_empty() { + ui.label( + egui::RichText::new("🔑") + .size(48.0) + .color(egui::Color32::GRAY), + ); + ui.label( + egui::RichText::new("No SSH keys available") + .size(18.0) + .color(egui::Color32::GRAY), + ); + ui.label( + egui::RichText::new("Keys will appear here once loaded from the server") + .size(14.0) + .color(egui::Color32::DARK_GRAY), + ); + } else if !admin_state.search_term.is_empty() { + ui.label( + egui::RichText::new("🔍") + .size(48.0) + .color(egui::Color32::GRAY), + ); + ui.label( + egui::RichText::new("No results found") + .size(18.0) + .color(egui::Color32::GRAY), + ); + ui.label( + egui::RichText::new(format!( + "Try adjusting your search: '{}'", + admin_state.search_term + )) + .size(14.0) + .color(egui::Color32::DARK_GRAY), + ); + } else { + ui.label( + egui::RichText::new("❌") + .size(48.0) + .color(egui::Color32::GRAY), + ); + ui.label( + egui::RichText::new("No keys match current filters") + .size(18.0) + .color(egui::Color32::GRAY), + ); + ui.label( + egui::RichText::new("Try adjusting your search or filter settings") + .size(14.0) + .color(egui::Color32::DARK_GRAY), + ); + } + }); +} + +/// Render individual key item +fn render_key_item( + ui: &mut egui::Ui, + key: &crate::web_gui::state::SshKey, + server_name: &str, +) -> Option { + let mut action = None; + + ui.group(|ui| { + ui.horizontal(|ui| { + // Key type badge + let key_type = get_key_type(&key.public_key); + let badge_color = match key_type.as_str() { + "RSA" => egui::Color32::from_rgb(52, 144, 220), + "ED25519" => egui::Color32::from_rgb(46, 204, 113), + "ECDSA" => egui::Color32::from_rgb(241, 196, 15), + "DSA" => egui::Color32::from_rgb(230, 126, 34), + _ => egui::Color32::GRAY, + }; + + ui.colored_label(badge_color, &key_type); + ui.add_space(5.0); + + // Status badge + if key.deprecated { + ui.colored_label(egui::Color32::RED, "❗ DEPRECATED"); + } else { + ui.colored_label(egui::Color32::GREEN, "✅ ACTIVE"); + } + + ui.add_space(5.0); + + // Key preview + ui.monospace(get_key_preview(&key.public_key)); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + // Key action buttons + if key.deprecated { + if ui.small_button("Restore").clicked() { + action = Some(KeyAction::RestoreKey(server_name.to_string())); + } + if ui.small_button("Delete").clicked() { + action = Some(KeyAction::DeleteKey(server_name.to_string())); + } + } else { + if ui.small_button("Deprecate").clicked() { + action = Some(KeyAction::DeprecateKey(server_name.to_string())); + } + } + + if ui.small_button("Copy").clicked() { + ui.output_mut(|o| o.copied_text = key.public_key.clone()); + } + }); + }); + }); + + action +} + +#[derive(Debug, Clone)] +pub enum ConnectionAction { + None, + LoadFlows, + TestConnection, + LoadKeys, + LoadVersion, +} \ No newline at end of file diff --git a/src/web_gui/wasm.rs b/src/web_gui/wasm.rs new file mode 100644 index 0000000..def5e86 --- /dev/null +++ b/src/web_gui/wasm.rs @@ -0,0 +1,43 @@ +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +use wasm_bindgen::prelude::*; + +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +use super::app::WebAdminApp; + +/// WASM entry point for the web admin application +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +#[wasm_bindgen] +pub fn start_web_admin(canvas_id: &str) -> Result<(), JsValue> { + // Setup console logging for WASM + console_error_panic_hook::set_once(); + tracing_wasm::set_as_global_default(); + + let web_options = eframe::WebOptions::default(); + let canvas_id = canvas_id.to_string(); + + wasm_bindgen_futures::spawn_local(async move { + let app = WebAdminApp::default(); + + let result = eframe::WebRunner::new() + .start( + &canvas_id, + web_options, + Box::new(|_cc| Ok(Box::new(app))), + ) + .await; + + match result { + Ok(_) => web_sys::console::log_1(&"eframe started successfully".into()), + Err(e) => web_sys::console::error_1(&format!("Failed to start eframe: {:?}", e).into()), + } + }); + + Ok(()) +} + +/// Initialize the WASM module +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +#[wasm_bindgen(start)] +pub fn wasm_main() { + console_error_panic_hook::set_once(); +} \ No newline at end of file diff --git a/src/web_gui/wasm_api.rs b/src/web_gui/wasm_api.rs new file mode 100644 index 0000000..31ca522 --- /dev/null +++ b/src/web_gui/wasm_api.rs @@ -0,0 +1,131 @@ +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +use super::state::{SshKey, DnsResult, AdminSettings}; +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +use wasm_bindgen::prelude::*; +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +use wasm_bindgen_futures::JsFuture; +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +use web_sys::{Request, RequestInit, RequestMode, Response}; + +/// Simplified API for WASM - uses browser fetch API +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +pub async fn test_connection(settings: &AdminSettings) -> Result { + let url = format!("{}/{}/keys", settings.server_url.trim_end_matches('/'), settings.selected_flow); + + let response = fetch_json(&url).await?; + let keys: Result, _> = serde_json::from_str(&response); + + match keys { + Ok(keys) => Ok(format!("Connection successful! Found {} SSH keys from flow '{}'", keys.len(), settings.selected_flow)), + Err(e) => Err(format!("Failed to parse response: {}", e)), + } +} + +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +pub async fn load_flows(settings: &AdminSettings) -> Result, String> { + let url = format!("{}/api/flows", settings.server_url.trim_end_matches('/')); + + let response = fetch_json(&url).await?; + let flows: Result, _> = serde_json::from_str(&response); + + flows.map_err(|e| format!("Failed to parse flows: {}", e)) +} + +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +pub async fn fetch_keys(settings: &AdminSettings) -> Result, String> { + let url = format!("{}/{}/keys", settings.server_url.trim_end_matches('/'), settings.selected_flow); + + let response = fetch_json(&url).await?; + let keys: Result, _> = serde_json::from_str(&response); + + keys.map_err(|e| format!("Failed to parse keys: {}", e)) +} + +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +pub async fn get_version(settings: &AdminSettings) -> Result { + let url = format!("{}/api/version", settings.server_url.trim_end_matches('/')); + + let response = fetch_json(&url).await?; + let version_response: Result = serde_json::from_str(&response); + + match version_response { + Ok(data) => { + let version = data.get("version") + .and_then(|v| v.as_str()) + .unwrap_or("unknown") + .to_string(); + Ok(version) + } + Err(e) => Err(format!("Failed to parse version: {}", e)), + } +} + +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +pub async fn deprecate_key(_settings: &AdminSettings, server: &str) -> Result { + Ok(format!("WASM: Would deprecate key for {}", server)) +} + +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +pub async fn restore_key(_settings: &AdminSettings, server: &str) -> Result { + Ok(format!("WASM: Would restore key for {}", server)) +} + +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +pub async fn delete_key(_settings: &AdminSettings, server: &str) -> Result { + Ok(format!("WASM: Would delete key for {}", server)) +} + +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +pub async fn bulk_deprecate_servers(_settings: &AdminSettings, servers: Vec) -> Result { + Ok(format!("WASM: Would bulk deprecate {} servers", servers.len())) +} + +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +pub async fn bulk_restore_servers(_settings: &AdminSettings, servers: Vec) -> Result { + Ok(format!("WASM: Would bulk restore {} servers", servers.len())) +} + +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +pub async fn scan_dns_resolution(_settings: &AdminSettings) -> Result, String> { + Ok(vec![ + DnsResult { + server: "demo-server".to_string(), + resolved: true, + error: None, + } + ]) +} + +/// Helper function to make HTTP requests using browser's fetch API +#[cfg(all(target_arch = "wasm32", feature = "web-gui"))] +async fn fetch_json(url: &str) -> Result { + let window = web_sys::window().ok_or("No window object")?; + + let mut opts = RequestInit::new(); + opts.method("GET"); + opts.mode(RequestMode::Cors); + + let request = Request::new_with_str_and_init(url, &opts) + .map_err(|e| format!("Failed to create request: {:?}", e))?; + + let resp_value = JsFuture::from(window.fetch_with_request(&request)) + .await + .map_err(|e| format!("Request failed: {:?}", e))?; + + let resp: Response = resp_value.dyn_into() + .map_err(|e| format!("Failed to cast response: {:?}", e))?; + + if !resp.ok() { + return Err(format!("HTTP error: {} {}", resp.status(), resp.status_text())); + } + + let text_promise = resp.text() + .map_err(|e| format!("Failed to get text promise: {:?}", e))?; + + let text_value = JsFuture::from(text_promise) + .await + .map_err(|e| format!("Failed to get text: {:?}", e))?; + + text_value.as_string() + .ok_or("Response is not a string".to_string()) +} \ No newline at end of file