diff --git a/crates/qbz-qt/src/auth_qt.rs b/crates/qbz-qt/src/auth_qt.rs index b7513fee7..cab876a49 100644 --- a/crates/qbz-qt/src/auth_qt.rs +++ b/crates/qbz-qt/src/auth_qt.rs @@ -409,10 +409,10 @@ where /// STILL ASYMMETRIC (an `init_for_user` above with no teardown here, because /// the module exposes none — each needs its own file changed, so it is not /// done from this side): `local_plex`, `library_qt::init_local_favorites`, -/// `sidebar_qt::init_pinned`, `search_qt::init`, `playlist_qt::set_user_id`. -/// The reference DOES tear the last three down (`pinned::teardown`, -/// `local_favorites::teardown`, `search_service::teardown` — -/// `qbz/src/auth.rs:349-352`). +/// `sidebar_qt::init_pinned`, `playlist_qt::set_user_id`. The reference DOES +/// tear the first three down (`pinned::teardown`, +/// `local_favorites::teardown` — `qbz/src/auth.rs:349-352`). +/// `search_qt::teardown` was the fourth and is now WIRED below. pub async fn logout(runtime: &Arc>) -> Result<(), String> where A: FrontendAdapter + Send + Sync + 'static, @@ -420,6 +420,10 @@ where let _ = qbz_credentials::clear_oauth_token(); let _ = runtime.core().logout().await; crate::fav_cache_qt::teardown(); + // Intelligent Search: the LEARNED ranking is per-user. Without this the + // next account inherits a stranger's most-clicked results as its promoted + // top result, and keeps writing into their buckets. + crate::search_qt::teardown(); // MyQBZ: grids + branding/view-prefs binding, then the detail page's // caches and the two modal documents. crate::myqbz_qt::teardown(); diff --git a/crates/qbz-qt/src/search_qt.rs b/crates/qbz-qt/src/search_qt.rs index 5ec972fdf..60b9a26a8 100644 --- a/crates/qbz-qt/src/search_qt.rs +++ b/crates/qbz-qt/src/search_qt.rs @@ -87,6 +87,24 @@ pub fn is_enabled() -> bool { with_service(false, |s| s.enabled()) } +/// Drop the per-user search service on logout. +/// +/// Without this the NEXT account inherits the previous one's learned ranking: +/// `top_for_query` would promote a stranger's most-clicked result, and the +/// interaction store would keep accumulating under their bucket. The reference +/// tears it down for the same reason. +/// +/// The version counters and payload snapshots are cleared with it, so a load +/// still in flight at logout cannot publish into the next session. +pub fn teardown() { + if let Ok(mut guard) = SERVICE.lock() { + *guard = None; + } + next_cort_version(); + *LAST_CORT.lock().unwrap_or_else(|e| e.into_inner()) = None; + *LAST_CORT_LOCAL.lock().unwrap_or_else(|e| e.into_inner()) = Vec::new(); +} + pub fn set_enabled(on: bool) { with_service((), |s| s.set_enabled(on)); } @@ -755,9 +773,16 @@ pub async fn live(runtime: &Arc>, query: &str) { return; } if !is_enabled() { - // Module OFF: the reference live-navigates to the results page - // instead (300 ms debounce). Not wired here yet — Enter still - // reaches the page via submit(). + // Module OFF is a SWAP, not a removal: the reference live-navigates + // to the full results page on a 300 ms debounce instead of showing a + // dropdown. Returning here made the kill switch remove a capability + // rather than exchange it — typing did nothing at all until Enter. + let version = next_cort_version(); + tokio::time::sleep(std::time::Duration::from_millis(300)).await; + if !is_current_cort_version(version) { + return; + } + submit(runtime, &q, None).await; return; } set_selected(-1, 0.0);