frontend: polish app launch and release experience

This commit is contained in:
archipelago
2026-06-11 00:24:40 -04:00
parent c393b96da3
commit 1a3d726eac
140 changed files with 5930 additions and 920 deletions
@@ -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')
})
})