fix: fresh-ISO feedback bug-bash — onboarding, status truthfulness, recovery, kiosk, logs
Fixes from real fresh-install feedback (Framework node .81) + its log bundle:
Backend:
- websocket: subscribe before initial snapshot — broadcasts in the gap were
silently lost, stranding clients on stale state until a hard refresh
(the "everything needs ctrl-r" bug: My Apps stuck Loading, App Store
stuck Checking, containers-scanned never arriving)
- crash recovery: check the crash marker BEFORE writing our own PID —
recovery had never run on any node (always saw its own PID and skipped);
PID-reuse guard via /proc cmdline
- boot status: pending-boot-starts registry (recovery, stack recovery,
reconciler, adoption) — scanner overlays queued-but-down apps as
Restarting instead of Stopped after a reboot; scanner-authored
Restarting resolves immediately on a settled scan (no transitional wedge)
- install deps: bounded wait (36x5s) when a dependency is installed but
still starting ("Waiting for Bitcoin to start…") instead of instant
rejection; dependency-gate rejections remove the optimistic entry (no
phantom Stopped tile) and surface as a notification
- seed backup: auth.setup persists the onboarding mnemonic as the
encrypted seed backup (reveal previously failed on EVERY node — nothing
ever wrote master_seed.enc); seed.restore stashes too; error sanitizer
lets seed/2FA errors through instead of "Check server logs"
- lnd: bitcoind.rpchost resolved from the running Bitcoin variant
(hardcoded bitcoin-knots broke Core nodes); manifest uses derived_env
- bitcoin status: clean human message for connection-reset/startup; raw
URLs + os-error chains no longer reach the app card
- fedimint-clientd: chown /var/lib/archipelago/fmcd to 1000:1000 (root-
created dir crash-looped the rootless container, EACCES) — first-boot
script + pre-start self-heal
- log volume (>1GB/day on a day-old node): journald caps drop-in (ISO +
bootstrap self-heal), bitcoind -printtoconsole=0 everywhere (90% of the
journal was IBD UpdateTip spam), tracing default debug→info
Frontend:
- Login: Enter advances to confirm field then submits; submit always
clickable with inline errors (was silently disabled on mismatch);
Restart Onboarding needs a confirming second click (the mismatch →
"onboarding restarted" trap)
- sync store: 30s state reconciliation + refetch on re-entrant connect;
20s containers-scanned escape hatch so Checking can never show forever;
fresh empty node reaches the real "no apps yet" state
- intro video: CRF20 re-encode (SSIM 0.988) + faststart — moov was at EOF
so playback needed the full 15MB first (the intro lag)
- backgrounds: 10 heaviest JPEGs → WebP q90 (9.4MB→6.6MB); 7 stayed JPEG
(WebP larger on noisy sources)
- Web5ConnectedNodes: drop unused template ref that failed vue-tsc -b
ISO/kiosk:
- nginx: /assets/ 404s no longer cached immutable for a year; HTTPS block
gained the missing /assets/ location (served index.html as images)
- kiosk: launcher/service spliced from configs/ at ISO build (stale
heredoc force-disabled GPU); MemoryHigh/Max 1200/1500→2200/2800M (kiosk
rode the reclaim throttle = the lag); firmware-intel-graphics +
firmware-amd-graphics (trixie split DMC blobs out of misc-nonfree)
Verified: cargo test 898/898 green, npm run build green with dist
contents confirmed (webp refs, lnd.png, faststart video, new strings).
Handover for ISO build + deploy: docs/HANDOVER-2026-07-02-iso-feedback.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8256fde1a6
commit
c375ecc441
@@ -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',
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<!-- Connected Nodes (P2P over Tor) -->
|
||||
<div ref="nodesContainerRef" data-controller-container tabindex="0" class="glass-card p-6 scroll-mt-24 flex flex-col">
|
||||
<div data-controller-container tabindex="0" class="glass-card p-6 scroll-mt-24 flex flex-col">
|
||||
<!-- Desktop: side-by-side layout -->
|
||||
<div class="hidden md:flex items-start gap-4 mb-4">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
@@ -319,7 +319,6 @@ function writeConnectedNodesCache(state: ConnectedNodesCache) {
|
||||
}
|
||||
}
|
||||
|
||||
const nodesContainerRef = ref<HTMLElement | null>(null)
|
||||
const nodesContainerTab = ref<'trusted' | 'observers' | 'requests'>('trusted')
|
||||
const { loadReceivedMessages } = messageToast
|
||||
|
||||
|
||||
Reference in New Issue
Block a user