diff --git a/src/bin/vimit-gui.rs b/src/bin/vimit-gui.rs index 801e835..cdbaa27 100644 --- a/src/bin/vimit-gui.rs +++ b/src/bin/vimit-gui.rs @@ -25,7 +25,7 @@ fn main() { #[cfg(test)] mod tests { use super::*; - use crate::config::{GuiAccount, endpoint_for_label, runtime_config}; + use crate::config::{GuiAccount, runtime_config}; use crate::overlay::{ CreatureSound, CreatureState, OVERLAY_HISTORY_RETENTION, OverlayHistory, build_overlay_state, creature_node_count, creature_node_count_for_skin, @@ -152,15 +152,15 @@ mod tests { #[test] fn endpoint_label_maps_to_visible_api_url() { assert_eq!( - endpoint_for_label("api", "https://custom.example"), + ng::dashboard_endpoint_for_label("api", "https://custom.example"), ng::DEFAULT_API_BASE ); assert_eq!( - endpoint_for_label("r-api", "https://custom.example"), + ng::dashboard_endpoint_for_label("r-api", "https://custom.example"), ng::FALLBACK_API_BASE ); assert_eq!( - endpoint_for_label("custom", "https://custom.example"), + ng::dashboard_endpoint_for_label("custom", "https://custom.example"), "https://custom.example" ); } diff --git a/src/bin/vimit-gui/config.rs b/src/bin/vimit-gui/config.rs index 5233d59..9de2ae8 100644 --- a/src/bin/vimit-gui/config.rs +++ b/src/bin/vimit-gui/config.rs @@ -55,11 +55,3 @@ pub(crate) fn runtime_config( auto_failover, } } - -pub(crate) fn endpoint_for_label(label: &str, configured_api_base: &str) -> String { - match label { - "api" => ng::DEFAULT_API_BASE.to_string(), - "r-api" => ng::FALLBACK_API_BASE.to_string(), - _ => configured_api_base.to_string(), - } -} diff --git a/src/bin/vimit-gui/dashboard.rs b/src/bin/vimit-gui/dashboard.rs index dca1b2e..9714538 100644 --- a/src/bin/vimit-gui/dashboard.rs +++ b/src/bin/vimit-gui/dashboard.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use std::thread; @@ -5,7 +6,7 @@ use std::thread; use slint::{ModelRc, VecModel, Weak}; use crate::AppWindow; -use crate::config::{GuiAccount, endpoint_for_label, gui_load_dotenv, runtime_config}; +use crate::config::{GuiAccount, gui_load_dotenv, runtime_config}; use crate::ng; use crate::overlay::{ CreatureState, OverlayHistory, OverlayState, build_overlay_state, creature_node_count_for_skin, @@ -281,61 +282,36 @@ fn load_dashboard( overlay_history: &Arc>, auto_failover: bool, ) -> Result { - let (payload, source, active_endpoint_label, abtop_bin, live_api_key_present) = if force_demo { - ( - ng::demo_payload(), - "источник: встроенные демо-данные".to_string(), - "demo".to_string(), - String::new(), - false, - ) - } else if let Some(path) = mock_path { - ( - ng::load_mock(path)?, - format!("источник: mock {}", path), - "mock".to_string(), - String::new(), - false, - ) + let dotenv = if force_demo || mock_path.is_some() { + HashMap::new() } else { - let dotenv = gui_load_dotenv()?; - let config = runtime_config(&dotenv, account, auto_failover); - if config.api_key.is_empty() { - ( - ng::demo_payload(), - "источник: демо; добавьте VIBEMODE_API_KEY в .env для live-лимитов".to_string(), - "demo".to_string(), - config.abtop_bin, - false, - ) - } else { - let (value, label) = if config.auto_failover - && config.api_base.trim_end_matches('/') == ng::DEFAULT_API_BASE - { - let mut router = router.lock().unwrap(); - http.fetch_me_with_retry(&config.api_key, &mut router, &config.api_base)? - } else { - let mut router = ng::Router::new( - config.api_base.clone(), - ng::api_fallbacks_for(&config.api_base, config.auto_failover), - ); - http.fetch_me_with_retry(&config.api_key, &mut router, &config.api_base)? - }; - let endpoint = endpoint_for_label(&label, &config.api_base); - ( - value, - format!("источник: live VibeMode /v1/me на {endpoint}"), - label, - config.abtop_bin, - true, - ) - } + gui_load_dotenv()? }; + let config = runtime_config(&dotenv, account, auto_failover); + let request = ng::DashboardRefreshRequest { + force_demo, + mock_path, + warning_threshold: warning, + danger_threshold: danger, + missing_api_key_source: "источник: демо; добавьте VIBEMODE_API_KEY в .env для live-лимитов", + }; + let shared = + if config.auto_failover && config.api_base.trim_end_matches('/') == ng::DEFAULT_API_BASE { + let mut shared_router = router.lock().unwrap(); + ng::load_dashboard_refresh(request, &config, http, &mut shared_router)? + } else { + let mut local_router = ng::Router::new( + config.api_base.clone(), + ng::api_fallbacks_for(&config.api_base, config.auto_failover), + ); + ng::load_dashboard_refresh(request, &config, http, &mut local_router)? + }; - let windows = ng::summarize_me(&payload, warning, danger); - let status = ng::dashboard_status(&windows); + let abtop_bin = config.abtop_bin.clone(); + let live_api_key_present = shared.live_api_key_present; + let mut dashboard = shared.dashboard; let agent = read_agent_status_for_gui(&abtop_bin); - let overlay = build_overlay_state(&windows, &agent.token_rate, overlay_history); + let overlay = build_overlay_state(&dashboard.windows, &agent.token_rate, overlay_history); let mut five_trend_data = Vec::new(); let mut day_trend_data = Vec::new(); @@ -344,7 +320,7 @@ fn load_dashboard( if let Ok(Some(store)) = ng::cli::trends::TrendStore::open() { if live_api_key_present { - let _ = store.save_snapshot(&windows, chrono::Utc::now()); + let _ = store.save_snapshot(&dashboard.windows, chrono::Utc::now()); } if let Ok(days) = store.query_trends(15) { for day in &days { @@ -382,16 +358,12 @@ fn load_dashboard( ]; } + dashboard.agent = agent.summary; + dashboard.token_rate = agent.token_rate; + Ok(GuiDashboardResult { - dashboard: ng::Dashboard { - source, - status, - agent: agent.summary, - token_rate: agent.token_rate, - windows, - daily: None, - }, - active_endpoint_label, + dashboard, + active_endpoint_label: shared.active_endpoint_label, five_trend_data, day_trend_data, week_trend_data, diff --git a/src/dashboard_refresh.rs b/src/dashboard_refresh.rs new file mode 100644 index 0000000..76bf499 --- /dev/null +++ b/src/dashboard_refresh.rs @@ -0,0 +1,156 @@ +use crate::{ + DEFAULT_API_BASE, DEFAULT_DANGER_THRESHOLD, DEFAULT_WARNING_THRESHOLD, Dashboard, HttpClient, + Router, RuntimeConfig, demo_payload, load_mock, summarize_me, +}; + +#[derive(Debug, Clone)] +pub struct DashboardRefreshResult { + pub dashboard: Dashboard, + pub active_endpoint_label: String, + pub live_api_key_present: bool, +} + +#[derive(Debug, Clone, Copy)] +pub struct DashboardRefreshRequest<'a> { + pub force_demo: bool, + pub mock_path: Option<&'a str>, + pub warning_threshold: f64, + pub danger_threshold: f64, + pub missing_api_key_source: &'a str, +} + +impl<'a> Default for DashboardRefreshRequest<'a> { + fn default() -> Self { + Self { + force_demo: false, + mock_path: None, + warning_threshold: DEFAULT_WARNING_THRESHOLD, + danger_threshold: DEFAULT_DANGER_THRESHOLD, + missing_api_key_source: "источник: демо; добавьте VIBEMODE_API_KEY в .env для live-лимитов", + } + } +} + +pub fn load_dashboard_refresh( + request: DashboardRefreshRequest<'_>, + config: &RuntimeConfig, + http: &HttpClient, + router: &mut Router, +) -> Result { + let (payload, source, active_endpoint_label, live_api_key_present) = if request.force_demo { + ( + demo_payload(), + "источник: встроенные демо-данные".to_string(), + "demo".to_string(), + false, + ) + } else if let Some(path) = request.mock_path { + ( + load_mock(path)?, + format!("источник: mock {path}"), + "mock".to_string(), + false, + ) + } else if config.api_key.is_empty() { + ( + demo_payload(), + request.missing_api_key_source.to_string(), + "demo".to_string(), + false, + ) + } else { + let (payload, label) = + http.fetch_me_with_retry(&config.api_key, router, &config.api_base)?; + let endpoint = dashboard_endpoint_for_label(&label, &config.api_base); + ( + payload, + format!("источник: live VibeMode /v1/me на {endpoint}"), + label, + true, + ) + }; + + let windows = summarize_me( + &payload, + request.warning_threshold, + request.danger_threshold, + ); + let status = crate::dashboard_status(&windows); + + Ok(DashboardRefreshResult { + dashboard: Dashboard { + source, + status, + agent: String::new(), + token_rate: String::new(), + windows, + daily: None, + }, + active_endpoint_label, + live_api_key_present, + }) +} + +pub fn dashboard_endpoint_for_label(label: &str, configured_api_base: &str) -> String { + match label { + "api" => DEFAULT_API_BASE.to_string(), + "r-api" => crate::FALLBACK_API_BASE.to_string(), + _ => configured_api_base.to_string(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn load_dashboard_refresh_falls_back_to_demo_without_api_key() { + let http = HttpClient::new(crate::USER_AGENT).unwrap(); + let mut router = Router::new( + crate::DEFAULT_API_BASE.to_string(), + crate::api_fallbacks_for(crate::DEFAULT_API_BASE, true), + ); + let config = RuntimeConfig { + api_base: crate::DEFAULT_API_BASE.to_string(), + api_key: String::new(), + abtop_bin: String::new(), + auto_failover: true, + }; + + let result = load_dashboard_refresh( + DashboardRefreshRequest { + missing_api_key_source: + "источник: демо; добавьте VIBEMODE_API_KEY в .env для live-лимитов", + ..DashboardRefreshRequest::default() + }, + &config, + &http, + &mut router, + ) + .unwrap(); + + assert_eq!(result.active_endpoint_label, "demo"); + assert!(!result.live_api_key_present); + assert_eq!( + result.dashboard.source, + "источник: демо; добавьте VIBEMODE_API_KEY в .env для live-лимитов" + ); + assert!(!result.dashboard.windows.is_empty()); + } + + #[test] + fn dashboard_endpoint_for_label_maps_known_hosts() { + assert_eq!( + dashboard_endpoint_for_label("api", "https://custom.example"), + crate::DEFAULT_API_BASE + ); + assert_eq!( + dashboard_endpoint_for_label("r-api", "https://custom.example"), + crate::FALLBACK_API_BASE + ); + assert_eq!( + dashboard_endpoint_for_label("custom", "https://custom.example"), + "https://custom.example" + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index a8131e4..d235192 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub mod android; pub mod api; pub mod cli; +pub mod dashboard_refresh; pub mod parse; #[cfg(all(target_os = "android", feature = "android-gui"))] @@ -12,6 +13,7 @@ pub use android::{ android_runaway_alert_text, }; pub use api::*; +pub use dashboard_refresh::*; pub use parse::*; use chrono::{SecondsFormat, Utc};