3 Commits

Author SHA1 Message Date
karim 144f2be07c Einstellungen: Tab «Updates & Support» + Bump auf 0.7.0
Neuer Settings-Tab mit manueller Update-Suche (ignoriert die Skip-
Markierung), Zeitstempel der letzten Prüfung, Link zur Dokumentation
auf rapport.kgva.ch. Update-Helper sind in src/utils/updater.js
zentralisiert; UpdateNotifier schreibt jetzt auch beim Auto-Check
das Datum der letzten Prüfung mit.

Version 0.6.0 → 0.7.0 in package.json, tauri.conf.json, Cargo.toml
und allen UI-Referenzen. Changelog-Eintrag 0.7 mit den drei
Highlights dieses Releases ergänzt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 01:29:49 +02:00
karim 82d9d41bdb System-Tray: Quick-Access-Menü, Hide-on-Close, Cmd+Q beendet
Tray-Icon in der macOS-Menüleiste mit Linksklick zum Fokussieren und
Rechtsklick-Menü: Rapport öffnen, Dashboard, Zeiterfassung, Projekte,
Buchhaltung, Einstellungen, Beenden. Menü-Klicks senden ein
rapport:navigate-Event ans Frontend.

Der rote Schliessen-Button versteckt nur — die App läuft im Hintergrund
weiter. Cmd+Q (RunEvent::ExitRequested) und der Tray-Quit-Eintrag
setzen ein is_quitting-Flag und beenden die App wirklich.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 01:29:49 +02:00
karim 896aa75a39 App-Updater: Tauri-Plugin mit Auto-Check und Skip-Funktion
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 <noreply@anthropic.com>
2026-05-16 01:29:49 +02:00
15 changed files with 1110 additions and 39 deletions
+5
View File
@@ -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
+2 -2
View File
@@ -121,8 +121,8 @@ Zusätzlich im UI-Changelog: [`src/App.jsx`](src/App.jsx) — Konstante `CHANGEL
# 1. Versionen anheben (package.json, tauri.conf.json, Cargo.toml)
# 2. Changelog in src/App.jsx ergänzen
# 3. Commit + Tag
git tag -a v0.6.0 -m "Rapport 0.6"
git push origin main v0.6.0
git tag -a v0.7.0 -m "Rapport 0.7"
git push origin main v0.7.0
# 4. Bundle bauen
npx tauri build
+24 -5
View File
@@ -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",
+3 -1
View File
@@ -1,7 +1,7 @@
{
"name": "rapport",
"private": true,
"version": "0.6.0",
"version": "0.7.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -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"
+92
View File
@@ -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<VERSION> 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 <<EOF
{
"version": "$VERSION",
"notes": $(node -e "process.stdout.write(JSON.stringify(process.argv[1]))" "$NOTES"),
"pub_date": "$PUB_DATE",
"platforms": {
"$PLATFORM_KEY": {
"signature": $(node -e "process.stdout.write(JSON.stringify(process.argv[1]))" "$SIGNATURE"),
"url": "$DOWNLOAD_URL"
}
}
}
EOF
echo
echo "✓ Build fertig"
echo " Bundle: $TAR_GZ"
echo " Signatur: $SIG_FILE"
echo " DMG: $(ls "$BUNDLE_DIR"/*.dmg 2>/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"
+420 -6
View File
@@ -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"
@@ -2750,7 +2880,7 @@ dependencies = [
[[package]]
name = "rapport"
version = "0.6.0"
version = "0.7.0"
dependencies = [
"log",
"serde",
@@ -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"
+4 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "rapport"
version = "0.6.0"
version = "0.7.0"
description = "Rapport — Studio-Management für Architekturbüros"
authors = ["Karim Gabriele Varano <karim@gabrielevarano.ch>"]
license = "AGPL-3.0-or-later"
@@ -21,5 +21,7 @@ tauri-build = { version = "2.5.6", features = [] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.10.3", features = [] }
tauri = { version = "2.10.3", features = ["tray-icon"] }
tauri-plugin-log = "2"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
+3 -1
View File
@@ -7,6 +7,8 @@
],
"permissions": [
"core:default",
"core:webview:allow-print"
"core:webview:allow-print",
"updater:default",
"process:allow-restart"
]
}
+92 -5
View File
@@ -1,7 +1,41 @@
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tauri::{
menu::{Menu, MenuItem, PredefinedMenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Emitter, Manager,
};
fn show_main_window(app: &tauri::AppHandle) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.unminimize();
let _ = window.set_focus();
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let is_quitting = Arc::new(AtomicBool::new(false));
tauri::Builder::default()
.setup(|app| {
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
.on_window_event({
let is_quitting = is_quitting.clone();
move |window, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
if !is_quitting.load(Ordering::SeqCst) {
api.prevent_close();
let _ = window.hide();
}
}
}
})
.setup({
let is_quitting = is_quitting.clone();
move |app| {
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()
@@ -9,8 +43,61 @@ pub fn run() {
.build(),
)?;
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
let show = MenuItem::with_id(app, "show", "Rapport öffnen", true, None::<&str>)?;
let dashboard = MenuItem::with_id(app, "nav:dashboard", "Dashboard", true, None::<&str>)?;
let time = MenuItem::with_id(app, "nav:time", "Zeiterfassung", true, None::<&str>)?;
let projects = MenuItem::with_id(app, "nav:projects", "Projekte", true, None::<&str>)?;
let accounting = MenuItem::with_id(app, "nav:buchhaltung", "Buchhaltung", true, None::<&str>)?;
let settings = MenuItem::with_id(app, "nav:settings", "Einstellungen", true, None::<&str>)?;
let sep1 = PredefinedMenuItem::separator(app)?;
let sep2 = PredefinedMenuItem::separator(app)?;
let quit = MenuItem::with_id(app, "quit", "Beenden", true, Some("Cmd+Q"))?;
let menu = Menu::with_items(
app,
&[&show, &sep1, &dashboard, &time, &projects, &accounting, &settings, &sep2, &quit],
)?;
let is_quitting_menu = is_quitting.clone();
let _tray = TrayIconBuilder::with_id("main")
.icon(app.default_window_icon().unwrap().clone())
.icon_as_template(true)
.menu(&menu)
.show_menu_on_left_click(false)
.on_menu_event(move |app, event| match event.id.as_ref() {
"show" => show_main_window(app),
"quit" => {
is_quitting_menu.store(true, Ordering::SeqCst);
app.exit(0);
}
id if id.starts_with("nav:") => {
show_main_window(app);
let view = &id["nav:".len()..];
let _ = app.emit("rapport:navigate", view.to_string());
}
_ => {}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
show_main_window(tray.app_handle());
}
})
.build(app)?;
Ok(())
}
})
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(move |_app, event| {
if let tauri::RunEvent::ExitRequested { .. } = event {
is_quitting.store(true, Ordering::SeqCst);
}
});
}
+10 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "RAPPORT PRE-RELEASE",
"version": "0.6.0",
"version": "0.7.0",
"identifier": "com.karimgabrielevarano.rapport",
"build": {
"frontendDist": "../dist",
@@ -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"
}
}
}
+34 -8
View File
@@ -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"));
@@ -230,8 +231,8 @@ export default function App() {
const [modal, setModal] = useState(null);
const [printContent, setPrintContent] = useState(null);
const [darkMode, setDarkMode] = useState(() => localStorage.getItem("rapport_dark") === "1");
const [showChangelog, setShowChangelog] = useState(() => localStorage.getItem("rapport_changelog_seen") !== "0.6");
const [changelogVersion, setChangelogVersion] = useState("0.6");
const [showChangelog, setShowChangelog] = useState(() => localStorage.getItem("rapport_changelog_seen") !== "0.7");
const [changelogVersion, setChangelogVersion] = useState("0.7");
const [showAbout, setShowAbout] = useState(false);
const [navOpen, setNavOpen] = useState(false);
const [expandedNav, setExpandedNav] = useState(new Set(["buchhaltung"]));
@@ -276,6 +277,22 @@ export default function App() {
return () => window.removeEventListener("openProtokoll", handler);
}, []);
// Tray-Menü: „Zeiterfassung", „Projekte" usw. springen zur passenden View
useEffect(() => {
if (!window.__TAURI_INTERNALS__) return;
let unlisten = null;
import("@tauri-apps/api/event").then(({ listen }) => {
listen("rapport:navigate", (event) => {
const target = event.payload;
if (typeof target === "string") {
navigate(target);
setSelectedProjectId(null);
}
}).then((fn) => { unlisten = fn; });
});
return () => { if (unlisten) unlisten(); };
}, []);
// Auto-expand parent when navigating to a child
useEffect(() => {
NAV_ITEMS.forEach(item => {
@@ -317,7 +334,7 @@ export default function App() {
}
if (!currentUser) {
return <Login verifyLogin={verifyLogin} settings={data.settings} version="0.6" />;
return <Login verifyLogin={verifyLogin} settings={data.settings} version="0.7" />;
}
if (printContent) {
@@ -590,8 +607,8 @@ export default function App() {
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<button onClick={() => setShowAbout(true)} style={{ background: "none", border: "none", padding: 0, color: "#555", fontSize: 10, letterSpacing: "0.08em", cursor: "pointer", fontFamily: "inherit", textAlign: "left" }}
onMouseEnter={e => e.currentTarget.style.color = "#aaa"} onMouseLeave={e => e.currentTarget.style.color = "#555"}>ÜBER RAPPORT</button>
<button onClick={() => { setChangelogVersion("0.6"); setShowChangelog(true); }} style={{ background: "none", border: "none", padding: 0, color: "#aaa", fontSize: 10, letterSpacing: "0.08em", cursor: "pointer", fontFamily: "inherit" }}
onMouseEnter={e => e.currentTarget.style.color = "#f0ede8"} onMouseLeave={e => e.currentTarget.style.color = "#aaa"}>0.6</button>
<button onClick={() => { setChangelogVersion("0.7"); setShowChangelog(true); }} style={{ background: "none", border: "none", padding: 0, color: "#aaa", fontSize: 10, letterSpacing: "0.08em", cursor: "pointer", fontFamily: "inherit" }}
onMouseEnter={e => e.currentTarget.style.color = "#f0ede8"} onMouseLeave={e => e.currentTarget.style.color = "#aaa"}>0.7</button>
</div>
</div>}
@@ -652,8 +669,17 @@ export default function App() {
</Suspense>
</div>
<UpdateNotifier />
{showChangelog && (() => {
const CHANGELOGS = {
"0.7": {
items: [
["Automatische Updates", "Rapport prüft beim Start, ob eine neue Version unter git.kgva.ch verfügbar ist, und installiert sie auf Knopfdruck — kein manuelles DMG-Download mehr nötig. Updates lassen sich überspringen oder verschieben; Pakete werden vor der Installation per Signaturprüfung verifiziert."],
["System-Tray-Icon", "Rapport läuft im Hintergrund weiter, wenn das Fenster geschlossen wird, und ist über ein Menüleisten-Icon erreichbar. Schnellzugriff auf Dashboard, Zeiterfassung, Projekte und Buchhaltung; Cmd+Q beendet die App vollständig."],
["Einstellungen: Updates & Support", "Neuer Tab «Updates & Support» mit manueller Update-Suche, Zeitstempel der letzten Prüfung und Link zur Dokumentation auf rapport.kgva.ch."],
],
},
"0.6": {
items: [
["Sicherheit: Passwort-Hashing", "Passwörter werden jetzt mit PBKDF2 (SHA-256, 100 000 Iterationen) und einem zufälligen Salt gespeichert. Bestehende Klartext-Passwörter werden beim ersten erfolgreichen Login transparent migriert."],
@@ -711,7 +737,7 @@ export default function App() {
},
};
const versions = Object.keys(CHANGELOGS);
const current = CHANGELOGS[changelogVersion] || CHANGELOGS["0.6"];
const current = CHANGELOGS[changelogVersion] || CHANGELOGS["0.7"];
return (
<div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.55)", zIndex: 200, display: "flex", alignItems: "center", justifyContent: "center", padding: 24 }}>
<div style={{ background: "#fff", borderRadius: 10, width: "100%", maxWidth: 480, boxShadow: "0 8px 40px rgba(0,0,0,0.18)", overflow: "hidden" }}>
@@ -740,7 +766,7 @@ export default function App() {
))}
</div>
<div style={{ padding: "12px 32px 24px" }}>
<button className="btn btn-primary" style={{ width: "100%", fontSize: 13 }} onClick={() => { setShowChangelog(false); localStorage.setItem("rapport_changelog_seen", "0.6"); }}>
<button className="btn btn-primary" style={{ width: "100%", fontSize: 13 }} onClick={() => { setShowChangelog(false); localStorage.setItem("rapport_changelog_seen", "0.7"); }}>
Schliessen
</button>
</div>
@@ -755,7 +781,7 @@ export default function App() {
<div style={{ background: "#1a1a18", padding: "28px 32px 24px" }}>
<div style={{ fontSize: 10, letterSpacing: "0.18em", color: "#b07848", marginBottom: 8, fontWeight: 600 }}>ÜBER RAPPORT</div>
<div style={{ fontFamily: "'Playfair Display', serif", fontSize: 28, color: "#f0ede8", fontWeight: 400, lineHeight: 1.1 }}>Rapport</div>
<div style={{ fontSize: 11, color: "#888", marginTop: 6, letterSpacing: "0.04em" }}>Alpha 0.6 · Studio-Management für Architekturbüros</div>
<div style={{ fontSize: 11, color: "#888", marginTop: 6, letterSpacing: "0.04em" }}>Alpha 0.7 · Studio-Management für Architekturbüros</div>
</div>
<div style={{ padding: "20px 32px 8px" }}>
<div style={{ fontSize: 11, fontWeight: 600, color: "#888", letterSpacing: "0.1em", marginBottom: 12 }}>LIZENZ</div>
+163
View File
@@ -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 (
<div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.55)", zIndex: 300, display: "flex", alignItems: "center", justifyContent: "center", padding: 24, backdropFilter: "blur(4px)" }}>
<div style={{ background: "#fff", borderRadius: 10, width: "100%", maxWidth: 460, boxShadow: "0 8px 40px rgba(0,0,0,0.18)", overflow: "hidden" }}>
<div style={{ background: "#1a1a18", padding: "28px 32px 20px" }}>
<div style={{ fontSize: 10, letterSpacing: "0.18em", color: "#b07848", marginBottom: 8, fontWeight: 600 }}>UPDATE VERFÜGBAR</div>
<div style={{ display: "flex", alignItems: "baseline", gap: 12 }}>
<div style={{ fontFamily: "'Playfair Display', serif", fontSize: 26, color: "#f0ede8", fontWeight: 400, lineHeight: 1.1 }}>
Rapport {update.version}
</div>
{update.currentVersion && (
<div style={{ fontSize: 11, color: "#666", letterSpacing: "0.04em" }}>von {update.currentVersion}</div>
)}
</div>
</div>
<div style={{ padding: "20px 32px 8px", maxHeight: 320, overflowY: "auto" }}>
{update.body && (
<div style={{ display: "flex", gap: 14, marginBottom: 14 }}>
<div style={{ width: 4, flexShrink: 0, background: "#b07848", borderRadius: 2, marginTop: 2 }} />
<div style={{ fontSize: 12, color: "#555", lineHeight: 1.55, whiteSpace: "pre-wrap" }}>{update.body}</div>
</div>
)}
{error && (
<div style={{ marginTop: 8, padding: "10px 12px", background: "#fdf0f0", border: "1px solid #f3c4c4", borderRadius: 6, fontSize: 12, color: "#8a1a1a" }}>
{error}
</div>
)}
{isBusy && (
<div style={{ marginTop: 12 }}>
<div style={{ fontSize: 11, color: "#888", marginBottom: 6, letterSpacing: "0.04em" }}>
{state === "downloading"
? (pct !== null ? `Wird heruntergeladen … ${pct}%` : "Wird heruntergeladen …")
: "Wird installiert …"}
</div>
<div style={{ height: 4, background: "#eee", borderRadius: 2, overflow: "hidden" }}>
<div style={{
height: "100%",
width: pct !== null ? `${pct}%` : "100%",
background: "#b07848",
transition: "width 0.2s",
animation: pct === null ? "rapport-update-pulse 1.2s ease-in-out infinite" : undefined,
}} />
</div>
<style>{`@keyframes rapport-update-pulse { 0%,100% { opacity: 0.5; } 50% { opacity: 1; } }`}</style>
</div>
)}
</div>
<div style={{ padding: "16px 32px 24px", display: "flex", flexDirection: "column", gap: 8 }}>
<button
className="btn btn-primary"
style={{ width: "100%", fontSize: 13 }}
onClick={install}
disabled={isBusy}
>
{isBusy ? "Bitte warten …" : "Jetzt installieren"}
</button>
<div style={{ display: "flex", gap: 8 }}>
<button
className="btn btn-ghost"
style={{ flex: 1, fontSize: 12 }}
onClick={later}
disabled={isBusy}
>
Später
</button>
<button
className="btn btn-ghost"
style={{ flex: 1, fontSize: 12 }}
onClick={skipVersion}
disabled={isBusy}
>
Diese Version überspringen
</button>
</div>
</div>
</div>
</div>
);
}
+197
View File
@@ -0,0 +1,197 @@
import { useEffect, useState, useCallback } from "react";
import {
checkForAppUpdate,
installAppUpdate,
skipUpdateVersion,
getLastUpdateCheck,
formatLastCheck,
isTauri,
} from "../utils/updater.js";
function Section({ title, children }) {
return (
<div style={{ marginBottom: 28 }}>
<div style={{ fontSize: 10, letterSpacing: "0.12em", color: "#aaa", fontWeight: 600, marginBottom: 14, paddingBottom: 8, borderBottom: "1px solid #ece8e2" }}>{title}</div>
{children}
</div>
);
}
export default function UpdatesSupport({ fallbackVersion }) {
const [version, setVersion] = useState(fallbackVersion || "");
const [lastCheck, setLastCheck] = useState(() => getLastUpdateCheck());
const [state, setState] = useState("idle");
const [update, setUpdate] = useState(null);
const [error, setError] = useState(null);
const [downloaded, setDownloaded] = useState(0);
const [total, setTotal] = useState(0);
useEffect(() => {
if (!isTauri()) return;
import("@tauri-apps/api/app").then(({ getVersion }) => {
getVersion().then(setVersion).catch(() => {});
});
}, []);
const runCheck = useCallback(async () => {
setError(null);
if (!isTauri()) {
setState("not-tauri");
return;
}
setState("checking");
try {
const res = await checkForAppUpdate({ respectSkip: false });
setLastCheck(getLastUpdateCheck());
if (res.available) {
setUpdate(res.update);
setState("available");
} else {
setUpdate(null);
setState("no-update");
}
} catch (e) {
console.error("Update-Check fehlgeschlagen:", e);
setError(String(e?.message || e));
setState("idle");
}
}, []);
const install = async () => {
if (!update) return;
setError(null);
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 isBusy = state === "downloading" || state === "installing";
const pct = total > 0 ? Math.min(100, Math.round((downloaded / total) * 100)) : null;
return (
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 20 }} className="responsive-grid-2">
<div className="card">
<Section title="UPDATES">
<div style={{ display: "flex", justifyContent: "space-between", padding: "4px 0", fontSize: 12, borderBottom: "1px solid #f0ede8" }}>
<span style={{ color: "#888" }}>Aktuelle Version</span>
<strong style={{ color: "#555" }}>{version || "—"}</strong>
</div>
<div style={{ display: "flex", justifyContent: "space-between", padding: "4px 0", fontSize: 12, borderBottom: "1px solid #f0ede8", marginBottom: 16 }}>
<span style={{ color: "#888" }}>Letzte Prüfung</span>
<span style={{ color: "#555" }}>{formatLastCheck(lastCheck)}</span>
</div>
<button
className="btn btn-primary"
style={{ width: "100%", marginBottom: 10 }}
onClick={runCheck}
disabled={state === "checking" || isBusy}
>
{state === "checking" ? "Wird geprüft …" : "Nach Updates suchen"}
</button>
{state === "no-update" && (
<div style={{ marginTop: 6, padding: "10px 12px", background: "#f0f7f0", border: "1px solid #c8e0c8", borderRadius: 8, fontSize: 12, color: "#2d6a4f", lineHeight: 1.5 }}>
Rapport ist auf dem neuesten Stand.
</div>
)}
{state === "available" && update && (
<div style={{ marginTop: 10, padding: "14px 14px 12px", background: "#fbf6ef", border: "1px solid #e6d4b3", borderRadius: 8 }}>
<div style={{ fontSize: 10, letterSpacing: "0.12em", color: "#b07848", fontWeight: 600, marginBottom: 4 }}>UPDATE VERFÜGBAR</div>
<div style={{ fontSize: 14, fontWeight: 600, color: "#1a1a18", marginBottom: 6 }}>Rapport {update.version}</div>
{update.body && (
<div style={{ fontSize: 12, color: "#666", lineHeight: 1.5, whiteSpace: "pre-wrap", marginBottom: 10, maxHeight: 160, overflowY: "auto" }}>
{update.body}
</div>
)}
<button className="btn btn-primary" style={{ width: "100%", marginBottom: 8 }} onClick={install} disabled={isBusy}>
{isBusy ? "Bitte warten …" : "Installieren und neu starten"}
</button>
<button className="btn btn-ghost" style={{ width: "100%", fontSize: 12 }} onClick={skipVersion} disabled={isBusy}>
Diese Version überspringen
</button>
</div>
)}
{isBusy && (
<div style={{ marginTop: 12 }}>
<div style={{ fontSize: 11, color: "#888", marginBottom: 6, letterSpacing: "0.04em" }}>
{state === "downloading"
? (pct !== null ? `Wird heruntergeladen … ${pct}%` : "Wird heruntergeladen …")
: "Wird installiert …"}
</div>
<div style={{ height: 4, background: "#eee", borderRadius: 2, overflow: "hidden" }}>
<div style={{
height: "100%",
width: pct !== null ? `${pct}%` : "100%",
background: "#b07848",
transition: "width 0.2s",
animation: pct === null ? "rapport-us-pulse 1.2s ease-in-out infinite" : undefined,
}} />
</div>
<style>{`@keyframes rapport-us-pulse { 0%,100% { opacity: 0.5; } 50% { opacity: 1; } }`}</style>
</div>
)}
{state === "not-tauri" && (
<div style={{ marginTop: 6, padding: "10px 12px", background: "#f5f5f0", border: "1px solid #e0dbd4", borderRadius: 8, fontSize: 12, color: "#666" }}>
Updates sind nur in der Desktop-App verfügbar.
</div>
)}
{error && (
<div style={{ marginTop: 10, padding: "10px 12px", background: "#fdf0f0", border: "1px solid #f3c4c4", borderRadius: 6, fontSize: 12, color: "#8a1a1a" }}>
{error}
</div>
)}
<p style={{ fontSize: 11, color: "#888", lineHeight: 1.6, marginTop: 16 }}>
Updates werden automatisch beim Start der App geprüft. Hier kannst du manuell suchen z.B. wenn die App selten geschlossen wird.
</p>
</Section>
</div>
<div className="card">
<Section title="SUPPORT & DOKUMENTATION">
<p style={{ fontSize: 13, color: "#666", lineHeight: 1.7, marginBottom: 16 }}>
Anleitungen, Dokumentation und Support findest du auf der offiziellen Website:
</p>
<a
href="https://rapport.kgva.ch/"
target="_blank"
rel="noreferrer"
className="btn btn-ghost"
style={{ width: "100%", textDecoration: "none", marginBottom: 10 }}
>
rapport.kgva.ch
</a>
<p style={{ fontSize: 11, color: "#888", lineHeight: 1.6, marginTop: 16 }}>
Dort findest du auch das Changelog, Fehlerberichte und Kontaktmöglichkeiten für direkten Support.
</p>
</Section>
</div>
</div>
);
}
+48
View File
@@ -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 "—";
}
}
+5
View File
@@ -2,6 +2,7 @@ import React, { useState } from "react";
import { STORAGE_KEY, DEFAULT_ABSENZ_TYPES } from "../constants.js";
import { formatIban, isQRIban, applyProjectNumberFormat, applyProtoNumberFormat, generateId, getFeiertageForYear, getAbsenzTypes } from "../utils.js";
import { Header, FormField, Modal, DateInput, useConfirm } from "../components/UI.jsx";
import UpdatesSupport from "../components/UpdatesSupport.jsx";
const PERMISSION_GROUPS = [
{ label: "Grundmodule", items: [
@@ -37,6 +38,7 @@ const TABS = [
{ id: "team", label: "Team & Rollen" },
{ id: "kalender", label: "Feiertage & Absenzen" },
{ id: "system", label: "System" },
{ id: "support", label: "Updates & Support" },
{ id: "profil", label: "Mein Profil" },
];
@@ -858,6 +860,9 @@ export default function Settings({ data, update, currentUser, uiZoom, setUiZoom
</div>
</div>
)}
{/* ── Tab: Updates & Support ── */}
{tab === "support" && <UpdatesSupport />}
</>}
</div>
);