chore(release): stage v1.7.54-alpha

This commit is contained in:
archipelago
2026-05-06 09:23:57 -04:00
parent 1a0d8a432c
commit c0751e2551
30 changed files with 1871 additions and 102 deletions

View File

@@ -14,7 +14,7 @@ export const useAppStore = defineStore('app', () => {
// Writable refs — delegate reads and writes to the sub-stores
const { isAuthenticated, isLoading, error } = storeToRefs(auth)
const { data, isConnected, isReconnecting } = storeToRefs(sync)
const { data, isConnected, isReconnecting, hasLoadedInitialData } = storeToRefs(sync)
// Read-only computed — delegate to sub-stores
const { serverInfo, packages, peerHealth, uiData } = storeToRefs(sync)
@@ -30,6 +30,7 @@ export const useAppStore = defineStore('app', () => {
data,
isConnected,
isReconnecting,
hasLoadedInitialData,
// Sync computed (read-only)
serverInfo,

View File

@@ -11,6 +11,7 @@ export const useSyncStore = defineStore('sync', () => {
const data = ref<DataModel | null>(null)
const isConnected = ref(false)
const isReconnecting = ref(false)
const hasLoadedInitialData = ref(false)
let isWsSubscribed = false
let isWsConnecting = false
@@ -47,12 +48,14 @@ export const useSyncStore = defineStore('sync', () => {
if (update?.type === 'initial' && update?.data) {
if (import.meta.env.DEV) console.log('[Store] Received initial data from mock backend')
data.value = update.data
hasLoadedInitialData.value = true
isConnected.value = true
isReconnecting.value = false
}
// Handle real backend format: {rev: 0, data: {...}}
else if (update?.data && update?.rev !== undefined) {
data.value = update.data
hasLoadedInitialData.value = true
isConnected.value = true
isReconnecting.value = false
}
@@ -90,6 +93,7 @@ export const useSyncStore = defineStore('sync', () => {
const freshState = await rpcClient.call<{ data: DataModel }>({ method: 'server.get-state' })
if (freshState?.data) {
data.value = freshState.data
hasLoadedInitialData.value = true
}
} catch {
// Non-fatal: WebSocket patches will still work
@@ -149,11 +153,13 @@ export const useSyncStore = defineStore('sync', () => {
theme: 'dark',
},
}
hasLoadedInitialData.value = false
}
/** Reset sync state on logout — called by auth store */
function resetOnLogout(): void {
data.value = null
hasLoadedInitialData.value = false
isWsSubscribed = false
wsClient.disconnect()
isConnected.value = false
@@ -165,6 +171,7 @@ export const useSyncStore = defineStore('sync', () => {
data,
isConnected,
isReconnecting,
hasLoadedInitialData,
// Computed
serverInfo,

View File

@@ -267,8 +267,7 @@ const canLaunch = computed(() => {
if (!pkg.value) return false
if (isWebOnly.value) return true
const hasUI = !!(pkg.value.manifest.interfaces?.main?.ui || pkg.value.installed?.['interface-addresses']?.main)
const isRunning = pkg.value.state === 'running'
return hasUI && isRunning
return hasUI && pkg.value.state === 'running' && pkg.value.health !== 'starting' && pkg.value.health !== 'unhealthy'
})
const features = computed(() => [

View File

@@ -40,19 +40,14 @@
</div>
<!-- Loading Skeleton -->
<div v-if="!store.isConnected && sortedPackageEntries.length === 0 && !connectionError" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 pb-6">
<div v-for="i in 3" :key="i" class="glass-card p-6 animate-pulse">
<div class="flex items-start gap-4">
<div class="w-16 h-16 rounded-lg bg-white/10"></div>
<div class="flex-1">
<div class="h-5 w-32 bg-white/10 rounded mb-2"></div>
<div class="h-4 w-48 bg-white/5 rounded mb-3"></div>
<div class="h-6 w-20 bg-white/5 rounded"></div>
</div>
</div>
<div class="mt-4 flex gap-2">
<div class="flex-1 h-9 bg-white/5 rounded-lg"></div>
</div>
<div v-if="isLoadingApps" class="text-center py-16 pb-6">
<div class="glass-card p-8 max-w-md mx-auto">
<svg class="animate-spin h-8 w-8 mx-auto mb-4 text-white/70" viewBox="0 0 24 24" fill="none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<h3 class="text-lg font-semibold text-white mb-2">Loading apps</h3>
<p class="text-white/60 text-sm">Checking the latest app status before showing launch controls.</p>
</div>
</div>
@@ -222,6 +217,8 @@ const packages = computed(() => {
const categoriesWithApps = useCategoriesWithApps(packages, ALL_CATEGORIES)
const isLoadingApps = computed(() => !store.hasLoadedInitialData && !connectionError.value)
// Connection error state
const connectionError = ref('')
let connectionTimer: ReturnType<typeof setTimeout> | undefined
@@ -230,7 +227,7 @@ onMounted(() => {
appsAnimationDone = true
if (!store.isConnected) {
connectionTimer = setTimeout(() => {
if (!store.isConnected && sortedPackageEntries.value.length === 0) {
if (!store.hasLoadedInitialData && sortedPackageEntries.value.length === 0) {
connectionError.value = 'Unable to connect to server. Check that the backend is running.'
}
}, 15000)

View File

@@ -34,7 +34,7 @@
<template v-if="mustOpenNewTab">{{ appTitle }} sets security headers that prevent iframe embedding.<br>Open it in a new browser tab instead.</template>
<template v-else>{{ appTitle }} may still be starting up or the container is stopped.<br><span v-if="autoRetryCount > 0" class="text-yellow-400/70">Retrying automatically ({{ autoRetryCount }})...</span></template>
</p>
<div class="flex items-center gap-3">
<div class="flex flex-wrap items-center justify-center gap-3">
<button
v-if="!mustOpenNewTab"
@click="$emit('refresh')"

View File

@@ -145,8 +145,7 @@ export function resolveAppIcon(id: string, pkg: PackageDataEntry, curatedIcon?:
export function canLaunch(pkg: PackageDataEntry): boolean {
if (isWebOnlyApp(pkg.manifest.id)) return true
const hasUI = pkg.manifest.interfaces?.main?.ui || pkg.installed?.['interface-addresses']?.main
const canLaunchState = pkg.state === 'running' || pkg.state === 'starting'
return !!hasUI && canLaunchState
return !!hasUI && pkg.state === 'running' && pkg.health !== 'starting' && pkg.health !== 'unhealthy'
}
export function getStatusClass(state: PackageState, health?: string | null, exitCode?: number | null): string {