diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 0fb20d7..dd04aaa 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -114,10 +114,17 @@ pub struct AppState { /// HTTP client for uteke-serve (all memory CRUD + graph operations). /// None if server is not running. pub uteke_client: Option, - /// Cached installed uteke version ("X.Y.Z"), probed once at startup via - /// `uteke --version`. `None` if the CLI couldn't be probed. Used to gate - /// features that require a minimum server version (e.g. Documents → 0.7.0). + /// Cached installed uteke version ("X.Y.Z"), probed once at startup. + /// For local servers this is the `uteke --version` CLI output; for remote + /// servers it is the version reported by the server's `/health` (if any). + /// `None` if unknown. Used to gate features that require a minimum server + /// version (e.g. Documents → 0.7.1). pub uteke_version: Option, + /// `true` when Corin talks to a remote uteke-serve (https/host URL). + /// Remote servers are user-managed, so an unknown version is treated + /// leniently (no upgrade banner, no hard gate) — the server's own + /// responses surface any real incompatibility. + pub uteke_remote: bool, } // --------------------------------------------------------------------------- @@ -2669,7 +2676,7 @@ pub async fn disconnect_connection( // // Since uteke v0.7.0 (#614) documents are global — unique slugs across all // namespaces. `namespace` is no longer accepted. Every command gates on -// `MIN_UTEKE_FOR_DOCS` ("0.7.0") so older servers never get ambiguous +// `MIN_UTEKE_FOR_DOCS` ("0.7.1") so older servers never get ambiguous // global-slug requests. Frontend reads `uteke_version_status` to render an // upgrade banner; `uteke_self_update` runs `uteke upgrade` and re-detects. @@ -2678,46 +2685,89 @@ pub async fn disconnect_connection( pub struct VersionStatus { /// Detected `X.Y.Z`, or `None` if the `uteke` CLI couldn't be probed. pub current: Option, - /// Minimum uteke version that supports global documents ("0.7.0"). + /// Minimum uteke version that supports global documents ("0.7.1"). pub required: String, /// `true` iff `current >= required` (false when current is unknown). pub supported: bool, } -/// Reject the call unless the cached uteke version meets `min`. +/// Resolve the effective uteke version for gating. /// -/// Reads `AppState::uteke_version` (set once at startup). Returns a clear, -/// upgrade-instructing message when unsupported or when the version is unknown. +/// - **Remote server**: probe the server's self-reported version via +/// `/health` (authoritative for a user-managed server). The local CLI +/// version is irrelevant — it may be older or newer than the server. +/// - **Local server**: use the cached `uteke --version` output. +async fn resolve_uteke_version( + state: &tauri::State<'_, Arc>>, +) -> (bool, Option) { + let (is_remote, client, cached) = { + let s = state.lock().await; + ( + s.uteke_remote, + s.uteke_client.clone(), + s.uteke_version.clone(), + ) + }; + if is_remote { + if let Some(client) = client + && client.is_available().await + { + let v = client.server_version().await; + return (true, v); + } + // Remote unreachable during this probe — fall back to whatever we know. + (true, cached) + } else { + (false, cached) + } +} + +/// Reject the call unless the effective uteke version meets `min`. +/// +/// For **remote** servers with an unknown version (e.g. an older server +/// that doesn't report version in `/health`) the call is allowed through — +/// remote servers are user-managed, and the server's own response surfaces +/// any real incompatibility (e.g. a 404/400 for a missing route). async fn require_uteke_version( state: &tauri::State<'_, Arc>>, min: &str, ) -> Result<(), CommandError> { - let current = state.lock().await.uteke_version.clone(); + let (is_remote, current) = resolve_uteke_version(state).await; let ok = current .as_ref() .map(|c| crate::version_meets(c, min)) - .unwrap_or(false); + // Remote + unknown version → don't block (user-managed server). + // Local + unknown version → block (we can't confirm capability). + .unwrap_or(is_remote); if ok { Ok(()) } else { + let hint = if is_remote { + "upgrade the uteke-serve on the remote host" + } else { + "run 'uteke upgrade' or use the Update button in Documents" + }; Err(CommandError::Uteke(format!( - "uteke {min} or newer is required for documents (current: {}). \ - Run 'uteke upgrade' or use the Update button in Documents.", + "uteke {min} or newer is required for documents (current: {}). Please {hint}.", current.as_deref().unwrap_or("unknown") ))) } } -/// Report the installed uteke version and whether it supports Documents. +/// Report the effective uteke version and whether it supports Documents. +/// +/// Drives the Documents upgrade banner. For remote servers with an unknown +/// version, `supported` is `true` so users are not nagged to upgrade a server +/// they manage (and which may well be newer than the local CLI). #[tauri::command] pub async fn uteke_version_status( state: tauri::State<'_, Arc>>, ) -> Result { - let current = state.lock().await.uteke_version.clone(); + let (is_remote, current) = resolve_uteke_version(&state).await; let supported = current .as_ref() .map(|c| crate::version_meets(c, crate::MIN_UTEKE_FOR_DOCS)) - .unwrap_or(false); + .unwrap_or(is_remote); Ok(VersionStatus { current, required: crate::MIN_UTEKE_FOR_DOCS.to_string(), @@ -2733,6 +2783,21 @@ pub async fn uteke_version_status( pub async fn uteke_self_update( state: tauri::State<'_, Arc>>, ) -> Result { + let is_remote = state.lock().await.uteke_remote; + if is_remote { + // Upgrading the local CLI won't affect a user-managed remote server. + // Just re-probe the server's version so the banner can refresh. + let (_, current) = resolve_uteke_version(&state).await; + let supported = current + .as_ref() + .map(|c| crate::version_meets(c, crate::MIN_UTEKE_FOR_DOCS)) + .unwrap_or(true); + return Ok(VersionStatus { + current, + required: crate::MIN_UTEKE_FOR_DOCS.to_string(), + supported, + }); + } let uteke = crate::find_uteke_cli().ok_or_else(|| { CommandError::Uteke("uteke CLI not found — install via the setup script".into()) })?; @@ -2783,6 +2848,9 @@ pub async fn doc_list( if !client.is_available().await { return Err(CommandError::Uteke("uteke-serve not reachable".into())); } + // uteke-serve defaults /doc/list limit to 5 (shared with memory pagination). + // The document tree needs the full set client-side, so default high here. + let limit = Some(limit.unwrap_or(1000)); let docs = client .doc_list(limit, roots_only, parent.as_deref()) .await diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c7933a3..c292ac1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -195,8 +195,10 @@ fn find_uteke_serve() -> Option { None } -/// Minimum uteke version required for the global Documents feature (v0.7.0, #614). -pub(crate) const MIN_UTEKE_FOR_DOCS: &str = "0.7.0"; +/// Minimum uteke version required for the global Documents feature. +/// v0.7.0 introduced global docs (#614), but document delete/move landed in +/// v0.7.1 — require it so the full doc feature set is available. +pub(crate) const MIN_UTEKE_FOR_DOCS: &str = "0.7.1"; /// Find the `uteke` CLI binary in PATH or ~/.local/bin. pub(crate) fn find_uteke_cli() -> Option { @@ -509,8 +511,15 @@ pub fn run() { s.uteke_client = Some(client); } + // Cache whether we're talking to a user-managed remote + // server (vs. a locally spawned uteke-serve). + s.uteke_remote = config::is_remote_url(&server_url); + // Cache the installed uteke version for feature - // gating (e.g. Documents requires >= 0.7.0). + // gating (e.g. Documents requires >= 0.7.1). + // For remote servers the authoritative version is + // probed live from /health in the gating commands; + // the local CLI version is only a fallback there. s.uteke_version = detect_uteke_version(); } Err(e) => { @@ -602,6 +611,15 @@ mod tests { assert!(version_meets("1.0.0", "0.7.0")); } + #[test] + fn docs_gate_requires_0_7_1() { + // Document delete/move landed in 0.7.1; 0.7.0 must be rejected so the + // UI prompts for an upgrade instead of hitting a missing /doc/delete. + assert!(!version_meets("0.7.0", MIN_UTEKE_FOR_DOCS)); + assert!(version_meets("0.7.1", MIN_UTEKE_FOR_DOCS)); + assert!(version_meets("0.8.0", MIN_UTEKE_FOR_DOCS)); + } + #[test] fn version_meets_lower_rejected() { assert!(!version_meets("0.6.7", "0.7.0")); diff --git a/src-tauri/src/uteke_client.rs b/src-tauri/src/uteke_client.rs index b2137f2..f5d99b0 100644 --- a/src-tauri/src/uteke_client.rs +++ b/src-tauri/src/uteke_client.rs @@ -286,6 +286,27 @@ impl UtekeClient { .unwrap_or(false) } + /// Probe the server's self-reported version via `/health`. + /// + /// Returns `Some("X.Y.Z")` when the server includes a `version` field + /// (uteke ≥ the /health-version patch), otherwise `None`. Used to gate + /// features for remote servers where the local `uteke` CLI version is + /// irrelevant — the server may be newer or older than the CLI. + pub async fn server_version(&self) -> Option { + let resp = self + .authed(self.client.get(format!("{}/health", self.base_url))) + .send() + .await + .ok()?; + if !resp.status().is_success() { + return None; + } + let body: serde_json::Value = resp.json().await.ok()?; + body.get("version") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + } + /// Semantic recall (vector + FTS5 hybrid search via RRF). pub async fn recall( &self, @@ -931,7 +952,7 @@ impl UtekeClient { // // Since uteke v0.7.0 (#614) documents are GLOBAL — unique slugs across all // namespaces. These methods send no `namespace`. Callers MUST gate access - // on `crate::MIN_UTEKE_FOR_DOCS` ("0.7.0") so older servers (where docs + // on `crate::MIN_UTEKE_FOR_DOCS` ("0.7.1") so older servers (where docs // were namespace-scoped) never receive ambiguous global-slug requests. /// List documents via POST /doc/list.