From c36b320e185707e2de05e58d4a71fe8507eed57f Mon Sep 17 00:00:00 2001 From: Zhicheng Han <43314240+hanzckernel@users.noreply.github.com> Date: Tue, 12 May 2026 03:03:07 +0200 Subject: [PATCH] fix: prompt reload for stale Web UI bundle (#641) --- .../src/components/layout/AppSidebar.vue | 7 +++ packages/client/src/i18n/locales/de.ts | 1 + packages/client/src/i18n/locales/en.ts | 1 + packages/client/src/i18n/locales/es.ts | 1 + packages/client/src/i18n/locales/fr.ts | 1 + packages/client/src/i18n/locales/ja.ts | 1 + packages/client/src/i18n/locales/ko.ts | 1 + packages/client/src/i18n/locales/pt.ts | 1 + packages/client/src/i18n/locales/zh-TW.ts | 1 + packages/client/src/i18n/locales/zh.ts | 1 + packages/client/src/stores/hermes/app.ts | 11 ++++ tests/client/app-store.test.ts | 32 +++++++++++ tests/client/sidebar-search.test.ts | 56 +++++++++++++++---- 13 files changed, 104 insertions(+), 11 deletions(-) diff --git a/packages/client/src/components/layout/AppSidebar.vue b/packages/client/src/components/layout/AppSidebar.vue index 48aafaf..041e72f 100644 --- a/packages/client/src/components/layout/AppSidebar.vue +++ b/packages/client/src/components/layout/AppSidebar.vue @@ -43,6 +43,10 @@ async function handleUpdate() { } } +function handleReloadClient() { + appStore.reloadClient(); +} + function handleLogout() { localStorage.clear(); router.replace({ name: 'login' }); @@ -296,6 +300,9 @@ function openChangelog() { Web UI v{{ appStore.serverVersion || "0.1.0" }} + + {{ t('sidebar.reloadClientVersion', { version: appStore.serverVersion }) }} + {{ appStore.updating ? t('sidebar.updating') : t('sidebar.updateVersion', { version: appStore.latestVersion }) }} diff --git a/packages/client/src/i18n/locales/de.ts b/packages/client/src/i18n/locales/de.ts index a4ad438..779993d 100644 --- a/packages/client/src/i18n/locales/de.ts +++ b/packages/client/src/i18n/locales/de.ts @@ -89,6 +89,7 @@ export default { disconnected: 'Getrennt', updateTip: 'Fuhren Sie "hermes-web-ui update" im Terminal aus, um zu aktualisieren', updateVersion: 'Aktualisieren auf v{version}', + reloadClientVersion: 'Für v{version} neu laden', updating: 'Aktualisierung...', updateSuccess: 'Aktualisierung abgeschlossen, bitte Server neu starten', updateFailed: 'Aktualisierung fehlgeschlagen', diff --git a/packages/client/src/i18n/locales/en.ts b/packages/client/src/i18n/locales/en.ts index 67b26e8..cf59dee 100644 --- a/packages/client/src/i18n/locales/en.ts +++ b/packages/client/src/i18n/locales/en.ts @@ -101,6 +101,7 @@ export default { expand: 'Expand menu', updateTip: 'Run "hermes-web-ui update" in terminal to update', updateVersion: 'Upgrade to v{version}', + reloadClientVersion: 'Reload for v{version}', updating: 'Updating...', updateSuccess: 'Update complete, please restart the server', updateFailed: 'Update failed', diff --git a/packages/client/src/i18n/locales/es.ts b/packages/client/src/i18n/locales/es.ts index e62011b..801d2db 100644 --- a/packages/client/src/i18n/locales/es.ts +++ b/packages/client/src/i18n/locales/es.ts @@ -89,6 +89,7 @@ export default { disconnected: 'Desconectado', updateTip: 'Ejecuta "hermes-web-ui update" en la terminal para actualizar', updateVersion: 'Actualizar a v{version}', + reloadClientVersion: 'Recargar para v{version}', updating: 'Actualizando...', updateSuccess: 'Actualizacion completa, por favor reinicia el servidor', updateFailed: 'Error al actualizar', diff --git a/packages/client/src/i18n/locales/fr.ts b/packages/client/src/i18n/locales/fr.ts index 2c5927b..be9c571 100644 --- a/packages/client/src/i18n/locales/fr.ts +++ b/packages/client/src/i18n/locales/fr.ts @@ -89,6 +89,7 @@ export default { disconnected: 'Deconnecte', updateTip: 'Executez "hermes-web-ui update" dans le terminal pour mettre a jour', updateVersion: 'Mettre a jour vers v{version}', + reloadClientVersion: 'Recharger pour v{version}', updating: 'Mise a jour...', updateSuccess: 'Mise a jour terminee, veuillez redemarrer le serveur', updateFailed: 'Echec de la mise a jour', diff --git a/packages/client/src/i18n/locales/ja.ts b/packages/client/src/i18n/locales/ja.ts index 92f8bd8..0f85513 100644 --- a/packages/client/src/i18n/locales/ja.ts +++ b/packages/client/src/i18n/locales/ja.ts @@ -89,6 +89,7 @@ export default { disconnected: '未接続', updateTip: 'ターミナルで "hermes-web-ui update" を実行して更新してください', updateVersion: 'v{version} にアップグレード', + reloadClientVersion: 'v{version} に再読み込み', updating: '更新中...', updateSuccess: '更新が完了しました。サーバーを再起動してください', updateFailed: '更新に失敗しました', diff --git a/packages/client/src/i18n/locales/ko.ts b/packages/client/src/i18n/locales/ko.ts index e891823..613f58e 100644 --- a/packages/client/src/i18n/locales/ko.ts +++ b/packages/client/src/i18n/locales/ko.ts @@ -89,6 +89,7 @@ export default { disconnected: '연결 끊김', updateTip: '터미널에서 "hermes-web-ui update"를 실행하여 업데이트하세요', updateVersion: 'v{version}(으)로 업그레이드', + reloadClientVersion: 'v{version}(으)로 새로고침', updating: '업데이트 중...', updateSuccess: '업데이트 완료, 서버를 재시작해 주세요', updateFailed: '업데이트 실패', diff --git a/packages/client/src/i18n/locales/pt.ts b/packages/client/src/i18n/locales/pt.ts index 4e32d7a..e53c502 100644 --- a/packages/client/src/i18n/locales/pt.ts +++ b/packages/client/src/i18n/locales/pt.ts @@ -89,6 +89,7 @@ export default { disconnected: 'Desconectado', updateTip: 'Execute "hermes-web-ui update" no terminal para atualizar', updateVersion: 'Atualizar para v{version}', + reloadClientVersion: 'Recarregar para v{version}', updating: 'Atualizando...', updateSuccess: 'Atualizacao concluida, por favor reinicie o servidor', updateFailed: 'Falha na atualizacao', diff --git a/packages/client/src/i18n/locales/zh-TW.ts b/packages/client/src/i18n/locales/zh-TW.ts index d5c48f4..08af624 100644 --- a/packages/client/src/i18n/locales/zh-TW.ts +++ b/packages/client/src/i18n/locales/zh-TW.ts @@ -101,6 +101,7 @@ export default { expand: '展開選單', updateTip: '在終端機執行 "hermes-web-ui update" 即可更新', updateVersion: '升級版本 v{version}', + reloadClientVersion: '重新整理到 v{version}', updating: '正在更新...', updateSuccess: '更新完成,請重新啟動服務', updateFailed: '更新失敗', diff --git a/packages/client/src/i18n/locales/zh.ts b/packages/client/src/i18n/locales/zh.ts index 4c32bf7..a992ef5 100644 --- a/packages/client/src/i18n/locales/zh.ts +++ b/packages/client/src/i18n/locales/zh.ts @@ -101,6 +101,7 @@ export default { expand: '展开菜单', updateTip: '在终端运行 "hermes-web-ui update" 即可更新', updateVersion: '升级版本 v{version}', + reloadClientVersion: '刷新到 v{version}', updating: '正在更新...', updateSuccess: '更新完成,请重启服务', updateFailed: '更新失败', diff --git a/packages/client/src/stores/hermes/app.ts b/packages/client/src/stores/hermes/app.ts index 38c3395..a4bd511 100644 --- a/packages/client/src/stores/hermes/app.ts +++ b/packages/client/src/stores/hermes/app.ts @@ -26,6 +26,7 @@ export const useAppStore = defineStore('app', () => { const serverVersion = ref(WEB_UI_VERSION) const latestVersion = ref('') const updateAvailable = ref(false) + const clientOutdated = ref(false) const updating = ref(false) const modelGroups = ref([]) const selectedModel = ref('') @@ -63,11 +64,13 @@ export const useAppStore = defineStore('app', () => { const res = await checkHealth() connected.value = res.status === 'ok' if (res.webui_version) serverVersion.value = res.webui_version + clientOutdated.value = !!res.webui_version && res.webui_version !== WEB_UI_VERSION if (res.webui_latest) latestVersion.value = res.webui_latest updateAvailable.value = !!res.webui_update_available if (res.node_version) nodeVersion.value = res.node_version } catch { connected.value = false + clientOutdated.value = false } } @@ -231,6 +234,12 @@ export const useAppStore = defineStore('app', () => { } } + function reloadClient() { + const url = new URL(window.location.href) + url.searchParams.set('__hwui_reload', Date.now().toString()) + window.location.replace(url.toString()) + } + function toggleSidebar() { sidebarOpen.value = !sidebarOpen.value } @@ -259,8 +268,10 @@ export const useAppStore = defineStore('app', () => { latestVersion, nodeVersion, updateAvailable, + clientOutdated, updating, doUpdate, + reloadClient, modelGroups, customModels, modelAliases, diff --git a/tests/client/app-store.test.ts b/tests/client/app-store.test.ts index fc43f9f..0dcd78a 100644 --- a/tests/client/app-store.test.ts +++ b/tests/client/app-store.test.ts @@ -152,6 +152,38 @@ describe('App Store', () => { expect(mockSystemApi.updateDefaultModel).not.toHaveBeenCalled() }) + it('marks the client stale when the served Web UI version changes', async () => { + mockSystemApi.checkHealth.mockResolvedValue({ + status: 'ok', + webui_version: '0.5.17', + webui_latest: '0.5.17', + webui_update_available: false, + }) + const store = useAppStore() + + await store.checkConnection() + + expect(store.connected).toBe(true) + expect(store.serverVersion).toBe('0.5.17') + expect(store.clientOutdated).toBe(true) + expect(store.updateAvailable).toBe(false) + }) + + it('does not mark the client stale when the served Web UI version matches this bundle', async () => { + mockSystemApi.checkHealth.mockResolvedValue({ + status: 'ok', + webui_version: 'test', + webui_latest: 'test', + webui_update_available: false, + }) + const store = useAppStore() + + await store.checkConnection() + + expect(store.serverVersion).toBe('test') + expect(store.clientOutdated).toBe(false) + }) + it('clears the updating state and reports failure when self-update request fails', async () => { const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) mockSystemApi.triggerUpdate.mockRejectedValue(new Error('install failed')) diff --git a/tests/client/sidebar-search.test.ts b/tests/client/sidebar-search.test.ts index 0e3a55e..6a12fe1 100644 --- a/tests/client/sidebar-search.test.ts +++ b/tests/client/sidebar-search.test.ts @@ -3,6 +3,21 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { mount } from '@vue/test-utils' const openSessionSearchMock = vi.hoisted(() => vi.fn()) +const mockAppStore = vi.hoisted(() => ({ + sidebarOpen: true, + sidebarCollapsed: false, + connected: true, + serverVersion: 'test', + latestVersion: '', + updateAvailable: false, + clientOutdated: false, + updating: false, + toggleSidebar: vi.fn(), + toggleSidebarCollapsed: vi.fn(), + closeSidebar: vi.fn(), + doUpdate: vi.fn(), + reloadClient: vi.fn(), +})) vi.mock('@/composables/useSessionSearch', () => ({ useSessionSearch: () => ({ @@ -11,16 +26,7 @@ vi.mock('@/composables/useSessionSearch', () => ({ })) vi.mock('@/stores/hermes/app', () => ({ - useAppStore: () => ({ - sidebarOpen: true, - connected: true, - serverVersion: 'test', - updateAvailable: false, - updating: false, - toggleSidebar: vi.fn(), - closeSidebar: vi.fn(), - doUpdate: vi.fn(), - }), + useAppStore: () => mockAppStore, })) vi.mock('vue-router', async (importOriginal) => { @@ -55,7 +61,7 @@ vi.mock('naive-ui', async () => { error: vi.fn(), }), NButton: { - template: '', + template: '', }, NSelect: { template: '
', @@ -68,6 +74,12 @@ import AppSidebar from '@/components/layout/AppSidebar.vue' describe('AppSidebar search entry', () => { beforeEach(() => { openSessionSearchMock.mockClear() + mockAppStore.serverVersion = 'test' + mockAppStore.latestVersion = '' + mockAppStore.updateAvailable = false + mockAppStore.clientOutdated = false + mockAppStore.updating = false + mockAppStore.reloadClient.mockClear() }) it('opens the session search modal from the sidebar button', async () => { @@ -90,4 +102,26 @@ describe('AppSidebar search entry', () => { await searchButton!.trigger('click') expect(openSessionSearchMock).toHaveBeenCalledTimes(1) }) + + it('offers a client reload when the server version differs from the loaded bundle', async () => { + mockAppStore.clientOutdated = true + mockAppStore.serverVersion = '0.5.17' + const wrapper = mount(AppSidebar, { + global: { + stubs: { + ProfileSelector: true, + ModelSelector: true, + LanguageSwitch: true, + ThemeSwitch: true, + }, + }, + }) + + const reloadButton = wrapper.findAll('button') + .find(node => node.text().includes('sidebar.reloadClientVersion')) + expect(reloadButton).toBeTruthy() + + await reloadButton!.trigger('click') + expect(mockAppStore.reloadClient).toHaveBeenCalledTimes(1) + }) })