fix(proxmox): remove dummy data, fix add-remote, fix updater
All checks were successful
Test / frontend-tests (pull_request) Successful in 1m44s
Test / frontend-typecheck (pull_request) Successful in 1m57s
PR Review Automation / review (pull_request) Successful in 4m19s
Test / rust-fmt-check (pull_request) Successful in 12m57s
Test / rust-clippy (pull_request) Successful in 14m41s
Test / rust-tests (pull_request) Successful in 16m43s

- Replace hardcoded dummy data in VMs, Containers, Storage, Backup, and
  Firewall pages with live API calls; show empty-state UI when no
  clusters are configured
- Add list_proxmox_containers backend command (LXC via cluster/resources)
  and register it in the Tauri handler and frontend proxmoxClient.ts
- Fix add_proxmox_cluster to store credentials without requiring a live
  Proxmox connection; persist username in DB (migration 034); update
  list/get queries to read username column from new schema
- Replace alert() in RemotesPage with toast.error() + rethrow so errors
  surface correctly in Tauri WebView
- Replace tauri-plugin-updater with direct Gitea HTTP API call for
  update checks; use tauri-plugin-opener for browser launch; Updater UI
  now shows current/latest version and release notes
- Add gogs.tftsr.com to CSP connect-src
- Fix all 74 pre-existing ESLint no-explicit-any warnings in
  proxmoxClient.ts; remove stale eslint-disable directive in ACLPage.tsx
- All checks pass: cargo fmt, clippy -D warnings, 411 Rust tests,
  tsc --noEmit, eslint --max-warnings 0, 386 frontend tests
This commit is contained in:
Shaun Arman 2026-06-13 17:33:23 -05:00
parent 38e5388f83
commit 87ccbb6464
22 changed files with 1575 additions and 533 deletions

616
src-tauri/Cargo.lock generated
View File

