From 10f249be248f78d7800d361d93c37138800a3432 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:45:50 -0600 Subject: [PATCH] feat(qt): module-OFF results fallback and search service teardown Two gaps that both made the port quietly lose a capability. Turning Intelligent Search off is a SWAP in the reference, not a removal: typing live-navigates to the full results page on a 300ms debounce instead of opening a dropdown. Here it returned, so with the module off typing did nothing at all until the user pressed Enter - the kill switch removed the feature rather than exchanging it. The per-user search service had no teardown on logout, so the next account inherited 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 buckets. The reference tears it down for exactly this reason. The version counter and both payload snapshots are cleared with it, so a load still in flight at logout cannot publish into the next session. auth_qt's asymmetry note is updated rather than left claiming a gap that is now closed. Contract: qbz-nix-docs/qt-frontend/2026-08-03-cortinilla-parity, commit C10 (J5, J7, R7-5). Gate: build clean, 258/258 tests, both QML audits OK, offscreen smoke 0 complaints + home published. cc --- crates/qbz-qt/src/auth_qt.rs | 12 ++++++++---- crates/qbz-qt/src/search_qt.rs | 31 ++++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 7 deletions(-) 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);