Merge branch 'iso-feedback-fixes-2026-07-02' into merge-iso-feedback
# Conflicts: # core/archipelago/src/api/rpc/middleware.rs
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { useContainersScanTimeout } from '../useContainersScanTimeout'
|
||||
|
||||
describe('useContainersScanTimeout', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('reflects the real scanned flag when it arrives before the timeout', async () => {
|
||||
const scanned = ref(false)
|
||||
const loaded = ref(true)
|
||||
const { effectiveContainersScanned, scanTimedOut } = useContainersScanTimeout(scanned, loaded, 20_000)
|
||||
|
||||
expect(effectiveContainersScanned.value).toBe(false)
|
||||
scanned.value = true
|
||||
await nextTick()
|
||||
expect(effectiveContainersScanned.value).toBe(true)
|
||||
expect(scanTimedOut.value).toBe(false)
|
||||
})
|
||||
|
||||
it('does not start the timeout until initial data has loaded', async () => {
|
||||
const scanned = ref(false)
|
||||
const loaded = ref(false)
|
||||
const { effectiveContainersScanned } = useContainersScanTimeout(scanned, loaded, 20_000)
|
||||
|
||||
vi.advanceTimersByTime(60_000)
|
||||
expect(effectiveContainersScanned.value).toBe(false)
|
||||
|
||||
loaded.value = true
|
||||
await nextTick()
|
||||
vi.advanceTimersByTime(20_000)
|
||||
expect(effectiveContainersScanned.value).toBe(true)
|
||||
})
|
||||
|
||||
it('falls through after the timeout even if the flag never arrives', async () => {
|
||||
const scanned = ref(false)
|
||||
const loaded = ref(true)
|
||||
const { effectiveContainersScanned, scanTimedOut } = useContainersScanTimeout(scanned, loaded, 20_000)
|
||||
|
||||
vi.advanceTimersByTime(19_999)
|
||||
expect(effectiveContainersScanned.value).toBe(false)
|
||||
vi.advanceTimersByTime(1)
|
||||
expect(effectiveContainersScanned.value).toBe(true)
|
||||
expect(scanTimedOut.value).toBe(true)
|
||||
})
|
||||
|
||||
it('cancels the escape hatch when the real flag arrives', async () => {
|
||||
const scanned = ref(false)
|
||||
const loaded = ref(true)
|
||||
const { scanTimedOut } = useContainersScanTimeout(scanned, loaded, 20_000)
|
||||
|
||||
vi.advanceTimersByTime(10_000)
|
||||
scanned.value = true
|
||||
await nextTick()
|
||||
vi.advanceTimersByTime(60_000)
|
||||
expect(scanTimedOut.value).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
// Escape hatch for the "Checking containers…" / "Checking..." states.
|
||||
//
|
||||
// If the server never flips `containers-scanned` to true (e.g. the UI missed
|
||||
// a websocket broadcast), views gating on it would spin forever. This starts
|
||||
// a timeout once initial data has loaded and, if the scan flag is still false
|
||||
// when it fires, treats the scan as complete so the UI falls through to its
|
||||
// real empty/install states. A periodic store-level resync exists too — this
|
||||
// is the belt-and-suspenders guarantee that the spinner is always bounded.
|
||||
|
||||
import { computed, getCurrentInstance, onBeforeUnmount, ref, watch, type Ref } from 'vue'
|
||||
|
||||
const DEFAULT_SCAN_TIMEOUT_MS = 20_000
|
||||
|
||||
export function useContainersScanTimeout(
|
||||
containersScanned: Ref<boolean>,
|
||||
hasLoadedInitialData: Ref<boolean>,
|
||||
timeoutMs: number = DEFAULT_SCAN_TIMEOUT_MS,
|
||||
) {
|
||||
const scanTimedOut = ref(false)
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
function clearTimer(): void {
|
||||
if (timer !== undefined) {
|
||||
clearTimeout(timer)
|
||||
timer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
[containersScanned, hasLoadedInitialData],
|
||||
([scanned, loaded]) => {
|
||||
if (scanned) {
|
||||
// Real signal arrived — cancel the escape hatch.
|
||||
clearTimer()
|
||||
scanTimedOut.value = false
|
||||
return
|
||||
}
|
||||
if (loaded && timer === undefined && !scanTimedOut.value) {
|
||||
timer = setTimeout(() => {
|
||||
scanTimedOut.value = true
|
||||
timer = undefined
|
||||
}, timeoutMs)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
if (getCurrentInstance()) onBeforeUnmount(clearTimer)
|
||||
|
||||
/** True once the server reports the scan done OR the timeout has elapsed. */
|
||||
const effectiveContainersScanned = computed(
|
||||
() => containersScanned.value || scanTimedOut.value,
|
||||
)
|
||||
|
||||
return { effectiveContainersScanned, scanTimedOut }
|
||||
}
|
||||
@@ -96,6 +96,7 @@
|
||||
"serverStarting": "Server starting up...",
|
||||
"replayIntro": "Replay Intro",
|
||||
"onboarding": "Onboarding",
|
||||
"restartConfirm": "Are you sure? This wipes onboarding progress — click again to confirm",
|
||||
"resetting": "Resetting...",
|
||||
"recoveryNote": "Password recovery requires SSH access to the server.",
|
||||
"errorMinLength": "Password must be at least 8 characters",
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
"serverStarting": "El servidor est\u00e1 iniciando...",
|
||||
"replayIntro": "Repetir introducci\u00f3n",
|
||||
"onboarding": "Configuraci\u00f3n inicial",
|
||||
"restartConfirm": "Are you sure? This wipes onboarding progress \u2014 click again to confirm",
|
||||
"resetting": "Restableciendo...",
|
||||
"recoveryNote": "La recuperaci\u00f3n de contrase\u00f1a requiere acceso SSH al servidor.",
|
||||
"errorMinLength": "La contrase\u00f1a debe tener al menos 8 caracteres",
|
||||
|
||||
+51
-10
@@ -14,6 +14,9 @@ export const useSyncStore = defineStore('sync', () => {
|
||||
const hasLoadedInitialData = ref(false)
|
||||
let isWsSubscribed = false
|
||||
let isWsConnecting = false
|
||||
let isRefreshingState = false
|
||||
let stalenessTimer: ReturnType<typeof setInterval> | null = null
|
||||
const STALENESS_INTERVAL_MS = 30_000
|
||||
|
||||
// Computed
|
||||
const serverInfo = computed(() => data.value?.['server-info'])
|
||||
@@ -21,6 +24,47 @@ export const useSyncStore = defineStore('sync', () => {
|
||||
const peerHealth = computed<Record<string, boolean>>(() => data.value?.['peer-health'] || {})
|
||||
const uiData = computed(() => data.value?.ui)
|
||||
|
||||
/**
|
||||
* Refetch the full state snapshot via RPC and apply it through the same
|
||||
* path as the post-connect fetch (revision/hasLoadedInitialData handling
|
||||
* stays identical). Guarded against overlapping fetches.
|
||||
*/
|
||||
async function refreshStateFromServer(): Promise<void> {
|
||||
if (isRefreshingState) return
|
||||
isRefreshingState = true
|
||||
try {
|
||||
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
|
||||
if (import.meta.env.DEV) console.warn('[Store] Failed to refresh state from server')
|
||||
} finally {
|
||||
isRefreshingState = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Belt-and-suspenders against missed broadcasts: while the WebSocket is
|
||||
* connected, periodically resync the full state so a dropped patch can
|
||||
* never permanently strand the UI on stale data.
|
||||
*/
|
||||
function startStalenessReconciliation(): void {
|
||||
if (stalenessTimer) return
|
||||
stalenessTimer = setInterval(() => {
|
||||
if (wsClient.isConnected()) void refreshStateFromServer()
|
||||
}, STALENESS_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function stopStalenessReconciliation(): void {
|
||||
if (stalenessTimer) {
|
||||
clearInterval(stalenessTimer)
|
||||
stalenessTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
async function connectWebSocket(): Promise<void> {
|
||||
// Prevent concurrent connection attempts
|
||||
@@ -82,6 +126,10 @@ export const useSyncStore = defineStore('sync', () => {
|
||||
if (import.meta.env.DEV) console.log('[Store] WebSocket already connected')
|
||||
isConnected.value = true
|
||||
isReconnecting.value = false
|
||||
// Re-entrant call while already connected (e.g. after onboarding
|
||||
// completes): resync state in case a broadcast was missed.
|
||||
void refreshStateFromServer()
|
||||
startStalenessReconciliation()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,16 +137,8 @@ export const useSyncStore = defineStore('sync', () => {
|
||||
if (import.meta.env.DEV) console.log('[Store] WebSocket connected')
|
||||
|
||||
// Fetch fresh state after reconnect to avoid stale patch application
|
||||
try {
|
||||
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
|
||||
if (import.meta.env.DEV) console.warn('[Store] Failed to fetch fresh state after reconnect')
|
||||
}
|
||||
await refreshStateFromServer()
|
||||
startStalenessReconciliation()
|
||||
|
||||
// Connection state will be updated via the callback
|
||||
if (wsClient.isConnected()) {
|
||||
@@ -158,6 +198,7 @@ export const useSyncStore = defineStore('sync', () => {
|
||||
|
||||
/** Reset sync state on logout — called by auth store */
|
||||
function resetOnLogout(): void {
|
||||
stopStalenessReconciliation()
|
||||
data.value = null
|
||||
hasLoadedInitialData.value = false
|
||||
isWsSubscribed = false
|
||||
|
||||
@@ -397,6 +397,7 @@ import { useAppsActions } from './apps/useAppsActions'
|
||||
import { validateSideloadRequest } from './apps/sideloadValidation'
|
||||
import { useMarketplaceApp } from '@/composables/useMarketplaceApp'
|
||||
import { useCollapsingHeaderTabs } from '@/composables/useCollapsingHeaderTabs'
|
||||
import { useContainersScanTimeout } from '@/composables/useContainersScanTimeout'
|
||||
import {
|
||||
type AppsTab, filterEntriesForTab, isWebOnlyApp, isWebsitePackage, opensInTab, resolveRuntimeLaunchUrl,
|
||||
WEB_ONLY_APPS, WEB_ONLY_APP_URLS, buildAllCategories, useCategoriesWithApps,
|
||||
@@ -461,7 +462,14 @@ const ALL_CATEGORIES = computed(() => buildAllCategories(t))
|
||||
const SERVICE_CATEGORIES = computed(() => buildServiceCategories(t))
|
||||
|
||||
const livePackages = computed(() => store.packages || {})
|
||||
const containersScanned = computed(() => store.data?.['server-info']?.['status-info']?.['containers-scanned'] !== false)
|
||||
// Field missing from server data = not scanned yet (consistent with Discover/Marketplace)
|
||||
const containersScannedRaw = computed(() => store.data?.['server-info']?.['status-info']?.['containers-scanned'] ?? false)
|
||||
// Escape hatch: never show "Checking containers…" forever — after a timeout,
|
||||
// fall through to the real (empty) state even if the scanned flag never arrives.
|
||||
const { effectiveContainersScanned: containersScanned } = useContainersScanTimeout(
|
||||
containersScannedRaw,
|
||||
computed(() => store.hasLoadedInitialData),
|
||||
)
|
||||
const {
|
||||
packages: stablePackages,
|
||||
isUsingLastKnownPackages,
|
||||
|
||||
@@ -178,12 +178,12 @@ const backgroundImage = computed(() => {
|
||||
if (mapped) return mapped
|
||||
// Detail/sub pages inherit their parent tab's background so they stay
|
||||
// visually "inside" the section instead of snapping to the home backdrop.
|
||||
if (route.path.startsWith('/dashboard/cloud/')) return 'bg-cloud.jpg'
|
||||
if (route.path.startsWith('/dashboard/cloud/')) return 'bg-cloud.webp'
|
||||
if (route.path.startsWith('/dashboard/web5/')) return 'bg-web5.jpg'
|
||||
if (route.path.startsWith('/dashboard/server/')) return 'bg-web5.jpg'
|
||||
if (route.path.startsWith('/dashboard/settings/')) return 'bg-settings.jpg'
|
||||
if (route.path.startsWith('/dashboard/settings/')) return 'bg-settings.webp'
|
||||
if (isDetailRoute(route.path)) return 'bg-intro.jpg'
|
||||
return 'bg-home.jpg'
|
||||
return 'bg-home.webp'
|
||||
})
|
||||
|
||||
const isDarkRoute = computed(() => {
|
||||
|
||||
@@ -235,6 +235,7 @@ import { useMarketplaceApp } from '@/composables/useMarketplaceApp'
|
||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useCollapsingHeaderTabs } from '@/composables/useCollapsingHeaderTabs'
|
||||
import { useContainersScanTimeout } from '@/composables/useContainersScanTimeout'
|
||||
import { APP_STORE_SECTIONS } from './appStoreCategories'
|
||||
import DiscoverHero from './discover/DiscoverHero.vue'
|
||||
import FeaturedApps from './discover/FeaturedApps.vue'
|
||||
@@ -335,7 +336,13 @@ function retryNostr() {
|
||||
}
|
||||
|
||||
const installedPackages = computed(() => store.data?.['package-data'] || {})
|
||||
const containersScanned = computed(() => store.data?.['server-info']?.['status-info']?.['containers-scanned'] ?? false)
|
||||
const containersScannedRaw = computed(() => store.data?.['server-info']?.['status-info']?.['containers-scanned'] ?? false)
|
||||
// Escape hatch: never leave app cards on "Checking..." forever — after a
|
||||
// timeout, treat the scan as done so cards render their normal install state.
|
||||
const { effectiveContainersScanned: containersScanned } = useContainersScanTimeout(
|
||||
containersScannedRaw,
|
||||
computed(() => store.hasLoadedInitialData),
|
||||
)
|
||||
|
||||
|
||||
const allApps = computed(() => {
|
||||
|
||||
@@ -66,7 +66,8 @@
|
||||
data-form-type="other"
|
||||
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors"
|
||||
:placeholder="t('login.enterPasswordSetup')"
|
||||
@keydown.enter="handleSetupWithSound"
|
||||
@keydown.enter="confirmPasswordInputRef?.focus()"
|
||||
@input="error = null"
|
||||
:disabled="loading || formDisabled"
|
||||
/>
|
||||
</div>
|
||||
@@ -77,6 +78,7 @@
|
||||
</label>
|
||||
<input
|
||||
id="setup-confirm-password"
|
||||
ref="confirmPasswordInputRef"
|
||||
v-model="confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
@@ -84,14 +86,16 @@
|
||||
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors"
|
||||
:placeholder="t('login.confirmPasswordPlaceholder')"
|
||||
@keydown.enter="handleSetupWithSound"
|
||||
@input="error = null"
|
||||
:disabled="loading || formDisabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="handleSetupWithSound"
|
||||
:disabled="loading || formDisabled || !password || password.length < 8 || password !== confirmPassword"
|
||||
:disabled="loading || formDisabled"
|
||||
class="w-full glass-button px-6 py-3 rounded-lg font-medium transition-all hover:bg-black/70 hover:border-white/30 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:class="{ 'opacity-60': !password || password.length < 8 || password !== confirmPassword }"
|
||||
>
|
||||
<span v-if="!loading">{{ t('login.setupButton') }}</span>
|
||||
<span v-else class="flex items-center justify-center">
|
||||
@@ -213,9 +217,10 @@
|
||||
<button
|
||||
@click="restartOnboarding"
|
||||
:disabled="isResettingOnboarding"
|
||||
class="text-xs text-white/50 hover:text-white/70 transition-colors underline-offset-2 hover:underline disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="text-xs transition-colors underline-offset-2 hover:underline disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:class="confirmingRestartOnboarding ? 'text-orange-400 hover:text-orange-300' : 'text-white/50 hover:text-white/70'"
|
||||
>
|
||||
{{ isResettingOnboarding ? t('login.resetting') : t('login.onboarding') }}
|
||||
{{ isResettingOnboarding ? t('login.resetting') : (confirmingRestartOnboarding ? t('login.restartConfirm') : t('login.onboarding')) }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
@@ -260,6 +265,7 @@ const requiresTotp = ref(false)
|
||||
const totpCode = ref('')
|
||||
const useBackupCode = ref(false)
|
||||
const totpInputRef = ref<HTMLInputElement | null>(null)
|
||||
const confirmPasswordInputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
// Server startup state
|
||||
const serverReady = ref(false)
|
||||
@@ -338,6 +344,7 @@ onBeforeUnmount(() => {
|
||||
removeUnlockListeners()
|
||||
if (startupPollTimer) clearTimeout(startupPollTimer)
|
||||
if (startupProgressInterval) clearInterval(startupProgressInterval)
|
||||
if (confirmRestartTimer) clearTimeout(confirmRestartTimer)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -536,9 +543,26 @@ function replayIntro() {
|
||||
}
|
||||
|
||||
const isResettingOnboarding = ref(false)
|
||||
const confirmingRestartOnboarding = ref(false)
|
||||
let confirmRestartTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
async function restartOnboarding() {
|
||||
if (isResettingOnboarding.value) return
|
||||
// First click arms a confirmation state; only a second explicit click restarts.
|
||||
if (!confirmingRestartOnboarding.value) {
|
||||
confirmingRestartOnboarding.value = true
|
||||
if (confirmRestartTimer) clearTimeout(confirmRestartTimer)
|
||||
confirmRestartTimer = setTimeout(() => {
|
||||
confirmingRestartOnboarding.value = false
|
||||
confirmRestartTimer = null
|
||||
}, 5000)
|
||||
return
|
||||
}
|
||||
if (confirmRestartTimer) {
|
||||
clearTimeout(confirmRestartTimer)
|
||||
confirmRestartTimer = null
|
||||
}
|
||||
confirmingRestartOnboarding.value = false
|
||||
isResettingOnboarding.value = true
|
||||
// Local-only reset — no RPC needed since user isn't logged in.
|
||||
// Onboarding pages are all public, so clearing localStorage is enough.
|
||||
|
||||
@@ -170,6 +170,7 @@ import { useMarketplaceApp } from '@/composables/useMarketplaceApp'
|
||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useCollapsingHeaderTabs } from '@/composables/useCollapsingHeaderTabs'
|
||||
import { useContainersScanTimeout } from '@/composables/useContainersScanTimeout'
|
||||
import { APP_STORE_CATEGORIES, APP_STORE_SECTIONS } from './appStoreCategories'
|
||||
import MarketplaceAppCard from './marketplace/MarketplaceAppCard.vue'
|
||||
import {
|
||||
@@ -286,9 +287,15 @@ const installedPackages = computed(() => {
|
||||
return store.data?.['package-data'] || {}
|
||||
})
|
||||
|
||||
const containersScanned = computed(() => {
|
||||
const containersScannedRaw = computed(() => {
|
||||
return store.data?.['server-info']?.['status-info']?.['containers-scanned'] ?? false
|
||||
})
|
||||
// Escape hatch: never leave app cards on "Checking..." forever — after a
|
||||
// timeout, treat the scan as done so cards render their normal install state.
|
||||
const { effectiveContainersScanned: containersScanned } = useContainersScanTimeout(
|
||||
containersScannedRaw,
|
||||
computed(() => store.hasLoadedInitialData),
|
||||
)
|
||||
|
||||
// Combine curated apps with Nostr relay-discovered apps
|
||||
const allApps = computed(() => {
|
||||
|
||||
@@ -109,16 +109,16 @@ const useVideoBackground = computed(() => {
|
||||
// Note: bg-intro.jpg is used for splash and /onboarding/intro for seamless transition
|
||||
const routeBackgrounds: Record<string, string> = {
|
||||
'/onboarding/intro': 'bg-intro.jpg', // Video will be used instead
|
||||
'/onboarding/options': 'bg-intro-4.jpg',
|
||||
'/onboarding/options': 'bg-intro-4.webp',
|
||||
'/onboarding/path': 'bg-intro-3.jpg',
|
||||
'/onboarding/seed': 'bg-intro-1.jpg',
|
||||
'/onboarding/seed-verify': 'bg-intro-1.jpg',
|
||||
'/onboarding/seed-restore': 'bg-intro-1.jpg',
|
||||
'/onboarding/did': 'bg-intro-4.jpg',
|
||||
'/onboarding/identity': 'bg-intro-1.jpg',
|
||||
'/onboarding/backup': 'bg-intro-6.jpg',
|
||||
'/onboarding/seed': 'bg-intro-1.webp',
|
||||
'/onboarding/seed-verify': 'bg-intro-1.webp',
|
||||
'/onboarding/seed-restore': 'bg-intro-1.webp',
|
||||
'/onboarding/did': 'bg-intro-4.webp',
|
||||
'/onboarding/identity': 'bg-intro-1.webp',
|
||||
'/onboarding/backup': 'bg-intro-6.webp',
|
||||
'/onboarding/verify': 'bg-intro-2.jpg',
|
||||
'/onboarding/done': 'bg-intro-1.jpg',
|
||||
'/onboarding/done': 'bg-intro-1.webp',
|
||||
'/login': 'bg-intro.jpg' // Video loops from splash (same as intro)
|
||||
}
|
||||
|
||||
@@ -126,12 +126,12 @@ const routeBackgrounds: Record<string, string> = {
|
||||
// identical on every logout. Cycles through bg-intro-1..6 using a
|
||||
// counter persisted to localStorage so subsequent visits advance.
|
||||
const LOGIN_BACKGROUNDS = [
|
||||
'bg-intro-1.jpg',
|
||||
'bg-intro-1.webp',
|
||||
'bg-intro-2.jpg',
|
||||
'bg-intro-3.jpg',
|
||||
'bg-intro-4.jpg',
|
||||
'bg-intro-5.jpg',
|
||||
'bg-intro-6.jpg',
|
||||
'bg-intro-4.webp',
|
||||
'bg-intro-5.webp',
|
||||
'bg-intro-6.webp',
|
||||
]
|
||||
function pickNextLoginBackground(): string {
|
||||
try {
|
||||
@@ -307,7 +307,7 @@ watch(() => route.path, (newPath, oldPath) => {
|
||||
|
||||
// Login route: set background immediately, no zoom, no transition (glitch is always-on)
|
||||
if (newPath === '/login') {
|
||||
currentBackground.value = 'bg-intro-1.jpg'
|
||||
currentBackground.value = 'bg-intro-1.webp'
|
||||
isTransitioning.value = false
|
||||
isGlitching.value = false
|
||||
return
|
||||
|
||||
@@ -19,19 +19,19 @@ const WEB5_TAB_ORDER = ['/dashboard/web5', '/dashboard/cloud', '/dashboard/serve
|
||||
|
||||
/** Route-to-background image mapping */
|
||||
export const ROUTE_BACKGROUNDS: Record<string, string> = {
|
||||
'/dashboard': 'bg-home.jpg',
|
||||
'/dashboard/': 'bg-home.jpg',
|
||||
'/dashboard/apps': 'bg-myapps.jpg',
|
||||
'/dashboard/discover': 'bg-appstore.jpg',
|
||||
'/dashboard/marketplace': 'bg-appstore.jpg',
|
||||
'/dashboard/cloud': 'bg-cloud.jpg',
|
||||
'/dashboard/mesh': 'bg-mesh.jpg',
|
||||
'/dashboard': 'bg-home.webp',
|
||||
'/dashboard/': 'bg-home.webp',
|
||||
'/dashboard/apps': 'bg-myapps.webp',
|
||||
'/dashboard/discover': 'bg-appstore.webp',
|
||||
'/dashboard/marketplace': 'bg-appstore.webp',
|
||||
'/dashboard/cloud': 'bg-cloud.webp',
|
||||
'/dashboard/mesh': 'bg-mesh.webp',
|
||||
'/dashboard/server': 'bg-network.jpg',
|
||||
'/dashboard/web5': 'bg-web5.jpg',
|
||||
'/dashboard/server/federation': 'bg-web5.jpg',
|
||||
'/dashboard/monitoring': 'bg-web5.jpg',
|
||||
'/dashboard/fleet': 'bg-web5.jpg',
|
||||
'/dashboard/settings': 'bg-settings.jpg',
|
||||
'/dashboard/settings': 'bg-settings.webp',
|
||||
'/dashboard/chat': 'bg-aiui.jpg',
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user