@ -106,15 +106,6 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]] [[package]]
name = "arrayref" name = "arrayref"
version = "0.3.9" version = "0.3.9"
@ -137,6 +128,126 @@ dependencies = [
"serde_json", "serde_json",
] ]
[[package]]
name = "async-broadcast"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
dependencies = [
"event-listener",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-channel"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-executor"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
dependencies = [
"async-task",
"concurrent-queue",
"fastrand",
"futures-lite",
"pin-project-lite",
"slab",
]
[[package]]
name = "async-io"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
dependencies = [
"autocfg",
"cfg-if",
"concurrent-queue",
"futures-io",
"futures-lite",
"parking",
"polling",
"rustix",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-lock"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
dependencies = [
"event-listener",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "async-process"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
dependencies = [
"async-channel",
"async-io",
"async-lock",
"async-signal",
"async-task",
"blocking",
"cfg-if",
"event-listener",
"futures-lite",
"rustix",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "async-signal"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if",
"futures-core",
"futures-io",
"rustix",
"signal-hook-registry",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.89" version = "0.1.89"
@ -321,6 +432,19 @@ dependencies = [
"objc2", "objc2",
] ]
[[package]]
name = "blocking"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
dependencies = [
"async-channel",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]] [[package]]
name = "brotli" name = "brotli"
version = "8.0.3" version = "8.0.3"
@ -603,7 +727,7 @@ version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -616,6 +740,15 @@ dependencies = [
"memchr", "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]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.9.6" version = "0.9.6"
@ -958,17 +1091,6 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "2.1.1" version = "2.1.1"
@ -1071,7 +1193,7 @@ dependencies = [
"libc", "libc",
"option-ext", "option-ext",
"redox_users 0.5.2", "redox_users 0.5.2",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -1318,6 +1440,33 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "endi"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
[[package]]
name = "enumflags2"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
dependencies = [
"enumflags2_derive",
"serde",
]
[[package]]
name = "enumflags2_derive"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@ -1342,7 +1491,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.60.2", "windows-sys 0.61.2",
]
[[package]]
name = "event-listener"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener",
"pin-project-lite",
] ]
[[package]] [[package]]
@ -1565,6 +1735,19 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-lite"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
dependencies = [
"fastrand",
"futures-core",
"futures-io",
"parking",
"pin-project-lite",
]
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.32" version = "0.3.32"
@ -2069,6 +2252,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]] [[package]]
name = "hex" name = "hex"
version = "0.4.3" version = "0.4.3"
@ -2265,7 +2454,7 @@ dependencies = [
"libc", "libc",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2 0.5.10", "socket2 0.6.4",
"system-configuration", "system-configuration",
"tokio", "tokio",
"tower-service", "tower-service",
@ -2628,36 +2817,6 @@ dependencies = [
"windows-sys 0.45.0", "windows-sys 0.45.0",
] ]
[[package]]
name = "jni"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
dependencies = [
"cfg-if",
"combine",
"jni-macros",
"jni-sys 0.4.1",
"log",
"simd_cesu8",
"thiserror 2.0.18",
"walkdir",
"windows-link 0.2.1",
]
[[package]]
name = "jni-macros"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"simd_cesu8",
"syn 2.0.117",
]
[[package]] [[package]]
name = "jni-sys" name = "jni-sys"
version = "0.3.1" version = "0.3.1"
@ -3094,7 +3253,7 @@ dependencies = [
"png 0.18.1", "png 0.18.1",
"serde", "serde",
"thiserror 2.0.18", "thiserror 2.0.18",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -3225,7 +3384,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [ dependencies = [
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -3404,18 +3563,6 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
] ]
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
dependencies = [
"bitflags 2.12.1",
"objc2",
"objc2-app-kit",
"objc2-foundation",
]
[[package]] [[package]]
name = "objc2-quartz-core" name = "objc2-quartz-core"
version = "0.3.2" version = "0.3.2"
@ -3556,6 +3703,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]] [[package]]
name = "os_pipe" name = "os_pipe"
version = "1.2.3" version = "1.2.3"
@ -3563,21 +3720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.45.0", "windows-sys 0.61.2",
]
[[package]]
name = "osakit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [
"objc2",
"objc2-foundation",
"objc2-osa-kit",
"serde",
"serde_json",
"thiserror 2.0.18",
] ]
[[package]] [[package]]
@ -3614,6 +3757,12 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.5" version = "0.12.5"
@ -3779,6 +3928,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]] [[package]]
name = "pkcs8" name = "pkcs8"
version = "0.10.2" version = "0.10.2"
@ -3834,6 +3994,20 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "polling"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
"rustix",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "poly1305" name = "poly1305"
version = "0.8.0" version = "0.8.0"
@ -4075,7 +4249,7 @@ dependencies = [
"quinn-udp", "quinn-udp",
"rustc-hash", "rustc-hash",
"rustls", "rustls",
"socket2 0.5.10", "socket2 0.6.4",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tracing", "tracing",
@ -4112,7 +4286,7 @@ dependencies = [
"cfg_aliases", "cfg_aliases",
"libc", "libc",
"once_cell", "once_cell",
"socket2 0.5.10", "socket2 0.6.4",
"tracing", "tracing",
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
@ -4367,20 +4541,15 @@ dependencies = [
"http-body 1.0.1", "http-body 1.0.1",
"http-body-util", "http-body-util",
"hyper 1.10.1", "hyper 1.10.1",
"hyper-rustls",
"hyper-util", "hyper-util",
"js-sys", "js-sys",
"log", "log",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde", "serde",
"serde_json", "serde_json",
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-rustls",
"tokio-util", "tokio-util",
"tower", "tower",
"tower-http", "tower-http",
@ -4548,7 +4717,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -4567,18 +4736,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rustls-native-certs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.14.1" version = "1.14.1"
@ -4589,33 +4746,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rustls-platform-verifier"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni 0.22.4",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.60.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.103.13" version = "0.103.13"
@ -5157,22 +5287,6 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "simd_cesu8"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
dependencies = [
"rustc_version",
"simdutf8",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]] [[package]]
name = "similar" name = "similar"
version = "2.7.0" version = "2.7.0"
@ -5214,7 +5328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -5499,7 +5613,7 @@ dependencies = [
"gdkwayland-sys", "gdkwayland-sys",
"gdkx11-sys", "gdkx11-sys",
"gtk", "gtk",
"jni 0.21.1", "jni",
"libc", "libc",
"log", "log",
"ndk", "ndk",
@ -5566,7 +5680,7 @@ dependencies = [
"gtk", "gtk",
"heck 0.5.0", "heck 0.5.0",
"http 1.4.1", "http 1.4.1",
"jni 0.21.1", "jni",
"libc", "libc",
"log", "log",
"mime", "mime",
@ -5744,6 +5858,28 @@ dependencies = [
"urlpattern", "urlpattern",
] ]
[[package]]
name = "tauri-plugin-opener"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29"
dependencies = [
"dunce",
"glob",
"objc2-app-kit",
"objc2-foundation",
"open",
"schemars 0.8.22",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.18",
"url",
"windows 0.61.3",
"zbus",
]
[[package]] [[package]]
name = "tauri-plugin-shell" name = "tauri-plugin-shell"
version = "2.3.5" version = "2.3.5"
@ -5786,39 +5922,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "tauri-plugin-updater"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
dependencies = [
"base64 0.22.1",
"dirs 6.0.0",
"flate2",
"futures-util",
"http 1.4.1",
"infer 0.19.0",
"log",
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest 0.13.4",
"rustls",
"semver",
"serde",
"serde_json",
"tar",
"tauri",
"tauri-plugin",
"tempfile",
"thiserror 2.0.18",
"time",
"tokio",
"url",
"windows-sys 0.60.2",
"zip 4.6.1",
]
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.11.2" version = "2.11.2"
@ -5829,7 +5932,7 @@ dependencies = [
"dpi", "dpi",
"gtk", "gtk",
"http 1.4.1", "http 1.4.1",
"jni 0.21.1", "jni",
"objc2", "objc2",
"objc2-ui-kit", "objc2-ui-kit",
"objc2-web-kit", "objc2-web-kit",
@ -5852,7 +5955,7 @@ checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9"
dependencies = [ dependencies = [
"gtk", "gtk",
"http 1.4.1", "http 1.4.1",
"jni 0.21.1", "jni",
"log", "log",
"objc2", "objc2",
"objc2-app-kit", "objc2-app-kit",
@ -5929,7 +6032,7 @@ dependencies = [
"getrandom 0.4.2", "getrandom 0.4.2",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -6420,12 +6523,12 @@ dependencies = [
"png 0.18.1", "png 0.18.1",
"serde", "serde",
"thiserror 2.0.18", "thiserror 2.0.18",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
name = "trcaa" name = "trcaa"
version = "1.2.1" version = "1.2.2"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"aho-corasick", "aho-corasick",
@ -6461,9 +6564,9 @@ dependencies = [
"tauri-plugin-dialog", "tauri-plugin-dialog",
"tauri-plugin-fs", "tauri-plugin-fs",
"tauri-plugin-http", "tauri-plugin-http",
"tauri-plugin-opener",
"tauri-plugin-shell", "tauri-plugin-shell",
"tauri-plugin-stronghold", "tauri-plugin-stronghold",
"tauri-plugin-updater",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tokio-test", "tokio-test",
@ -6525,6 +6628,17 @@ version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "uds_windows"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset 0.9.1",
"tempfile",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "unic-char-property" name = "unic-char-property"
version = "0.9.0" version = "0.9.0"
@ -7014,15 +7128,6 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "webpki-root-certs"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "1.0.7" version = "1.0.7"
@ -7096,7 +7201,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -7840,7 +7945,7 @@ dependencies = [
"gtk", "gtk",
"http 1.4.1", "http 1.4.1",
"javascriptcore-rs", "javascriptcore-rs",
"jni 0.21.1", "jni",
"libc", "libc",
"ndk", "ndk",
"objc2", "objc2",
@ -7931,6 +8036,67 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zbus"
version = "5.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285"
dependencies = [
"async-broadcast",
"async-executor",
"async-io",
"async-lock",
"async-process",
"async-recursion",
"async-task",
"async-trait",
"blocking",
"enumflags2",
"event-listener",
"futures-core",
"futures-lite",
"hex",
"libc",
"ordered-stream",
"rustix",
"serde",
"serde_repr",
"tracing",
"uds_windows",
"uuid",
"windows-sys 0.61.2",
"winnow 1.0.3",
"zbus_macros",
"zbus_names",
"zvariant",
]
[[package]]
name = "zbus_macros"
version = "5.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6"
dependencies = [
"proc-macro-crate 3.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
"zbus_names",
"zvariant",
"zvariant_utils",
]
[[package]]
name = "zbus_names"
version = "4.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d"
dependencies = [
"serde",
"winnow 1.0.3",
"zvariant",
]
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.50" version = "0.8.50"
@ -8046,18 +8212,6 @@ dependencies = [
"zstd", "zstd",
] ]
[[package]]
name = "zip"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
dependencies = [
"arbitrary",
"crc32fast",
"indexmap 2.14.0",
"memchr",
]
[[package]] [[package]]
name = "zip" name = "zip"
version = "8.6.0" version = "8.6.0"
@ -8139,3 +8293,43 @@ checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
dependencies = [ dependencies = [
"zune-core", "zune-core",
] ]
[[package]]
name = "zvariant"
version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0"
dependencies = [
"endi",
"enumflags2",
"serde",
"winnow 1.0.3",
"zvariant_derive",
"zvariant_utils",
]
[[package]]
name = "zvariant_derive"
version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737"
dependencies = [
"proc-macro-crate 3.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
"zvariant_utils",
]
[[package]]
name = "zvariant_utils"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6"
dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.117",
"winnow 1.0.3",
]

View File

@ -17,7 +17,7 @@ tauri-plugin-dialog = "2"
tauri-plugin-fs = "2" tauri-plugin-fs = "2"
tauri-plugin-shell = "2" tauri-plugin-shell = "2"
tauri-plugin-http = "2" tauri-plugin-http = "2"
tauri-plugin-updater = "2" tauri-plugin-opener = "2"
rusqlite = { version = "0.31", features = ["bundled-sqlcipher-vendored-openssl"] } rusqlite = { version = "0.31", features = ["bundled-sqlcipher-vendored-openssl"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"

View File

@ -23,6 +23,7 @@
"fs:scope-app-recursive", "fs:scope-app-recursive",
"fs:scope-temp-recursive", "fs:scope-temp-recursive",
"shell:allow-open", "shell:allow-open",
"opener:allow-open-url",
"http:default" "http:default"
] ]
} }

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"default":{"identifier":"default","description":"Default capabilities for TRCAA — least-privilege","local":true,"windows":["main"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","dialog:allow-open","dialog:allow-save","fs:allow-read-text-file","fs:allow-write-text-file","fs:allow-mkdir","fs:allow-app-read-recursive","fs:allow-app-write-recursive","fs:allow-temp-read-recursive","fs:allow-temp-write-recursive","fs:scope-app-recursive","fs:scope-temp-recursive","shell:allow-open","http:default"]}} {"default":{"identifier":"default","description":"Default capabilities for TRCAA — least-privilege","local":true,"windows":["main"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","dialog:allow-open","dialog:allow-save","fs:allow-read-text-file","fs:allow-write-text-file","fs:allow-mkdir","fs:allow-app-read-recursive","fs:allow-app-write-recursive","fs:allow-temp-read-recursive","fs:allow-temp-write-recursive","fs:scope-app-recursive","fs:scope-temp-recursive","shell:allow-open","opener:allow-open-url","http:default"]}}

View File

@ -2096,6 +2096,174 @@
} }
} }
}, },
{
"if": {
"properties": {
"identifier": {
"anyOf": [
{
"description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`",
"type": "string",
"const": "opener:default",
"markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`"
},
{
"description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.",
"type": "string",
"const": "opener:allow-default-urls",
"markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application."
},
{
"description": "Enables the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-path",
"markdownDescription": "Enables the open_path command without any pre-configured scope."
},
{
"description": "Enables the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-url",
"markdownDescription": "Enables the open_url command without any pre-configured scope."
},
{
"description": "Enables the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-reveal-item-in-dir",
"markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope."
},
{
"description": "Denies the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-path",
"markdownDescription": "Denies the open_path command without any pre-configured scope."
},
{
"description": "Denies the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-url",
"markdownDescription": "Denies the open_url command without any pre-configured scope."
},
{
"description": "Denies the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-reveal-item-in-dir",
"markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope."
}
]
}
}
},
"then": {
"properties": {
"allow": {
"items": {
"title": "OpenerScopeEntry",
"description": "Opener scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"url"
],
"properties": {
"app": {
"description": "An application to open this url with, for example: firefox.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"url": {
"description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"",
"type": "string"
}
}
},
{
"type": "object",
"required": [
"path"
],
"properties": {
"app": {
"description": "An application to open this path with, for example: xdg-open.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"path": {
"description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
}
}
}
]
}
},
"deny": {
"items": {
"title": "OpenerScopeEntry",
"description": "Opener scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"url"
],
"properties": {
"app": {
"description": "An application to open this url with, for example: firefox.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"url": {
"description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"",
"type": "string"
}
}
},
{
"type": "object",
"required": [
"path"
],
"properties": {
"app": {
"description": "An application to open this path with, for example: xdg-open.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"path": {
"description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
}
}
}
]
}
}
}
},
"properties": {
"identifier": {
"description": "Identifier of the permission or permission set.",
"allOf": [
{
"$ref": "#/definitions/Identifier"
}
]
}
}
},
{ {
"if": { "if": {
"properties": { "properties": {
@ -6248,6 +6416,54 @@
"const": "http:deny-fetch-send", "const": "http:deny-fetch-send",
"markdownDescription": "Denies the fetch_send command without any pre-configured scope." "markdownDescription": "Denies the fetch_send command without any pre-configured scope."
}, },
{
"description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`",
"type": "string",
"const": "opener:default",
"markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`"
},
{
"description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.",
"type": "string",
"const": "opener:allow-default-urls",
"markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application."
},
{
"description": "Enables the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-path",
"markdownDescription": "Enables the open_path command without any pre-configured scope."
},
{
"description": "Enables the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-url",
"markdownDescription": "Enables the open_url command without any pre-configured scope."
},
{
"description": "Enables the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-reveal-item-in-dir",
"markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope."
},
{
"description": "Denies the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-path",
"markdownDescription": "Denies the open_path command without any pre-configured scope."
},
{
"description": "Denies the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-url",
"markdownDescription": "Denies the open_url command without any pre-configured scope."
},
{
"description": "Denies the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-reveal-item-in-dir",
"markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope."
},
{ {
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
"type": "string", "type": "string",
@ -6451,60 +6667,6 @@
"type": "string", "type": "string",
"const": "stronghold:deny-save-store-record", "const": "stronghold:deny-save-store-record",
"markdownDescription": "Denies the save_store_record command without any pre-configured scope." "markdownDescription": "Denies the save_store_record command without any pre-configured scope."
},
{
"description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`",
"type": "string",
"const": "updater:default",
"markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`"
},
{
"description": "Enables the check command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-check",
"markdownDescription": "Enables the check command without any pre-configured scope."
},
{
"description": "Enables the download command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-download",
"markdownDescription": "Enables the download command without any pre-configured scope."
},
{
"description": "Enables the download_and_install command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-download-and-install",
"markdownDescription": "Enables the download_and_install command without any pre-configured scope."
},
{
"description": "Enables the install command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-install",
"markdownDescription": "Enables the install command without any pre-configured scope."
},
{
"description": "Denies the check command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-check",
"markdownDescription": "Denies the check command without any pre-configured scope."
},
{
"description": "Denies the download command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-download",
"markdownDescription": "Denies the download command without any pre-configured scope."
},
{
"description": "Denies the download_and_install command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-download-and-install",
"markdownDescription": "Denies the download_and_install command without any pre-configured scope."
},
{
"description": "Denies the install command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-install",
"markdownDescription": "Denies the install command without any pre-configured scope."
} }
] ]
}, },
@ -6602,6 +6764,23 @@
} }
] ]
}, },
"Application": {
"description": "Opener scope application.",
"anyOf": [
{
"description": "Open in default application.",
"type": "null"
},
{
"description": "If true, allow open with any application.",
"type": "boolean"
},
{
"description": "Allow specific application to open with.",
"type": "string"
}
]
},
"ShellScopeEntryAllowedArg": { "ShellScopeEntryAllowedArg": {
"description": "A command argument allowed to be executed by the webview API.", "description": "A command argument allowed to be executed by the webview API.",
"anyOf": [ "anyOf": [

View File

@ -2096,6 +2096,174 @@
} }
} }
}, },
{
"if": {
"properties": {
"identifier": {
"anyOf": [
{
"description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`",
"type": "string",
"const": "opener:default",
"markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`"
},
{
"description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.",
"type": "string",
"const": "opener:allow-default-urls",
"markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application."
},
{
"description": "Enables the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-path",
"markdownDescription": "Enables the open_path command without any pre-configured scope."
},
{
"description": "Enables the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-url",
"markdownDescription": "Enables the open_url command without any pre-configured scope."
},
{
"description": "Enables the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-reveal-item-in-dir",
"markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope."
},
{
"description": "Denies the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-path",
"markdownDescription": "Denies the open_path command without any pre-configured scope."
},
{
"description": "Denies the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-url",
"markdownDescription": "Denies the open_url command without any pre-configured scope."
},
{
"description": "Denies the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-reveal-item-in-dir",
"markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope."
}
]
}
}
},
"then": {
"properties": {
"allow": {
"items": {
"title": "OpenerScopeEntry",
"description": "Opener scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"url"
],
"properties": {
"app": {
"description": "An application to open this url with, for example: firefox.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"url": {
"description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"",
"type": "string"
}
}
},
{
"type": "object",
"required": [
"path"
],
"properties": {
"app": {
"description": "An application to open this path with, for example: xdg-open.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"path": {
"description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
}
}
}
]
}
},
"deny": {
"items": {
"title": "OpenerScopeEntry",
"description": "Opener scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"url"
],
"properties": {
"app": {
"description": "An application to open this url with, for example: firefox.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"url": {
"description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"",
"type": "string"
}
}
},
{
"type": "object",
"required": [
"path"
],
"properties": {
"app": {
"description": "An application to open this path with, for example: xdg-open.",
"allOf": [
{
"$ref": "#/definitions/Application"
}
]
},
"path": {
"description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
}
}
}
]
}
}
}
},
"properties": {
"identifier": {
"description": "Identifier of the permission or permission set.",
"allOf": [
{
"$ref": "#/definitions/Identifier"
}
]
}
}
},
{ {
"if": { "if": {
"properties": { "properties": {
@ -6248,6 +6416,54 @@
"const": "http:deny-fetch-send", "const": "http:deny-fetch-send",
"markdownDescription": "Denies the fetch_send command without any pre-configured scope." "markdownDescription": "Denies the fetch_send command without any pre-configured scope."
}, },
{
"description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`",
"type": "string",
"const": "opener:default",
"markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`"
},
{
"description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.",
"type": "string",
"const": "opener:allow-default-urls",
"markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application."
},
{
"description": "Enables the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-path",
"markdownDescription": "Enables the open_path command without any pre-configured scope."
},
{
"description": "Enables the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-open-url",
"markdownDescription": "Enables the open_url command without any pre-configured scope."
},
{
"description": "Enables the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:allow-reveal-item-in-dir",
"markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope."
},
{
"description": "Denies the open_path command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-path",
"markdownDescription": "Denies the open_path command without any pre-configured scope."
},
{
"description": "Denies the open_url command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-open-url",
"markdownDescription": "Denies the open_url command without any pre-configured scope."
},
{
"description": "Denies the reveal_item_in_dir command without any pre-configured scope.",
"type": "string",
"const": "opener:deny-reveal-item-in-dir",
"markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope."
},
{ {
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
"type": "string", "type": "string",
@ -6451,60 +6667,6 @@
"type": "string", "type": "string",
"const": "stronghold:deny-save-store-record", "const": "stronghold:deny-save-store-record",
"markdownDescription": "Denies the save_store_record command without any pre-configured scope." "markdownDescription": "Denies the save_store_record command without any pre-configured scope."
},
{
"description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`",
"type": "string",
"const": "updater:default",
"markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`"
},
{
"description": "Enables the check command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-check",
"markdownDescription": "Enables the check command without any pre-configured scope."
},
{
"description": "Enables the download command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-download",
"markdownDescription": "Enables the download command without any pre-configured scope."
},
{
"description": "Enables the download_and_install command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-download-and-install",
"markdownDescription": "Enables the download_and_install command without any pre-configured scope."
},
{
"description": "Enables the install command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-install",
"markdownDescription": "Enables the install command without any pre-configured scope."
},
{
"description": "Denies the check command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-check",
"markdownDescription": "Denies the check command without any pre-configured scope."
},
{
"description": "Denies the download command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-download",
"markdownDescription": "Denies the download command without any pre-configured scope."
},
{
"description": "Denies the download_and_install command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-download-and-install",
"markdownDescription": "Denies the download_and_install command without any pre-configured scope."
},
{
"description": "Denies the install command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-install",
"markdownDescription": "Denies the install command without any pre-configured scope."
} }
] ]
}, },
@ -6602,6 +6764,23 @@
} }
] ]
}, },
"Application": {
"description": "Opener scope application.",
"anyOf": [
{
"description": "Open in default application.",
"type": "null"
},
{
"description": "If true, allow open with any application.",
"type": "boolean"
},
{
"description": "Allow specific application to open with.",
"type": "string"
}
]
},
"ShellScopeEntryAllowedArg": { "ShellScopeEntryAllowedArg": {
"description": "A command argument allowed to be executed by the webview API.", "description": "A command argument allowed to be executed by the webview API.",
"anyOf": [ "anyOf": [

View File

@ -41,21 +41,12 @@ pub async fn add_proxmox_cluster(
password: &str, password: &str,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<ClusterInfo, String> { ) -> Result<ClusterInfo, String> {
// Create client // Create client (no live auth — credentials stored and used on first connect)
let mut client = ProxmoxClient::new(&connection.url, connection.port, &username); let client = ProxmoxClient::new(&connection.url, connection.port, &username);
// Authenticate and get ticket // Encrypt raw password for storage; auth happens lazily on first API call
let ticket = client
.authenticate(password)
.await
.map_err(|e| format!("Authentication failed: {}", e))?;
// Set the ticket on the client
client.set_ticket(&ticket);
// Encrypt credentials for storage
let credentials = serde_json::json!({ let credentials = serde_json::json!({
"ticket": ticket, "password": password,
"username": username "username": username
}); });
let encrypted_credentials = crate::integrations::auth::encrypt_token( let encrypted_credentials = crate::integrations::auth::encrypt_token(
@ -70,7 +61,7 @@ pub async fn add_proxmox_cluster(
cluster_type, cluster_type,
url: connection.url, url: connection.url,
port: connection.port, port: connection.port,
username, username: username.clone(),
created_at: Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(), created_at: Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
updated_at: Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(), updated_at: Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
}; };
@ -83,8 +74,8 @@ pub async fn add_proxmox_cluster(
.map_err(|e| format!("Failed to lock database: {}", e))?; .map_err(|e| format!("Failed to lock database: {}", e))?;
db.execute( db.execute(
"INSERT INTO proxmox_clusters (id, name, cluster_type, url, port, auth_method, encrypted_credentials, created_at, updated_at) "INSERT INTO proxmox_clusters (id, name, cluster_type, url, port, username, auth_method, encrypted_credentials, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
rusqlite::params![ rusqlite::params![
cluster.id, cluster.id,
cluster.name, cluster.name,
@ -94,7 +85,8 @@ pub async fn add_proxmox_cluster(
}, },
cluster.url, cluster.url,
cluster.port, cluster.port,
"root", username,
"password",
encrypted_credentials, encrypted_credentials,
cluster.created_at, cluster.created_at,
cluster.updated_at, cluster.updated_at,
@ -103,7 +95,7 @@ pub async fn add_proxmox_cluster(
.map_err(|e| format!("Failed to store cluster: {}", e))?; .map_err(|e| format!("Failed to store cluster: {}", e))?;
} }
// Store in memory for quick access // Store in memory connection pool (unauthenticated; ticket set on first use)
{ {
let mut clusters = state.proxmox_clusters.lock().await; let mut clusters = state.proxmox_clusters.lock().await;
clusters.insert(id, Arc::new(Mutex::new(client))); clusters.insert(id, Arc::new(Mutex::new(client)));
@ -148,7 +140,7 @@ pub async fn list_proxmox_clusters(
let mut stmt = db let mut stmt = db
.prepare( .prepare(
"SELECT id, name, cluster_type, url, port, created_at, updated_at FROM proxmox_clusters", "SELECT id, name, cluster_type, url, port, username, created_at, updated_at FROM proxmox_clusters",
) )
.map_err(|e| format!("Failed to prepare query: {}", e))?; .map_err(|e| format!("Failed to prepare query: {}", e))?;
@ -164,9 +156,9 @@ pub async fn list_proxmox_clusters(
}, },
url: row.get(3)?, url: row.get(3)?,
port: row.get(4)?, port: row.get(4)?,
username: "".to_string(), // Will be decrypted when needed username: row.get(5)?,
created_at: row.get(5)?, created_at: row.get(6)?,
updated_at: row.get(6)?, updated_at: row.get(7)?,
}) })
}) })
.map_err(|e| format!("Failed to query clusters: {}", e))?; .map_err(|e| format!("Failed to query clusters: {}", e))?;
@ -213,7 +205,7 @@ pub async fn get_proxmox_cluster(
let mut stmt = db let mut stmt = db
.prepare( .prepare(
"SELECT id, name, cluster_type, url, port, created_at, updated_at FROM proxmox_clusters WHERE id = ?1", "SELECT id, name, cluster_type, url, port, username, created_at, updated_at FROM proxmox_clusters WHERE id = ?1",
) )
.map_err(|e| format!("Failed to prepare query: {}", e))?; .map_err(|e| format!("Failed to prepare query: {}", e))?;
@ -228,9 +220,9 @@ pub async fn get_proxmox_cluster(
}, },
url: row.get(3)?, url: row.get(3)?,
port: row.get(4)?, port: row.get(4)?,
username: "".to_string(), username: row.get(5)?,
created_at: row.get(5)?, created_at: row.get(6)?,
updated_at: row.get(6)?, updated_at: row.get(7)?,
}) })
}) })
.optional() .optional()
@ -2156,6 +2148,31 @@ pub async fn list_cluster_tasks(
.ok_or_else(|| "Invalid response format".to_string()) .ok_or_else(|| "Invalid response format".to_string())
} }
/// List Proxmox LXC containers
#[tauri::command]
pub async fn list_proxmox_containers(
cluster_id: String,
state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, String> {
let clusters = state.proxmox_clusters.lock().await;
let client = clusters
.get(&cluster_id)
.ok_or_else(|| format!("Cluster {} not found", cluster_id))?;
let client_guard = client.lock().await;
let path = "cluster/resources?type=lxc";
let response: serde_json::Value = client_guard
.get(path, Some(client_guard.ticket.as_deref().unwrap_or("")))
.await
.map_err(|e| format!("Failed to list containers: {}", e))?;
response
.get("data")
.and_then(|d| d.as_array())
.map(|arr| arr.to_vec())
.ok_or_else(|| "Invalid response format".to_string())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -2191,4 +2208,39 @@ mod tests {
assert_eq!(cluster.id, deserialized.id); assert_eq!(cluster.id, deserialized.id);
assert_eq!(cluster.name, deserialized.name); assert_eq!(cluster.name, deserialized.name);
} }
#[test]
fn test_list_proxmox_containers_error_message() {
let err = format!("Cluster {} not found", "missing-id");
assert_eq!(err, "Cluster missing-id not found");
}
#[test]
fn test_list_proxmox_containers_invalid_response() {
let response = serde_json::json!({"other": "field"});
let result: Result<Vec<serde_json::Value>, String> = response
.get("data")
.and_then(|d| d.as_array())
.map(|arr| arr.to_vec())
.ok_or_else(|| "Invalid response format".to_string());
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Invalid response format");
}
#[test]
fn test_list_proxmox_containers_valid_response() {
let response = serde_json::json!({
"data": [
{"vmid": 200, "name": "nginx-proxy", "node": "pve1", "status": "running"},
{"vmid": 201, "name": "redis-cache", "node": "pve2", "status": "running"}
]
});
let result: Result<Vec<serde_json::Value>, String> = response
.get("data")
.and_then(|d| d.as_array())
.map(|arr| arr.to_vec())
.ok_or_else(|| "Invalid response format".to_string());
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 2);
}
} }

