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:
archipelago
2026-07-02 08:03:25 -04:00
64 changed files with 1513 additions and 208 deletions
+9 -1
View File
@@ -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,
+3 -3
View File
@@ -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(() => {
+8 -1
View File
@@ -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(() => {
+28 -4
View File
@@ -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.
+8 -1
View File
@@ -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(() => {
+13 -13
View File
@@ -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',
}