archy/neode-ui/src/views/RootRedirect.vue
Dorian 08bb2c80d4 feat: LUKS2 encryption, boot sequence fixes, onboarding auth, CI/CD
- LUKS2 full-partition encryption for /var/lib/archipelago/ (TASK-42)
  4-partition layout: BIOS + EFI + root (30GB) + encrypted data
  AES-256-XTS with AES-NI detection, ChaCha20 fallback for ARM
  Auto-unlock via crypttab + random key file

- Fix EFI boot errors: remove shim-signed, clean shim artifacts
- Fix first-boot sequence: always show boot animation before onboarding
- Fix stale localStorage causing login instead of onboarding (BUG-47)

- Add auth.setup + auth.isSetup RPC handlers for password on clean install
- Add onboarding methods to UNAUTHENTICATED_METHODS (DID sign 403 fix)

- FileBrowser bundled in unbundled ISO, fix auto-login Secure cookie (BUG-46)
- Kiosk mode: xorg/chromium in rootfs, toggle script, MOTD instructions

- Add Gitea Actions CI/CD workflow for automatic ISO builds

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:12:16 +00:00

130 lines
4.0 KiB
Vue

<template>
<div class="min-h-full">
<BootScreen :visible="showBootScreen" @ready="onServerReady" />
<div v-if="!showBootScreen" class="min-h-full flex items-center justify-center">
<div class="flex flex-col items-center gap-4 opacity-0 root-redirect-fade">
<svg class="animate-spin h-8 w-8 text-white/60" xmlns="http://www.w3.org/2000/svg" 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>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { isOnboardingComplete } from '@/composables/useOnboarding'
import BootScreen from '@/components/BootScreen.vue'
const router = useRouter()
const showBootScreen = ref(false)
async function quickHealthCheck(): Promise<boolean> {
try {
const ac = new AbortController()
const t = setTimeout(() => ac.abort(), 2000)
const res = await fetch('/rpc/v1', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'server.echo', params: { message: 'ping' } }),
signal: ac.signal,
})
clearTimeout(t)
return res.status !== 502 && res.status !== 503
} catch {
return false
}
}
async function checkOnboarded(): Promise<boolean> {
try {
const result = await Promise.race([
isOnboardingComplete(),
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 3000)),
])
return result
} catch {
// Backend unreachable — fall back to localStorage only as last resort
return localStorage.getItem('neode_onboarding_complete') === '1'
}
}
async function proceedToApp() {
const devMode = import.meta.env.VITE_DEV_MODE
if (devMode === 'setup' || devMode === 'existing') {
router.replace('/login').catch(() => {})
return
}
// Always check backend for authoritative onboarding state
// (localStorage can be stale from a previous install on the same IP)
const onboarded = await checkOnboarded()
router.replace(onboarded ? '/login' : '/onboarding/intro').catch(() => {})
}
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')
sessionStorage.setItem('archipelago_from_boot', '1')
window.location.href = '/'
}
onMounted(async () => {
const devMode = import.meta.env.VITE_DEV_MODE
// Coming back from boot screen — let App.vue's SplashScreen take over
if (sessionStorage.getItem('archipelago_from_boot') === '1') {
return
}
// Splash already completed this session — go to app
if (sessionStorage.getItem('archipelago_from_splash') === '1') {
proceedToApp()
return
}
// Standard dev modes
if (devMode === 'setup' || devMode === 'existing') {
proceedToApp()
return
}
// Boot dev mode — always show boot screen (first load only)
if (devMode === 'boot') {
showBootScreen.value = true
return
}
// Production: check server health
const isUp = await quickHealthCheck()
if (isUp) {
// Server is up — check if onboarding is complete
const onboarded = await checkOnboarded()
if (onboarded) {
// Returning user, server is up — go straight to login
proceedToApp()
return
}
// First boot: server is up but onboarding not done yet.
// Show boot animation anyway — it lets services fully warm up
// (containers, DID resolver, etc.) before onboarding starts.
}
// Server not ready OR first boot — show boot screen
showBootScreen.value = true
})
</script>
<style scoped>
.root-redirect-fade {
animation: root-fade-in 0.3s ease 0.5s forwards;
}
@keyframes root-fade-in {
to { opacity: 1; }
}
</style>