From a5997cb52bfa9b91b54505dca4c2e3e7e50606af Mon Sep 17 00:00:00 2001 From: karim Date: Sat, 16 May 2026 01:29:49 +0200 Subject: [PATCH] App-Updater: Tauri-Plugin mit Auto-Check und Skip-Funktion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Beim App-Start wird automatisch geprüft, ob bei git.kgva.ch/karim/RAPPORT eine neue Version verfügbar ist. Update-Modal mit Release-Notes und drei Aktionen: Jetzt installieren (Download → Signaturprüfung → Neustart), Später, oder Diese Version überspringen (in localStorage gemerkt). Signing via minisign-Keypair unter ~/.tauri/rapport_updater.key, Public Key im Tauri-Config. Release-Script scripts/release.sh baut, signiert und erzeugt latest.json für Gitea. Co-Authored-By: Claude Opus 4.7 --- .gitignore | 5 + package-lock.json | 29 +- package.json | 2 + scripts/release.sh | 92 ++++++ src-tauri/Cargo.lock | 424 +++++++++++++++++++++++++++- src-tauri/Cargo.toml | 2 + src-tauri/capabilities/default.json | 4 +- src-tauri/src/lib.rs | 2 + src-tauri/tauri.conf.json | 9 + src/App.jsx | 3 + src/components/UpdateNotifier.jsx | 163 +++++++++++ src/utils/updater.js | 48 ++++ 12 files changed, 772 insertions(+), 11 deletions(-) create mode 100755 scripts/release.sh create mode 100644 src/components/UpdateNotifier.jsx create mode 100644 src/utils/updater.js diff --git a/.gitignore b/.gitignore index 245445d..4b34a86 100755 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,11 @@ dist-ssr # Claude Code .claude/ +# Tauri updater signing keys — never commit private keys +*.key +*.key.pub +.tauri/ + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/package-lock.json b/package-lock.json index cc614e3..0d65586 100755 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,15 @@ { - "name": "rapportv01", - "version": "0.0.0", + "name": "rapport", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "rapportv01", - "version": "0.0.0", + "name": "rapport", + "version": "0.6.0", "dependencies": { + "@tauri-apps/plugin-process": "^2.3.1", + "@tauri-apps/plugin-updater": "^2.10.1", "react": "^19.2.5", "react-dom": "^19.2.5", "swissqrbill": "^4.3.0" @@ -859,7 +861,6 @@ "version": "2.10.1", "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==", - "dev": true, "license": "Apache-2.0 OR MIT", "funding": { "type": "opencollective", @@ -1098,6 +1099,24 @@ "node": ">= 10" } }, + "node_modules/@tauri-apps/plugin-process": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.1.tgz", + "integrity": "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, + "node_modules/@tauri-apps/plugin-updater": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.10.1.tgz", + "integrity": "sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.10.1" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", diff --git a/package.json b/package.json index 79fd312..eff4667 100755 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "preview": "vite preview" }, "dependencies": { + "@tauri-apps/plugin-process": "^2.3.1", + "@tauri-apps/plugin-updater": "^2.10.1", "react": "^19.2.5", "react-dom": "^19.2.5", "swissqrbill": "^4.3.0" diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..6a2ead1 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# Build + sign Rapport for the updater, and emit latest.json. +# +# Voraussetzungen: +# - Private Key liegt unter ~/.tauri/rapport_updater.key +# - Version wurde in src-tauri/tauri.conf.json + package.json hochgezählt +# +# Ablauf: +# 1) npm run tauri build (mit Signing-Env) +# 2) liest die erzeugte .sig-Datei +# 3) schreibt latest.json im Repo-Root mit URLs auf Gitea-Release-Assets +# +# Danach manuell: +# - auf Gitea einen Release mit Tag v erstellen +# - die .app.tar.gz und (optional) die .dmg als Assets hochladen +# - latest.json committen + auf main pushen + +set -euo pipefail + +cd "$(dirname "$0")/.." + +KEY_PATH="${TAURI_SIGNING_PRIVATE_KEY_PATH:-$HOME/.tauri/rapport_updater.key}" +GITEA_REPO="https://git.kgva.ch/karim/RAPPORT" + +if [ ! -f "$KEY_PATH" ]; then + echo "Private Key fehlt: $KEY_PATH" >&2 + exit 1 +fi + +VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version") +PKG_VERSION=$(node -p "require('./package.json').version") +if [ "$VERSION" != "$PKG_VERSION" ]; then + echo "Version mismatch: tauri.conf.json=$VERSION package.json=$PKG_VERSION" >&2 + exit 1 +fi + +ARCH=$(uname -m) +case "$ARCH" in + arm64|aarch64) PLATFORM_KEY="darwin-aarch64" ;; + x86_64) PLATFORM_KEY="darwin-x86_64" ;; + *) echo "Unsupported arch: $ARCH" >&2; exit 1 ;; +esac + +echo "→ Build Rapport $VERSION ($PLATFORM_KEY)" +TAURI_SIGNING_PRIVATE_KEY_PATH="$KEY_PATH" \ +TAURI_SIGNING_PRIVATE_KEY_PASSWORD="" \ + npm run tauri build + +BUNDLE_DIR="src-tauri/target/release/bundle/macos" +TAR_GZ=$(ls "$BUNDLE_DIR"/*.app.tar.gz 2>/dev/null | head -n1 || true) +SIG_FILE="${TAR_GZ}.sig" + +if [ -z "$TAR_GZ" ] || [ ! -f "$SIG_FILE" ]; then + echo "Bundle oder Signatur fehlt in $BUNDLE_DIR" >&2 + exit 1 +fi + +ASSET_NAME=$(basename "$TAR_GZ") +SIGNATURE=$(cat "$SIG_FILE") +PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +DOWNLOAD_URL="$GITEA_REPO/releases/download/v$VERSION/$ASSET_NAME" + +NOTES=${RELEASE_NOTES:-"Rapport $VERSION"} + +cat > latest.json </dev/null | head -n1 || echo '(keine DMG gefunden)')" +echo " Platform: $PLATFORM_KEY" +echo " latest.json wurde im Repo-Root geschrieben." +echo +echo "Nächste Schritte:" +echo " 1) Auf Gitea Release v$VERSION erstellen und folgende Assets hochladen:" +echo " - $ASSET_NAME" +echo " - (optional) DMG für Erstinstallation" +echo " 2) latest.json committen + auf main pushen:" +echo " git add latest.json && git commit -m 'Release v$VERSION' && git push origin main" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index be221df..a8fc76a 100755 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -75,6 +75,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -625,6 +634,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -834,6 +854,16 @@ dependencies = [ "typeid", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -868,6 +898,16 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1461,6 +1501,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1726,6 +1781,36 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -1866,6 +1951,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.2" @@ -1959,6 +2050,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2159,6 +2256,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.11.1", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -2174,6 +2272,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + [[package]] name = "objc2-quartz-core" version = "0.3.2" @@ -2218,12 +2328,32 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2", + "objc2-foundation", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "pango" version = "0.18.3" @@ -2758,6 +2888,8 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-log", + "tauri-plugin-process", + "tauri-plugin-updater", ] [[package]] @@ -2858,15 +2990,20 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -2878,6 +3015,20 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rkyv" version = "0.7.46" @@ -2939,6 +3090,92 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -2954,6 +3191,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -3017,6 +3263,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.24.0" @@ -3254,6 +3523,16 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -3403,6 +3682,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3486,7 +3771,7 @@ dependencies = [ "gdkwayland-sys", "gdkx11-sys", "gtk", - "jni", + "jni 0.21.1", "libc", "log", "ndk", @@ -3524,6 +3809,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -3547,7 +3843,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", - "jni", + "jni 0.21.1", "libc", "log", "mime", @@ -3683,6 +3979,49 @@ dependencies = [ "time", ] +[[package]] +name = "tauri-plugin-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" +dependencies = [ + "tauri", + "tauri-plugin", +] + +[[package]] +name = "tauri-plugin-updater" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af" +dependencies = [ + "base64 0.22.1", + "dirs", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest", + "rustls", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.18", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip", +] + [[package]] name = "tauri-runtime" version = "2.10.1" @@ -3693,7 +4032,7 @@ dependencies = [ "dpi", "gtk", "http", - "jni", + "jni 0.21.1", "objc2", "objc2-ui-kit", "objc2-web-kit", @@ -3716,7 +4055,7 @@ checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" dependencies = [ "gtk", "http", - "jni", + "jni 0.21.1", "log", "objc2", "objc2-app-kit", @@ -3783,6 +4122,19 @@ dependencies = [ "toml 1.1.2+spec-1.1.0", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "tendril" version = "0.4.3" @@ -3916,6 +4268,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4212,6 +4574,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -4522,6 +4890,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -4752,6 +5129,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -5135,7 +5521,7 @@ dependencies = [ "gtk", "http", "javascriptcore-rs", - "jni", + "jni 0.21.1", "libc", "ndk", "objc2", @@ -5191,6 +5577,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yoke" version = "0.8.2" @@ -5255,6 +5651,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.4" @@ -5288,6 +5690,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.14.0", + "memchr", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 55c8932..6823087 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,3 +23,5 @@ serde = { version = "1.0", features = ["derive"] } log = "0.4" tauri = { version = "2.10.3", features = [] } tauri-plugin-log = "2" +tauri-plugin-updater = "2" +tauri-plugin-process = "2" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 5203780..c3fb48b 100755 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -7,6 +7,8 @@ ], "permissions": [ "core:default", - "core:webview:allow-print" + "core:webview:allow-print", + "updater:default", + "process:allow-restart" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9c3118c..4c432de 100755 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,6 +1,8 @@ #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_process::init()) .setup(|app| { if cfg!(debug_assertions) { app.handle().plugin( diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b377dec..f45d61e 100755 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -26,6 +26,7 @@ "bundle": { "active": true, "targets": "all", + "createUpdaterArtifacts": true, "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -36,5 +37,13 @@ "macOS": { "signingIdentity": "-" } + }, + "plugins": { + "updater": { + "endpoints": [ + "https://git.kgva.ch/karim/RAPPORT/raw/branch/main/latest.json" + ], + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUyMzE3RTg1MzlEQkU2NTEKUldSUjV0czVoWDR4NHQwY2RHM3JUV0VCaFRTWjdEci9RYkFZQUJqV0NoRWxDV0prcWFoKzJubFAK" + } } } diff --git a/src/App.jsx b/src/App.jsx index f118505..01586bd 100755 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,6 +4,7 @@ import { migrateDashboardLayout, verifyPassword, withHashedPassword, stripCreden import Login from "./views/Login.jsx"; import Setup from "./views/Setup.jsx"; import MigrationScreen from "./views/MigrationScreen.jsx"; +import UpdateNotifier from "./components/UpdateNotifier.jsx"; // Code-split: each view loads on demand to keep the initial bundle small. const Dashboard = lazy(() => import("./views/Dashboard.jsx")); @@ -652,6 +653,8 @@ export default function App() { + + {showChangelog && (() => { const CHANGELOGS = { "0.6": { diff --git a/src/components/UpdateNotifier.jsx b/src/components/UpdateNotifier.jsx new file mode 100644 index 0000000..9489a3d --- /dev/null +++ b/src/components/UpdateNotifier.jsx @@ -0,0 +1,163 @@ +import { useEffect, useState, useCallback } from "react"; +import { checkForAppUpdate, installAppUpdate, skipUpdateVersion, isTauri } from "../utils/updater.js"; + +export default function UpdateNotifier() { + const [update, setUpdate] = useState(null); + const [state, setState] = useState("idle"); + const [downloaded, setDownloaded] = useState(0); + const [total, setTotal] = useState(0); + const [error, setError] = useState(null); + + const runCheck = useCallback(async ({ silent }) => { + if (!isTauri()) return; + try { + setState("checking"); + setError(null); + const res = await checkForAppUpdate({ respectSkip: silent }); + if (!res.available) { + setUpdate(null); + setState("idle"); + return; + } + setUpdate(res.update); + setState("available"); + } catch (e) { + console.error("Update-Check fehlgeschlagen:", e); + setError(String(e?.message || e)); + setState("idle"); + } + }, []); + + useEffect(() => { + const t = setTimeout(() => runCheck({ silent: true }), 1500); + return () => clearTimeout(t); + }, [runCheck]); + + // Settings → "Nach Updates suchen" feuert dieses Event; wir lassen das Modal aufpoppen, + // falls etwas gefunden wird (Skip-Flag wird dabei ignoriert). + useEffect(() => { + const handler = () => runCheck({ silent: false }); + window.addEventListener("rapport:check-update", handler); + return () => window.removeEventListener("rapport:check-update", handler); + }, [runCheck]); + + const install = async () => { + if (!update) return; + try { + setState("downloading"); + setDownloaded(0); + setTotal(0); + await installAppUpdate(update, (event) => { + if (event.event === "Started") { + setTotal(event.data.contentLength || 0); + } else if (event.event === "Progress") { + setDownloaded((d) => d + (event.data.chunkLength || 0)); + } else if (event.event === "Finished") { + setState("installing"); + } + }); + } catch (e) { + console.error("Update-Installation fehlgeschlagen:", e); + setError(String(e?.message || e)); + setState("available"); + } + }; + + const skipVersion = () => { + skipUpdateVersion(update?.version); + setUpdate(null); + setState("idle"); + }; + + const later = () => { + setUpdate(null); + setState("idle"); + }; + + if (!isTauri() || !update || state === "idle" || state === "checking") return null; + + const isBusy = state === "downloading" || state === "installing"; + const pct = total > 0 ? Math.min(100, Math.round((downloaded / total) * 100)) : null; + + return ( +
+
+
+
UPDATE VERFÜGBAR
+
+
+ Rapport {update.version} +
+ {update.currentVersion && ( +
von {update.currentVersion}
+ )} +
+
+ +
+ {update.body && ( +
+
+
{update.body}
+
+ )} + + {error && ( +
+ {error} +
+ )} + + {isBusy && ( +
+
+ {state === "downloading" + ? (pct !== null ? `Wird heruntergeladen … ${pct}%` : "Wird heruntergeladen …") + : "Wird installiert …"} +
+
+
+
+ +
+ )} +
+ +
+ +
+ + +
+
+
+
+ ); +} diff --git a/src/utils/updater.js b/src/utils/updater.js new file mode 100644 index 0000000..4d16e5e --- /dev/null +++ b/src/utils/updater.js @@ -0,0 +1,48 @@ +// Shared helpers for the Tauri app updater. Used by the auto-check modal +// (UpdateNotifier) and the manual check in Settings → Updates & Support. + +export const SKIP_KEY = "rapport_update_skipped_version"; +export const LAST_CHECK_KEY = "rapport_update_last_check"; + +export function isTauri() { + return typeof window !== "undefined" && !!window.__TAURI_INTERNALS__; +} + +export async function checkForAppUpdate({ respectSkip = true } = {}) { + if (!isTauri()) return { available: false, isTauri: false }; + const { check } = await import("@tauri-apps/plugin-updater"); + const result = await check(); + localStorage.setItem(LAST_CHECK_KEY, new Date().toISOString()); + if (!result?.available) return { available: false, update: null, isTauri: true }; + if (respectSkip && localStorage.getItem(SKIP_KEY) === result.version) { + return { available: false, update: result, isTauri: true, skipped: true }; + } + return { available: true, update: result, isTauri: true }; +} + +export async function installAppUpdate(update, onProgress) { + await update.downloadAndInstall(onProgress); + const { relaunch } = await import("@tauri-apps/plugin-process"); + await relaunch(); +} + +export function skipUpdateVersion(version) { + if (version) localStorage.setItem(SKIP_KEY, version); +} + +export function getLastUpdateCheck() { + return localStorage.getItem(LAST_CHECK_KEY); +} + +export function formatLastCheck(iso) { + if (!iso) return "noch nie"; + try { + const d = new Date(iso); + return d.toLocaleString("de-CH", { + day: "2-digit", month: "long", year: "numeric", + hour: "2-digit", minute: "2-digit", + }); + } catch { + return "—"; + } +}