From 9db52595c73960d2fcfec80312aa3773411284f8 Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Mon, 15 Jun 2026 14:23:30 +0700 Subject: [PATCH] Add update check functionality Implement version checking and update notifications in the GUI --- Makefile | 16 ++- app/src-tauri/Cargo.lock | 251 +++++++++++++++++++++++++++++++++++- app/src-tauri/Cargo.toml | 2 + app/src-tauri/src/bridge.rs | 63 +++++++++ app/src-tauri/src/lib.rs | 2 + app/src/App.tsx | 22 +++- app/src/TopBar.tsx | 89 ++++++++++++- app/src/socketBridge.ts | 10 ++ 8 files changed, 446 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index b1b9608..9d9cf04 100644 --- a/Makefile +++ b/Makefile @@ -42,8 +42,12 @@ deps: ## install frontend deps (npm ci) targets: ## add rust targets for the universal build rustup target add aarch64-apple-darwin x86_64-apple-darwin +.PHONY: bump +bump: ## increment the patch version in tauri.conf.json (single source of truth) + @node -e "const f='$(APP_DIR)/src-tauri/tauri.conf.json';const fs=require('fs');const j=JSON.parse(fs.readFileSync(f));const p=j.version.split('.').map(Number);p[2]=(p[2]||0)+1;j.version=p.join('.');fs.writeFileSync(f, JSON.stringify(j,null,2)+'\n');console.log('version → '+j.version)" + .PHONY: dmg -dmg: targets ## build the universal (Intel + Apple Silicon) .dmg — UNSIGNED +dmg: bump targets ## bump version + build the universal (Intel + Apple Silicon) .dmg — UNSIGNED # Tauri's universal build needs BOTH the per-arch sidecars (resolved during each # arch sub-build) AND a fat spaceshd-universal-apple-darwin (copied into the final # bundle — Tauri does not lipo sidecars itself). spaceshd ships inside @@ -60,7 +64,7 @@ dmg: targets ## build the universal (Intel + Apple Silicon) .dmg — UNSIGNED @echo "→ $(DMG_DIR)" && ls -lh $(DMG_DIR)/*.dmg .PHONY: dmg-native -dmg-native: ## build a .dmg for the current arch only (faster) +dmg-native: bump ## bump version + build a .dmg for the current arch only (faster) cargo build --release -p spaceshd rm -rf $(SIDECAR_DIR) && mkdir -p $(SIDECAR_DIR) # avoid stale sidecars poisoning the bundle cp target/release/spaceshd $(SIDECAR_DIR)/spaceshd-$(NATIVE_TRIPLE) @@ -130,10 +134,14 @@ landing-push: landing-image ## tag & push the landing image to the registry # ---- Prod deploy ---- .PHONY: deploy-dmg -deploy-dmg: dmg ## upload the universal .dmg to the prod download dir (stable spacesh.dmg) +deploy-dmg: dmg ## upload the .dmg + update manifest (latest.json) to the prod download dir + @VER=$$(node -p "require('./$(APP_DIR)/src-tauri/tauri.conf.json').version"); \ + printf '{"version":"%s","url":"https://spaceshell.ru/download/spacesh.dmg"}\n' "$$VER" > /tmp/spacesh-latest.json; \ + echo "manifest version → $$VER" ssh $(SSH_OPTS) $(SSH_USER)@$(SSH_HOST) "mkdir -p $(SSH_REMOTE_DIR)/download" scp $(SSH_OPTS) $(DMG_DIR)/*.dmg "$(SSH_USER)@$(SSH_HOST):$(SSH_REMOTE_DIR)/download/spacesh.dmg" - @echo "Uploaded → https://spaceshell.ru/download/spacesh.dmg" + scp $(SSH_OPTS) /tmp/spacesh-latest.json "$(SSH_USER)@$(SSH_HOST):$(SSH_REMOTE_DIR)/download/latest.json" + @echo "Uploaded → https://spaceshell.ru/download/spacesh.dmg + latest.json" .PHONY: deploy-stack deploy-stack: ## sync compose+proxy.conf to prod and pull/up (manual; CI does this on push) diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock index a4c6423..7bb5373 100644 --- a/app/src-tauri/Cargo.lock +++ b/app/src-tauri/Cargo.lock @@ -453,6 +453,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.45" @@ -1246,8 +1252,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1257,9 +1265,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1537,6 +1547,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1955,6 +1981,12 @@ version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "mac-notification-sys" version = "0.6.13" @@ -2649,6 +2681,61 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.45" @@ -2785,6 +2872,44 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + [[package]] name = "reqwest" version = "0.13.4" @@ -2819,6 +2944,20 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -2847,12 +2986,53 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -3043,6 +3223,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.21.0" @@ -3222,6 +3414,7 @@ dependencies = [ "anyhow", "base64 0.22.1", "dirs 5.0.1", + "reqwest 0.12.28", "serde", "serde_json", "spacesh-proto", @@ -3280,6 +3473,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3432,7 +3631,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.4", "serde", "serde_json", "serde_repr", @@ -3824,6 +4023,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4143,6 +4352,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -4379,6 +4594,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web_atoms" version = "0.2.4" @@ -4435,6 +4660,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -4674,6 +4908,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -5212,6 +5455,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml index 1ed9326..3a1668b 100644 --- a/app/src-tauri/Cargo.toml +++ b/app/src-tauri/Cargo.toml @@ -23,3 +23,5 @@ serde_json = "1" base64 = "0.22" anyhow = "1" dirs = "5" +# rustls (no openssl) so the universal-apple-darwin cross-build stays self-contained. +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } diff --git a/app/src-tauri/src/bridge.rs b/app/src-tauri/src/bridge.rs index 2257a64..0738e17 100644 --- a/app/src-tauri/src/bridge.rs +++ b/app/src-tauri/src/bridge.rs @@ -422,6 +422,69 @@ pub async fn health(state: BridgeState<'_>) -> Result { data_of(state.request(Cmd::Health).await.map_err(|e| e.to_string())?) } +// ---- Update check ---- + +/// Where the GUI looks for the published app version. Overridable via +/// SPACESH_UPDATE_URL for local testing against a staging server. +const DEFAULT_UPDATE_URL: &str = "https://spaceshell.ru/download/latest.json"; + +#[derive(serde::Serialize)] +pub struct UpdateInfo { + current: String, + latest: String, + has_update: bool, + url: String, +} + +/// Parse a `major.minor.patch` string (tolerating a leading `v` and a +/// `-prerelease` suffix) into a comparable tuple; missing parts are 0. +fn parse_ver(v: &str) -> (u64, u64, u64) { + let core = v.trim().trim_start_matches('v').split('-').next().unwrap_or(""); + let mut it = core.split('.').map(|p| p.parse::().unwrap_or(0)); + (it.next().unwrap_or(0), it.next().unwrap_or(0), it.next().unwrap_or(0)) +} + +/// Fetch the server manifest and compare against the bundled app version. +/// `current` is the Tauri package version (single source of truth: tauri.conf.json), +/// which `make dmg` bumps on every build. +#[tauri::command] +pub async fn check_update(app: AppHandle) -> Result { + let current = app.package_info().version.to_string(); + let manifest_url = + std::env::var("SPACESH_UPDATE_URL").ok().filter(|s| !s.is_empty()).unwrap_or_else(|| DEFAULT_UPDATE_URL.to_string()); + + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(10)) + .build() + .map_err(|e| e.to_string())?; + let manifest: Value = client + .get(&manifest_url) + .send() + .await + .map_err(|e| e.to_string())? + .json() + .await + .map_err(|e| e.to_string())?; + + let latest = manifest.get("version").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let url = manifest + .get("url") + .and_then(|v| v.as_str()) + .unwrap_or("https://spaceshell.ru/download/spacesh.dmg") + .to_string(); + let has_update = !latest.is_empty() && parse_ver(&latest) > parse_ver(¤t); + + Ok(UpdateInfo { current, latest, has_update, url }) +} + +/// Open a URL in the default browser (macOS `open`). Used by the update popover's +/// download action — the GUI itself never streams the .dmg. +#[tauri::command] +pub fn open_external(url: String) -> Result<(), String> { + std::process::Command::new("open").arg(&url).spawn().map_err(|e| e.to_string())?; + Ok(()) +} + // ---- Settings commands ---- #[tauri::command] diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index cb7a412..39b6777 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -54,6 +54,8 @@ pub fn run() { bridge::mark_read, bridge::clear_events, bridge::health, + bridge::check_update, + bridge::open_external, bridge::get_config, bridge::set_config, bridge::shutdown_daemon, diff --git a/app/src/App.tsx b/app/src/App.tsx index 3a31821..663c00a 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -9,8 +9,8 @@ import { Settings } from "./Settings"; import { EventCenter } from "./EventCenter"; import { maybeNotify } from "./notify"; import { COLORS, applyTheme, resolvePalette } from "./theme"; -import { getStatusFull, applyPreset, onDaemonEvent, onDaemonRawEvent, setWorkspaceMeta, focusSurface, getEventLog, markEventsRead, clearEvents, getHealth, closeWorkspaceCmd, getConfig } from "./socketBridge"; -import type { EventRecord, DaemonHealth, ConfigView } from "./socketBridge"; +import { getStatusFull, applyPreset, onDaemonEvent, onDaemonRawEvent, setWorkspaceMeta, focusSurface, getEventLog, markEventsRead, clearEvents, getHealth, closeWorkspaceCmd, getConfig, checkUpdate } from "./socketBridge"; +import type { EventRecord, DaemonHealth, ConfigView, UpdateInfo } from "./socketBridge"; import { leafIds } from "./layoutTypes"; import type { Group, WorkspaceView, SurfaceState } from "./layoutTypes"; @@ -36,6 +36,8 @@ export function App() { const [eventsOpen, setEventsOpen] = useState(() => loadFlag("spacesh.eventsOpen", true)); const [sidebarOpen, setSidebarOpen] = useState(() => loadFlag("spacesh.sidebarOpen", true)); const [health, setHealth] = useState(null); + const [update, setUpdate] = useState(null); + const [updateChecking, setUpdateChecking] = useState(false); const [config, setConfigState] = useState(null); // Bumped when the daemon connection is re-established; used to remount the // layout so terminals re-attach (snapshot + live stream) to the restarted daemon. @@ -77,6 +79,13 @@ export function App() { catch { setConnected(false); } }, []); + const runUpdateCheck = useCallback(async () => { + setUpdateChecking(true); + try { setUpdate(await checkUpdate()); } + catch { /* offline / server unreachable — leave the last known result */ } + finally { setUpdateChecking(false); } + }, []); + const wsOf = (surfaceId: string): WorkspaceView | undefined => wsRef.current.find((w) => surfaceId in w.surfaces); @@ -142,6 +151,13 @@ export function App() { return () => window.removeEventListener("keydown", onKey); }, []); + // Update check: once on launch, then every 6h. + useEffect(() => { + void runUpdateCheck(); + const id = setInterval(() => { void runUpdateCheck(); }, 6 * 60 * 60 * 1000); + return () => clearInterval(id); + }, [runUpdateCheck]); + useEffect(() => { saveFlag("spacesh.eventsOpen", eventsOpen); }, [eventsOpen]); useEffect(() => { saveFlag("spacesh.sidebarOpen", sidebarOpen); }, [sidebarOpen]); @@ -162,7 +178,7 @@ export function App() { return (
- setEventsOpen((v) => !v)} onShowEvents={() => setEventsOpen(true)} sidebarOpen={sidebarOpen} onToggleSidebar={() => setSidebarOpen((v) => !v)} unread={unread} onOpenSettings={() => { if (config) setSettingsOpen(true); }} /> + setEventsOpen((v) => !v)} onShowEvents={() => setEventsOpen(true)} sidebarOpen={sidebarOpen} onToggleSidebar={() => setSidebarOpen((v) => !v)} unread={unread} onOpenSettings={() => { if (config) setSettingsOpen(true); }} update={update} updateChecking={updateChecking} onCheckUpdate={() => { void runUpdateCheck(); }} />
setWizard(true)} onDelete={setDeleteTarget} health={health} connected={connected} />
diff --git a/app/src/TopBar.tsx b/app/src/TopBar.tsx index 7424233..13c3c1c 100644 --- a/app/src/TopBar.tsx +++ b/app/src/TopBar.tsx @@ -1,7 +1,10 @@ -import { FolderGit2, PanelLeft, PanelRight, Search, Bell, Settings, ChevronDown } from "lucide-react"; +import { useState } from "react"; +import { FolderGit2, PanelLeft, PanelRight, Search, Bell, Settings, ChevronDown, RefreshCw, Download } from "lucide-react"; import { COLORS, FONT } from "./theme"; import type { WorkspaceView } from "./layoutTypes"; import { leafIds } from "./layoutTypes"; +import { openExternal } from "./socketBridge"; +import type { UpdateInfo } from "./socketBridge"; /** Human-readable descriptor of the active workspace layout (mock until a real preset id is tracked). */ function describeLayout(w: WorkspaceView | null): string { @@ -28,8 +31,83 @@ function IconBtn({ icon, onClick, active, title }: { icon: React.ReactNode; onCl ); } +/** Update-check button (lights up when the server has a newer version) plus its popover. */ +function UpdateControl({ update, checking, onCheck }: { update: UpdateInfo | null; checking: boolean; onCheck: () => void }) { + const [open, setOpen] = useState(false); + const hasUpdate = !!update?.has_update; + + return ( +
+ + + {open && ( + <> + {/* click-away backdrop */} +
setOpen(false)} style={{ position: "fixed", inset: 0, zIndex: 200 }} /> +
+
Обновление
+
+ Установлено{update?.current ?? "—"} +
+
+ На сервере{update?.latest || "—"} +
+ + {hasUpdate ? ( + + ) : ( +
Установлена последняя версия
+ )} + + +
+ + )} +
+ ); +} + export function TopBar({ active, eventsOpen, onToggleEvents, onShowEvents, sidebarOpen, onToggleSidebar, unread, onOpenSettings, + update, updateChecking, onCheckUpdate, }: { active: WorkspaceView | null; eventsOpen: boolean; @@ -39,6 +117,9 @@ export function TopBar({ onToggleSidebar: () => void; unread: number; onOpenSettings: () => void; + update: UpdateInfo | null; + updateChecking: boolean; + onCheckUpdate: () => void; }) { return (
+ + {/* Right cluster */}
} title="Search (mock)" /> +
} onClick={onShowEvents} active={eventsOpen} title="Open activity log" /> {unread > 0 && ( diff --git a/app/src/socketBridge.ts b/app/src/socketBridge.ts index c774e20..06612ed 100644 --- a/app/src/socketBridge.ts +++ b/app/src/socketBridge.ts @@ -189,6 +189,16 @@ export async function getHealth(): Promise { return await invoke("health"); } +export interface UpdateInfo { current: string; latest: string; has_update: boolean; url: string } + +export async function checkUpdate(): Promise { + return await invoke("check_update"); +} + +export async function openExternal(url: string): Promise { + await invoke("open_external", { url }); +} + export async function setZoom(workspaceId: string, surfaceId: string | null): Promise { await invoke("set_zoom", { workspaceId, surfaceId }); }