fix: batch beta fixes — 13 issues from 2026-03-28 testing

Frontend (neode-ui):
- Login double-enter: change @keyup.enter to @keydown.enter (#10)
- Login loop on LAN: post-login session verify before navigation (#12)
- Splash flash: reorder isReady/showSplash, add black fallback div (#7)
- Skip button text: remove "skip this step" from onboarding (#8)
- Password UI: import existing ChangePasswordSection in Settings (#11)
- Arrow key focus trap: add tab-order fallback when spatial nav fails (#13)

ISO/Boot (image-recipe):
- Step counter: TOTAL_STEPS=7 → 8 to match actual step count
- GRUB theme: add desktop-image-scale-method stretch, widen menu
- Boot noise: add loglevel=0, rd.systemd.show_status=false to kernel
- USB removal: copy reboot script to tmpfs, exec from there
- Tor setup: rewrite python3 JSON generation as bash heredoc
- Doctor/reconcile: copy scripts into rootfs, fix missing file errors
- zstd: add to rootfs packages for initramfs compression

Docs:
- BETA-ISSUES-20260328.md: full issue tracker
- INSTALL-SCREENS-DESIGN.md: editable TUI mockups

522 tests pass, vue-tsc clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-28 23:41:40 +00:00
parent bdd9578bf8
commit 6e356412b8
10 changed files with 330 additions and 57 deletions

View File

@@ -4,7 +4,8 @@
<SplashScreen v-if="showSplash" @complete="handleSplashComplete" />
<!-- Main App Content - only show after splash and routing is complete -->
<RouterView v-if="!showSplash && isReady" />
<div v-if="!showSplash && !isReady" class="min-h-screen bg-black" />
<RouterView v-else-if="!showSplash && isReady" />
<!-- Spotlight command palette (Cmd+K / Ctrl+K) -->
<SpotlightSearch />
@@ -211,10 +212,11 @@ onMounted(async () => {
showSplash.value = true
} else {
// Already seen intro, direct route, or boot mode (boot screen handles intro)
showSplash.value = false
document.body.classList.add('splash-complete')
// Set isReady BEFORE hiding splash to prevent flash of partial content
await router.isReady()
isReady.value = true
showSplash.value = false
document.body.classList.add('splash-complete')
}
})

View File

@@ -240,9 +240,17 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
// Up/Down: exit field, navigate spatially
e.preventDefault()
const dir = e.key === 'ArrowDown' ? 'down' as const : 'up' as const
const candidates = getFocusableElements(containerRef?.value ?? document).filter(el => el !== target)
const all = getFocusableElements(containerRef?.value ?? document)
const candidates = all.filter(el => el !== target)
const nearest = findNearestInDirection(target, candidates, dir)
if (nearest) focusEl(nearest)
if (nearest) {
focusEl(nearest)
} else {
// Fallback: tab order when spatial navigation fails
const idx = all.indexOf(target)
const fallback = dir === 'down' ? all[idx + 1] : all[idx - 1]
if (fallback) focusEl(fallback)
}
return
}
// Left/Right: stay in field (cursor movement). Escape: handled below.
@@ -353,7 +361,7 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
const next = items[nextIdx]
if (next && next !== activeEl) {
focusEl(next)
// Auto-navigate sidebar links
// Auto-navigate sidebar links (not buttons — Logout etc. require Enter)
if (next.tagName === 'A') {
const href = (next as HTMLAnchorElement).getAttribute('href')
if (href?.startsWith('/')) router.push(href).catch(() => {})
@@ -493,10 +501,14 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
function autoFocusMain() {
const active = document.activeElement as HTMLElement | null
// Don't steal focus from inputs, modals, or sidebar
if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')) return
if (document.querySelector('[role="dialog"]')) return
if (isInZone(active, 'sidebar')) return
requestAnimationFrame(() => {
// Re-check sidebar after RAF — user may still be navigating
if (isInZone(document.activeElement as HTMLElement, 'sidebar')) return
const remembered = recallFocus('main')
if (remembered) { remembered.focus({ preventScroll: true }); return }
const containers = getContainers()

View File

@@ -56,7 +56,7 @@
autocomplete="off"
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')"
@keyup.enter="handleSetupWithSound"
@keydown.enter="handleSetupWithSound"
:disabled="loading || formDisabled"
/>
</div>
@@ -72,7 +72,7 @@
autocomplete="off"
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')"
@keyup.enter="handleSetupWithSound"
@keydown.enter="handleSetupWithSound"
:disabled="loading || formDisabled"
/>
</div>
@@ -156,7 +156,7 @@
autocomplete="off"
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.enterPasswordPlaceholder')"
@keyup.enter="handleLoginWithSound"
@keydown.enter="handleLoginWithSound"
:disabled="loading || formDisabled"
/>
</div>
@@ -424,6 +424,14 @@ async function handleLogin() {
setTimeout(() => totpInputRef.value?.focus(), 100)
return
}
// Verify session cookie works before navigating (prevents login loop on LAN)
try {
await rpcClient.call({ method: 'server.echo', params: { message: 'session-check' } })
} catch {
error.value = 'Login succeeded but session could not be established. Try clearing cookies and refreshing.'
store.logout()
return
}
stopSynthwave()
whooshAway.value = true
playLoginSuccessWhoosh()

View File

@@ -15,7 +15,7 @@
<!-- Content Area -->
<div class="flex flex-col items-center gap-4 sm:gap-6 mb-4 sm:mb-6 px-3 sm:px-4">
<div class="w-full max-w-[600px] space-y-4 sm:space-y-6">
<p v-if="serverStarting" class="text-orange-400/80 text-sm">Server is still starting up. You can try again shortly or skip this step.</p>
<p v-if="serverStarting" class="text-orange-400/80 text-sm">Server is still starting up. Please try again shortly.</p>
<p v-else-if="errorMessage" class="text-red-400 text-sm">{{ errorMessage }}</p>
<!-- Passphrase Input -->
<div class="path-option-card cursor-default px-4 py-4 sm:px-6 sm:py-6">

View File

@@ -14,7 +14,7 @@
<!-- Content Area -->
<div class="flex flex-col items-center gap-6 mb-6">
<p v-if="serverStarting" class="text-orange-400/80 text-sm">Server is still starting up. You can try again shortly or skip this step.</p>
<p v-if="serverStarting" class="text-orange-400/80 text-sm">Server is still starting up. Please try again shortly.</p>
<p v-else-if="errorMessage" class="text-red-400 text-sm">{{ errorMessage }}</p>
<!-- Sign Button (if not verified yet) -->
<button
@@ -127,7 +127,7 @@ async function signChallenge() {
if (isRetryable) {
serverStarting.value = true
} else {
errorMessage.value = msg || 'Failed to sign challenge. You can retry or skip this step.'
errorMessage.value = msg || 'Failed to sign challenge. Please try again.'
}
} else {
await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)))

View File

@@ -1,11 +1,13 @@
<script setup lang="ts">
import AccountSection from '@/views/settings/AccountSection.vue'
import ChangePasswordSection from '@/views/settings/ChangePasswordSection.vue'
import SystemSection from '@/views/settings/SystemSection.vue'
</script>
<template>
<div class="pb-6">
<AccountSection />
<ChangePasswordSection />
<SystemSection />
</div>
</template>