frontend: polish app launch and release experience
This commit is contained in:
@@ -41,11 +41,30 @@ import { useAppStore } from '@/stores/app'
|
||||
|
||||
const store = useAppStore()
|
||||
|
||||
const HEALTH_NOTIFICATION_MAX_AGE_MS = 30 * 60 * 1000
|
||||
const GENERIC_NOTIFICATION_MAX_AGE_MS = 10 * 60 * 1000
|
||||
|
||||
const dismissedNotifications = ref<Set<string>>(new Set())
|
||||
|
||||
const healthNotifications = computed(() => {
|
||||
const notifs = store.data?.notifications ?? []
|
||||
const visible = notifs.filter(n => !dismissedNotifications.value.has(n.id))
|
||||
const packages = store.data?.['package-data'] ?? {}
|
||||
const visible = notifs.filter((n) => {
|
||||
if (dismissedNotifications.value.has(n.id)) return false
|
||||
|
||||
const appId = n.app_id || appIdFromNotificationTitle(n.title)
|
||||
if (appId) {
|
||||
if (isOlderThan(n.timestamp, HEALTH_NOTIFICATION_MAX_AGE_MS)) return false
|
||||
const pkg = packages[appId]
|
||||
if (!pkg) return false
|
||||
if (pkg.health !== 'unhealthy') return false
|
||||
if (pkg.state === 'removing' || pkg.state === 'stopped' || pkg.state === 'exited') return false
|
||||
} else if (isOlderThan(n.timestamp, GENERIC_NOTIFICATION_MAX_AGE_MS)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
// Deduplicate: keep only the latest notification per container/title
|
||||
const seen = new Map<string, typeof visible[0]>()
|
||||
for (const n of visible) {
|
||||
@@ -64,4 +83,14 @@ function dismissNotification(id: string) {
|
||||
}
|
||||
dismissedNotifications.value.add(id)
|
||||
}
|
||||
|
||||
function appIdFromNotificationTitle(title: string): string | undefined {
|
||||
const suffix = ' is unhealthy'
|
||||
return title.endsWith(suffix) ? title.slice(0, -suffix.length) : undefined
|
||||
}
|
||||
|
||||
function isOlderThan(timestamp: string, maxAgeMs: number): boolean {
|
||||
const ts = Date.parse(timestamp)
|
||||
return Number.isFinite(ts) && Date.now() - ts > maxAgeMs
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { PackageState, type DataModel, type PackageDataEntry } from '@/types/api'
|
||||
import HealthNotifications from '../HealthNotifications.vue'
|
||||
|
||||
function makePkg(id: string, state: PackageState = PackageState.Running, health: string | null = 'healthy'): PackageDataEntry {
|
||||
return {
|
||||
state,
|
||||
health,
|
||||
manifest: {
|
||||
id,
|
||||
title: id,
|
||||
version: '1.0.0',
|
||||
description: { short: '', long: '' },
|
||||
'release-notes': '',
|
||||
license: '',
|
||||
'wrapper-repo': '',
|
||||
'upstream-repo': '',
|
||||
'support-site': '',
|
||||
'marketing-site': '',
|
||||
'donation-url': null,
|
||||
interfaces: { main: { ui: true } },
|
||||
} as unknown as PackageDataEntry['manifest'],
|
||||
}
|
||||
}
|
||||
|
||||
function makeData(pkg?: PackageDataEntry, timestamp = new Date().toISOString()): DataModel {
|
||||
return {
|
||||
'server-info': {
|
||||
id: 'node',
|
||||
version: '1.0.0',
|
||||
name: null,
|
||||
pubkey: '',
|
||||
'status-info': {
|
||||
restarting: false,
|
||||
'shutting-down': false,
|
||||
updated: false,
|
||||
'backup-progress': null,
|
||||
'update-progress': null,
|
||||
},
|
||||
'lan-address': null,
|
||||
'tor-address': null,
|
||||
unread: 0,
|
||||
'wifi-ssids': [],
|
||||
'zram-enabled': false,
|
||||
'seed-backed': false,
|
||||
},
|
||||
'package-data': pkg ? { indeedhub: pkg } : {},
|
||||
notifications: [{
|
||||
id: 'health-1',
|
||||
level: 'error',
|
||||
title: 'indeedhub is unhealthy',
|
||||
message: 'indeedhub health check failed',
|
||||
timestamp,
|
||||
app_id: 'indeedhub',
|
||||
}],
|
||||
ui: {
|
||||
name: null,
|
||||
'ack-welcome': '',
|
||||
marketplace: {
|
||||
'selected-hosts': [],
|
||||
'known-hosts': {},
|
||||
},
|
||||
theme: 'dark',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('HealthNotifications', () => {
|
||||
let pinia: ReturnType<typeof createPinia>
|
||||
|
||||
beforeEach(() => {
|
||||
pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('shows active unhealthy package notifications', () => {
|
||||
const store = useAppStore(pinia)
|
||||
store.data = makeData(makePkg('indeedhub', PackageState.Running, 'unhealthy'))
|
||||
|
||||
const wrapper = mount(HealthNotifications, {
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('indeedhub is unhealthy')
|
||||
})
|
||||
|
||||
it('hides stale package notifications once health recovers', () => {
|
||||
const store = useAppStore(pinia)
|
||||
store.data = makeData(makePkg('indeedhub', PackageState.Running, 'healthy'))
|
||||
|
||||
const wrapper = mount(HealthNotifications, {
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.text()).not.toContain('indeedhub is unhealthy')
|
||||
})
|
||||
|
||||
it('hides package notifications while an app is being removed', () => {
|
||||
const store = useAppStore(pinia)
|
||||
store.data = makeData(makePkg('indeedhub', PackageState.Removing, 'unhealthy'))
|
||||
|
||||
const wrapper = mount(HealthNotifications, {
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.text()).not.toContain('indeedhub is unhealthy')
|
||||
})
|
||||
|
||||
it('hides old package health notifications on reload even if the app is still unhealthy', () => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date('2026-06-10T12:00:00Z'))
|
||||
|
||||
const store = useAppStore(pinia)
|
||||
store.data = makeData(
|
||||
makePkg('indeedhub', PackageState.Running, 'unhealthy'),
|
||||
new Date('2026-06-10T11:20:00Z').toISOString(),
|
||||
)
|
||||
|
||||
const wrapper = mount(HealthNotifications, {
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.text()).not.toContain('indeedhub is unhealthy')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user