feat(app): shared offline-mode engine with connectivity actor and grace gate

New qbz_app::offline_mode (frontend-agnostic, ADR-006): three-state mode
machine (Online / RealOffline session-scoped / InducedOffline persisted),
Tauri-compatible offline_settings.db store with the issue #279
stream_first_track snapshot, and a layered connectivity actor (default-route
signal on Linux, audio liveness window, IP-literal + vendor-diverse strict
probes, asymmetric hysteresis with confirmation burst, captive-portal and
suspend/resume handling).

qbz-qobuz gains the process-wide offline_gate and ApiError::OfflineMode (the
single Qobuz choke point; HTTP funnel wiring lands next).
SubscriptionStateStore gains offline_playback_allowed: binary 30-day-grace
playback gate for the offline cache, never a preview-degradation path.

cc
This commit is contained in:
vicrodh committed 2026-06-09 19:48:13 -06:00
1 parent 10b13be6e3
commit edae9d2463
10 files changed
+1416

No files matched your search

+3
View File
@@ -31,6 +31,9 @@ pub enum ApiError {
#[error("Track {0} is no longer available on Qobuz")]
TrackUnavailable(u64),
#[error("Offline mode is active - Qobuz services are disabled")]
OfflineMode,
#[error("Network error: {0}")]
NetworkError(#[from] reqwest::Error),
+1
View File
@@ -37,6 +37,7 @@ pub mod cmaf;
pub mod endpoints;
pub mod error;
pub mod link_resolver;
pub mod offline_gate;
pub mod performers;
pub mod retry;
+47
View File
@@ -0,0 +1,47 @@
//! Process-wide offline gate — the single Qobuz choke point.
//!
//! Offline MODE (induced from Settings or detected loss of connectivity)
//! means ZERO Qobuz services: while the gate is closed, every HTTP request
//! the client would issue fails fast with [`crate::error::ApiError::OfflineMode`]
//! instead of timing out against the network. The owner of the flag is the
//! shared offline-mode engine in `qbz-app`; frontends never set it directly.
//!
//! The flag is process-global on purpose: there is exactly one Qobuz client
//! per process, the gate must hold across re-logins, and a per-instance flag
//! would silently reset when the client is rebuilt.
use std::sync::atomic::{AtomicBool, Ordering};
static OFFLINE: AtomicBool = AtomicBool::new(false);
/// Close (true) or open (false) the gate. Called by the offline-mode engine
/// on every mode transition.
pub fn set_offline(offline: bool) {
let was = OFFLINE.swap(offline, Ordering::Relaxed);
if was != offline {
log::info!(
"[OfflineGate] Qobuz API gate {}",
if offline { "CLOSED (offline mode)" } else { "OPEN" }
);
}
}
/// Whether the gate is currently closed (offline mode active).
pub fn is_offline() -> bool {
OFFLINE.load(Ordering::Relaxed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gate_toggles() {
set_offline(false);
assert!(!is_offline());
set_offline(true);
assert!(is_offline());
set_offline(false);
assert!(!is_offline());
}
}