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:
@@ -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')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user