Fix RAG index cascade on bulk file operations
Details
RagIndexService now uses batch-coalescing debounce instead of per-entity
timers. Incoming EntitySyncedMessages are queued and deduplicated, then
dispatched as a single batch — calling each IIndexableContentProvider at
most once per batch and loading hashes once. This reduces bulk operations
(e.g. trashing 40 files) from ~560 ReadList calls to ~14.
SdkMessage gains a SuppressChangeNotification property (JsonIgnored) that
tells SdkHost to skip the EntitySyncedMessage broadcast while still
recording sync outbound snapshots. Batch callers send a single summary
notification after all items are processed.
Version bump: 1.67.0 → 1.68.0 (SDK + Desktop)
Add biometric unlock (Touch ID / Windows Hello) support
Details
Implement platform-native biometric authentication as a convenience option
for unlocking PrivStack. The master password remains the root of trust —
biometric enrollment stores it in the OS keychain (macOS Keychain / Windows
Credential Manager), gated by biometric access control. On unlock, biometric
verification retrieves the password and feeds it through the existing
UnlockApp path. Zero Rust core changes required.
New files:
- IBiometricService interface with IsSupported, IsAvailable, Enroll,
Authenticate, Unenroll
- MacBiometricService: Security.framework P/Invoke for Keychain +
LocalAuthentication.framework for Touch ID prompts
- WindowsBiometricService: advapi32.dll CredWrite/Read/Delete +
UserConsentVerifier for Windows Hello
- NullBiometricService: Linux/unsupported fallback
Integration points:
- UnlockView: biometric button with "or" separator, auto-attempts on load
- SensitiveUnlockOverlay: biometric option for re-authentication
- Settings > Security: toggle with inline password enrollment, auto
re-enrollment on password change
- Setup Wizard: optional biometric step after password creation (shown only
when hardware is available)
- AppSettings: BiometricUnlockEnabled persisted setting
- ServiceRegistration: platform-conditional DI registration
Version: 1.66.5 → 1.67.0
Modernize startup loader UX: shimmer animation, chromeless windows, auto-focus
Details
Replace the plain indeterminate ProgressBar in UnlockView and SetupWizardView
with a custom shimmer ping-pong animation using Avalonia keyframe animations.
A 60px indicator slides left-to-right inside a 200x3px track with opacity-masked
edges for a polished shimmer effect.
Remove macOS title bar chrome from UnlockWindow and SetupWindow by switching to
SystemDecorations="BorderOnly" with ExtendClientAreaToDecorationsHint and
NoChrome hints. This removes the traffic lights while preserving the window
shadow, letting the existing CornerRadius="16" card design stand on its own.
Add auto-focus behavior: UnlockWindow.Opened calls Activate() to bring the
window to the front, and UnlockView.AttachedToVisualTree focuses the PasswordBox
so users can immediately start typing their master password.
Version bump to 1.66.5.
Fix crash on photo drop from cloud-mounted drives
Details
LocalStorageProvider.StoreFileAsync was synchronous despite the async
signature — it called File.OpenRead + SHA256.HashData on the UI thread.
When the source file is on a cloud mount (Google Drive, iCloud), the
read can stall or timeout, throwing an IOException that propagated up
through the Avalonia dispatcher as an unhandled exception, killing the app.
Fixed by making StoreFileAsync truly async: file hashing now uses an
async FileStream with SHA256.ComputeHashAsync on a background thread,
with a 30-second timeout via linked CancellationTokenSource.
Desktop v1.66.4
Enforce proper rate limit backoff across cloud sync stack
Details
The cloud sync was hitting 429 rate limits and not properly backing off,
causing repeated error spam. The C# refresh timer kept calling GetQuota()
every 15 seconds even during rate limits, generating cascading 429 errors.
Rust API client (api_client.rs):
- Added sliding window request counter that enforces 75% of the server's
advertised rate limit (450 req/60s instead of 600). Proactively throttles
before hitting the server limit.
- Changed 429 backoff to use 2x the server's Retry-After header with a
60-second floor. Previously used 1x which allowed immediate re-triggering.
- Counter limits are updated from server config on sync engine startup.
Rust FFI layer:
- Added PrivStackError::RateLimited (= 36) to distinguish rate limits from
generic CloudSyncError. Previously all rate limit errors mapped to
CloudSyncError and C# couldn't tell them apart.
- Rate limit errors now log as WARN instead of ERROR (expected transient).
- CloudSyncStatus now includes is_rate_limited and rate_limit_remaining_secs
fields, populated from the API client's gate state.
C# desktop:
- CloudSyncSettingsViewModel.RefreshStatusAsync() checks is_rate_limited
from status and skips all API calls (quota, devices, tokens) when true.
This eliminates the cascading 429 errors from the refresh timer.
- Catches PrivStackError.RateLimited specifically instead of falling through
to the generic exception handler.
- Status refresh timer slows from 15s to 30s during rate limit pause (still
updates the remaining-seconds display but doesn't hit the API).
- Added IsRateLimited and RateLimitDisplay observable properties for UI.
Rust v1.15.2, Desktop v1.66.3
Get notified about new releases