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:
Dorian 2026-03-31 21:00:01 +01:00
parent 08f7f58a9d
commit 73fb961b9a
12 changed files with 50 additions and 41 deletions

View File

@ -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;
});
}

View File

@ -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 }

View File

@ -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,

View File

@ -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"

View File

@ -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() {

View File

@ -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>

View File

@ -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">

View File

@ -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',

View File

@ -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 = '/'
}

View File

@ -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>

View File

@ -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
}
}

View File

@ -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