chore(release): stage v1.7.54-alpha
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(() => [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user