//! Read-only background polling of the OpenFUT account summary. //! //! The launcher already POSTs `/openfut/account/sync` once at launch time //! (see [`crate::account_sync::sync`]) to select the active profile. This //! module reuses that request in a background thread so the Dashboard can show //! a live "Your Club" card — coins, level, packs — without ever blocking the UI //! thread on the network. It mirrors [`crate::health::HealthMonitor`]: a shared //! target the UI re-points when the server config changes, and a shared state //! snapshot the UI renders each frame. use parking_lot::Mutex; use std::{ sync::{ atomic::{AtomicBool, Ordering}, Arc, }, thread, time::{Duration, Instant}, }; use crate::account_sync::{self, AccountSummary}; use crate::config::LauncherConfig; const POLL_INTERVAL: Duration = Duration::from_secs(5); /// A snapshot of the last account fetch, rendered by the dashboard. #[derive(Clone, Default)] pub struct AccountState { /// The most recently fetched summary, or None while none has succeeded. pub summary: Option, /// The error from the latest failed attempt (cleared on success). pub error: Option, /// Whether a server target is currently configured. `false` = idle: the /// launcher has nothing to poll, so the UI shows the "connect" prompt. pub configured: bool, pub last_checked: Option, } impl AccountState { /// True when the latest error looks like a connectivity failure (server /// down / unresolvable) rather than a protocol/validation error. Lets the /// UI show the calm "offline" prompt for the common "server not up" case /// and reserve the loud error state for genuinely broken responses. pub fn unreachable(&self) -> bool { self.error.as_deref().is_some_and(|e| { e.contains("cannot connect") || e.contains("cannot resolve") || e.contains("resolved to no addresses") }) } } /// Background poller. Holds a shared target config the UI can update when the /// user changes the server address/account, and a shared state the UI reads. pub struct AccountMonitor { pub state: Arc>, target: Arc>>, running: Arc, } impl AccountMonitor { pub fn new() -> Self { let state = Arc::new(Mutex::new(AccountState::default())); let target: Arc>> = Arc::new(Mutex::new(None)); let running = Arc::new(AtomicBool::new(true)); let t_state = Arc::clone(&state); let t_target = Arc::clone(&target); let t_running = Arc::clone(&running); thread::spawn(move || { while t_running.load(Ordering::Relaxed) { let target = t_target.lock().clone(); match target { None => { // No server configured — reset to the idle prompt state. *t_state.lock() = AccountState::default(); } Some(config) => { let result = account_sync::sync(&config); let mut state = t_state.lock(); state.configured = true; state.last_checked = Some(Instant::now()); match result { Ok(summary) => { state.summary = Some(summary); state.error = None; } Err(error) => { // Drop the stale summary so the card never shows // populated data alongside an error/offline pill. state.summary = None; state.error = Some(error); } } } } thread::sleep(POLL_INTERVAL); } }); Self { state, target, running, } } /// Point the monitor at a new server/account. `None` (no server configured) /// puts it back into the idle prompt state. pub fn set_target(&self, target: Option) { *self.target.lock() = target; } pub fn snapshot(&self) -> AccountState { self.state.lock().clone() } } impl Drop for AccountMonitor { fn drop(&mut self) { self.running.store(false, Ordering::Relaxed); } }