fix: disable boot reconciler, fix onboarding loop, UI polish
Critical flow fixes: - Disable boot reconciliation that auto-created ALL containers on unbundled installs (only FileBrowser should exist on first boot) - Fix onboarding loop: RootRedirect no longer clears the neode_onboarding_complete flag on boot screen completion - Seed phrase persists when navigating back (no regeneration) UI fixes: - Boot screen: removed github and save icons from animation loop - Seed screens: viewport height scaling with 100dvh - Seed restore: removed outer card container from word input grid - Seed screens use distinct background (bg-intro-1.jpg) - Install progress simplified to "Installing" button style - Uninstall state moved to global store (persists across navigation) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
08f7f58a9d
commit
73fb961b9a
@ -104,8 +104,11 @@ async fn main() -> Result<()> {
|
||||
// Signal to health monitor that boot recovery is done
|
||||
crash_recovery::mark_recovery_complete();
|
||||
|
||||
// Reconcile containers against canonical specs (fixes config drift)
|
||||
crash_recovery::run_boot_reconciliation().await;
|
||||
// Boot reconciliation disabled — the reconciler creates ALL containers
|
||||
// from specs, which is wrong on unbundled installs where only user-chosen
|
||||
// apps should exist. The health monitor handles restarting existing
|
||||
// containers. Run reconcile-containers.sh manually when needed.
|
||||
// crash_recovery::run_boot_reconciliation().await;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -79,8 +79,6 @@ const glitching = ref(false)
|
||||
const iconSources = [
|
||||
'/assets/icon/bitcoin.svg',
|
||||
'/assets/icon/cloud-done.svg',
|
||||
'/assets/icon/github.svg',
|
||||
'/assets/icon/save.svg',
|
||||
]
|
||||
|
||||
interface LogLine { prefix: string; text: string; type: string }
|
||||
|
||||
@ -9,8 +9,9 @@ import type { InstallProgress } from '../views/marketplace/marketplaceData'
|
||||
export const useServerStore = defineStore('server', () => {
|
||||
const sync = useSyncStore()
|
||||
|
||||
// Global install tracking — persists across navigation
|
||||
// Global install/uninstall tracking — persists across navigation
|
||||
const installingApps = ref<Map<string, InstallProgress>>(new Map())
|
||||
const uninstallingApps = ref<Set<string>>(new Set())
|
||||
|
||||
function setInstallProgress(appId: string, progress: Partial<InstallProgress> & { id: string; title: string }) {
|
||||
const existing = installingApps.value.get(appId)
|
||||
@ -94,11 +95,12 @@ export const useServerStore = defineStore('server', () => {
|
||||
isShuttingDown,
|
||||
isOffline,
|
||||
|
||||
// Install tracking (global, persists across navigation)
|
||||
// Install/uninstall tracking (global, persists across navigation)
|
||||
installingApps,
|
||||
setInstallProgress,
|
||||
clearInstallProgress,
|
||||
isInstalling,
|
||||
uninstallingApps,
|
||||
|
||||
// Actions
|
||||
installPackage,
|
||||
|
||||
@ -111,7 +111,7 @@
|
||||
:is-loading="!!actions.loadingActions.value[id as string]"
|
||||
:is-installing="serverStore.isInstalling(id as string)"
|
||||
:install-progress="serverStore.installingApps.get(id as string)"
|
||||
:is-uninstalling="actions.uninstallingApps.value.has(id as string)"
|
||||
:is-uninstalling="actions.uninstallingApps.has(id as string)"
|
||||
@go-to-app="goToApp"
|
||||
@launch="launchApp"
|
||||
@start="actions.startApp"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="h-full flex items-center justify-center p-3 sm:p-4 md:p-6">
|
||||
<div class="h-[100dvh] flex items-center justify-center p-3 sm:p-4 md:p-6">
|
||||
<div class="max-w-[800px] w-full max-h-full relative z-10 path-glass-container onb-scroll-container flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="text-center flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6 pb-2 sm:pb-3">
|
||||
@ -143,7 +143,20 @@ watch(confirmed, (val) => {
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => { generateSeed() })
|
||||
onMounted(() => {
|
||||
// Restore previously generated seed if navigating back (don't regenerate)
|
||||
const saved = sessionStorage.getItem('_seed_words')
|
||||
if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved)
|
||||
if (Array.isArray(parsed) && parsed.length === 24) {
|
||||
words.value = parsed
|
||||
return
|
||||
}
|
||||
} catch { /* regenerate */ }
|
||||
}
|
||||
generateSeed()
|
||||
})
|
||||
onUnmounted(() => { stopTimers() })
|
||||
|
||||
function proceed() {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="h-full flex items-center justify-center p-3 sm:p-4 md:p-6">
|
||||
<div class="h-[100dvh] flex items-center justify-center p-3 sm:p-4 md:p-6">
|
||||
<div class="max-w-[800px] w-full max-h-full relative z-10 path-glass-container onb-scroll-container flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="text-center flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6 pb-2 sm:pb-3">
|
||||
@ -43,7 +43,6 @@
|
||||
|
||||
<!-- Word Input Grid -->
|
||||
<div v-if="!restored" class="w-full max-w-[600px]">
|
||||
<div class="path-option-card cursor-default px-3 py-3 sm:px-5 sm:py-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-1.5 sm:gap-2">
|
||||
<div v-for="i in 24" :key="i" class="relative">
|
||||
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-white/30 text-[1rem] font-mono pointer-events-none">{{ i }}.</span>
|
||||
@ -62,7 +61,6 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-white/40 mt-2 text-center">
|
||||
Paste all 24 words into the first field to auto-fill
|
||||
</p>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="h-full flex items-center justify-center p-3 sm:p-4 md:p-6">
|
||||
<div class="h-[100dvh] flex items-center justify-center p-3 sm:p-4 md:p-6">
|
||||
<div class="max-w-[800px] w-full max-h-full relative z-10 path-glass-container onb-scroll-container flex flex-col">
|
||||
<!-- Header (hidden after verification) -->
|
||||
<div v-if="!verified" class="text-center flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6 pb-2 sm:pb-3">
|
||||
|
||||
@ -97,9 +97,9 @@ const routeBackgrounds: Record<string, string> = {
|
||||
'/onboarding/intro': 'bg-intro.jpg', // Video will be used instead
|
||||
'/onboarding/options': 'bg-intro-4.jpg',
|
||||
'/onboarding/path': 'bg-intro-3.jpg',
|
||||
'/onboarding/seed': 'bg-intro-5.jpg',
|
||||
'/onboarding/seed-verify': 'bg-intro-6.jpg',
|
||||
'/onboarding/seed-restore': 'bg-intro-2.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',
|
||||
|
||||
@ -81,7 +81,8 @@ async function proceedToApp() {
|
||||
function onServerReady() {
|
||||
if (import.meta.env.DEV) console.log('[RootRedirect] onServerReady — setting flag and reloading')
|
||||
localStorage.removeItem('neode_intro_seen')
|
||||
localStorage.removeItem('neode_onboarding_complete')
|
||||
// Do NOT clear neode_onboarding_complete here — that flag must persist
|
||||
// across boot screen reloads so completed onboarding isn't lost.
|
||||
sessionStorage.setItem('archipelago_from_boot', '1')
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
@ -13,17 +13,14 @@
|
||||
<!-- Installing overlay -->
|
||||
<div
|
||||
v-if="isInstalling"
|
||||
class="absolute inset-0 z-20 flex flex-col items-center justify-center bg-black/70 backdrop-blur-sm rounded-xl gap-2"
|
||||
class="absolute inset-0 z-20 flex items-center justify-center bg-black/70 backdrop-blur-sm rounded-xl"
|
||||
>
|
||||
<div class="flex items-center gap-3 text-amber-400">
|
||||
<div class="flex items-center gap-3 text-white/90">
|
||||
<svg class="animate-spin h-5 w-5" fill="none" viewBox="0 0 24 24">
|
||||
<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>
|
||||
<span class="text-sm font-medium">{{ installProgress?.message || t('common.installing') }}...</span>
|
||||
</div>
|
||||
<div v-if="installProgress" class="w-3/4 h-1 bg-white/10 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-amber-500 rounded-full transition-all" :style="{ width: `${installProgress.progress}%` }"></div>
|
||||
<span class="text-sm font-medium">Installing</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -2,16 +2,19 @@
|
||||
|
||||
import { ref, onBeforeUnmount } from 'vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useServerStore } from '@/stores/server'
|
||||
|
||||
export function useAppsActions() {
|
||||
const store = useAppStore()
|
||||
const serverStore = useServerStore()
|
||||
const loadingActions = ref<Record<string, boolean>>({})
|
||||
const actionError = ref('')
|
||||
let errorTimer: ReturnType<typeof setTimeout> | undefined
|
||||
const actionTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const uninstalling = ref(false)
|
||||
const uninstallingApps = ref<Set<string>>(new Set())
|
||||
// Use global store so uninstall state persists across navigation
|
||||
const uninstallingApps = serverStore.uninstallingApps
|
||||
|
||||
function showActionError(msg: string) {
|
||||
actionError.value = msg
|
||||
@ -70,14 +73,14 @@ export function useAppsActions() {
|
||||
async function confirmUninstall(appId: string) {
|
||||
uninstalling.value = true
|
||||
try {
|
||||
uninstallingApps.value.add(appId)
|
||||
uninstallingApps.add(appId)
|
||||
await store.uninstallPackage(appId)
|
||||
// State update comes via WebSocket — no manual deletion needed
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) console.error('Failed to uninstall app:', err)
|
||||
showActionError(`Failed to uninstall: ${err instanceof Error ? err.message : 'Unknown error'}`)
|
||||
} finally {
|
||||
uninstallingApps.value.delete(appId)
|
||||
uninstallingApps.delete(appId)
|
||||
uninstalling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,26 +96,20 @@
|
||||
Checking...
|
||||
</span>
|
||||
</span>
|
||||
<!-- Installing — inline progress on card -->
|
||||
<div
|
||||
<!-- Installing — simple button-style indicator -->
|
||||
<button
|
||||
v-else-if="!installed && installing"
|
||||
class="flex-1"
|
||||
disabled
|
||||
class="flex-1 px-4 py-2 glass-button glass-button-sm rounded-lg text-sm font-medium opacity-80 cursor-wait"
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-1.5">
|
||||
<svg class="animate-spin h-4 w-4 text-blue-400 shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<span class="flex items-center justify-center gap-2">
|
||||
<svg class="animate-spin h-3.5 w-3.5" fill="none" viewBox="0 0 24 24">
|
||||
<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>
|
||||
<span class="text-sm text-white/80 truncate">{{ installProgress?.message || t('common.installing') }}</span>
|
||||
<span class="text-xs text-white/50 shrink-0">{{ installProgress?.progress || 0 }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-white/10 rounded-full h-1.5 overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full transition-all duration-500 bg-blue-500"
|
||||
:style="{ width: `${installProgress?.progress || 0}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
Installing
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-else-if="!installed && (app.source === 'local' || app.dockerImage)"
|
||||
data-controller-install-btn
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user