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
This commit is contained in:
vicrodh committed 2026-08-03 17:45:50 -06:00
1 parent 06e8b4ba4d
commit 10f249be24
2 files changed
+36 -7

No files matched your search

+8 -4
View File
@@ -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<A>(runtime: &Arc<AppRuntime<A>>) -> 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();
+28 -3
View File
@@ -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<AppRuntime<LoggingAdapter>>, 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);