fix: prompt reload for stale Web UI bundle (#641)
This commit is contained in:
@@ -43,6 +43,10 @@ async function handleUpdate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleReloadClient() {
|
||||||
|
appStore.reloadClient();
|
||||||
|
}
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
router.replace({ name: 'login' });
|
router.replace({ name: 'login' });
|
||||||
@@ -296,6 +300,9 @@ function openChangelog() {
|
|||||||
<span class="version-text" @click="openChangelog">Web UI v{{ appStore.serverVersion || "0.1.0" }}</span>
|
<span class="version-text" @click="openChangelog">Web UI v{{ appStore.serverVersion || "0.1.0" }}</span>
|
||||||
<ThemeSwitch />
|
<ThemeSwitch />
|
||||||
</div>
|
</div>
|
||||||
|
<NButton v-if="appStore.clientOutdated" type="warning" size="tiny" block class="update-btn" @click="handleReloadClient">
|
||||||
|
{{ t('sidebar.reloadClientVersion', { version: appStore.serverVersion }) }}
|
||||||
|
</NButton>
|
||||||
<NButton v-if="appStore.updateAvailable" type="primary" size="tiny" block class="update-btn" :loading="appStore.updating" @click="handleUpdate">
|
<NButton v-if="appStore.updateAvailable" type="primary" size="tiny" block class="update-btn" :loading="appStore.updating" @click="handleUpdate">
|
||||||
{{ appStore.updating ? t('sidebar.updating') : t('sidebar.updateVersion', { version: appStore.latestVersion }) }}
|
{{ appStore.updating ? t('sidebar.updating') : t('sidebar.updateVersion', { version: appStore.latestVersion }) }}
|
||||||
</NButton>
|
</NButton>
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export default {
|
|||||||
disconnected: 'Getrennt',
|
disconnected: 'Getrennt',
|
||||||
updateTip: 'Fuhren Sie "hermes-web-ui update" im Terminal aus, um zu aktualisieren',
|
updateTip: 'Fuhren Sie "hermes-web-ui update" im Terminal aus, um zu aktualisieren',
|
||||||
updateVersion: 'Aktualisieren auf v{version}',
|
updateVersion: 'Aktualisieren auf v{version}',
|
||||||
|
reloadClientVersion: 'Für v{version} neu laden',
|
||||||
updating: 'Aktualisierung...',
|
updating: 'Aktualisierung...',
|
||||||
updateSuccess: 'Aktualisierung abgeschlossen, bitte Server neu starten',
|
updateSuccess: 'Aktualisierung abgeschlossen, bitte Server neu starten',
|
||||||
updateFailed: 'Aktualisierung fehlgeschlagen',
|
updateFailed: 'Aktualisierung fehlgeschlagen',
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export default {
|
|||||||
expand: 'Expand menu',
|
expand: 'Expand menu',
|
||||||
updateTip: 'Run "hermes-web-ui update" in terminal to update',
|
updateTip: 'Run "hermes-web-ui update" in terminal to update',
|
||||||
updateVersion: 'Upgrade to v{version}',
|
updateVersion: 'Upgrade to v{version}',
|
||||||
|
reloadClientVersion: 'Reload for v{version}',
|
||||||
updating: 'Updating...',
|
updating: 'Updating...',
|
||||||
updateSuccess: 'Update complete, please restart the server',
|
updateSuccess: 'Update complete, please restart the server',
|
||||||
updateFailed: 'Update failed',
|
updateFailed: 'Update failed',
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export default {
|
|||||||
disconnected: 'Desconectado',
|
disconnected: 'Desconectado',
|
||||||
updateTip: 'Ejecuta "hermes-web-ui update" en la terminal para actualizar',
|
updateTip: 'Ejecuta "hermes-web-ui update" en la terminal para actualizar',
|
||||||
updateVersion: 'Actualizar a v{version}',
|
updateVersion: 'Actualizar a v{version}',
|
||||||
|
reloadClientVersion: 'Recargar para v{version}',
|
||||||
updating: 'Actualizando...',
|
updating: 'Actualizando...',
|
||||||
updateSuccess: 'Actualizacion completa, por favor reinicia el servidor',
|
updateSuccess: 'Actualizacion completa, por favor reinicia el servidor',
|
||||||
updateFailed: 'Error al actualizar',
|
updateFailed: 'Error al actualizar',
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export default {
|
|||||||
disconnected: 'Deconnecte',
|
disconnected: 'Deconnecte',
|
||||||
updateTip: 'Executez "hermes-web-ui update" dans le terminal pour mettre a jour',
|
updateTip: 'Executez "hermes-web-ui update" dans le terminal pour mettre a jour',
|
||||||
updateVersion: 'Mettre a jour vers v{version}',
|
updateVersion: 'Mettre a jour vers v{version}',
|
||||||
|
reloadClientVersion: 'Recharger pour v{version}',
|
||||||
updating: 'Mise a jour...',
|
updating: 'Mise a jour...',
|
||||||
updateSuccess: 'Mise a jour terminee, veuillez redemarrer le serveur',
|
updateSuccess: 'Mise a jour terminee, veuillez redemarrer le serveur',
|
||||||
updateFailed: 'Echec de la mise a jour',
|
updateFailed: 'Echec de la mise a jour',
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export default {
|
|||||||
disconnected: '未接続',
|
disconnected: '未接続',
|
||||||
updateTip: 'ターミナルで "hermes-web-ui update" を実行して更新してください',
|
updateTip: 'ターミナルで "hermes-web-ui update" を実行して更新してください',
|
||||||
updateVersion: 'v{version} にアップグレード',
|
updateVersion: 'v{version} にアップグレード',
|
||||||
|
reloadClientVersion: 'v{version} に再読み込み',
|
||||||
updating: '更新中...',
|
updating: '更新中...',
|
||||||
updateSuccess: '更新が完了しました。サーバーを再起動してください',
|
updateSuccess: '更新が完了しました。サーバーを再起動してください',
|
||||||
updateFailed: '更新に失敗しました',
|
updateFailed: '更新に失敗しました',
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export default {
|
|||||||
disconnected: '연결 끊김',
|
disconnected: '연결 끊김',
|
||||||
updateTip: '터미널에서 "hermes-web-ui update"를 실행하여 업데이트하세요',
|
updateTip: '터미널에서 "hermes-web-ui update"를 실행하여 업데이트하세요',
|
||||||
updateVersion: 'v{version}(으)로 업그레이드',
|
updateVersion: 'v{version}(으)로 업그레이드',
|
||||||
|
reloadClientVersion: 'v{version}(으)로 새로고침',
|
||||||
updating: '업데이트 중...',
|
updating: '업데이트 중...',
|
||||||
updateSuccess: '업데이트 완료, 서버를 재시작해 주세요',
|
updateSuccess: '업데이트 완료, 서버를 재시작해 주세요',
|
||||||
updateFailed: '업데이트 실패',
|
updateFailed: '업데이트 실패',
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export default {
|
|||||||
disconnected: 'Desconectado',
|
disconnected: 'Desconectado',
|
||||||
updateTip: 'Execute "hermes-web-ui update" no terminal para atualizar',
|
updateTip: 'Execute "hermes-web-ui update" no terminal para atualizar',
|
||||||
updateVersion: 'Atualizar para v{version}',
|
updateVersion: 'Atualizar para v{version}',
|
||||||
|
reloadClientVersion: 'Recarregar para v{version}',
|
||||||
updating: 'Atualizando...',
|
updating: 'Atualizando...',
|
||||||
updateSuccess: 'Atualizacao concluida, por favor reinicie o servidor',
|
updateSuccess: 'Atualizacao concluida, por favor reinicie o servidor',
|
||||||
updateFailed: 'Falha na atualizacao',
|
updateFailed: 'Falha na atualizacao',
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export default {
|
|||||||
expand: '展開選單',
|
expand: '展開選單',
|
||||||
updateTip: '在終端機執行 "hermes-web-ui update" 即可更新',
|
updateTip: '在終端機執行 "hermes-web-ui update" 即可更新',
|
||||||
updateVersion: '升級版本 v{version}',
|
updateVersion: '升級版本 v{version}',
|
||||||
|
reloadClientVersion: '重新整理到 v{version}',
|
||||||
updating: '正在更新...',
|
updating: '正在更新...',
|
||||||
updateSuccess: '更新完成,請重新啟動服務',
|
updateSuccess: '更新完成,請重新啟動服務',
|
||||||
updateFailed: '更新失敗',
|
updateFailed: '更新失敗',
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export default {
|
|||||||
expand: '展开菜单',
|
expand: '展开菜单',
|
||||||
updateTip: '在终端运行 "hermes-web-ui update" 即可更新',
|
updateTip: '在终端运行 "hermes-web-ui update" 即可更新',
|
||||||
updateVersion: '升级版本 v{version}',
|
updateVersion: '升级版本 v{version}',
|
||||||
|
reloadClientVersion: '刷新到 v{version}',
|
||||||
updating: '正在更新...',
|
updating: '正在更新...',
|
||||||
updateSuccess: '更新完成,请重启服务',
|
updateSuccess: '更新完成,请重启服务',
|
||||||
updateFailed: '更新失败',
|
updateFailed: '更新失败',
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
const serverVersion = ref(WEB_UI_VERSION)
|
const serverVersion = ref(WEB_UI_VERSION)
|
||||||
const latestVersion = ref('')
|
const latestVersion = ref('')
|
||||||
const updateAvailable = ref(false)
|
const updateAvailable = ref(false)
|
||||||
|
const clientOutdated = ref(false)
|
||||||
const updating = ref(false)
|
const updating = ref(false)
|
||||||
const modelGroups = ref<AvailableModelGroup[]>([])
|
const modelGroups = ref<AvailableModelGroup[]>([])
|
||||||
const selectedModel = ref('')
|
const selectedModel = ref('')
|
||||||
@@ -63,11 +64,13 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
const res = await checkHealth()
|
const res = await checkHealth()
|
||||||
connected.value = res.status === 'ok'
|
connected.value = res.status === 'ok'
|
||||||
if (res.webui_version) serverVersion.value = res.webui_version
|
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
|
if (res.webui_latest) latestVersion.value = res.webui_latest
|
||||||
updateAvailable.value = !!res.webui_update_available
|
updateAvailable.value = !!res.webui_update_available
|
||||||
if (res.node_version) nodeVersion.value = res.node_version
|
if (res.node_version) nodeVersion.value = res.node_version
|
||||||
} catch {
|
} catch {
|
||||||
connected.value = false
|
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() {
|
function toggleSidebar() {
|
||||||
sidebarOpen.value = !sidebarOpen.value
|
sidebarOpen.value = !sidebarOpen.value
|
||||||
}
|
}
|
||||||
@@ -259,8 +268,10 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
latestVersion,
|
latestVersion,
|
||||||
nodeVersion,
|
nodeVersion,
|
||||||
updateAvailable,
|
updateAvailable,
|
||||||
|
clientOutdated,
|
||||||
updating,
|
updating,
|
||||||
doUpdate,
|
doUpdate,
|
||||||
|
reloadClient,
|
||||||
modelGroups,
|
modelGroups,
|
||||||
customModels,
|
customModels,
|
||||||
modelAliases,
|
modelAliases,
|
||||||
|
|||||||
@@ -152,6 +152,38 @@ describe('App Store', () => {
|
|||||||
expect(mockSystemApi.updateDefaultModel).not.toHaveBeenCalled()
|
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 () => {
|
it('clears the updating state and reports failure when self-update request fails', async () => {
|
||||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||||
mockSystemApi.triggerUpdate.mockRejectedValue(new Error('install failed'))
|
mockSystemApi.triggerUpdate.mockRejectedValue(new Error('install failed'))
|
||||||
|
|||||||
@@ -3,6 +3,21 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
|
|
||||||
const openSessionSearchMock = vi.hoisted(() => vi.fn())
|
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', () => ({
|
vi.mock('@/composables/useSessionSearch', () => ({
|
||||||
useSessionSearch: () => ({
|
useSessionSearch: () => ({
|
||||||
@@ -11,16 +26,7 @@ vi.mock('@/composables/useSessionSearch', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/stores/hermes/app', () => ({
|
vi.mock('@/stores/hermes/app', () => ({
|
||||||
useAppStore: () => ({
|
useAppStore: () => mockAppStore,
|
||||||
sidebarOpen: true,
|
|
||||||
connected: true,
|
|
||||||
serverVersion: 'test',
|
|
||||||
updateAvailable: false,
|
|
||||||
updating: false,
|
|
||||||
toggleSidebar: vi.fn(),
|
|
||||||
closeSidebar: vi.fn(),
|
|
||||||
doUpdate: vi.fn(),
|
|
||||||
}),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('vue-router', async (importOriginal) => {
|
vi.mock('vue-router', async (importOriginal) => {
|
||||||
@@ -55,7 +61,7 @@ vi.mock('naive-ui', async () => {
|
|||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
}),
|
}),
|
||||||
NButton: {
|
NButton: {
|
||||||
template: '<button><slot /></button>',
|
template: '<button v-bind="$attrs"><slot /></button>',
|
||||||
},
|
},
|
||||||
NSelect: {
|
NSelect: {
|
||||||
template: '<div />',
|
template: '<div />',
|
||||||
@@ -68,6 +74,12 @@ import AppSidebar from '@/components/layout/AppSidebar.vue'
|
|||||||
describe('AppSidebar search entry', () => {
|
describe('AppSidebar search entry', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
openSessionSearchMock.mockClear()
|
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 () => {
|
it('opens the session search modal from the sidebar button', async () => {
|
||||||
@@ -90,4 +102,26 @@ describe('AppSidebar search entry', () => {
|
|||||||
await searchButton!.trigger('click')
|
await searchButton!.trigger('click')
|
||||||
expect(openSessionSearchMock).toHaveBeenCalledTimes(1)
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user