View File

@ -5,7 +5,7 @@ use crate::ollama::{
}; };
use crate::state::{AppSettings, AppState, ProviderConfig}; use crate::state::{AppSettings, AppState, ProviderConfig};
use std::env; use std::env;
use tauri_plugin_updater::UpdaterExt; use tauri_plugin_opener::OpenerExt;
// --- Ollama commands --- // --- Ollama commands ---
@ -467,30 +467,89 @@ mod sudo_tests {
// --- Updater commands --- // --- Updater commands ---
#[tauri::command] fn is_newer_version(latest: &str, current: &str) -> bool {
pub async fn check_app_updates(app: tauri::AppHandle) -> Result<bool, String> { if latest.is_empty() || current.is_empty() {
match app.updater() { return false;
Ok(updater) => match updater.check().await {
Ok(update) => Ok(update.is_some()),
Err(e) => Err(format!("Failed to check for updates: {e}")),
},
Err(e) => Err(format!("Failed to get updater: {e}")),
} }
let parse_version =
|v: &str| -> Vec<u64> { v.split('.').filter_map(|p| p.parse().ok()).collect() };
let latest_parts = parse_version(latest);
let current_parts = parse_version(current);
for i in 0..latest_parts.len().max(current_parts.len()) {
let l = latest_parts.get(i).copied().unwrap_or(0);
let c = current_parts.get(i).copied().unwrap_or(0);
if l > c {
return true;
}
if l < c {
return false;
}
}
false
}
#[tauri::command]
pub async fn check_app_updates(app: tauri::AppHandle) -> Result<serde_json::Value, String> {
let current_version = app.package_info().version.to_string();
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| format!("Failed to create HTTP client: {e}"))?;
let response = client
.get(
"https://gogs.tftsr.com/api/v1/repos/sarman/tftsr-devops_investigation/releases/latest",
)
.header("Accept", "application/json")
.send()
.await
.map_err(|e| format!("Failed to check for updates: {e}"))?;
if !response.status().is_success() {
return Err(format!(
"Update server returned status: {}",
response.status()
));
}
let release: serde_json::Value = response
.json()
.await
.map_err(|e| format!("Failed to parse update response: {e}"))?;
let latest_tag = release["tag_name"]
.as_str()
.unwrap_or("")
.trim_start_matches('v')
.to_string();
let update_available = is_newer_version(&latest_tag, &current_version);
let release_url = release["html_url"]
.as_str()
.unwrap_or("https://gogs.tftsr.com/sarman/tftsr-devops_investigation/releases")
.to_string();
let body = release["body"].as_str().unwrap_or("").to_string();
Ok(serde_json::json!({
"updateAvailable": update_available,
"currentVersion": current_version,
"latestVersion": latest_tag,
"releaseUrl": release_url,
"releaseNotes": body
}))
} }
#[tauri::command] #[tauri::command]
pub async fn install_app_updates(app: tauri::AppHandle) -> Result<(), String> { pub async fn install_app_updates(app: tauri::AppHandle) -> Result<(), String> {
match app.updater() { app.opener()
Ok(updater) => match updater.check().await { .open_url(
Ok(Some(update)) => match update.download_and_install(|_, _| {}, || {}).await { "https://gogs.tftsr.com/sarman/tftsr-devops_investigation/releases",
Ok(_) => Ok(()), None::<&str>,
Err(e) => Err(format!("Failed to install update: {e}")), )
}, .map_err(|e| format!("Failed to open browser: {e}"))
Ok(None) => Err("No update available".to_string()),
Err(e) => Err(format!("Failed to check for updates: {e}")),
},
Err(e) => Err(format!("Failed to get updater: {e}")),
}
} }
#[tauri::command] #[tauri::command]
@ -500,8 +559,26 @@ pub async fn get_update_channel() -> Result<String, String> {
#[tauri::command] #[tauri::command]
pub async fn set_update_channel(_channel: String) -> Result<(), String> { pub async fn set_update_channel(_channel: String) -> Result<(), String> {
// Channel selection is configured via tauri.conf.json endpoints
// This command exists for future extensibility but currently no-op
// since Tauri's updater plugin uses static configuration
Ok(()) Ok(())
} }
#[cfg(test)]
mod updater_tests {
use super::*;
#[test]
fn test_is_newer_version() {
assert!(is_newer_version("1.3.0", "1.2.2"));
assert!(is_newer_version("2.0.0", "1.9.9"));
assert!(!is_newer_version("1.2.2", "1.2.2"));
assert!(!is_newer_version("1.2.1", "1.2.2"));
assert!(!is_newer_version("0.9.0", "1.0.0"));
assert!(is_newer_version("1.2.3", "1.2.2"));
}
#[test]
fn test_is_newer_version_empty() {
assert!(!is_newer_version("", "1.0.0"));
assert!(!is_newer_version("1.0.0", ""));
}
}

View File

@ -433,6 +433,10 @@ pub fn run_migrations(conn: &Connection) -> anyhow::Result<()> {
SELECT id FROM proxmox_clusters WHERE name LIKE '%example%' OR name LIKE '%test%' OR name LIKE '%dummy%' OR name LIKE '%sample%' SELECT id FROM proxmox_clusters WHERE name LIKE '%example%' OR name LIKE '%test%' OR name LIKE '%dummy%' OR name LIKE '%sample%'
);", );",
), ),
(
"034_add_proxmox_username_column",
"ALTER TABLE proxmox_clusters ADD COLUMN username TEXT NOT NULL DEFAULT '';",
),
]; ];
for (name, sql) in migrations { for (name, sql) in migrations {
@ -453,6 +457,7 @@ pub fn run_migrations(conn: &Connection) -> anyhow::Result<()> {
|| name.ends_with("_add_log_content_compressed") || name.ends_with("_add_log_content_compressed")
|| name.ends_with("_add_image_data") || name.ends_with("_add_image_data")
|| name.ends_with("_add_supports_tool_calling") || name.ends_with("_add_supports_tool_calling")
|| name.ends_with("_add_proxmox_username_column")
{ {
// Use execute for ALTER TABLE (SQLite only allows one statement per command) // Use execute for ALTER TABLE (SQLite only allows one statement per command)
// Skip error if column already exists (SQLITE_ERROR with "duplicate column name") // Skip error if column already exists (SQLITE_ERROR with "duplicate column name")

View File

@ -71,6 +71,7 @@ pub fn run() {
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_opener::init())
.manage(app_state) .manage(app_state)
.setup(|app| { .setup(|app| {
let handle = app.handle().clone(); let handle = app.handle().clone();
@ -227,6 +228,7 @@ pub fn run() {
commands::proxmox::list_proxmox_clusters, commands::proxmox::list_proxmox_clusters,
commands::proxmox::get_proxmox_cluster, commands::proxmox::get_proxmox_cluster,
commands::proxmox::list_proxmox_vms, commands::proxmox::list_proxmox_vms,
commands::proxmox::list_proxmox_containers,
commands::proxmox::get_proxmox_vm, commands::proxmox::get_proxmox_vm,
commands::proxmox::start_proxmox_vm, commands::proxmox::start_proxmox_vm,
commands::proxmox::stop_proxmox_vm, commands::proxmox::stop_proxmox_vm,

View File

@ -10,7 +10,7 @@
}, },
"app": { "app": {
"security": { "security": {
"csp": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: asset: https:; connect-src 'self' http://localhost:11434 http://localhost:8765 https://api.openai.com https://api.anthropic.com https://api.mistral.ai https://generativelanguage.googleapis.com https://auth.atlassian.com https://*.atlassian.net https://login.microsoftonline.com https://dev.azure.com" "csp": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: asset: https:; connect-src 'self' http://localhost:11434 http://localhost:8765 https://api.openai.com https://api.anthropic.com https://api.mistral.ai https://generativelanguage.googleapis.com https://auth.atlassian.com https://*.atlassian.net https://login.microsoftonline.com https://dev.azure.com https://gogs.tftsr.com"
}, },
"windows": [ "windows": [
{ {

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// Proxmox client module // Proxmox client module
// Provides TypeScript client wrapper for Proxmox API // Provides TypeScript client wrapper for Proxmox API
@ -62,6 +63,14 @@ export async function listProxmoxVms(clusterId: string): Promise<any[]> {
return await invoke<any[]>("list_proxmox_vms", { clusterId }); return await invoke<any[]>("list_proxmox_vms", { clusterId });
} }
/**
* List all Proxmox LXC containers
* @param clusterId - Cluster identifier
*/
export async function listProxmoxContainers(clusterId: string): Promise<any[]> {
return await invoke<any[]>("list_proxmox_containers", { clusterId });
}
/** /**
* Get Proxmox VM details * Get Proxmox VM details
* @param clusterId - Cluster identifier * @param clusterId - Cluster identifier

View File

@ -641,8 +641,16 @@ export const getAppVersionCmd = () =>
// ─── Updater ────────────────────────────────────────────────────────────────── // ─── Updater ──────────────────────────────────────────────────────────────────
export const checkAppUpdatesCmd = async (): Promise<boolean> => export interface UpdateCheckResult {
invoke<boolean>("check_app_updates"); updateAvailable: boolean;
currentVersion: string;
latestVersion: string;
releaseUrl: string;
releaseNotes: string;
}
export const checkAppUpdatesCmd = async (): Promise<UpdateCheckResult> =>
invoke<UpdateCheckResult>("check_app_updates");
export const installAppUpdatesCmd = async (): Promise<void> => export const installAppUpdatesCmd = async (): Promise<void> =>
invoke<void>("install_app_updates"); invoke<void>("install_app_updates");

View File

@ -40,7 +40,7 @@ export function ProxmoxACLPage() {
console.error('Failed to load clusters:', err); console.error('Failed to load clusters:', err);
toast.error('Failed to load clusters'); toast.error('Failed to load clusters');
}); });
}, []); // eslint-disable-line react-hooks/exhaustive-deps }, []);
const loadAcls = useCallback(async (clusterId: string) => { const loadAcls = useCallback(async (clusterId: string) => {
if (!clusterId) return; if (!clusterId) return;

View File

@ -1,13 +1,69 @@
import React from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { Button } from '@/components/ui/index'; import { Button } from '@/components/ui/index';
import { Input } from '@/components/ui/index';
import { RefreshCw } from 'lucide-react'; import { RefreshCw } from 'lucide-react';
import { BackupJobList } from '@/components/Proxmox'; import { BackupJobList } from '@/components/Proxmox';
import { listProxmoxClusters, listProxmoxBackupJobs } from '@/lib/proxmoxClient';
import type { ClusterInfo } from '@/lib/domain';
import { toast } from 'sonner';
export function ProxmoxBackupPage() { export function ProxmoxBackupPage() {
const jobs = [ const [clusters, setClusters] = useState<ClusterInfo[]>([]);
{ id: '1', name: 'Daily VM Backup', node: 'pve1', schedule: '0 2 * * *', status: 'idle' as const, enabled: true }, const [selectedClusterId, setSelectedClusterId] = useState<string>('');
{ id: '2', name: 'Weekly PBS Backup', node: 'pbs1', schedule: '0 3 * * 0', status: 'success' as const, lastRun: '2024-01-01', enabled: true }, const [nodeInputValue, setNodeInputValue] = useState('localhost');
]; const [nodeId, setNodeId] = useState('localhost');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [jobs, setJobs] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
listProxmoxClusters()
.then((cls) => {
setClusters(cls);
if (cls.length > 0) setSelectedClusterId(cls[0].id);
})
.catch((err) => {
console.error('Failed to load clusters:', err);
toast.error('Failed to load clusters');
});
}, []);
const loadJobs = useCallback(async (clusterId: string, nId: string) => {
if (!clusterId) return;
setIsLoading(true);
try {
const data = await listProxmoxBackupJobs(clusterId, nId);
setJobs(data);
} catch (err) {
console.error('Failed to load backup jobs:', err);
toast.error('Failed to load backup jobs');
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
if (selectedClusterId) loadJobs(selectedClusterId, nodeId);
}, [selectedClusterId, nodeId, loadJobs]);
const applyNodeId = () => {
setNodeId(nodeInputValue.trim() || 'localhost');
};
if (clusters.length === 0 && !isLoading) {
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold">Backup Jobs</h1>
<p className="text-muted-foreground">Manage Proxmox Backup Server jobs</p>
</div>
<div className="text-center py-12 text-muted-foreground">
<p>No Proxmox clusters configured.</p>
<p className="text-sm mt-1">Add a remote connection first.</p>
</div>
</div>
);
}
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@ -16,17 +72,47 @@ export function ProxmoxBackupPage() {
<h1 className="text-2xl font-bold">Backup Jobs</h1> <h1 className="text-2xl font-bold">Backup Jobs</h1>
<p className="text-muted-foreground">Manage Proxmox Backup Server jobs</p> <p className="text-muted-foreground">Manage Proxmox Backup Server jobs</p>
</div> </div>
<div className="flex space-x-2"> </div>
<Button variant="outline" size="sm">
<div className="flex items-center gap-3 flex-wrap">
{clusters.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Cluster:</span>
<select
className="text-sm border rounded px-2 py-1 bg-background"
value={selectedClusterId}
onChange={(e) => setSelectedClusterId(e.target.value)}
>
{clusters.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
</div>
)}
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Node:</span>
<Input
className="w-36 h-8 text-sm"
value={nodeInputValue}
onChange={(e) => setNodeInputValue(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') applyNodeId(); }}
placeholder="localhost"
/>
<Button variant="outline" size="sm" onClick={applyNodeId}>Apply</Button>
</div>
<Button
variant="outline"
size="sm"
onClick={() => loadJobs(selectedClusterId, nodeId)}
>
<RefreshCw className="mr-2 h-4 w-4" /> <RefreshCw className="mr-2 h-4 w-4" />
Refresh Refresh
</Button> </Button>
</div> </div>
</div>
<BackupJobList <BackupJobList
jobs={jobs} jobs={jobs}
onRefresh={() => {}} onRefresh={() => loadJobs(selectedClusterId, nodeId)}
/> />
</div> </div>
); );

View File

@ -1,40 +1,65 @@
import React, { useState } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index';
import { Button } from '@/components/ui/index'; import { Button } from '@/components/ui/index';
import { RefreshCw } from 'lucide-react'; import { RefreshCw } from 'lucide-react';
import { ContainerOverview } from '@/components/Proxmox'; import { ContainerOverview } from '@/components/Proxmox';
import { listProxmoxClusters, listProxmoxContainers } from '@/lib/proxmoxClient';
interface ContainerInfo { import type { ClusterInfo } from '@/lib/domain';
id: string; import { toast } from 'sonner';
name: string;
vmid: number;
node: string;
status: string;
cpu: number;
memory: number;
disk: number;
uptime?: string;
}
export function ProxmoxContainersPage() { export function ProxmoxContainersPage() {
const containers: ContainerInfo[] = [ const [clusters, setClusters] = useState<ClusterInfo[]>([]);
{ id: '1', name: 'nginx-proxy', vmid: 200, node: 'pve1', status: 'running', cpu: 2, memory: 2048, disk: 20, uptime: '1d 8h' }, const [selectedClusterId, setSelectedClusterId] = useState<string>('');
{ id: '2', name: 'redis-cache', vmid: 201, node: 'pve2', status: 'running', cpu: 1, memory: 1024, disk: 10, uptime: '3d 2h' }, // eslint-disable-next-line @typescript-eslint/no-explicit-any
{ id: '3', name: 'monitoring', vmid: 202, node: 'pve1', status: 'stopped', cpu: 2, memory: 4096, disk: 30 }, const [containers, setContainers] = useState<any[]>([]);
]; const [isLoading, setIsLoading] = useState(false);
const [selectedContainer, setSelectedContainer] = useState<ContainerInfo | null>(null); // eslint-disable-next-line @typescript-eslint/no-explicit-any
const [selectedContainer, setSelectedContainer] = useState<any | null>(null);
const handlePowerAction = (_action: string) => { useEffect(() => {
// Power action handler listProxmoxClusters()
}; .then((cls) => {
setClusters(cls);
if (cls.length > 0) setSelectedClusterId(cls[0].id);
})
.catch((err) => {
console.error('Failed to load clusters:', err);
toast.error('Failed to load clusters');
});
}, []);
const handleConsole = () => { const loadContainers = useCallback(async (clusterId: string) => {
// Console handler if (!clusterId) return;
}; setIsLoading(true);
try {
const data = await listProxmoxContainers(clusterId);
setContainers(data);
} catch (err) {
console.error('Failed to load containers:', err);
toast.error('Failed to load containers');
} finally {
setIsLoading(false);
}
}, []);
const handleContainerSelect = (container: ContainerInfo) => { useEffect(() => {
setSelectedContainer(container); if (selectedClusterId) loadContainers(selectedClusterId);
}; }, [selectedClusterId, loadContainers]);
if (clusters.length === 0 && !isLoading) {
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold">Containers</h1>
<p className="text-muted-foreground">Manage LXC containers</p>
</div>
<div className="text-center py-12 text-muted-foreground">
<p>No Proxmox clusters configured.</p>
<p className="text-sm mt-1">Add a remote connection first.</p>
</div>
</div>
);
}
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@ -43,8 +68,19 @@ export function ProxmoxContainersPage() {
<h1 className="text-2xl font-bold">Containers</h1> <h1 className="text-2xl font-bold">Containers</h1>
<p className="text-muted-foreground">Manage LXC containers</p> <p className="text-muted-foreground">Manage LXC containers</p>
</div> </div>
<div className="flex space-x-2"> <div className="flex items-center space-x-2">
<Button variant="outline" size="sm"> {clusters.length > 1 && (
<select
className="rounded-md border px-3 py-1.5 text-sm bg-background"
value={selectedClusterId}
onChange={(e) => setSelectedClusterId(e.target.value)}
>
{clusters.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
)}
<Button variant="outline" size="sm" onClick={() => loadContainers(selectedClusterId)}>
<RefreshCw className="mr-2 h-4 w-4" /> <RefreshCw className="mr-2 h-4 w-4" />
Refresh Refresh
</Button> </Button>
@ -54,16 +90,20 @@ export function ProxmoxContainersPage() {
{selectedContainer ? ( {selectedContainer ? (
<ContainerOverview <ContainerOverview
container={selectedContainer} container={selectedContainer}
onRefresh={() => {}} onRefresh={() => loadContainers(selectedClusterId)}
onPowerAction={handlePowerAction} onPowerAction={(_action) => { toast.info('Power action — not yet implemented'); }}
onConsole={handleConsole} onConsole={() => { toast.info('Console — not yet implemented'); }}
/> />
) : ( ) : (
<div className="grid grid-cols-1 gap-4"> <div className="grid grid-cols-1 gap-4">
{containers.map((container) => ( {containers.map((container) => (
<Card key={container.id} className="cursor-pointer hover:shadow-md" onClick={() => handleContainerSelect(container)}> <Card
key={container.vmid ?? container.id}
className="cursor-pointer hover:shadow-md"
onClick={() => setSelectedContainer(container)}
>
<CardHeader> <CardHeader>
<CardTitle>{container.name}</CardTitle> <CardTitle>{container.name ?? `CT ${container.vmid}`}</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-4 gap-4 text-sm"> <div className="grid grid-cols-4 gap-4 text-sm">
@ -81,7 +121,15 @@ export function ProxmoxContainersPage() {
</div> </div>
<div> <div>
<div className="text-muted-foreground">Resources</div> <div className="text-muted-foreground">Resources</div>
<div className="font-medium">{container.cpu} CPU / {container.memory}MB RAM</div> <div className="font-medium">
{container.maxcpu ?? container.cpu ?? '?'} CPU /{' '}
{container.maxmem
? `${Math.round(container.maxmem / 1048576)} MB`
: container.memory
? `${container.memory} MB`
: '?'}{' '}
RAM
</div>
</div> </div>
</div> </div>
</CardContent> </CardContent>

View File

@ -1,14 +1,69 @@
import React from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { Button } from '@/components/ui/index'; import { Button } from '@/components/ui/index';
import { Input } from '@/components/ui/index';
import { RefreshCw } from 'lucide-react'; import { RefreshCw } from 'lucide-react';
import { FirewallRuleList } from '@/components/Proxmox'; import { FirewallRuleList } from '@/components/Proxmox';
import { listProxmoxClusters, listFirewallRules } from '@/lib/proxmoxClient';
import type { ClusterInfo } from '@/lib/domain';
import { toast } from 'sonner';
export function ProxmoxFirewallPage() { export function ProxmoxFirewallPage() {
const rules = [ const [clusters, setClusters] = useState<ClusterInfo[]>([]);
{ id: '1', rule: 100, action: 'ACCEPT', protocol: 'tcp', source: '192.168.1.0/24', destination: 'any', port: '22', status: 'enabled' }, const [selectedClusterId, setSelectedClusterId] = useState<string>('');
{ id: '2', rule: 200, action: 'ACCEPT', protocol: 'tcp', source: 'any', destination: 'any', port: '80,443', status: 'enabled' }, const [nodeInputValue, setNodeInputValue] = useState('localhost');
{ id: '3', rule: 999, action: 'DROP', protocol: 'any', source: 'any', destination: 'any', status: 'enabled' }, const [nodeId, setNodeId] = useState('localhost');
]; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const [rules, setRules] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
listProxmoxClusters()
.then((cls) => {
setClusters(cls);
if (cls.length > 0) setSelectedClusterId(cls[0].id);
})
.catch((err) => {
console.error('Failed to load clusters:', err);
toast.error('Failed to load clusters');
});
}, []);
const loadRules = useCallback(async (clusterId: string, nId: string) => {
if (!clusterId) return;
setIsLoading(true);
try {
const data = await listFirewallRules(clusterId, nId);
setRules(data);
} catch (err) {
console.error('Failed to load firewall rules:', err);
toast.error('Failed to load firewall rules');
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
if (selectedClusterId) loadRules(selectedClusterId, nodeId);
}, [selectedClusterId, nodeId, loadRules]);
const applyNodeId = () => {
setNodeId(nodeInputValue.trim() || 'localhost');
};
if (clusters.length === 0 && !isLoading) {
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold">Firewall</h1>
<p className="text-muted-foreground">Configure firewall rules</p>
</div>
<div className="text-center py-12 text-muted-foreground">
<p>No Proxmox clusters configured.</p>
<p className="text-sm mt-1">Add a remote connection first.</p>
</div>
</div>
);
}
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@ -17,17 +72,47 @@ export function ProxmoxFirewallPage() {
<h1 className="text-2xl font-bold">Firewall</h1> <h1 className="text-2xl font-bold">Firewall</h1>
<p className="text-muted-foreground">Configure firewall rules</p> <p className="text-muted-foreground">Configure firewall rules</p>
</div> </div>
<div className="flex space-x-2"> </div>
<Button variant="outline" size="sm">
<div className="flex items-center gap-3 flex-wrap">
{clusters.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Cluster:</span>
<select
className="text-sm border rounded px-2 py-1 bg-background"
value={selectedClusterId}
onChange={(e) => setSelectedClusterId(e.target.value)}
>
{clusters.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
</div>
)}
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Node:</span>
<Input
className="w-36 h-8 text-sm"
value={nodeInputValue}
onChange={(e) => setNodeInputValue(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') applyNodeId(); }}
placeholder="localhost"
/>
<Button variant="outline" size="sm" onClick={applyNodeId}>Apply</Button>
</div>
<Button
variant="outline"
size="sm"
onClick={() => loadRules(selectedClusterId, nodeId)}
>
<RefreshCw className="mr-2 h-4 w-4" /> <RefreshCw className="mr-2 h-4 w-4" />
Refresh Refresh
</Button> </Button>
</div> </div>
</div>
<FirewallRuleList <FirewallRuleList
rules={rules} rules={rules}
onRefresh={() => {}} onRefresh={() => loadRules(selectedClusterId, nodeId)}
/> />
</div> </div>
); );

View File

@ -8,6 +8,7 @@ import { RemoveRemoteDialog } from '@/components/Proxmox';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/index'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/index';
import { listProxmoxClusters, addProxmoxCluster, removeProxmoxCluster } from '@/lib/proxmoxClient'; import { listProxmoxClusters, addProxmoxCluster, removeProxmoxCluster } from '@/lib/proxmoxClient';
import { ClusterType } from '@/lib/domain'; import { ClusterType } from '@/lib/domain';
import { toast } from 'sonner';
interface RemoteInfo { interface RemoteInfo {
id: string; id: string;
@ -69,7 +70,8 @@ export function ProxmoxRemotesPage() {
setShowAddDialog(false); setShowAddDialog(false);
} catch (err) { } catch (err) {
console.error('Failed to add remote:', err); console.error('Failed to add remote:', err);
alert('Failed to add remote: ' + String(err)); toast.error('Failed to add remote: ' + String(err));
throw err;
} }
}; };
@ -92,7 +94,8 @@ export function ProxmoxRemotesPage() {
setEditingRemote(null); setEditingRemote(null);
} catch (err) { } catch (err) {
console.error('Failed to edit remote:', err); console.error('Failed to edit remote:', err);
alert('Failed to edit remote: ' + String(err)); toast.error('Failed to edit remote: ' + String(err));
throw err;
} }
}; };
@ -104,7 +107,7 @@ export function ProxmoxRemotesPage() {
setRemovingRemote(null); setRemovingRemote(null);
} catch (err) { } catch (err) {
console.error('Failed to remove remote:', err); console.error('Failed to remove remote:', err);
alert('Failed to remove remote: ' + String(err)); toast.error('Failed to remove remote: ' + String(err));
} }
} }
}; };

View File

@ -1,14 +1,62 @@
import React from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { Button } from '@/components/ui/index'; import { Button } from '@/components/ui/index';
import { RefreshCw } from 'lucide-react'; import { RefreshCw } from 'lucide-react';
import { StorageList } from '@/components/Proxmox'; import { StorageList } from '@/components/Proxmox';
import { listProxmoxClusters, listProxmoxDatastores } from '@/lib/proxmoxClient';
import type { ClusterInfo } from '@/lib/domain';
import { toast } from 'sonner';
export function ProxmoxStoragePage() { export function ProxmoxStoragePage() {
const storages = [ const [clusters, setClusters] = useState<ClusterInfo[]>([]);
{ id: '1', name: 'local', type: 'dir', remote: 'local', node: 'pve1', used: '50 GB', total: '500 GB', available: '450 GB', status: 'active' }, const [selectedClusterId, setSelectedClusterId] = useState<string>('');
{ id: '2', name: 'local-lvm', type: 'lvm', remote: 'local', node: 'pve1', used: '100 GB', total: '1000 GB', available: '900 GB', status: 'active' }, // eslint-disable-next-line @typescript-eslint/no-explicit-any
{ id: '3', name: 'nfs-backup', type: 'nfs', remote: 'nfs', node: 'pve2', used: '200 GB', total: '2000 GB', available: '1800 GB', status: 'active' }, const [storages, setStorages] = useState<any[]>([]);
]; const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
listProxmoxClusters()
.then((cls) => {
setClusters(cls);
if (cls.length > 0) setSelectedClusterId(cls[0].id);
})
.catch((err) => {
console.error('Failed to load clusters:', err);
toast.error('Failed to load clusters');
});
}, []);
const loadStorages = useCallback(async (clusterId: string) => {
if (!clusterId) return;
setIsLoading(true);
try {
const data = await listProxmoxDatastores(clusterId);
setStorages(data);
} catch (err) {
console.error('Failed to load storages:', err);
toast.error('Failed to load storages');
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
if (selectedClusterId) loadStorages(selectedClusterId);
}, [selectedClusterId, loadStorages]);
if (clusters.length === 0 && !isLoading) {
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold">Storage</h1>
<p className="text-muted-foreground">Manage storage pools and volumes</p>
</div>
<div className="text-center py-12 text-muted-foreground">
<p>No Proxmox clusters configured.</p>
<p className="text-sm mt-1">Add a remote connection first.</p>
</div>
</div>
);
}
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@ -17,8 +65,19 @@ export function ProxmoxStoragePage() {
<h1 className="text-2xl font-bold">Storage</h1> <h1 className="text-2xl font-bold">Storage</h1>
<p className="text-muted-foreground">Manage storage pools and volumes</p> <p className="text-muted-foreground">Manage storage pools and volumes</p>
</div> </div>
<div className="flex space-x-2"> <div className="flex items-center space-x-2">
<Button variant="outline" size="sm"> {clusters.length > 1 && (
<select
className="rounded-md border px-3 py-1.5 text-sm bg-background"
value={selectedClusterId}
onChange={(e) => setSelectedClusterId(e.target.value)}
>
{clusters.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
)}
<Button variant="outline" size="sm" onClick={() => loadStorages(selectedClusterId)}>
<RefreshCw className="mr-2 h-4 w-4" /> <RefreshCw className="mr-2 h-4 w-4" />
Refresh Refresh
</Button> </Button>
@ -27,7 +86,7 @@ export function ProxmoxStoragePage() {
<StorageList <StorageList
storages={storages} storages={storages}
onRefresh={() => {}} onRefresh={() => loadStorages(selectedClusterId)}
/> />
</div> </div>
); );

View File

@ -1,28 +1,63 @@
import React from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { Button } from '@/components/ui/index'; import { Button } from '@/components/ui/index';
import { RefreshCw } from 'lucide-react'; import { RefreshCw } from 'lucide-react';
import { VMList } from '@/components/Proxmox'; import { VMList } from '@/components/Proxmox';
import { listProxmoxClusters, listProxmoxVms } from '@/lib/proxmoxClient';
interface VMInfo { import type { ClusterInfo } from '@/lib/domain';
id: string; import { toast } from 'sonner';
vmid: number;
name: string;
node: string;
status: 'running' | 'stopped' | 'paused';
cpu: number;
memory: number;
memoryTotal: number;
disk: number;
diskTotal: number;
uptime?: string;
}
export function ProxmoxVMsPage() { export function ProxmoxVMsPage() {
const vms: VMInfo[] = [ const [clusters, setClusters] = useState<ClusterInfo[]>([]);
{ id: '1', name: 'web-server-01', vmid: 100, node: 'pve1', status: 'running', cpu: 4, memory: 8192, memoryTotal: 8192, disk: 100, diskTotal: 100, uptime: '2d 4h' }, const [selectedClusterId, setSelectedClusterId] = useState<string>('');
{ id: '2', name: 'db-server-01', vmid: 101, node: 'pve2', status: 'running', cpu: 8, memory: 16384, memoryTotal: 16384, disk: 500, diskTotal: 500, uptime: '5d 12h' }, // eslint-disable-next-line @typescript-eslint/no-explicit-any
{ id: '3', name: 'dev-vm', vmid: 102, node: 'pve1', status: 'stopped', cpu: 2, memory: 4096, memoryTotal: 4096, disk: 50, diskTotal: 50 }, const [vms, setVms] = useState<any[]>([]);
]; const [isLoading, setIsLoading] = useState(false);
const [selectedVMs, setSelectedVMs] = useState<Set<string>>(new Set());
useEffect(() => {
listProxmoxClusters()
.then((cls) => {
setClusters(cls);
if (cls.length > 0) setSelectedClusterId(cls[0].id);
})
.catch((err) => {
console.error('Failed to load clusters:', err);
toast.error('Failed to load clusters');
});
}, []);
const loadVms = useCallback(async (clusterId: string) => {
if (!clusterId) return;
setIsLoading(true);
try {
const data = await listProxmoxVms(clusterId);
setVms(data);
} catch (err) {
console.error('Failed to load VMs:', err);
toast.error('Failed to load VMs');
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
if (selectedClusterId) loadVms(selectedClusterId);
}, [selectedClusterId, loadVms]);
if (clusters.length === 0 && !isLoading) {
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold">Virtual Machines</h1>
<p className="text-muted-foreground">Manage QEMU/KVM virtual machines</p>
</div>
<div className="text-center py-12 text-muted-foreground">
<p>No Proxmox clusters configured.</p>
<p className="text-sm mt-1">Add a remote connection first.</p>
</div>
</div>
);
}
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@ -31,8 +66,19 @@ export function ProxmoxVMsPage() {
<h1 className="text-2xl font-bold">Virtual Machines</h1> <h1 className="text-2xl font-bold">Virtual Machines</h1>
<p className="text-muted-foreground">Manage QEMU/KVM virtual machines</p> <p className="text-muted-foreground">Manage QEMU/KVM virtual machines</p>
</div> </div>
<div className="flex space-x-2"> <div className="flex items-center space-x-2">
<Button variant="outline" size="sm"> {clusters.length > 1 && (
<select
className="rounded-md border px-3 py-1.5 text-sm bg-background"
value={selectedClusterId}
onChange={(e) => setSelectedClusterId(e.target.value)}
>
{clusters.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
)}
<Button variant="outline" size="sm" onClick={() => loadVms(selectedClusterId)}>
<RefreshCw className="mr-2 h-4 w-4" /> <RefreshCw className="mr-2 h-4 w-4" />
Refresh Refresh
</Button> </Button>
@ -41,25 +87,20 @@ export function ProxmoxVMsPage() {
<VMList <VMList
vms={vms} vms={vms}
onRefresh={() => {}} onRefresh={() => loadVms(selectedClusterId)}
onVMAction={(_vm, _action) => { onVMAction={(_vm, _action) => { toast.info('VM action — not yet implemented'); }}
// VM action handler onSnapshotAction={(_vm, _action) => { toast.info('Snapshot action — not yet implemented'); }}
}} onMigrate={(_vm) => { toast.info('Migrate — not yet implemented'); }}
onSnapshotAction={(_vm, _action) => { onClone={(_vm) => { toast.info('Clone — not yet implemented'); }}
// Snapshot action handler onDelete={(_vm) => { toast.info('Delete — not yet implemented'); }}
}} selectedVMs={selectedVMs}
onMigrate={(_vm) => { onToggleSelect={(vm) => {
// Migrate handler setSelectedVMs((prev) => {
}} const next = new Set(prev);
onClone={(_vm) => { const id = String(vm.vmid);
// Clone handler if (next.has(id)) next.delete(id); else next.add(id);
}} return next;
onDelete={(_vm) => { });
// Delete handler
}}
selectedVMs={new Set()}
onToggleSelect={(_vm) => {
// VM select handler
}} }}
/> />
</div> </div>

View File

@ -1,18 +1,19 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index';
import { Button } from '@/components/ui/index'; import { Button } from '@/components/ui/index';
import { RefreshCw, Check, AlertCircle, Loader } from 'lucide-react'; import { RefreshCw, Check, AlertCircle, Loader, ExternalLink } from 'lucide-react';
import { import {
checkAppUpdatesCmd, checkAppUpdatesCmd,
installAppUpdatesCmd, installAppUpdatesCmd,
getUpdateChannelCmd, getUpdateChannelCmd,
setUpdateChannelCmd, setUpdateChannelCmd,
type UpdateCheckResult,
} from '@/lib/tauriCommands'; } from '@/lib/tauriCommands';
export function Updater() { export function Updater() {
const [channel, setChannel] = useState('stable'); const [channel, setChannel] = useState('stable');
const [checking, setChecking] = useState(false); const [checking, setChecking] = useState(false);
const [updateAvailable, setUpdateAvailable] = useState(false); const [result, setResult] = useState<UpdateCheckResult | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const loadChannel = async () => { const loadChannel = async () => {
@ -28,21 +29,20 @@ export function Updater() {
setChecking(true); setChecking(true);
setError(null); setError(null);
try { try {
const available = await checkAppUpdatesCmd(); const data = await checkAppUpdatesCmd();
setUpdateAvailable(available); setResult(data);
} catch { } catch (err) {
setError('Failed to check for updates'); setError(String(err));
} finally { } finally {
setChecking(false); setChecking(false);
} }
}; };
const handleInstallUpdate = async () => { const handleDownloadUpdate = async () => {
try { try {
await installAppUpdatesCmd(); await installAppUpdatesCmd();
setUpdateAvailable(false); } catch (err) {
} catch { setError('Failed to open releases page: ' + String(err));
setError('Failed to install update');
} }
}; };
@ -64,7 +64,7 @@ export function Updater() {
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h1 className="text-2xl font-bold">Updater</h1> <h1 className="text-2xl font-bold">Updater</h1>
<p className="text-muted-foreground">Configure application auto-updates</p> <p className="text-muted-foreground">Configure application updates</p>
</div> </div>
<Card> <Card>
@ -121,15 +121,23 @@ export function Updater() {
)} )}
</Button> </Button>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="space-y-4">
{error && ( {error && (
<div className="mb-4 flex items-center space-x-2 rounded-lg bg-destructive/15 p-3 text-destructive"> <div className="flex items-center space-x-2 rounded-lg bg-destructive/15 p-3 text-destructive">
<AlertCircle className="h-4 w-4" /> <AlertCircle className="h-4 w-4 flex-shrink-0" />
<span className="text-sm">{error}</span> <span className="text-sm">{error}</span>
</div> </div>
)} )}
{updateAvailable ? ( {result && (
<div className="text-sm text-muted-foreground space-y-1">
<div>Current version: <span className="font-mono font-medium text-foreground">{result.currentVersion}</span></div>
<div>Latest version: <span className="font-mono font-medium text-foreground">{result.latestVersion || '—'}</span></div>
</div>
)}
{result?.updateAvailable ? (
<div className="space-y-3">
<div className="flex items-center justify-between rounded-lg bg-green-50 p-4 dark:bg-green-900/20"> <div className="flex items-center justify-between rounded-lg bg-green-50 p-4 dark:bg-green-900/20">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="rounded-full bg-green-600 p-1 text-white"> <div className="rounded-full bg-green-600 p-1 text-white">
@ -137,20 +145,27 @@ export function Updater() {
</div> </div>
<div> <div>
<div className="font-semibold text-green-900 dark:text-green-100"> <div className="font-semibold text-green-900 dark:text-green-100">
Update Available Update Available v{result.latestVersion}
</div> </div>
<div className="text-sm text-green-700 dark:text-green-300"> <div className="text-sm text-green-700 dark:text-green-300">
A new version is ready to install Click below to open the releases page and download
</div> </div>
</div> </div>
</div> </div>
<Button onClick={handleInstallUpdate}> <Button onClick={handleDownloadUpdate}>
Install Update <ExternalLink className="mr-2 h-4 w-4" />
Download Update
</Button> </Button>
</div> </div>
) : ( {result.releaseNotes && (
<div className="flex items-center justify-between rounded-lg bg-muted p-4"> <div className="rounded-lg border p-3 text-sm">
<div className="flex items-center space-x-3"> <div className="font-medium mb-1">Release Notes</div>
<pre className="whitespace-pre-wrap text-muted-foreground font-sans">{result.releaseNotes}</pre>
</div>
)}
</div>
) : result ? (
<div className="flex items-center space-x-3 rounded-lg bg-muted p-4">
<div className="rounded-full bg-muted-foreground p-1 text-background"> <div className="rounded-full bg-muted-foreground p-1 text-background">
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</div> </div>
@ -161,8 +176,7 @@ export function Updater() {
</div> </div>
</div> </div>
</div> </div>
</div> ) : null}
)}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>