Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c6a21e2c3 | |||
| 969bf2b191 | |||
| 7d67934a10 | |||
| 56904422af | |||
| 8bcdea9e4a | |||
| 7804285dec | |||
| 4a7029cea6 | |||
| 7317042e8d | |||
| 439b11d16d |
Generated
+578
-8
@@ -318,6 +318,23 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chacha20"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures 0.3.1",
|
||||||
|
"rand_core 0.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.45"
|
version = "0.4.45"
|
||||||
@@ -346,10 +363,39 @@ version = "0.18.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
"time",
|
"time",
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie_store"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
|
||||||
|
dependencies = [
|
||||||
|
"cookie",
|
||||||
|
"document-features",
|
||||||
|
"idna",
|
||||||
|
"log",
|
||||||
|
"publicsuffix",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"time",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -373,7 +419,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
|
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"core-graphics-types",
|
"core-graphics-types",
|
||||||
"foreign-types",
|
"foreign-types",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -386,7 +432,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -399,6 +445,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -506,6 +561,18 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"syn 2.0.118",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-encoding"
|
||||||
|
version = "2.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-url"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dbus"
|
name = "dbus"
|
||||||
version = "0.9.11"
|
version = "0.9.11"
|
||||||
@@ -635,6 +702,15 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"syn 2.0.118",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "document-features"
|
||||||
|
version = "0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||||
|
dependencies = [
|
||||||
|
"litrs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dom_query"
|
name = "dom_query"
|
||||||
version = "0.27.0"
|
version = "0.27.0"
|
||||||
@@ -721,6 +797,15 @@ version = "1.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -1034,8 +1119,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1057,8 +1144,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 6.0.0",
|
"r-efi 6.0.0",
|
||||||
|
"rand_core 0.10.1",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1209,6 +1299,25 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"syn 2.0.118",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h2"
|
||||||
|
version = "0.4.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"bytes",
|
||||||
|
"fnv",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"http",
|
||||||
|
"indexmap 2.14.0",
|
||||||
|
"slab",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
@@ -1298,6 +1407,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
@@ -1321,6 +1431,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"webpki-roots 1.0.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1341,9 +1452,11 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"windows-registry",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1744,6 +1857,12 @@ version = "0.8.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "litrs"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@@ -1759,6 +1878,12 @@ version = "0.4.33"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lru-slab"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markup5ever"
|
name = "markup5ever"
|
||||||
version = "0.38.0"
|
version = "0.38.0"
|
||||||
@@ -2178,8 +2303,11 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-http",
|
||||||
"tauri-plugin-process",
|
"tauri-plugin-process",
|
||||||
|
"tauri-plugin-store",
|
||||||
"tauri-plugin-updater",
|
"tauri-plugin-updater",
|
||||||
|
"tauri-plugin-websocket",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2330,6 +2458,15 @@ 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 = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "precomputed-hash"
|
name = "precomputed-hash"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -2398,6 +2535,22 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psl-types"
|
||||||
|
version = "2.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "publicsuffix"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf"
|
||||||
|
dependencies = [
|
||||||
|
"idna",
|
||||||
|
"psl-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.39.4"
|
version = "0.39.4"
|
||||||
@@ -2407,6 +2560,62 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn"
|
||||||
|
version = "0.11.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"cfg_aliases",
|
||||||
|
"pin-project-lite",
|
||||||
|
"quinn-proto",
|
||||||
|
"quinn-udp",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"socket2",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-proto"
|
||||||
|
version = "0.11.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"getrandom 0.4.3",
|
||||||
|
"lru-slab",
|
||||||
|
"rand 0.10.2",
|
||||||
|
"rand_pcg",
|
||||||
|
"ring",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"slab",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tinyvec",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-udp"
|
||||||
|
version = "0.5.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
|
||||||
|
dependencies = [
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"socket2",
|
||||||
|
"tracing",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.45"
|
version = "1.0.45"
|
||||||
@@ -2428,6 +2637,61 @@ version = "6.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||||
|
dependencies = [
|
||||||
|
"chacha20",
|
||||||
|
"getrandom 0.4.3",
|
||||||
|
"rand_core 0.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_pcg"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core 0.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raw-window-handle"
|
name = "raw-window-handle"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@@ -2503,6 +2767,49 @@ version = "0.8.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reqwest"
|
||||||
|
version = "0.12.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"cookie",
|
||||||
|
"cookie_store",
|
||||||
|
"encoding_rs",
|
||||||
|
"futures-core",
|
||||||
|
"h2",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-rustls",
|
||||||
|
"hyper-util",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"mime",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"quinn",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
|
"tower-service",
|
||||||
|
"url",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"webpki-roots 1.0.9",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.13.4"
|
version = "0.13.4"
|
||||||
@@ -2616,6 +2923,7 @@ version = "1.14.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"web-time",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2625,7 +2933,7 @@ version = "0.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
|
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"jni 0.22.4",
|
"jni 0.22.4",
|
||||||
"log",
|
"log",
|
||||||
@@ -2663,6 +2971,12 @@ version = "1.0.22"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -2745,7 +3059,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
@@ -2885,6 +3199,18 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_urlencoded"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with"
|
name = "serde_with"
|
||||||
version = "3.21.0"
|
version = "3.21.0"
|
||||||
@@ -2948,6 +3274,17 @@ dependencies = [
|
|||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures 0.2.17",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
@@ -2955,7 +3292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures 0.2.17",
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3137,6 +3474,17 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sync_wrapper"
|
name = "sync_wrapper"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -3157,6 +3505,27 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"syn 2.0.118",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-configuration"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"core-foundation 0.9.4",
|
||||||
|
"system-configuration-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-configuration-sys"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "system-deps"
|
name = "system-deps"
|
||||||
version = "6.2.2"
|
version = "6.2.2"
|
||||||
@@ -3178,7 +3547,7 @@ checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"block2",
|
"block2",
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"core-graphics",
|
"core-graphics",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dbus",
|
"dbus",
|
||||||
@@ -3268,7 +3637,7 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"plist",
|
"plist",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"reqwest",
|
"reqwest 0.13.4",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
@@ -3367,6 +3736,54 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-fs"
|
||||||
|
version = "2.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "de22eef34fd78c0da050e748710edd50bf127e651d02ea1b2bfada1523cc5c51"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"dunce",
|
||||||
|
"glob",
|
||||||
|
"log",
|
||||||
|
"objc2-foundation",
|
||||||
|
"percent-encoding",
|
||||||
|
"schemars 0.8.22",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tauri-utils",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"toml 1.1.2+spec-1.1.0",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-http"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7241a0c762649be8fba7dd4cc84684d0e409f26b335a978ef4dd5fe78da74ce6"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"cookie_store",
|
||||||
|
"data-url",
|
||||||
|
"http",
|
||||||
|
"regex",
|
||||||
|
"reqwest 0.12.28",
|
||||||
|
"schemars 0.8.22",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tauri-plugin-fs",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"url",
|
||||||
|
"urlpattern",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-process"
|
name = "tauri-plugin-process"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
@@ -3377,6 +3794,22 @@ dependencies = [
|
|||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-store"
|
||||||
|
version = "2.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6708afbe549f176b712066e71648ba8fafba20789453718260c7ca356733cb0c"
|
||||||
|
dependencies = [
|
||||||
|
"dunce",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-updater"
|
name = "tauri-plugin-updater"
|
||||||
version = "2.10.1"
|
version = "2.10.1"
|
||||||
@@ -3393,7 +3826,7 @@ dependencies = [
|
|||||||
"minisign-verify",
|
"minisign-verify",
|
||||||
"osakit",
|
"osakit",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"reqwest",
|
"reqwest 0.13.4",
|
||||||
"rustls",
|
"rustls",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -3410,6 +3843,26 @@ dependencies = [
|
|||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-websocket"
|
||||||
|
version = "2.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ca243c7f0bf935cd81123e07f82188ccb919b19fbfc74518b947eedc4619bbb"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"log",
|
||||||
|
"rand 0.9.5",
|
||||||
|
"rustls",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tokio-tungstenite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.11.3"
|
version = "2.11.3"
|
||||||
@@ -3639,9 +4092,21 @@ dependencies = [
|
|||||||
"mio",
|
"mio",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-rustls"
|
name = "tokio-rustls"
|
||||||
version = "0.26.4"
|
version = "0.26.4"
|
||||||
@@ -3652,6 +4117,22 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-tungstenite"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tungstenite",
|
||||||
|
"webpki-roots 0.26.11",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.18"
|
version = "0.7.18"
|
||||||
@@ -3837,9 +4318,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-attributes"
|
||||||
|
version = "0.1.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.118",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-core"
|
name = "tracing-core"
|
||||||
version = "0.1.36"
|
version = "0.1.36"
|
||||||
@@ -3877,6 +4370,24 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tungstenite"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"data-encoding",
|
||||||
|
"http",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
"rand 0.9.5",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"sha1",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typeid"
|
name = "typeid"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -4141,6 +4652,16 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-time"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web_atoms"
|
name = "web_atoms"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -4206,6 +4727,24 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "0.26.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||||
|
dependencies = [
|
||||||
|
"webpki-roots 1.0.9",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webview2-com"
|
name = "webview2-com"
|
||||||
version = "0.38.2"
|
version = "0.38.2"
|
||||||
@@ -4391,6 +4930,17 @@ dependencies = [
|
|||||||
"windows-link 0.1.3",
|
"windows-link 0.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-registry"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link 0.2.1",
|
||||||
|
"windows-result 0.4.1",
|
||||||
|
"windows-strings 0.5.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
@@ -4820,6 +5370,26 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.56"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.56"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.118",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
|||||||
@@ -22,6 +22,21 @@ serde_json = "1"
|
|||||||
# Auto-update: prompt the operator, download a signed update, relaunch.
|
# Auto-update: prompt the operator, download a signed update, relaunch.
|
||||||
tauri-plugin-updater = "2"
|
tauri-plugin-updater = "2"
|
||||||
tauri-plugin-process = "2"
|
tauri-plugin-process = "2"
|
||||||
|
# HTTP client for the SPA's API/WS calls to the local Fastify server. The window
|
||||||
|
# runs at tauri://localhost, which WebKitGTK treats as a secure origin — a plain
|
||||||
|
# http://127.0.0.1:3000 fetch() from inside it is blocked as mixed content (a
|
||||||
|
# long-standing WebKit limitation, not fixable via CSP). Routing through this
|
||||||
|
# plugin sends the request via Tauri's Rust side instead of the webview's own
|
||||||
|
# fetch, sidestepping the browser mixed-content check entirely.
|
||||||
|
tauri-plugin-http = "2"
|
||||||
|
# Same mixed-content problem as above, but for the live-feed WebSocket
|
||||||
|
# (ws://127.0.0.1:3000 from the secure tauri://localhost origin) — HTTP and WS
|
||||||
|
# are separate browser checks, so this needs its own plugin.
|
||||||
|
tauri-plugin-websocket = "2"
|
||||||
|
# Persists the operator-configured backend URL (host:port of the Fastify
|
||||||
|
# server this install talks to) across restarts. Read before any API call —
|
||||||
|
# see apps/web/src/lib/backend-config.ts.
|
||||||
|
tauri-plugin-store = "2"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# Used by `tauri dev`/CLI for hot-reload of the Rust side.
|
# Used by `tauri dev`/CLI for hot-reload of the Rust side.
|
||||||
|
|||||||
@@ -6,6 +6,18 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"updater:default",
|
"updater:default",
|
||||||
"process:default"
|
"process:default",
|
||||||
|
"websocket:default",
|
||||||
|
"store:default",
|
||||||
|
{
|
||||||
|
"identifier": "http:default",
|
||||||
|
"//": "Backend address is operator-configured at runtime (backend-config.ts) so the exact host:port can't be allow-listed at build time. Wildcarded to any host — the CSP forces ALL backend traffic through this plugin (see tauri.conf.json), so this scope is the real boundary; a compromised/malicious page still can't reach anything the operator hasn't pointed the app at, since the app only ever calls the one configured origin. All 4 forms needed: a known Tauri scope-matching quirk drops http://*:PORT unless both bare and :* variants are listed.",
|
||||||
|
"allow": [
|
||||||
|
{ "url": "http://*" },
|
||||||
|
{ "url": "https://*" },
|
||||||
|
{ "url": "http://*:*" },
|
||||||
|
{ "url": "https://*:*" }
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
// Intentionally minimal: build the default Tauri app and run it. The window
|
// Intentionally minimal: build the default Tauri app and run it. The window
|
||||||
// config (kiosk, fullscreen, which URL/assets to load) lives in tauri.conf.json.
|
// config (kiosk, fullscreen, which URL/assets to load) lives in tauri.conf.json.
|
||||||
// No custom commands are registered — the renderer (the @parking/web SPA) reaches
|
// No custom commands are registered — the renderer (the @parking/web SPA) reaches
|
||||||
// the backend over HTTP to the local Fastify server, NOT through Tauri IPC. This
|
// the backend over HTTP to a Fastify server (address operator-configured at
|
||||||
// keeps the shell a thin presentation wrapper with a deny-by-default native
|
// runtime, not baked in — see apps/web/src/lib/backend-config.ts), NOT through
|
||||||
// surface (see wiki/decisions/desktop-shell-tauri.md).
|
// Tauri IPC. This keeps the shell a thin presentation wrapper with a
|
||||||
|
// deny-by-default native surface (see wiki/decisions/desktop-shell-tauri.md).
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
@@ -16,6 +17,16 @@ pub fn run() {
|
|||||||
// endpoint + signing pubkey live in tauri.conf.json.
|
// endpoint + signing pubkey live in tauri.conf.json.
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
|
// Routes the SPA's fetch()/WS calls to the operator-configured Fastify
|
||||||
|
// server through Tauri's native HTTP client — see the Cargo.toml
|
||||||
|
// comment on why the webview's own fetch() can't reach it directly.
|
||||||
|
.plugin(tauri_plugin_http::init())
|
||||||
|
// Live-feed WebSocket — same mixed-content reason as the HTTP plugin
|
||||||
|
// above, but WS needs its own plugin (separate browser check).
|
||||||
|
.plugin(tauri_plugin_websocket::init())
|
||||||
|
// Persists the operator-configured backend URL across restarts (JSON
|
||||||
|
// file in the app's config dir) — see backend-config.ts.
|
||||||
|
.plugin(tauri_plugin_store::Builder::new().build())
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running the Parking System desktop shell");
|
.expect("error while running the Parking System desktop shell");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"devUrl": "http://localhost:5173",
|
"devUrl": "http://localhost:5173",
|
||||||
"frontendDist": "../../web/dist",
|
"frontendDist": "../../web/dist",
|
||||||
"beforeDevCommand": "pnpm --filter @parking/web dev",
|
"beforeDevCommand": "pnpm --filter @parking/web dev",
|
||||||
"beforeBuildCommand": "VITE_API_BASE=http://127.0.0.1:3000 pnpm --filter @parking/web build"
|
"beforeBuildCommand": "pnpm --filter @parking/web build"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"csp": "default-src 'self'; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; connect-src 'self' http://127.0.0.1:3000 http://localhost:3000 ws://127.0.0.1:3000 ws://localhost:3000"
|
"csp": "default-src 'self'; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; connect-src 'self'"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
|||||||
@@ -65,7 +65,21 @@ function cleanProfileField(v: string | null | undefined): string | null | undefi
|
|||||||
|
|
||||||
/** The session shape the SPA bootstraps from: identity + role + its permission
|
/** The session shape the SPA bootstraps from: identity + role + its permission
|
||||||
* list (so the UI can gate nav/routes) + language. Role NAME is for display; the
|
* list (so the UI can gate nav/routes) + language. Role NAME is for display; the
|
||||||
* permissions are the source of truth. */
|
* permissions are the source of truth.
|
||||||
|
*
|
||||||
|
* `csrf`, when passed, echoes the SAME value already sent as the readable
|
||||||
|
* parking_csrf cookie — not a new secret, just a second channel to learn it.
|
||||||
|
* The desktop shell needs this: tauri-plugin-http's fetch() runs through
|
||||||
|
* Rust's reqwest, which keeps its own cookie jar separate from the webview,
|
||||||
|
* so document.cookie on the tauri://localhost page never sees a cookie set
|
||||||
|
* on a plugin-routed response (open upstream bug, tauri-apps/tauri#13045).
|
||||||
|
* The cookie itself IS still sent back to the server by reqwest on
|
||||||
|
* subsequent requests — only the *client-side read* is broken — so
|
||||||
|
* api.ts's desktop path stashes this body value in memory instead of
|
||||||
|
* reading document.cookie, and echoes it in X-CSRF-Token exactly as the
|
||||||
|
* browser path echoes the cookie. See lib/api.ts and assertCsrf() in
|
||||||
|
* ../auth.ts (unchanged — this never touches verification, only how the
|
||||||
|
* desktop client learns what to send). */
|
||||||
function sessionView(
|
function sessionView(
|
||||||
db: Db,
|
db: Db,
|
||||||
user: {
|
user: {
|
||||||
@@ -78,6 +92,7 @@ function sessionView(
|
|||||||
fullName?: string | null;
|
fullName?: string | null;
|
||||||
email?: string | null;
|
email?: string | null;
|
||||||
},
|
},
|
||||||
|
csrf?: string,
|
||||||
) {
|
) {
|
||||||
const role = db.select().from(roles).where(eq(roles.id, user.roleId)).get();
|
const role = db.select().from(roles).where(eq(roles.id, user.roleId)).get();
|
||||||
const permissions = [...permissionsFor(user.roleId)];
|
const permissions = [...permissionsFor(user.roleId)];
|
||||||
@@ -92,6 +107,7 @@ function sessionView(
|
|||||||
fontScale: user.fontScale,
|
fontScale: user.fontScale,
|
||||||
fullName: user.fullName ?? null,
|
fullName: user.fullName ?? null,
|
||||||
email: user.email ?? null,
|
email: user.email ?? null,
|
||||||
|
...(csrf ? { csrfToken: csrf } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +142,7 @@ export async function authRoutes(app: FastifyInstance, db: Db): Promise<void> {
|
|||||||
setAuthCookies(reply, token, csrf);
|
setAuthCookies(reply, token, csrf);
|
||||||
// `language` is NOT in the JWT (identity/role only) — it's a mutable preference
|
// `language` is NOT in the JWT (identity/role only) — it's a mutable preference
|
||||||
// read from the DB, so changing it needs no token refresh.
|
// read from the DB, so changing it needs no token refresh.
|
||||||
return sessionView(db, user);
|
return sessionView(db, user, csrf);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/api/auth/logout", async (_req, reply) => {
|
app.post("/api/auth/logout", async (_req, reply) => {
|
||||||
@@ -146,7 +162,9 @@ export async function authRoutes(app: FastifyInstance, db: Db): Promise<void> {
|
|||||||
clearAuthCookies(reply);
|
clearAuthCookies(reply);
|
||||||
return reply.code(401).send({ error: "session no longer valid" });
|
return reply.code(401).send({ error: "session no longer valid" });
|
||||||
}
|
}
|
||||||
return sessionView(db, row);
|
// req.user.csrf is the value bound into the JWT at login (see assertCsrf in
|
||||||
|
// ../auth.ts) — same value as the cookie, re-surfaced for the desktop path.
|
||||||
|
return sessionView(db, row, req.user.csrf);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,12 @@
|
|||||||
"@radix-ui/react-tabs": "^1.1.15",
|
"@radix-ui/react-tabs": "^1.1.15",
|
||||||
"@tanstack/react-query": "^5.101.0",
|
"@tanstack/react-query": "^5.101.0",
|
||||||
"@tanstack/react-router": "^1.170.16",
|
"@tanstack/react-router": "^1.170.16",
|
||||||
|
"@tauri-apps/api": "^2.11.1",
|
||||||
|
"@tauri-apps/plugin-http": "^2.5.2",
|
||||||
"@tauri-apps/plugin-process": "^2.3.1",
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
|
"@tauri-apps/plugin-store": "^2.4.0",
|
||||||
"@tauri-apps/plugin-updater": "^2.10.1",
|
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||||
|
"@tauri-apps/plugin-websocket": "^2.3.0",
|
||||||
"i18next": "^26.3.1",
|
"i18next": "^26.3.1",
|
||||||
"react": "19.2.7",
|
"react": "19.2.7",
|
||||||
"react-dom": "19.2.7",
|
"react-dom": "19.2.7",
|
||||||
|
|||||||
@@ -3,25 +3,42 @@ import { QueryClientProvider } from "@tanstack/react-query";
|
|||||||
import { RouterProvider } from "@tanstack/react-router";
|
import { RouterProvider } from "@tanstack/react-router";
|
||||||
import { fetchMe, type SessionUser } from "./api.js";
|
import { fetchMe, type SessionUser } from "./api.js";
|
||||||
import { Login } from "./Login.js";
|
import { Login } from "./Login.js";
|
||||||
|
import { ConnectScreen } from "./ConnectScreen.js";
|
||||||
import { queryClient } from "./lib/query.js";
|
import { queryClient } from "./lib/query.js";
|
||||||
import { setLanguage } from "./lib/i18n/index.js";
|
import { setLanguage } from "./lib/i18n/index.js";
|
||||||
import { applyTheme, applyFontScale } from "./lib/theme.js";
|
import { applyTheme, applyFontScale } from "./lib/theme.js";
|
||||||
import { router } from "./router.js";
|
import { router } from "./router.js";
|
||||||
|
import { initApiBase, inTauri } from "./lib/origin.js";
|
||||||
|
|
||||||
// App root: bootstraps the session (cookie-based, from /api/auth/me), then hands
|
// App root: bootstraps the session (cookie-based, from /api/auth/me), then hands
|
||||||
// off to TanStack Router inside the QueryClient provider. The router renders the
|
// off to TanStack Router inside the QueryClient provider. The router renders the
|
||||||
// terminal chrome + screens; auth gating stays here (Login until signed in), and
|
// terminal chrome + screens; auth gating stays here (Login until signed in), and
|
||||||
// the signed-in user flows into the router context for role-based route guards.
|
// the signed-in user flows into the router context for role-based route guards.
|
||||||
// See wiki/entities/react-vite-spa.md and local-jwt-auth.md.
|
// See wiki/entities/react-vite-spa.md and local-jwt-auth.md.
|
||||||
|
//
|
||||||
|
// Desktop shell only: BEFORE any of that, the backend origin itself must be
|
||||||
|
// known — the same installer is used at every booth (see lib/origin.ts /
|
||||||
|
// backend-config.ts), so on first launch (or after the operator clears it)
|
||||||
|
// there is no server to call fetchMe() against yet. ConnectScreen gates that;
|
||||||
|
// a browser build always has a same-origin backend, so `needsConnect` is
|
||||||
|
// always false there and this is skipped entirely.
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [user, setUser] = useState<SessionUser | null>(null);
|
const [user, setUser] = useState<SessionUser | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [needsConnect, setNeedsConnect] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
initApiBase().then((saved) => {
|
||||||
|
if (inTauri() && !saved) {
|
||||||
|
setNeedsConnect(true);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
fetchMe()
|
fetchMe()
|
||||||
.then(setUser)
|
.then(setUser)
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Apply the signed-in user's preferred language + theme + font scale whenever they
|
// Apply the signed-in user's preferred language + theme + font scale whenever they
|
||||||
@@ -41,6 +58,19 @@ export function App() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="flex h-screen items-center justify-center text-term-muted">loading…</div>;
|
return <div className="flex h-screen items-center justify-center text-term-muted">loading…</div>;
|
||||||
}
|
}
|
||||||
|
if (needsConnect) {
|
||||||
|
return (
|
||||||
|
<ConnectScreen
|
||||||
|
onConnected={() => {
|
||||||
|
setNeedsConnect(false);
|
||||||
|
setLoading(true);
|
||||||
|
fetchMe()
|
||||||
|
.then(setUser)
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { setApiBase } from "./lib/origin.js";
|
||||||
|
|
||||||
|
// Desktop-only gate shown BEFORE Login whenever no backend has been
|
||||||
|
// configured yet (first launch of a generic .deb/.AppImage install, or after
|
||||||
|
// the operator clears it from Settings). Same installer works at any booth —
|
||||||
|
// see backend-config.ts for why this can't be a build-time value.
|
||||||
|
//
|
||||||
|
// backend-config.ts is imported dynamically (not at module top-level) purely
|
||||||
|
// to keep bundling consistent with origin.ts/router.tsx's other Tauri-only
|
||||||
|
// imports — this component itself only ever renders inside Tauri anyway, so
|
||||||
|
// it's not a functional requirement, just avoids an INEFFECTIVE_DYNAMIC_IMPORT
|
||||||
|
// warning from Vite (a static import here would defeat those other dynamic
|
||||||
|
// imports' chunk-splitting intent).
|
||||||
|
|
||||||
|
function normalizeHost(raw: string): string {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return trimmed;
|
||||||
|
return /^https?:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConnectScreen({ onConnected }: { onConnected: () => void }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [host, setHost] = useState("");
|
||||||
|
const [testing, setTesting] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [result, setResult] = useState<"ok" | "unreachable" | "bad_response" | null>(null);
|
||||||
|
const [detail, setDetail] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const url = normalizeHost(host);
|
||||||
|
const canSubmit = url.length > 0 && !testing && !saving;
|
||||||
|
|
||||||
|
async function handleTest(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!canSubmit) return;
|
||||||
|
setTesting(true);
|
||||||
|
setResult(null);
|
||||||
|
setDetail(undefined);
|
||||||
|
try {
|
||||||
|
const { testBackendUrl } = await import("./lib/backend-config.js");
|
||||||
|
const check = await testBackendUrl(url);
|
||||||
|
setResult(check.ok ? "ok" : (check.reason ?? "unreachable"));
|
||||||
|
setDetail(check.detail);
|
||||||
|
} finally {
|
||||||
|
setTesting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const { saveBackendUrl } = await import("./lib/backend-config.js");
|
||||||
|
await saveBackendUrl(url);
|
||||||
|
setApiBase(url);
|
||||||
|
onConnected();
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-screen items-center justify-center bg-term-bg px-4">
|
||||||
|
<form onSubmit={handleTest} className="card w-full max-w-sm p-6">
|
||||||
|
<h1 className="mb-1 text-h5 font-semibold uppercase tracking-widest text-term-amber">
|
||||||
|
{t("connect.title")}
|
||||||
|
</h1>
|
||||||
|
<p className="mb-5 text-[0.75rem] text-term-muted">{t("connect.hint")}</p>
|
||||||
|
|
||||||
|
<div className="field mb-3">
|
||||||
|
<label className="label">{t("connect.serverAddress")}</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
value={host}
|
||||||
|
onChange={(e) => {
|
||||||
|
setHost(e.target.value);
|
||||||
|
setResult(null);
|
||||||
|
}}
|
||||||
|
placeholder="192.168.1.50:3000"
|
||||||
|
autoFocus
|
||||||
|
autoCapitalize="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{result === "ok" && (
|
||||||
|
<p className="mb-3 text-[0.75rem] text-term-green">{t("connect.testOk")}</p>
|
||||||
|
)}
|
||||||
|
{result === "unreachable" && (
|
||||||
|
<p className="mb-3 text-[0.75rem] text-term-red">
|
||||||
|
{t("connect.testUnreachable")}
|
||||||
|
{detail ? ` (${detail})` : ""}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{result === "bad_response" && (
|
||||||
|
<p className="mb-3 text-[0.75rem] text-term-red">{t("connect.testBadResponse")}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button type="submit" className="btn flex-1" disabled={!canSubmit}>
|
||||||
|
{testing ? t("connect.testing") : t("connect.test")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary flex-1"
|
||||||
|
disabled={!canSubmit || result !== "ok"}
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
{saving ? t("connect.saving") : t("connect.save")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
+46
-11
@@ -1,12 +1,25 @@
|
|||||||
// Thin API client for the operator/admin UI.
|
// Thin API client for the operator/admin UI.
|
||||||
//
|
//
|
||||||
// Auth is cookie-based: the JWT lives in an HttpOnly cookie the browser sends
|
// Auth is cookie-based: the JWT lives in an HttpOnly cookie sent automatically
|
||||||
// automatically (credentials: 'include'). For mutations we echo the readable
|
// (credentials: 'include'). For mutations we echo the readable CSRF cookie
|
||||||
// CSRF cookie back in the X-CSRF-Token header (double-submit). See
|
// back in the X-CSRF-Token header (double-submit). See
|
||||||
// wiki/entities/local-jwt-auth.md.
|
// wiki/entities/local-jwt-auth.md.
|
||||||
|
//
|
||||||
|
// Desktop shell exception: tauri-plugin-http's fetch() runs through Rust's
|
||||||
|
// reqwest, which keeps its OWN cookie jar separate from the webview —
|
||||||
|
// document.cookie on the tauri://localhost page never sees a cookie set on a
|
||||||
|
// plugin-routed response (open upstream bug, tauri-apps/tauri#13045). The
|
||||||
|
// cookie itself IS still sent back to the server by reqwest on later
|
||||||
|
// requests (only the client-side *read* is broken), so the server also
|
||||||
|
// echoes the token in the login/me response BODY (sessionView's csrfToken —
|
||||||
|
// see routes/auth.ts) purely as a second channel for the desktop client to
|
||||||
|
// learn the value; desktopCsrfToken below stashes it in memory and
|
||||||
|
// setSessionUser() (called wherever a SessionUser is received) keeps it
|
||||||
|
// current. The browser path is untouched — it still reads document.cookie.
|
||||||
|
|
||||||
import { logFailedRequest } from "./lib/logger.js";
|
import { logFailedRequest } from "./lib/logger.js";
|
||||||
import { apiUrl } from "./lib/origin.js";
|
import { apiUrl, platformFetch } from "./lib/origin.js";
|
||||||
|
import { inTauri } from "./lib/tauri-env.js";
|
||||||
import type { AppLogRecord, ValidationLine, ValidationMode } from "@parking/shared";
|
import type { AppLogRecord, ValidationLine, ValidationMode } from "@parking/shared";
|
||||||
|
|
||||||
const CSRF_COOKIE = "parking_csrf";
|
const CSRF_COOKIE = "parking_csrf";
|
||||||
@@ -17,6 +30,18 @@ function readCookie(name: string): string | null {
|
|||||||
return m ? decodeURIComponent(m[1]!) : null;
|
return m ? decodeURIComponent(m[1]!) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Desktop-only in-memory CSRF stash — see file header. Never persisted (a
|
||||||
|
* fresh app launch always logs in again, or bootstraps via /api/auth/me
|
||||||
|
* which re-supplies it). */
|
||||||
|
let desktopCsrfToken: string | null = null;
|
||||||
|
|
||||||
|
/** Update the desktop CSRF stash. Called wherever a SessionUser is received
|
||||||
|
* (login, fetchMe). No-op / cheap in the browser (the value just goes
|
||||||
|
* unused there — reads still come from document.cookie). */
|
||||||
|
function setSessionUser(user: SessionUser): void {
|
||||||
|
if (user.csrfToken) desktopCsrfToken = user.csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
/** fetch wrapper: sends cookies, adds CSRF header on mutations, parses errors. */
|
/** fetch wrapper: sends cookies, adds CSRF header on mutations, parses errors. */
|
||||||
export async function apiFetch<T>(path: string, init: RequestInit = {}): Promise<T> {
|
export async function apiFetch<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||||
const method = (init.method ?? "GET").toUpperCase();
|
const method = (init.method ?? "GET").toUpperCase();
|
||||||
@@ -25,10 +50,10 @@ export async function apiFetch<T>(path: string, init: RequestInit = {}): Promise
|
|||||||
headers.set("content-type", "application/json");
|
headers.set("content-type", "application/json");
|
||||||
}
|
}
|
||||||
if (method !== "GET" && method !== "HEAD") {
|
if (method !== "GET" && method !== "HEAD") {
|
||||||
const csrf = readCookie(CSRF_COOKIE);
|
const csrf = inTauri() ? desktopCsrfToken : readCookie(CSRF_COOKIE);
|
||||||
if (csrf) headers.set(CSRF_HEADER, csrf);
|
if (csrf) headers.set(CSRF_HEADER, csrf);
|
||||||
}
|
}
|
||||||
const res = await fetch(apiUrl(path), { ...init, headers, credentials: "include" });
|
const res = await platformFetch(apiUrl(path), { ...init, headers, credentials: "include" });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const msg = (await res.json().catch(() => ({}))) as { error?: string; problems?: string[]; [k: string]: unknown };
|
const msg = (await res.json().catch(() => ({}))) as { error?: string; problems?: string[]; [k: string]: unknown };
|
||||||
const error = msg.error ?? `${path}: ${res.status}`;
|
const error = msg.error ?? `${path}: ${res.status}`;
|
||||||
@@ -82,6 +107,10 @@ export interface SessionUser {
|
|||||||
fullName: string | null;
|
fullName: string | null;
|
||||||
/** Optional contact email (profile metadata); null if unset. */
|
/** Optional contact email (profile metadata); null if unset. */
|
||||||
email: string | null;
|
email: string | null;
|
||||||
|
/** Desktop-only: the CSRF token also echoed via the (JS-unreadable, on
|
||||||
|
* desktop) parking_csrf cookie — see the file header. Absent/unused in the
|
||||||
|
* browser build, which reads the cookie directly instead. */
|
||||||
|
csrfToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Does this session grant the permission? Central authz check for the SPA. */
|
/** Does this session grant the permission? Central authz check for the SPA. */
|
||||||
@@ -89,15 +118,19 @@ export function can(user: SessionUser | null, perm: Permission): boolean {
|
|||||||
return !!user && user.permissions.includes(perm);
|
return !!user && user.permissions.includes(perm);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function login(username: string, password: string): Promise<SessionUser> {
|
export async function login(username: string, password: string): Promise<SessionUser> {
|
||||||
return apiFetch<SessionUser>("/api/auth/login", {
|
const user = await apiFetch<SessionUser>("/api/auth/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ username, password }),
|
body: JSON.stringify({ username, password }),
|
||||||
});
|
});
|
||||||
|
setSessionUser(user);
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logout(): Promise<{ ok: boolean }> {
|
export async function logout(): Promise<{ ok: boolean }> {
|
||||||
return apiFetch("/api/auth/logout", { method: "POST" });
|
const res = await apiFetch<{ ok: boolean }>("/api/auth/logout", { method: "POST" });
|
||||||
|
desktopCsrfToken = null;
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Persist the current user's UI language preference (restored on next login). */
|
/** Persist the current user's UI language preference (restored on next login). */
|
||||||
@@ -146,7 +179,9 @@ export function changeMyPassword(
|
|||||||
/** Returns the current user, or null if not authenticated. */
|
/** Returns the current user, or null if not authenticated. */
|
||||||
export async function fetchMe(): Promise<SessionUser | null> {
|
export async function fetchMe(): Promise<SessionUser | null> {
|
||||||
try {
|
try {
|
||||||
return await apiFetch<SessionUser>("/api/auth/me");
|
const user = await apiFetch<SessionUser>("/api/auth/me");
|
||||||
|
setSessionUser(user);
|
||||||
|
return user;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof ApiError && (e.status === 401 || e.status === 403)) return null;
|
if (e instanceof ApiError && (e.status === 401 || e.status === 403)) return null;
|
||||||
throw e;
|
throw e;
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
// Desktop-only: the operator-configured backend origin (host:port of the
|
||||||
|
// Fastify server this install talks to), persisted across restarts.
|
||||||
|
//
|
||||||
|
// The desktop shell is a generic .deb/.AppImage — it is NOT built for one
|
||||||
|
// specific booth, so the backend address can't be baked in at build time
|
||||||
|
// (that was the old VITE_API_BASE approach; a rebuild was needed to point the
|
||||||
|
// same installer at a different appliance). Instead the operator enters it
|
||||||
|
// once in the ConnectScreen (shown before login whenever nothing usable is
|
||||||
|
// stored yet) and it's saved to a JSON file in the OS config dir via
|
||||||
|
// tauri-plugin-store, read back on every launch before any API call.
|
||||||
|
//
|
||||||
|
// Browser build: this module is never reached (inTauri() gates every call
|
||||||
|
// site — see origin.ts), so there is no browser equivalent or fallback here.
|
||||||
|
|
||||||
|
import type { Store } from "@tauri-apps/plugin-store";
|
||||||
|
|
||||||
|
const STORE_FILE = "backend-config.json";
|
||||||
|
const KEY = "backendUrl";
|
||||||
|
|
||||||
|
let storeHandle: Store | null = null;
|
||||||
|
async function getStore(): Promise<Store> {
|
||||||
|
if (!storeHandle) {
|
||||||
|
const { load } = await import("@tauri-apps/plugin-store");
|
||||||
|
storeHandle = await load(STORE_FILE, { autoSave: true });
|
||||||
|
}
|
||||||
|
return storeHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The saved backend origin (no trailing slash), or null if never configured.
|
||||||
|
* Desktop only — throws if called from a browser build. */
|
||||||
|
export async function loadBackendUrl(): Promise<string | null> {
|
||||||
|
const store = await getStore();
|
||||||
|
const v = await store.get<string>(KEY);
|
||||||
|
return typeof v === "string" && v.length > 0 ? v.replace(/\/$/, "") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Persist a new backend origin (validated + reachable — call testBackendUrl
|
||||||
|
* first). Takes effect immediately for future platformFetch/wsUrl calls. */
|
||||||
|
export async function saveBackendUrl(url: string): Promise<void> {
|
||||||
|
const store = await getStore();
|
||||||
|
await store.set(KEY, url.replace(/\/$/, ""));
|
||||||
|
await store.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clear the saved backend (forces the ConnectScreen back up next launch). */
|
||||||
|
export async function clearBackendUrl(): Promise<void> {
|
||||||
|
const store = await getStore();
|
||||||
|
await store.delete(KEY);
|
||||||
|
await store.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackendCheck {
|
||||||
|
ok: boolean;
|
||||||
|
/** "unreachable" (network/DNS/refused) | "bad_response" (reachable, not our API). */
|
||||||
|
reason?: "unreachable" | "bad_response";
|
||||||
|
detail?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Probe a candidate origin by hitting /api/version. That route is behind
|
||||||
|
* requirePermission("site:read") (session cookie + site:read — see
|
||||||
|
* apps/server/src/routes/site.ts), so a pre-login probe can never get a 2xx;
|
||||||
|
* we're not checking "is this reachable and mine to use", only "is something
|
||||||
|
* that speaks our Fastify auth protocol listening here" — a 401 (missing/bad
|
||||||
|
* JWT) or 403 (valid session, wrong permission) from THIS specific route is
|
||||||
|
* as strong a signal of that as a 200 would be, and both are expected outcomes
|
||||||
|
* pre-login. Uses the same tauri-plugin-http path platformFetch does (raw
|
||||||
|
* fetch from the webview can't reach an arbitrary LAN host — mixed content,
|
||||||
|
* see origin.ts). */
|
||||||
|
export async function testBackendUrl(url: string): Promise<BackendCheck> {
|
||||||
|
const origin = url.replace(/\/$/, "");
|
||||||
|
try {
|
||||||
|
const { fetch: tauriFetch } = await import("@tauri-apps/plugin-http");
|
||||||
|
const res = await tauriFetch(`${origin}/api/version`, {
|
||||||
|
method: "GET",
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
});
|
||||||
|
if (!res.ok && res.status !== 401 && res.status !== 403) {
|
||||||
|
return { ok: false, reason: "bad_response", detail: `HTTP ${res.status}` };
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: "unreachable",
|
||||||
|
detail: err instanceof Error ? err.message : String(err),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,11 +9,18 @@
|
|||||||
// and never tries to resolve the Tauri APIs. Offline-first: a failed check (no
|
// and never tries to resolve the Tauri APIs. Offline-first: a failed check (no
|
||||||
// network — the appliance is usually offline) is swallowed; updates only happen
|
// network — the appliance is usually offline) is swallowed; updates only happen
|
||||||
// when someone has brought the box online (e.g. a phone hotspot) on purpose.
|
// when someone has brought the box online (e.g. a phone hotspot) on purpose.
|
||||||
|
//
|
||||||
|
// A release build's console.error is invisible with no way to attach devtools
|
||||||
|
// in the field (kiosk mode blocks the context menu; this WebKitGTK build's
|
||||||
|
// remote inspector doesn't answer standard discovery endpoints either — both
|
||||||
|
// confirmed dead ends 2026-09-03). logClient() ships straight to the
|
||||||
|
// server-side app_logs store regardless of the client's console-forward log
|
||||||
|
// level (that gate is meant for noisy console chatter, not this), so a real
|
||||||
|
// post-accept install failure is visible via wiki/concepts/app-logs.md /
|
||||||
|
// LogsViewer.tsx without needing a terminal or devtools at all.
|
||||||
|
|
||||||
/** True when running inside the Tauri webview (not a normal browser). */
|
import { logClient } from "./logger.js";
|
||||||
function inTauri(): boolean {
|
import { inTauri } from "./tauri-env.js";
|
||||||
return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdatePrompt {
|
export interface UpdatePrompt {
|
||||||
/** Newer version string offered by the server. */
|
/** Newer version string offered by the server. */
|
||||||
@@ -42,13 +49,24 @@ export async function checkForDesktopUpdate(
|
|||||||
// Download + install the signed update (signature verified against the
|
// Download + install the signed update (signature verified against the
|
||||||
// pubkey in tauri.conf.json), then relaunch into the new version.
|
// pubkey in tauri.conf.json), then relaunch into the new version.
|
||||||
try {
|
try {
|
||||||
await update.downloadAndInstall();
|
await update.downloadAndInstall((progress) => {
|
||||||
|
logClient({
|
||||||
|
level: "info",
|
||||||
|
message: `desktop update download progress: ${progress.event}`,
|
||||||
|
context: { kind: "desktop_update_progress", version: update.version, event: progress.event },
|
||||||
|
});
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// A real update WAS found and accepted — this is a genuine install
|
// A real update WAS found and accepted — this is a genuine install
|
||||||
// failure (bad signature, corrupted download, disk/permission issue),
|
// failure (bad signature, corrupted download, disk/permission issue),
|
||||||
// not "offline". Surface it instead of silently reverting to the old
|
// not "offline". Surface it instead of silently reverting to the old
|
||||||
// version with no explanation.
|
// version with no explanation.
|
||||||
console.error("desktop update download/install failed:", err);
|
logClient({
|
||||||
|
level: "error",
|
||||||
|
message: `desktop update download/install failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
stack: err instanceof Error ? err.stack : undefined,
|
||||||
|
context: { kind: "desktop_update_install_failed", version: update.version },
|
||||||
|
});
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
const { relaunch } = await import("@tauri-apps/plugin-process");
|
const { relaunch } = await import("@tauri-apps/plugin-process");
|
||||||
@@ -56,7 +74,13 @@ export async function checkForDesktopUpdate(
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Offline / endpoint unreachable / no update server yet → ignore. The app
|
// Offline / endpoint unreachable / no update server yet → ignore. The app
|
||||||
// keeps running on the current version; checking again next launch. Still
|
// keeps running on the current version; checking again next launch. Still
|
||||||
// log it so a real install failure (rethrown above) isn't invisible.
|
// log it (info, not error — this path is expected/normal far more often
|
||||||
console.warn("desktop update check/apply skipped:", err);
|
// than it's a real problem) so a real install failure (rethrown above,
|
||||||
|
// logged as error) isn't lost among routine offline checks.
|
||||||
|
logClient({
|
||||||
|
level: "info",
|
||||||
|
message: `desktop update check/apply skipped: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
context: { kind: "desktop_update_skipped" },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,20 @@ export const en: Catalog = {
|
|||||||
signIn: "Sign in",
|
signIn: "Sign in",
|
||||||
signingIn: "Signing in…",
|
signingIn: "Signing in…",
|
||||||
},
|
},
|
||||||
|
connect: {
|
||||||
|
title: "Connect to server",
|
||||||
|
hint: "Enter the address of the parking system server for this booth.",
|
||||||
|
serverAddress: "Server address",
|
||||||
|
test: "Test",
|
||||||
|
testing: "Testing…",
|
||||||
|
save: "Save & continue",
|
||||||
|
saving: "Saving…",
|
||||||
|
testOk: "Reachable — this looks like a Parking System server.",
|
||||||
|
testUnreachable: "Could not reach this address.",
|
||||||
|
testBadResponse: "Reachable, but this doesn't look like a Parking System server.",
|
||||||
|
changeServer: "Change server",
|
||||||
|
changeServerConfirm: "This signs you out and asks for a new server address on next launch. Continue?",
|
||||||
|
},
|
||||||
update: {
|
update: {
|
||||||
available: "Update available",
|
available: "Update available",
|
||||||
prompt: "Version {{version}} is available. Install now and restart?",
|
prompt: "Version {{version}} is available. Install now and restart?",
|
||||||
|
|||||||
@@ -45,6 +45,20 @@ export const sq = {
|
|||||||
signIn: "Hyr",
|
signIn: "Hyr",
|
||||||
signingIn: "Duke hyrë…",
|
signingIn: "Duke hyrë…",
|
||||||
},
|
},
|
||||||
|
connect: {
|
||||||
|
title: "Lidhu me serverin",
|
||||||
|
hint: "Vendos adresën e serverit të sistemit të parkimit për këtë kabinë.",
|
||||||
|
serverAddress: "Adresa e serverit",
|
||||||
|
test: "Testo",
|
||||||
|
testing: "Duke testuar…",
|
||||||
|
save: "Ruaj & vazhdo",
|
||||||
|
saving: "Duke ruajtur…",
|
||||||
|
testOk: "I arritshëm — duket si server i Sistemit të Parkimit.",
|
||||||
|
testUnreachable: "Nuk u arrit kjo adresë.",
|
||||||
|
testBadResponse: "I arritshëm, por nuk duket si server i Sistemit të Parkimit.",
|
||||||
|
changeServer: "Ndrysho serverin",
|
||||||
|
changeServerConfirm: "Kjo do t'ju dalë nga sesioni dhe do kërkojë adresë të re serveri në hapjen tjetër. Vazhdo?",
|
||||||
|
},
|
||||||
update: {
|
update: {
|
||||||
available: "Përditësim i disponueshëm",
|
available: "Përditësim i disponueshëm",
|
||||||
prompt: "Versioni {{version}} është i disponueshëm. Ta instaloj tani dhe ta rinis?",
|
prompt: "Versioni {{version}} është i disponueshëm. Ta instaloj tani dhe ta rinis?",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
// high-signal sources (failed requests, uncaught errors) are always captured.
|
// high-signal sources (failed requests, uncaught errors) are always captured.
|
||||||
|
|
||||||
import { LOG_LEVEL_ORDER, type ClientLogInput, type LogLevel } from "@parking/shared";
|
import { LOG_LEVEL_ORDER, type ClientLogInput, type LogLevel } from "@parking/shared";
|
||||||
|
import { apiUrl, platformFetch } from "./origin.js";
|
||||||
|
|
||||||
const ENDPOINT = "/api/logs";
|
const ENDPOINT = "/api/logs";
|
||||||
const FLUSH_MS = 4000;
|
const FLUSH_MS = 4000;
|
||||||
@@ -76,7 +77,7 @@ async function flush(): Promise<void> {
|
|||||||
const headers: Record<string, string> = { "content-type": "application/json" };
|
const headers: Record<string, string> = { "content-type": "application/json" };
|
||||||
const csrf = readCookie(CSRF_COOKIE);
|
const csrf = readCookie(CSRF_COOKIE);
|
||||||
if (csrf) headers[CSRF_HEADER] = csrf;
|
if (csrf) headers[CSRF_HEADER] = csrf;
|
||||||
await fetch(ENDPOINT, {
|
await platformFetch(apiUrl(ENDPOINT), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
@@ -90,7 +91,10 @@ async function flush(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Best-effort synchronous flush on page hide (sendBeacon survives unload). */
|
/** Best-effort synchronous flush on page hide (sendBeacon survives unload). Browser
|
||||||
|
* only — sendBeacon is a native browser API with no Tauri-HTTP-plugin equivalent,
|
||||||
|
* so this drops silently in the desktop shell (unload is rare there; the regular
|
||||||
|
* 4s-interval flush above covers the common case). */
|
||||||
function flushBeacon(): void {
|
function flushBeacon(): void {
|
||||||
if (queue.length === 0) return;
|
if (queue.length === 0) return;
|
||||||
const entries = queue.splice(0, queue.length);
|
const entries = queue.splice(0, queue.length);
|
||||||
@@ -99,7 +103,7 @@ function flushBeacon(): void {
|
|||||||
// sendBeacon can't set the CSRF header; the server accepts the ingest for any
|
// sendBeacon can't set the CSRF header; the server accepts the ingest for any
|
||||||
// signed-in session (cookie sent automatically). If CSRF later guards it strictly,
|
// signed-in session (cookie sent automatically). If CSRF later guards it strictly,
|
||||||
// this path degrades to "lost on unload" — acceptable for diagnostics.
|
// this path degrades to "lost on unload" — acceptable for diagnostics.
|
||||||
navigator.sendBeacon(ENDPOINT, blob);
|
navigator.sendBeacon(apiUrl(ENDPOINT), blob);
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,49 @@
|
|||||||
// In a browser (dev via the Vite proxy, or prod where Fastify serves the built
|
// In a browser (dev via the Vite proxy, or prod where Fastify serves the built
|
||||||
// SPA) this is EMPTY — requests stay relative (`/api/...`) and same-origin, so
|
// SPA) this is EMPTY — requests stay relative (`/api/...`) and same-origin, so
|
||||||
// nothing changes. The Tauri desktop shell (apps/desktop) serves the bundled
|
// nothing changes. The Tauri desktop shell (apps/desktop) serves the bundled
|
||||||
// SPA from `tauri://localhost`, which has no backend and no proxy; there we set
|
// SPA from `tauri://localhost`, which has no backend and no proxy; there the
|
||||||
// VITE_API_BASE to the appliance's Fastify origin (e.g. http://127.0.0.1:3000)
|
// operator enters the appliance's Fastify origin (e.g. http://192.168.1.50:3000)
|
||||||
// at build time so /api and the live WS feed resolve to the real server.
|
// once in the ConnectScreen and it's persisted via tauri-plugin-store (see
|
||||||
|
// backend-config.ts) — a RUNTIME value, not a build-time one, since the same
|
||||||
|
// installer is used across every booth and the backend can move (new box, new
|
||||||
|
// IP) without a rebuild. main.tsx calls initApiBase() before the app mounts.
|
||||||
//
|
//
|
||||||
// Keep this the SINGLE source for the backend origin — api.ts and the live-feed
|
// Keep this the SINGLE source for the backend origin — api.ts and the live-feed
|
||||||
// WebSocket both read it, so the web app and the desktop shell stay identical
|
// WebSocket both read it, so the web app and the desktop shell stay identical
|
||||||
// except for this one build-time value.
|
// except for this one runtime value.
|
||||||
|
//
|
||||||
|
// platformFetch(): WebKitGTK treats tauri://localhost as a SECURE origin, so a
|
||||||
|
// plain http://192.168.1.50:3000 fetch() from inside it is blocked as mixed
|
||||||
|
// content (a WebKit limitation — CSP's connect-src does NOT override this;
|
||||||
|
// found 2026-09-03 as "Load failed" on every desktop request). Inside Tauri we
|
||||||
|
// dynamically import @tauri-apps/plugin-http's fetch, which routes the request
|
||||||
|
// through Tauri's native side instead of the webview's own fetch, sidestepping
|
||||||
|
// the check entirely. Browser build never imports the plugin (dynamic import,
|
||||||
|
// same pattern as desktop-updater.ts).
|
||||||
|
|
||||||
/** Backend HTTP origin, no trailing slash. Empty string = same-origin/relative. */
|
import { inTauri } from "./tauri-env.js";
|
||||||
export const API_BASE: string = (import.meta.env.VITE_API_BASE ?? "").replace(/\/$/, "");
|
|
||||||
|
/** Backend HTTP origin, no trailing slash. Empty string = same-origin/relative
|
||||||
|
* (browser) or not-yet-configured (desktop, before the ConnectScreen runs). */
|
||||||
|
export let API_BASE: string = "";
|
||||||
|
|
||||||
|
/** Desktop only: load the persisted backend URL (if any) before the app
|
||||||
|
* mounts, so the very first fetchMe() call already has the right origin.
|
||||||
|
* No-op in the browser. Returns the loaded value (null = not configured yet,
|
||||||
|
* meaning main.tsx should show the ConnectScreen instead of the normal app). */
|
||||||
|
export async function initApiBase(): Promise<string | null> {
|
||||||
|
if (!inTauri()) return null;
|
||||||
|
const { loadBackendUrl } = await import("./backend-config.js");
|
||||||
|
const saved = await loadBackendUrl();
|
||||||
|
if (saved) API_BASE = saved;
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Desktop only: change the backend origin at runtime (after the operator
|
||||||
|
* saves a new one in Settings) without requiring a full app restart. */
|
||||||
|
export function setApiBase(url: string): void {
|
||||||
|
API_BASE = url.replace(/\/$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
/** Resolve an API path to a full URL (or a relative path when API_BASE is empty). */
|
/** Resolve an API path to a full URL (or a relative path when API_BASE is empty). */
|
||||||
export function apiUrl(path: string): string {
|
export function apiUrl(path: string): string {
|
||||||
@@ -28,3 +61,19 @@ export function wsUrl(path: string): string {
|
|||||||
const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
return `${proto}//${window.location.host}${path}`;
|
return `${proto}//${window.location.host}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { inTauri };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fetch(), but routed through @tauri-apps/plugin-http inside the desktop
|
||||||
|
* shell (see the file header for why the webview's own fetch can't reach
|
||||||
|
* the local backend). Same signature as the global fetch; a plain pass-
|
||||||
|
* through in the browser.
|
||||||
|
*/
|
||||||
|
export async function platformFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
||||||
|
if (inTauri()) {
|
||||||
|
const { fetch: tauriFetch } = await import("@tauri-apps/plugin-http");
|
||||||
|
return tauriFetch(input, init);
|
||||||
|
}
|
||||||
|
return fetch(input, init);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
// Desktop-only WebSocket adapter.
|
||||||
|
//
|
||||||
|
// WebKitGTK treats tauri://localhost as a SECURE origin, so a plain
|
||||||
|
// ws://127.0.0.1:3000 connection from inside it is blocked as mixed content —
|
||||||
|
// same root cause as the HTTP fetch() issue (see origin.ts's platformFetch),
|
||||||
|
// but WS is a separate browser check with its own plugin
|
||||||
|
// (@tauri-apps/plugin-websocket), which routes the connection through Tauri's
|
||||||
|
// native side instead of the webview's own WebSocket.
|
||||||
|
//
|
||||||
|
// That plugin's API is async/listener-based, not the synchronous
|
||||||
|
// onopen/onmessage/onclose event surface use-live-feed.ts is written against
|
||||||
|
// (and has already been hardened for — reconnect backoff, StrictMode
|
||||||
|
// double-invoke, cleanup). Rather than rewrite that hook around a different
|
||||||
|
// API shape, this adapter presents the same native-WebSocket-like interface
|
||||||
|
// use-live-feed.ts already expects, so that hook needs no changes at all.
|
||||||
|
//
|
||||||
|
// Browser build: plain pass-through to the real WebSocket (this file's
|
||||||
|
// createPlatformSocket is only called from inside inTauri() callers).
|
||||||
|
|
||||||
|
import { inTauri } from "./tauri-env.js";
|
||||||
|
|
||||||
|
export interface PlatformSocket {
|
||||||
|
onopen: (() => void) | null;
|
||||||
|
onmessage: ((ev: { data: string }) => void) | null;
|
||||||
|
onclose: (() => void) | null;
|
||||||
|
onerror: (() => void) | null;
|
||||||
|
close(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NativeSocketAdapter implements PlatformSocket {
|
||||||
|
onopen: (() => void) | null = null;
|
||||||
|
onmessage: ((ev: { data: string }) => void) | null = null;
|
||||||
|
onclose: (() => void) | null = null;
|
||||||
|
onerror: (() => void) | null = null;
|
||||||
|
#sock: WebSocket;
|
||||||
|
|
||||||
|
constructor(url: string) {
|
||||||
|
this.#sock = new WebSocket(url);
|
||||||
|
this.#sock.onopen = () => this.onopen?.();
|
||||||
|
this.#sock.onmessage = (ev) => this.onmessage?.({ data: ev.data as string });
|
||||||
|
this.#sock.onclose = () => this.onclose?.();
|
||||||
|
this.#sock.onerror = () => this.onerror?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.#sock.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TauriSocketAdapter implements PlatformSocket {
|
||||||
|
onopen: (() => void) | null = null;
|
||||||
|
onmessage: ((ev: { data: string }) => void) | null = null;
|
||||||
|
onclose: (() => void) | null = null;
|
||||||
|
onerror: (() => void) | null = null;
|
||||||
|
#closed = false;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
#conn: any = null;
|
||||||
|
|
||||||
|
constructor(url: string) {
|
||||||
|
void this.#connect(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #connect(url: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { default: TauriWebSocket } = await import("@tauri-apps/plugin-websocket");
|
||||||
|
if (this.#closed) return; // close() called before connect resolved
|
||||||
|
// Runs on Tauri's native (Rust) side, NOT inside the webview page — there
|
||||||
|
// is no page context to auto-attach an Origin header the way a real
|
||||||
|
// browser WebSocket would. The server's anti-CSWSH check (routes/ws.ts)
|
||||||
|
// rejects any handshake with a missing/mismatched Origin, so it must be
|
||||||
|
// set explicitly here to match what WS_ALLOWED_ORIGINS expects
|
||||||
|
// (tauri://localhost — see apps/server/.env.example).
|
||||||
|
const conn = await TauriWebSocket.connect(url, { headers: { Origin: "tauri://localhost" } });
|
||||||
|
if (this.#closed) {
|
||||||
|
void conn.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#conn = conn;
|
||||||
|
conn.addListener((msg: { type: string; data: unknown }) => {
|
||||||
|
if (msg.type === "Text") {
|
||||||
|
this.onmessage?.({ data: msg.data as string });
|
||||||
|
} else if (msg.type === "Close") {
|
||||||
|
this.onclose?.();
|
||||||
|
}
|
||||||
|
// Binary/Ping/Pong: the server protocol here is text-JSON only (see
|
||||||
|
// routes/ws.ts) — nothing else is expected.
|
||||||
|
});
|
||||||
|
this.onopen?.();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Tauri WebSocket connect failed:", url, err);
|
||||||
|
this.onerror?.();
|
||||||
|
this.onclose?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.#closed = true;
|
||||||
|
void this.#conn?.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Open a live-feed socket, routed through the Tauri WebSocket plugin inside the
|
||||||
|
* desktop shell (mixed-content workaround), or the native WebSocket in a browser. */
|
||||||
|
export function createPlatformSocket(url: string): PlatformSocket {
|
||||||
|
return inTauri() ? new TauriSocketAdapter(url) : new NativeSocketAdapter(url);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/** True when running inside the Tauri webview (not a normal browser). Single
|
||||||
|
* source for this check — origin.ts, platform-ws.ts, desktop-updater.ts, and
|
||||||
|
* backend-config.ts all gate their Tauri-only code paths on it. */
|
||||||
|
export function inTauri(): boolean {
|
||||||
|
return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import type { DeviceStatus, LedgerEvent, Occupancy } from "../api.js";
|
|||||||
import { qk } from "./query.js";
|
import { qk } from "./query.js";
|
||||||
import { useLiveStore, type LaneStatus, type LanePresence } from "./live-store.js";
|
import { useLiveStore, type LaneStatus, type LanePresence } from "./live-store.js";
|
||||||
import { wsUrl } from "./origin.js";
|
import { wsUrl } from "./origin.js";
|
||||||
|
import { createPlatformSocket, type PlatformSocket } from "./platform-ws.js";
|
||||||
|
|
||||||
// Booth WebSocket client. Opens ONE socket to /api/ws and turns server pushes into
|
// Booth WebSocket client. Opens ONE socket to /api/ws and turns server pushes into
|
||||||
// (a) live-store updates for the ticker/occupancy and (b) Query cache invalidations
|
// (a) live-store updates for the ticker/occupancy and (b) Query cache invalidations
|
||||||
@@ -36,7 +37,7 @@ export function useLiveFeed(enabled: boolean = true): void {
|
|||||||
useLiveStore();
|
useLiveStore();
|
||||||
// Hold the socket + reconnect timer across renders; guard against StrictMode
|
// Hold the socket + reconnect timer across renders; guard against StrictMode
|
||||||
// double-invoke and unmount.
|
// double-invoke and unmount.
|
||||||
const sockRef = useRef<WebSocket | null>(null);
|
const sockRef = useRef<PlatformSocket | null>(null);
|
||||||
const retryRef = useRef(0);
|
const retryRef = useRef(0);
|
||||||
const closedRef = useRef(false);
|
const closedRef = useRef(false);
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ export function useLiveFeed(enabled: boolean = true): void {
|
|||||||
const connect = () => {
|
const connect = () => {
|
||||||
if (closedRef.current) return;
|
if (closedRef.current) return;
|
||||||
setStatus(retryRef.current === 0 ? "connecting" : "connecting");
|
setStatus(retryRef.current === 0 ? "connecting" : "connecting");
|
||||||
const sock = new WebSocket(wsUrl("/api/ws"));
|
const sock = createPlatformSocket(wsUrl("/api/ws"));
|
||||||
sockRef.current = sock;
|
sockRef.current = sock;
|
||||||
|
|
||||||
sock.onopen = () => {
|
sock.onopen = () => {
|
||||||
|
|||||||
+76
-1
@@ -6,7 +6,7 @@ import {
|
|||||||
Outlet,
|
Outlet,
|
||||||
redirect,
|
redirect,
|
||||||
} from "@tanstack/react-router";
|
} from "@tanstack/react-router";
|
||||||
import { lazy, Suspense, useState } from "react";
|
import { lazy, Suspense, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import type { Lang, Permission, SessionUser, Theme } from "./api.js";
|
import type { Lang, Permission, SessionUser, Theme } from "./api.js";
|
||||||
@@ -30,6 +30,7 @@ import { Spinner } from "./ui/Spinner.js";
|
|||||||
import { setLanguage } from "./lib/i18n/index.js";
|
import { setLanguage } from "./lib/i18n/index.js";
|
||||||
import { applyTheme, applyFontScale } from "./lib/theme.js";
|
import { applyTheme, applyFontScale } from "./lib/theme.js";
|
||||||
import { useLiveFeed } from "./lib/use-live-feed.js";
|
import { useLiveFeed } from "./lib/use-live-feed.js";
|
||||||
|
import { inTauri } from "./lib/origin.js";
|
||||||
import { useShift } from "./lib/use-shift.js";
|
import { useShift } from "./lib/use-shift.js";
|
||||||
import { DeviceFooter } from "./ui/DeviceFooter.js";
|
import { DeviceFooter } from "./ui/DeviceFooter.js";
|
||||||
import { StatusDot } from "./ui/StatusDot.js";
|
import { StatusDot } from "./ui/StatusDot.js";
|
||||||
@@ -106,6 +107,78 @@ function VersionBadge() {
|
|||||||
return <span className="ml-auto shrink-0 pl-3 text-[0.7rem] text-term-muted">{version}</span>;
|
return <span className="ml-auto shrink-0 pl-3 text-[0.7rem] text-term-muted">{version}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The installed Tauri app's own "vX.Y.Z" (from tauri.conf.json, synced to the git tag by
|
||||||
|
* release.yml — see wiki/decisions/desktop-shell-tauri.md) — the client's version, distinct
|
||||||
|
* from VersionBadge's SERVER build. No-op / renders nothing in a browser (there's no Tauri
|
||||||
|
* API to call). Was invisible before this: an operator had no way to tell which desktop
|
||||||
|
* build was actually installed short of reading the update-available prompt. */
|
||||||
|
function DesktopVersionBadge() {
|
||||||
|
const [version, setVersion] = useState<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!inTauri()) return;
|
||||||
|
let cancelled = false;
|
||||||
|
void import("@tauri-apps/api/app").then(({ getVersion }) =>
|
||||||
|
getVersion().then((v) => {
|
||||||
|
if (!cancelled) setVersion(v);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
if (!version) return null;
|
||||||
|
return <span className="ml-auto shrink-0 pl-3 text-[0.7rem] text-term-muted">app v{version}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Desktop-only "change which server this install talks to" control. No-op /
|
||||||
|
* renders nothing in a browser (the concept doesn't apply — same-origin).
|
||||||
|
* Simplest correct action: clear the saved backend URL and reload, which
|
||||||
|
* drops the app back to ConnectScreen (see App.tsx) to re-enter it — this
|
||||||
|
* mirrors clearing the session (logout → back to Login), not an inline
|
||||||
|
* editor, since repointing the app is a rare, deliberate admin action. */
|
||||||
|
function DesktopServerButton() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [confirming, setConfirming] = useState(false);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
if (!inTauri()) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost btn-sm ml-2"
|
||||||
|
onClick={() => setConfirming(true)}
|
||||||
|
>
|
||||||
|
{t("connect.changeServer")}
|
||||||
|
</button>
|
||||||
|
{confirming && (
|
||||||
|
<Modal open onClose={() => setConfirming(false)} title={t("connect.changeServer")} width="max-w-sm">
|
||||||
|
<div className="text-[0.8125rem]">
|
||||||
|
<p className="text-term-muted">{t("connect.changeServerConfirm")}</p>
|
||||||
|
<div className="mt-3 flex justify-end gap-2">
|
||||||
|
<button type="button" className="btn btn-sm" onClick={() => setConfirming(false)} disabled={busy}>
|
||||||
|
{t("subs.cancel")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-danger"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={async () => {
|
||||||
|
setBusy(true);
|
||||||
|
const { clearBackendUrl } = await import("./lib/backend-config.js");
|
||||||
|
await clearBackendUrl();
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{busy ? <Spinner /> : t("connect.changeServer")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Setup layout — the config hub. Renders a permission-gated tab bar and the active
|
/** Setup layout — the config hub. Renders a permission-gated tab bar and the active
|
||||||
* tab's screen via <Outlet>. Each tab is a child route (its own URL + guard), so
|
* tab's screen via <Outlet>. Each tab is a child route (its own URL + guard), so
|
||||||
* deep links and the back button work and a denied tab redirects to the booth. */
|
* deep links and the back button work and a denied tab redirects to the booth. */
|
||||||
@@ -125,6 +198,8 @@ function SetupLayout() {
|
|||||||
{show("log:read") && <SetupTab to="/setup/logs" label={t("nav.logs")} />}
|
{show("log:read") && <SetupTab to="/setup/logs" label={t("nav.logs")} />}
|
||||||
{show("backup:read") && <SetupTab to="/setup/backup" label={t("nav.backup")} />}
|
{show("backup:read") && <SetupTab to="/setup/backup" label={t("nav.backup")} />}
|
||||||
{show("site:read") && <VersionBadge />}
|
{show("site:read") && <VersionBadge />}
|
||||||
|
<DesktopVersionBadge />
|
||||||
|
<DesktopServerButton />
|
||||||
</nav>
|
</nav>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+10
-4
@@ -49,10 +49,13 @@ REGISTRY=git.infra.msai.al/mca/parking_solution
|
|||||||
# Staging booth: pinned immutable stage-<sha>. After each promotion (merge dev → stage, CI builds
|
# Staging booth: pinned immutable stage-<sha>. After each promotion (merge dev → stage, CI builds
|
||||||
# :stage-<sha>), bump this to the new sha and re-sync/deploy from Core. The moving `:stage` tag
|
# :stage-<sha>), bump this to the new sha and re-sync/deploy from Core. The moving `:stage` tag
|
||||||
# exists as the pointer; we deploy the sha, not the mover.
|
# exists as the pointer; we deploy the sha, not the mover.
|
||||||
TAG=stage-28bd838
|
TAG=stage-7d67934
|
||||||
COOKIE_SECURE=0
|
COOKIE_SECURE=0
|
||||||
VISION_ENABLED=1
|
VISION_ENABLED=1
|
||||||
WS_ALLOWED_ORIGINS=
|
# Desktop app WS handshake: Origin is tauri://localhost (set explicitly by
|
||||||
|
# platform-ws.ts, since the native WS plugin has no page context to auto-attach
|
||||||
|
# one). Linux may also send http://tauri.localhost. See routes/ws.ts anti-CSWSH check.
|
||||||
|
WS_ALLOWED_ORIGINS=tauri://localhost,http://tauri.localhost
|
||||||
JWT_SECRET=[[park_buzi_jwt_secret]]
|
JWT_SECRET=[[park_buzi_jwt_secret]]
|
||||||
EVENT_SIGNING_KEY=[[park_buzi_event_signing_key]]
|
EVENT_SIGNING_KEY=[[park_buzi_event_signing_key]]
|
||||||
BACKUP_KEY=[[park_buzi_backup_key]]
|
BACKUP_KEY=[[park_buzi_backup_key]]
|
||||||
@@ -79,10 +82,13 @@ REGISTRY=git.infra.msai.al/mca/parking_solution
|
|||||||
# Staging booth: pinned immutable stage-<sha>. After each promotion (merge dev → stage, CI builds
|
# Staging booth: pinned immutable stage-<sha>. After each promotion (merge dev → stage, CI builds
|
||||||
# :stage-<sha>), bump this to the new sha and re-sync/deploy from Core. The moving `:stage` tag
|
# :stage-<sha>), bump this to the new sha and re-sync/deploy from Core. The moving `:stage` tag
|
||||||
# exists as the pointer; we deploy the sha, not the mover.
|
# exists as the pointer; we deploy the sha, not the mover.
|
||||||
TAG=stage-28bd838
|
TAG=stage-7d67934
|
||||||
COOKIE_SECURE=0
|
COOKIE_SECURE=0
|
||||||
VISION_ENABLED=1
|
VISION_ENABLED=1
|
||||||
WS_ALLOWED_ORIGINS=
|
# Desktop app WS handshake: Origin is tauri://localhost (set explicitly by
|
||||||
|
# platform-ws.ts, since the native WS plugin has no page context to auto-attach
|
||||||
|
# one). Linux may also send http://tauri.localhost. See routes/ws.ts anti-CSWSH check.
|
||||||
|
WS_ALLOWED_ORIGINS=tauri://localhost,http://tauri.localhost
|
||||||
JWT_SECRET=[[park_2_jwt_secret]]
|
JWT_SECRET=[[park_2_jwt_secret]]
|
||||||
EVENT_SIGNING_KEY=[[park_2_event_signing_key]]
|
EVENT_SIGNING_KEY=[[park_2_event_signing_key]]
|
||||||
BACKUP_KEY=[[park_2_backup_key]]
|
BACKUP_KEY=[[park_2_backup_key]]
|
||||||
|
|||||||
Generated
+33
@@ -108,12 +108,24 @@ importers:
|
|||||||
'@tanstack/react-router':
|
'@tanstack/react-router':
|
||||||
specifier: ^1.170.16
|
specifier: ^1.170.16
|
||||||
version: 1.170.16(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.170.16(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
|
'@tauri-apps/api':
|
||||||
|
specifier: ^2.11.1
|
||||||
|
version: 2.11.1
|
||||||
|
'@tauri-apps/plugin-http':
|
||||||
|
specifier: ^2.5.2
|
||||||
|
version: 2.6.0
|
||||||
'@tauri-apps/plugin-process':
|
'@tauri-apps/plugin-process':
|
||||||
specifier: ^2.3.1
|
specifier: ^2.3.1
|
||||||
version: 2.3.1
|
version: 2.3.1
|
||||||
|
'@tauri-apps/plugin-store':
|
||||||
|
specifier: ^2.4.0
|
||||||
|
version: 2.4.4
|
||||||
'@tauri-apps/plugin-updater':
|
'@tauri-apps/plugin-updater':
|
||||||
specifier: ^2.10.1
|
specifier: ^2.10.1
|
||||||
version: 2.10.1
|
version: 2.10.1
|
||||||
|
'@tauri-apps/plugin-websocket':
|
||||||
|
specifier: ^2.3.0
|
||||||
|
version: 2.4.3
|
||||||
i18next:
|
i18next:
|
||||||
specifier: ^26.3.1
|
specifier: ^26.3.1
|
||||||
version: 26.3.1(typescript@6.0.3)
|
version: 26.3.1(typescript@6.0.3)
|
||||||
@@ -1577,12 +1589,21 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-http@2.6.0':
|
||||||
|
resolution: {integrity: sha512-QYXwbGb4hQ9/8Riv/ejU/kPFFnbBIrBcWwV1LIXv2xBKfoj8lkWfGkd9pkCSsBI/pljPtz+IPqfrE3t3bVl3mg==}
|
||||||
|
|
||||||
'@tauri-apps/plugin-process@2.3.1':
|
'@tauri-apps/plugin-process@2.3.1':
|
||||||
resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==}
|
resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==}
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-store@2.4.4':
|
||||||
|
resolution: {integrity: sha512-oxSMaj/QpVfJcBMYX5aOQV94fWvga0MwQMfD6TLlbK2dh+ShPWAzefd8HWXhvOKjPRJdGVAkW7ZGO76JzzjaDA==}
|
||||||
|
|
||||||
'@tauri-apps/plugin-updater@2.10.1':
|
'@tauri-apps/plugin-updater@2.10.1':
|
||||||
resolution: {integrity: sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==}
|
resolution: {integrity: sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==}
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-websocket@2.4.3':
|
||||||
|
resolution: {integrity: sha512-c85ykljg6AzY6Zw4KpYsEBaLirjPIs6m8xxC6hZcdwAchckaBU368US+oSsa5B43PjSLukjwD5vOOqzYYnswWA==}
|
||||||
|
|
||||||
'@testing-library/dom@10.4.1':
|
'@testing-library/dom@10.4.1':
|
||||||
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
|
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -4089,14 +4110,26 @@ snapshots:
|
|||||||
'@tauri-apps/cli-win32-ia32-msvc': 2.11.3
|
'@tauri-apps/cli-win32-ia32-msvc': 2.11.3
|
||||||
'@tauri-apps/cli-win32-x64-msvc': 2.11.3
|
'@tauri-apps/cli-win32-x64-msvc': 2.11.3
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-http@2.6.0':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.11.1
|
||||||
|
|
||||||
'@tauri-apps/plugin-process@2.3.1':
|
'@tauri-apps/plugin-process@2.3.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.11.1
|
'@tauri-apps/api': 2.11.1
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-store@2.4.4':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.11.1
|
||||||
|
|
||||||
'@tauri-apps/plugin-updater@2.10.1':
|
'@tauri-apps/plugin-updater@2.10.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.11.1
|
'@tauri-apps/api': 2.11.1
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-websocket@2.4.3':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.11.1
|
||||||
|
|
||||||
'@testing-library/dom@10.4.1':
|
'@testing-library/dom@10.4.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.7
|
'@babel/code-frame': 7.29.7
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
type: decision
|
type: decision
|
||||||
tags: [parking, decisions, desktop, frontend]
|
tags: [parking, decisions, desktop, frontend]
|
||||||
sources: []
|
sources: []
|
||||||
updated: 2026-09-03
|
updated: 2026-09-04
|
||||||
status: settled
|
status: settled
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -140,6 +140,54 @@ Per the user's choices — the operator **keeps OS access** (no fullscreen lockd
|
|||||||
- **Right-click:** the context menu is blocked in **prod only** (`apps/web/src/lib/kiosk.ts`,
|
- **Right-click:** the context menu is blocked in **prod only** (`apps/web/src/lib/kiosk.ts`,
|
||||||
guarded on `import.meta.env.PROD`); dev keeps right-click + devtools. Applies to both the browser
|
guarded on `import.meta.env.PROD`); dev keeps right-click + devtools. Applies to both the browser
|
||||||
prod build and the desktop build (same SPA).
|
prod build and the desktop build (same SPA).
|
||||||
|
- **Mixed content blocks http(s)/ws(s) from the webview — fixed 2026-09-03.** Even with
|
||||||
|
`VITE_API_BASE` correctly set (below), login still failed with WebKit's generic `"Load failed"`.
|
||||||
|
Root cause is a separate, deeper issue: WebKitGTK treats `tauri://localhost` as a **secure
|
||||||
|
origin**, so a plain `http://127.0.0.1:3000` `fetch()` — or a `ws://127.0.0.1:3000` WebSocket —
|
||||||
|
from inside it is blocked as **mixed content**, a long-standing WebKit limitation
|
||||||
|
([bugs.webkit.org #171934](https://bugs.webkit.org/show_bug.cgi?id=171934)). `connect-src` in the
|
||||||
|
CSP does **not** override this — it's a different browser security layer entirely, so the request
|
||||||
|
never even reaches the network layer to be diagnosable via server logs. **Fix:** two Tauri plugins
|
||||||
|
route the SPA's traffic through Tauri's native (Rust) side instead of the webview's own
|
||||||
|
fetch/WebSocket, which sidesteps the check entirely:
|
||||||
|
- **`tauri-plugin-http`** — `apps/web/src/lib/origin.ts`'s `platformFetch()` dynamically imports
|
||||||
|
`@tauri-apps/plugin-http`'s `fetch` (a genuine drop-in for the standard Fetch API) inside Tauri,
|
||||||
|
plain `fetch` in the browser. `api.ts` and `logger.ts` both call `platformFetch` instead of the
|
||||||
|
global `fetch` now.
|
||||||
|
- **`tauri-plugin-websocket`** — NOT a drop-in (async `connect()`/listener-callback API, not
|
||||||
|
`onopen`/`onmessage`/sync `send`/`close`). `apps/web/src/lib/platform-ws.ts` adapts it behind
|
||||||
|
the same native-`WebSocket`-shaped interface `use-live-feed.ts` already expects (hardened for
|
||||||
|
reconnect backoff + StrictMode double-invoke), so that hook needed zero changes.
|
||||||
|
- Capability grants: `apps/desktop/src-tauri/capabilities/default.json` adds `websocket:default`
|
||||||
|
and a scoped `http:default` (`allow: [{url: "http://127.0.0.1:3000"}, {url:
|
||||||
|
"http://localhost:3000"}]`) — deny-by-default, matching the CSP's existing allowlist.
|
||||||
|
- `logger.ts`'s `flushBeacon()` (page-hide `navigator.sendBeacon`) is a native browser API with no
|
||||||
|
Tauri equivalent — it still drops silently in the desktop shell on unload. Accepted: the regular
|
||||||
|
4s-interval flush (now fixed, routes through `platformFetch`) covers the common case.
|
||||||
|
- **Gotcha (found immediately after shipping the above): the native WS plugin sends no `Origin`
|
||||||
|
header.** `tauri-plugin-websocket`'s `connect()` runs on Tauri's Rust side, not inside the
|
||||||
|
webview page — there's no page context to auto-attach `Origin: tauri://localhost` the way a real
|
||||||
|
browser `WebSocket` would. The server's anti-CSWSH check (`routes/ws.ts`, `isAllowedOrigin`)
|
||||||
|
treats a missing Origin as untrusted and 403s the handshake before touching auth — the live feed
|
||||||
|
showed **"JASHTË LINJË"** (offline) in the desktop app while the browser showed **"LIVE"**, same
|
||||||
|
server, same moment. **Fix (two parts, both needed):** `platform-ws.ts`'s `connect()` call now
|
||||||
|
passes `{ headers: { Origin: "tauri://localhost" } }` explicitly; separately, `komodo/
|
||||||
|
resources.toml`'s booth Stacks had `WS_ALLOWED_ORIGINS=` **empty** in production (despite
|
||||||
|
`.env.example` documenting `tauri://localhost,http://tauri.localhost` as required) — even a
|
||||||
|
correct Origin header is useless if the server's allowlist doesn't include it. Both fixed
|
||||||
|
together; a `resources.toml` change still needs a Komodo sync + Stack redeploy to take effect on
|
||||||
|
a live booth, it isn't automatic from a git push alone — and see [[fleet-deployment-komodo]] for
|
||||||
|
a real ResourceSync-branch gotcha this exact fix ran into.
|
||||||
|
- **No way to see the installed app's own version (found + fixed 2026-09-03).** `VersionBadge` in
|
||||||
|
`router.tsx` shows the *server's* `<branch>-<sha>` (from `/api/version`, gated `site:read`) — but
|
||||||
|
nothing showed the *desktop client's* own version. An operator debugging a stuck update had no way
|
||||||
|
to confirm which build was actually installed short of reading the update-available prompt's
|
||||||
|
target version and inferring backwards. Fixed with `DesktopVersionBadge`, next to `VersionBadge`:
|
||||||
|
calls `@tauri-apps/api/app`'s `getVersion()` (the real running app's version, baked in from
|
||||||
|
`tauri.conf.json` — synced to the git tag by `release.yml`, see the version-drift gotcha above),
|
||||||
|
no-ops/renders nothing in a browser (`inTauri()` guard, now exported from `origin.ts` instead of
|
||||||
|
redefined a 4th time). `@tauri-apps/api` added as an explicit dependency (was only ever transitive
|
||||||
|
via the plugins).
|
||||||
- **`VITE_API_BASE` — desktop vs. browser (regression found + fixed 2026-09-03):**
|
- **`VITE_API_BASE` — desktop vs. browser (regression found + fixed 2026-09-03):**
|
||||||
`apps/web/.env.production` (committed, shared by both builds) sets `VITE_API_BASE=` (empty) — this
|
`apps/web/.env.production` (committed, shared by both builds) sets `VITE_API_BASE=` (empty) — this
|
||||||
is correct for the **browser/booth** build (Fastify same-origin, stays relative) since commit
|
is correct for the **browser/booth** build (Fastify same-origin, stays relative) since commit
|
||||||
@@ -254,3 +302,70 @@ The desktop bundle now runs in CI under **two distinct workflows** — keep the
|
|||||||
above (download traffic visible, then nothing). Fixed by nesting `downloadAndInstall()` in its
|
above (download traffic visible, then nothing). Fixed by nesting `downloadAndInstall()` in its
|
||||||
own try/catch that logs and rethrows — offline/no-update still no-ops silently (outer catch),
|
own try/catch that logs and rethrows — offline/no-update still no-ops silently (outer catch),
|
||||||
but a failure *after* the operator accepted now logs to the console instead of vanishing.
|
but a failure *after* the operator accepted now logs to the console instead of vanishing.
|
||||||
|
|
||||||
|
### Runtime-configurable backend origin — no more one-install-per-booth builds (2026-09-04)
|
||||||
|
|
||||||
|
**Problem:** `VITE_API_BASE` was a **build-time** Vite env var (`tauri.conf.json`'s
|
||||||
|
`beforeBuildCommand`), hardcoded to `http://127.0.0.1:3000`. The desktop shell is a single
|
||||||
|
generic `.deb`/`.AppImage` distributed via [[fleet-deployment-komodo|mca/public_releases]] — it is
|
||||||
|
**not** built per-booth — so a build-time backend address meant the installer could only ever talk
|
||||||
|
to a server on the same machine, and pointing an install at any other host (a remote appliance, a
|
||||||
|
different port) needed a full rebuild. **Fix:** the backend origin is now an **operator-entered,
|
||||||
|
runtime-persisted** value.
|
||||||
|
|
||||||
|
- **`ConnectScreen.tsx`** — shown by `App.tsx` BEFORE `fetchMe()`/`Login` whenever running inside
|
||||||
|
Tauri (`inTauri()`) and no backend URL is saved yet (first launch, or after "Change server").
|
||||||
|
Operator types a host, hits **Test** (`backend-config.ts`'s `testBackendUrl`, an unauthenticated-
|
||||||
|
from-the-client's-perspective `GET /api/version` probe — see the CSRF gotcha below for why that
|
||||||
|
route isn't actually public), then **Save & continue**.
|
||||||
|
- **`tauri-plugin-store`** persists the value (`backend-config.json` in the OS config dir,
|
||||||
|
`autoSave: true`) — survives restarts, is NOT `localStorage` (deliberately; matches the existing
|
||||||
|
server-persisted-preference pattern elsewhere in this app, and a real file is easier to inspect/
|
||||||
|
back up on an appliance). `origin.ts`'s `API_BASE` changed from a `const` to a `let`, set once via
|
||||||
|
`initApiBase()` (called by `App.tsx` before mount) and again via `setApiBase()` after the
|
||||||
|
ConnectScreen saves — no restart required to start using it.
|
||||||
|
- **CSP had to loosen, deliberately, to a narrower real boundary.** `connect-src` was
|
||||||
|
`'self' http://127.0.0.1:3000 ... ws://127.0.0.1:3000 ...`; an operator-chosen arbitrary LAN host
|
||||||
|
can't be named at build time, so it's now **`'self'` only** — meaning a raw `fetch()`/`WebSocket`
|
||||||
|
from the webview is blocked to EVERY origin, not just disallowed ones. This is intentional, not a
|
||||||
|
regression: all backend traffic already went through `tauri-plugin-http`/`tauri-plugin-websocket`
|
||||||
|
anyway (the WebKit mixed-content fix above), and those plugins run on the Rust side, **outside**
|
||||||
|
`connect-src`'s jurisdiction entirely. The real access boundary moved to
|
||||||
|
`capabilities/default.json`'s `http:default` scope, which is now wildcarded
|
||||||
|
(`http://*`, `https://*`, `http://*:*`, `https://*:*` — all four forms needed, a known Tauri
|
||||||
|
scope-matching quirk drops bare `http://*` matches for a `host:port` URL otherwise). `websocket:
|
||||||
|
default` already had no scope restriction. Net effect: **the app can now reach any host the
|
||||||
|
operator types in, and nothing else** — same shape of guarantee as before, just operator-directed
|
||||||
|
instead of build-directed.
|
||||||
|
- **"Change server"** — `router.tsx`'s `DesktopServerButton`, in the Setup nav bar next to
|
||||||
|
`DesktopVersionBadge` (both `inTauri()`-gated, invisible in the browser). Confirm-modal (reuses
|
||||||
|
the shared `Modal`, not a bespoke dialog) → `clearBackendUrl()` → reload, which drops back to
|
||||||
|
ConnectScreen. Deliberately not an inline editor: repointing a booth's app is a rare, deliberate
|
||||||
|
admin action, not a frequent setting — same reasoning as why logout is a plain action button with
|
||||||
|
no separate "are you sure" for THAT (this one gets a confirm because it also blows away the
|
||||||
|
session, unlike a normal logout-then-relogin against the same server).
|
||||||
|
- **Gotcha (found via research before shipping, not in the field — worth recording anyway): the
|
||||||
|
CSRF double-submit cookie is invisible to `document.cookie` on desktop.** `tauri-plugin-http`'s
|
||||||
|
`fetch()` doesn't run through the webview — it's dispatched to Tauri's Rust side and executed by
|
||||||
|
`reqwest`, which keeps its **own** cookie jar, entirely separate from WebKitGTK's. `Set-Cookie` on
|
||||||
|
a `tauri-plugin-http` response is stored in that reqwest jar and IS correctly re-sent by
|
||||||
|
reqwest on later requests (so plain session auth — GETs — silently worked) — but it is **never**
|
||||||
|
synced into the webview's own cookie store, so `document.cookie` on the `tauri://localhost` page
|
||||||
|
can never see it. This is an open, unresolved upstream Tauri bug
|
||||||
|
([tauri-apps/tauri#13045](https://github.com/tauri-apps/tauri/issues/13045),
|
||||||
|
[#11518](https://github.com/tauri-apps/tauri/issues/11518)) — not something fixable on our side by
|
||||||
|
changing how/when we read the cookie. Since `api.ts`'s `apiFetch` reads the readable `parking_csrf`
|
||||||
|
cookie via `document.cookie` to echo it in `X-CSRF-Token` (double-submit — see
|
||||||
|
[[local-jwt-auth]]), this meant **every mutating request from the desktop app was silently sending
|
||||||
|
no CSRF header at all**, pre-dating this runtime-URL change (it was equally true against the old
|
||||||
|
hardcoded `127.0.0.1:3000`) — caught now because widening the backend to "any host" was the
|
||||||
|
occasion to actually trace the desktop auth path end-to-end. **Fix, without touching
|
||||||
|
`assertCsrf()`'s verification logic at all:** the server's `sessionView()` (`routes/auth.ts`,
|
||||||
|
shared by `login` and `me`) now optionally echoes the CSRF token value in the JSON response body
|
||||||
|
(`csrfToken`) — the SAME value already set as the cookie, just a second channel to learn it. The
|
||||||
|
desktop client (`api.ts`) stashes that value in an in-memory-only variable (`desktopCsrfToken`,
|
||||||
|
never persisted — a fresh launch always re-learns it via login or `/api/auth/me`) and echoes THAT
|
||||||
|
instead of reading `document.cookie` when `inTauri()`. The actual cookie is still what
|
||||||
|
`assertCsrf()` checks server-side (and reqwest still sends it correctly, per above) — this only
|
||||||
|
fixes how the desktop *client* learns what value to put in the header, so browser behavior and
|
||||||
|
server verification are both completely unchanged.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
type: decision
|
type: decision
|
||||||
tags: [parking, deployment, fleet, komodo, netbird, offline-first, threat-model]
|
tags: [parking, deployment, fleet, komodo, netbird, offline-first, threat-model]
|
||||||
sources: []
|
sources: []
|
||||||
updated: 2026-07-07
|
updated: 2026-09-03
|
||||||
status: settled
|
status: settled
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -192,3 +192,32 @@ FILE is read from, each stack's `branch` picks its compose files, `TAG` picks th
|
|||||||
secrets even in the lab (blast radius). The lab box earned its keep immediately: it caught the
|
secrets even in the lab (blast radius). The lab box earned its keep immediately: it caught the
|
||||||
USB close-cancel truncation, the printer/controller wizard gate, and the Periphery v2.2.0
|
USB close-cancel truncation, the printer/controller wizard gate, and the Periphery v2.2.0
|
||||||
root_directory default before any of them reached a real booth ([[appliance-provisioning]] §7a).
|
root_directory default before any of them reached a real booth ([[appliance-provisioning]] §7a).
|
||||||
|
|
||||||
|
## ResourceSync branch drift — the exact gotcha this page already warned about (2026-09-03)
|
||||||
|
|
||||||
|
This page's own §"park-lab" note (2026-07-07) already spelled it out: *"the ResourceSync's own
|
||||||
|
branch only governs where the FILE is read from"* — independent of any `[[stack]]`'s own `branch`
|
||||||
|
field. It bit anyway. `resource-sync-park-systems` in Komodo Core was pointed at **`dev`**, while
|
||||||
|
`park-buzi` and `park-2` are `stage`-tier Stacks (`branch = "stage"`, pinned `TAG=stage-<sha>`, per
|
||||||
|
the promotion-tiers model above). `resources.toml` had been byte-identical on `dev` and `stage`
|
||||||
|
since park-buzi's Stack was first written, so this had **zero observable effect for months** — until
|
||||||
|
a desktop-app debugging session (see [[desktop-shell-tauri]]) landed 9 real commits on `dev`
|
||||||
|
(including a `WS_ALLOWED_ORIGINS` fix) that were never merged to `stage`, creating the first genuine
|
||||||
|
divergence between the two branches.
|
||||||
|
|
||||||
|
**Symptom:** merged `dev` → `stage`, pushed, bumped `TAG` in `resources.toml` on `stage`, committed,
|
||||||
|
pushed — then destroyed + recreated the `park-2` Stack in Komodo Core and it STILL came back running
|
||||||
|
the old image. Every sync was silently re-reading `resources.toml` from `dev` (which still had the
|
||||||
|
stale `TAG`), overwriting the correct value just committed on `stage`. No error, no warning — the
|
||||||
|
sync just quietly did what it was configured to do, from the wrong branch.
|
||||||
|
|
||||||
|
**Fix:** pointed `resource-sync-park-systems` at `stage` in Komodo Core's UI (Sync config → branch
|
||||||
|
field), then re-synced + redeployed `park-2` — confirmed via `/api/version` (previously 404,
|
||||||
|
proving a stale image; correctly 401-auth-gated after the fix, proving the new image + route exist).
|
||||||
|
|
||||||
|
**Standing lesson, now written twice:** a `[[stack]]`'s promotion tier (which branch its own
|
||||||
|
`branch`/`TAG` fields track) and the ResourceSync resource's own git branch are **two independently
|
||||||
|
configured settings in Komodo Core — nothing enforces they agree**, and a mismatch is invisible
|
||||||
|
until the two branches' `resources.toml` actually diverge. **Check this FIRST** whenever a
|
||||||
|
redeploy doesn't pick up an expected `resources.toml` change, before assuming the change itself,
|
||||||
|
the CI build, or the deploy step is broken.
|
||||||
|
|||||||
+74
@@ -2767,3 +2767,77 @@ zero diagnostic trail. Fixed release.yml to sed-patch tauri.conf.json's version
|
|||||||
right before building (checked-in value is now dev-only, never hand-maintained for releases), and
|
right before building (checked-in value is now dev-only, never hand-maintained for releases), and
|
||||||
split desktop-updater.ts's catch so a real post-accept failure logs instead of vanishing. Full
|
split desktop-updater.ts's catch so a real post-accept failure logs instead of vanishing. Full
|
||||||
detail on [[desktop-shell-tauri]].
|
detail on [[desktop-shell-tauri]].
|
||||||
|
|
||||||
|
## [2026-09-03] fix | Desktop login "Load failed": WebKit mixed-content, not CORS/CSP
|
||||||
|
|
||||||
|
After fixing VITE_API_BASE, login still failed with WebKit's generic "Load failed" — a raw browser
|
||||||
|
fetch() rejection with no server-side trace, since the request never reached the network. Root
|
||||||
|
cause: WebKitGTK treats tauri://localhost as a secure origin, so http://127.0.0.1:3000 (and
|
||||||
|
ws://127.0.0.1:3000) from inside it is blocked as mixed content — a known WebKit limitation, NOT
|
||||||
|
fixable via CSP connect-src. Fixed by routing both through Tauri plugins that use the native (Rust)
|
||||||
|
HTTP/WS client instead of the webview's own: tauri-plugin-http (a genuine fetch() drop-in, wired
|
||||||
|
into api.ts/logger.ts via a new platformFetch() in origin.ts) and tauri-plugin-websocket (NOT a
|
||||||
|
drop-in — async/listener API — adapted behind a native-WebSocket-shaped interface in the new
|
||||||
|
platform-ws.ts so use-live-feed.ts needed no changes). Full detail on [[desktop-shell-tauri]].
|
||||||
|
|
||||||
|
## [2026-09-03] fix | Desktop live feed offline: native WS plugin sends no Origin, prod allowlist was empty
|
||||||
|
|
||||||
|
Login worked after the mixed-content fix, but the live feed showed offline in the desktop app while
|
||||||
|
the browser showed LIVE, same server. tauri-plugin-websocket's connect() runs on Tauri's Rust side,
|
||||||
|
not inside the webview page, so it never auto-attaches an Origin header — routes/ws.ts's anti-CSWSH
|
||||||
|
check treats a missing Origin as untrusted and 403s before auth. Compounded by a second, independent
|
||||||
|
gap: komodo/resources.toml's booth Stacks had WS_ALLOWED_ORIGINS= empty in production, despite
|
||||||
|
.env.example documenting tauri://localhost as required for the desktop app. Fixed both: platform-ws.ts
|
||||||
|
now passes Origin: tauri://localhost explicitly in connect()'s headers; resources.toml's two Stacks
|
||||||
|
get the real allowlist. Needs a Komodo sync + redeploy to reach a live booth, not just a git push.
|
||||||
|
Also confirmed the "update downloads then nothing happens" report was an older pre-fix build (v0.1.2)
|
||||||
|
self-updating — expected, not a new bug; v0.1.3 carries the error-logging fix from the mixed-content
|
||||||
|
commit and should surface a real error going forward. Full detail on [[desktop-shell-tauri]].
|
||||||
|
|
||||||
|
## [2026-09-03] fix | Update failures were invisible: console-forward gate blocked the error logging
|
||||||
|
|
||||||
|
The desktop-updater.ts error logging added earlier this session used console.error/console.warn,
|
||||||
|
but logger.ts only forwards console output to the server when the client log level is debug/trace
|
||||||
|
(default: info) — so the "fix" never actually surfaced anything, and a real v0.1.3→v0.1.4 update
|
||||||
|
failure showed zero logs anywhere, sending debugging in circles (a WebKit remote-inspector attempt
|
||||||
|
via WEBKIT_INSPECTOR_SERVER also dead-ended — this build doesn't answer standard discovery
|
||||||
|
endpoints). Fixed by calling logClient() directly in desktop-updater.ts, unconditionally, bypassing
|
||||||
|
the console-forward gate entirely — a genuine post-accept install failure now always reaches
|
||||||
|
app_logs regardless of client log level. Also added download-progress logging. Separately: found
|
||||||
|
and fixed a real, pre-existing Komodo ResourceSync misconfig (resource-sync-park-systems pointed at
|
||||||
|
`dev`, not `stage`, silently reading resources.toml from the wrong branch for months with zero
|
||||||
|
effect until dev/stage first diverged today) — full writeup on [[fleet-deployment-komodo]], which
|
||||||
|
had already warned about exactly this gotcha back in 2026-07-07 and it happened anyway.
|
||||||
|
|
||||||
|
## [2026-09-03] feat | Desktop app version now visible in the UI (was invisible)
|
||||||
|
|
||||||
|
There was no way to see which desktop build was actually installed anywhere in the app — an
|
||||||
|
operator debugging a stuck update had to infer it backwards from the update prompt's target
|
||||||
|
version ("it's offering v0.1.4, so I must be on v0.1.3"). Added DesktopVersionBadge next to the
|
||||||
|
existing server-side VersionBadge in router.tsx, using @tauri-apps/api's getVersion() (the real
|
||||||
|
running app version, synced to the git tag at build time by release.yml). No-ops in a browser.
|
||||||
|
Full detail on [[desktop-shell-tauri]].
|
||||||
|
|
||||||
|
## [2026-09-04] feat | Desktop backend origin is now runtime-configurable (was build-time)
|
||||||
|
|
||||||
|
The desktop shell is one generic .deb/.AppImage distributed via mca/public_releases — not built
|
||||||
|
per-booth — but VITE_API_BASE was a build-time env var hardcoded to http://127.0.0.1:3000, so the
|
||||||
|
same installer could only ever talk to a server on its own machine. Added ConnectScreen (shown
|
||||||
|
before Login in Tauri when no backend is saved), backed by tauri-plugin-store persisting the
|
||||||
|
operator-entered URL across restarts; origin.ts's API_BASE became a runtime-settable `let`. CSP's
|
||||||
|
connect-src tightened to 'self' only (all backend traffic already went through
|
||||||
|
tauri-plugin-http/websocket, which run Rust-side and are outside connect-src's reach anyway); the
|
||||||
|
real boundary moved to capabilities/default.json's http:default scope, wildcarded to any host so
|
||||||
|
the operator-chosen address is actually reachable. Added a "Change server" control (Setup nav,
|
||||||
|
desktop-only) that clears the saved URL and reloads back to ConnectScreen.
|
||||||
|
|
||||||
|
While tracing the desktop auth path for this, found a pre-existing (not newly introduced) bug:
|
||||||
|
tauri-plugin-http's fetch() runs through Rust's reqwest, which keeps its own cookie jar separate
|
||||||
|
from the webview — document.cookie on tauri://localhost never sees the parking_csrf cookie the
|
||||||
|
server sets (open upstream bug, tauri-apps/tauri#13045/#11518), so the desktop app has likely been
|
||||||
|
silently sending no CSRF header on every mutation since the shell was first built, regardless of
|
||||||
|
which host it targeted. Fixed by having sessionView() (routes/auth.ts) also echo the same csrf
|
||||||
|
value in the login/me JSON body; the desktop client stashes it in memory and echoes that instead of
|
||||||
|
reading document.cookie. assertCsrf() itself is untouched — the cookie is still what's verified,
|
||||||
|
and reqwest was already sending it correctly; this only fixes how the desktop client *learns* the
|
||||||
|
value. Full detail (including the exact CSP/capability tradeoffs) on [[desktop-shell-tauri]].
|
||||||
|
|||||||
Reference in New Issue
Block a user