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

@ -0,0 +1,96 @@
# Beta Test Issues — 2026-03-28 (ISO build 2137)
Hardware: Dell OptiPlex 3020M, i5, 8GB RAM, 465G HDD, UEFI+Legacy
## ISO / Boot (image-recipe)
### 1. UEFI autodetect broken
- **Severity**: High
- **Detail**: Only autodetects/boots in Legacy BIOS mode. UEFI boot does not autodetect the install disk.
- **Where**: `build-auto-installer-iso.sh` GRUB config, EFI boot chain
- **Status**: TODO
### 2. Installation TUI screens need redesign
- **Severity**: Medium
- **Detail**: Current installer output is plain/ugly. Needs polished design.
- **Action**: User will provide .md mockup for each screen, then we implement.
- **Where**: `build-auto-installer-iso.sh` auto-install.sh embedded script
- **Status**: AWAITING DESIGN
### 3. No TUI animations
- **Severity**: Low
- **Detail**: Would like Claude-style spinner/progress animations during install. May not be possible with bash.
- **Where**: auto-install.sh
- **Status**: TODO (investigate)
### 4. USB read errors on boot
- **Severity**: Medium (cosmetic but bad first impression)
- **Detail**: Read errors scroll on screen during USB boot before installer loads. Scares new users.
- **Where**: Kernel/initramfs boot, possibly `quiet` not suppressing early messages
- **Status**: TODO
### 5. GRUB background tiling + text cutoff
- **Severity**: Medium
- **Detail**: Boot menu background image tiles instead of scaling. Menu text ("Install Archipelago", "Failsafe mode") is cut off.
- **Where**: `branding/grub-theme/`, `boot/grub/grub.cfg`, theme.txt resolution settings
- **Status**: TODO
### 6. USB removal drops to command line
- **Severity**: Medium
- **Detail**: After install completes, removing USB drops to shell before user presses Enter to reboot. Confuses non-technical users.
- **Where**: auto-install.sh — end of install, before `read -s` / `reboot`
- **Status**: TODO
## Frontend / UI (neode-ui)
### 7. Broken splash screen flashes before onboarding
- **Severity**: High
- **Detail**: Black screen with "online/offline" top-right, broken archipelago image top-left, "use arrow keys" text. Flashes briefly before onboarding loads.
- **Where**: Likely `RootRedirect.vue` or `SplashScreen.vue` — routing/transition timing
- **Status**: TODO (reported before, persists)
### 8. Skip buttons still visible in onboarding
- **Severity**: Medium
- **Detail**: Onboarding flow still shows skip buttons. Should be removed for clean UX.
- **Where**: `src/views/onboarding/` components
- **Status**: TODO
### 9. App install UX outdated
- **Severity**: High
- **Detail**: Missing the yellow "Installing..." button that persists across navigation. Apps don't show as "installing" in My Apps view during install.
- **Where**: `src/views/marketplace/`, `src/views/myapps/`, app install store
- **Status**: TODO
### 10. Login requires double Enter
- **Severity**: Medium
- **Detail**: Password field on login page requires pressing Enter twice to submit.
- **Where**: `src/views/LoginView.vue` — form submission handler
- **Status**: TODO (reported before, persists)
### 11. No password setting UI
- **Severity**: High
- **Detail**: No way for user to set/change their password from the web UI. Currently hardcoded `password123`.
- **Where**: Settings view, backend auth API
- **Status**: TODO
### 12. Browser login loops (non-kiosk)
- **Severity**: High
- **Detail**: Logging in from a browser (not kiosk) on the same network redirects back to login in a loop. Kiosk mode works fine.
- **Where**: Auth/session handling — possibly cookie `SameSite` or redirect logic in `RootRedirect.vue`
- **Status**: TODO
### 13. Can't exit input fields with arrow keys
- **Severity**: Medium
- **Detail**: When focused on a text input, up/down arrow keys don't move focus to adjacent UI elements. Stuck in the field.
- **Where**: `useControllerNav.ts` — input field focus trap logic
- **Status**: TODO (reported before, persists)
---
## Summary
| Category | Critical | High | Medium | Low |
|----------|----------|------|--------|-----|
| ISO/Boot | 0 | 1 | 4 | 1 |
| Frontend | 0 | 4 | 3 | 0 |
| **Total** | **0** | **5** | **7** | **1** |

View File

@ -0,0 +1,117 @@
# Archipelago Installer — Screen Designs
Edit these screens to match your vision. I'll implement exactly what you specify.
Each screen is what the user sees at that moment on the console (80 columns wide).
Constraints: bash TUI only (no ncurses). ANSI colors available:
- `\033[1;37m` = bold white, `\033[1;33m` = bold yellow/orange
- `\033[32m` = green, `\033[31m` = red, `\033[37m` = dim gray
- `\033[0m` = reset. Box-drawing chars: ━ ─ │ ╭ ╮ ╰ ╯ ╔ ╗ ╚ ╝ █ ▓ ░ ▌▐
- Spinners possible: ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ or ◐◓◑◒ or |/-\
---
## Screen 1: Welcome / Press Enter
```
(clear screen, centered)
a r c h i p e l a g o
━━━━━━━━━━━━━━━━━━━━━
automatic installer
Press Enter to install | Ctrl+C for shell
```
---
## Screen 2: Detecting Disk
```
a r c h i p e l a g o
━━━━━━━━━━━━━━━━━━━━━
[1/7] Checking tools .............. ✓
[2/7] Detecting disks
Found: /dev/sda (465.8G) — TOSHIBA MQ01ACF0
──────────────────────────────────────────
⚠ All data on /dev/sda will be erased.
Press Enter to install | Ctrl+C to cancel
```
---
## Screen 3: Installing (progress)
```
a r c h i p e l a g o
━━━━━━━━━━━━━━━━━━━━━
[1/7] Checking tools .............. ✓
[2/7] Detecting disks ............. ✓
[3/7] Creating partitions ......... ✓
[4/7] Formatting .................. ✓
[5/7] Installing system ........... ✓
[6/7] Encrypting data partition ◐
AES-256-XTS (AES-NI detected)
──────────────────────────────────────────
```
---
## Screen 4: Bootloader
```
a r c h i p e l a g o
━━━━━━━━━━━━━━━━━━━━━
[1/7] Checking tools .............. ✓
[2/7] Detecting disks ............. ✓
[3/7] Creating partitions ......... ✓
[4/7] Formatting .................. ✓
[5/7] Installing system ........... ✓
[6/7] Encrypting data ............. ✓
[7/7] Installing bootloader ....... ✓
──────────────────────────────────────────
```
---
## Screen 5: Complete
```
a r c h i p e l a g o
━━━━━━━━━━━━━━━━━━━━━
Installation Complete
After reboot, open the Web UI from any device:
http://192.168.1.198
SSH: ssh archipelago@192.168.1.198
Password: archipelago
Web Login: password123
──────────────────────────────────────────
>>> REMOVE THE USB DRIVE NOW <<<
Press Enter to reboot
```
---
## Notes for Dorian
- Edit any screen above to match what you want to see
- Add/remove steps, change wording, change layout
- Specify colors per line if you want (e.g. "this line in yellow")
- I can add a spinner animation on the active step
- Box-drawing, progress bars, anything bash can render is fair game
- Once you're happy with the designs I'll implement them exactly

View File

@ -5,17 +5,18 @@
title-text: "" title-text: ""
desktop-color: "#0a0a0a" desktop-color: "#0a0a0a"
desktop-image: "background.png" desktop-image: "background.png"
desktop-image-scale-method: "stretch"
+ boot_menu { + boot_menu {
left = 25% left = 15%
top = 40% top = 40%
width = 50% width = 70%
height = 30% height = 35%
item_color = "#aaaaaa" item_color = "#aaaaaa"
selected_item_color = "#f7931a" selected_item_color = "#f7931a"
item_height = 36 item_height = 40
item_spacing = 8 item_spacing = 10
item_padding = 16 item_padding = 20
scrollbar = false scrollbar = false
} }

View File

@ -280,6 +280,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
xfonts-base \ xfonts-base \
plymouth \ plymouth \
plymouth-themes \ plymouth-themes \
zstd \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
@ -339,6 +340,13 @@ COPY archipelago-doctor.timer /etc/systemd/system/archipelago-doctor.timer
COPY archipelago-reconcile.service /etc/systemd/system/archipelago-reconcile.service COPY archipelago-reconcile.service /etc/systemd/system/archipelago-reconcile.service
COPY archipelago-reconcile.timer /etc/systemd/system/archipelago-reconcile.timer COPY archipelago-reconcile.timer /etc/systemd/system/archipelago-reconcile.timer
# Copy container doctor + reconcile scripts (referenced by the services above)
RUN mkdir -p /home/archipelago/archy/scripts
COPY container-doctor.sh /home/archipelago/archy/scripts/container-doctor.sh
COPY reconcile-containers.sh /home/archipelago/archy/scripts/reconcile-containers.sh
RUN chmod +x /home/archipelago/archy/scripts/*.sh && \
chown -R archipelago:archipelago /home/archipelago/archy
# Enable services # Enable services
RUN systemctl enable NetworkManager || true && \ RUN systemctl enable NetworkManager || true && \
systemctl enable ssh || true && \ systemctl enable ssh || true && \
@ -409,12 +417,18 @@ NGINXCONF
echo " Using archipelago-update.service + timer from configs/" echo " Using archipelago-update.service + timer from configs/"
fi fi
# Copy container doctor and reconciliation timers # Copy container doctor and reconciliation timers + scripts
if [ -f "$SCRIPT_DIR/configs/archipelago-doctor.service" ]; then if [ -f "$SCRIPT_DIR/configs/archipelago-doctor.service" ]; then
cp "$SCRIPT_DIR/configs/archipelago-doctor.service" "$WORK_DIR/archipelago-doctor.service" cp "$SCRIPT_DIR/configs/archipelago-doctor.service" "$WORK_DIR/archipelago-doctor.service"
cp "$SCRIPT_DIR/configs/archipelago-doctor.timer" "$WORK_DIR/archipelago-doctor.timer" cp "$SCRIPT_DIR/configs/archipelago-doctor.timer" "$WORK_DIR/archipelago-doctor.timer"
cp "$SCRIPT_DIR/configs/archipelago-reconcile.service" "$WORK_DIR/archipelago-reconcile.service" cp "$SCRIPT_DIR/configs/archipelago-reconcile.service" "$WORK_DIR/archipelago-reconcile.service"
cp "$SCRIPT_DIR/configs/archipelago-reconcile.timer" "$WORK_DIR/archipelago-reconcile.timer" cp "$SCRIPT_DIR/configs/archipelago-reconcile.timer" "$WORK_DIR/archipelago-reconcile.timer"
# Copy the actual scripts the services reference
for s in container-doctor.sh reconcile-containers.sh; do
if [ -f "$SCRIPT_DIR/../scripts/$s" ]; then
cp "$SCRIPT_DIR/../scripts/$s" "$WORK_DIR/$s"
fi
done
echo " Using container doctor + reconcile timers from configs/" echo " Using container doctor + reconcile timers from configs/"
fi fi
@ -1106,21 +1120,20 @@ LOG="/var/log/archipelago-tor.log"
mkdir -p "$ARCHY_TOR_DIR" mkdir -p "$ARCHY_TOR_DIR"
# Write services.json for the backend to read # Write services.json for the backend to read
python3 -c ' cat > "\$ARCHY_TOR_DIR/services.json" <<TORJSON
import json {
services = [ "services": [
{"name": "archipelago", "local_port": 80, "enabled": True}, {"name": "archipelago", "local_port": 80, "enabled": true},
{"name": "bitcoin", "local_port": 8333, "enabled": True}, {"name": "bitcoin", "local_port": 8333, "enabled": true},
{"name": "electrumx", "local_port": 50001, "enabled": True}, {"name": "electrumx", "local_port": 50001, "enabled": true},
{"name": "lnd", "local_port": 9735, "enabled": True}, {"name": "lnd", "local_port": 9735, "enabled": true},
{"name": "btcpay", "local_port": 23000, "enabled": True}, {"name": "btcpay", "local_port": 23000, "enabled": true},
{"name": "mempool", "local_port": 4080, "enabled": True}, {"name": "mempool", "local_port": 4080, "enabled": true},
{"name": "fedimint", "local_port": 8175, "enabled": True} {"name": "fedimint", "local_port": 8175, "enabled": true}
] ]
with open("'"$ARCHY_TOR_DIR"'/services.json", "w") as f: }
json.dump({"services": services}, f, indent=2) TORJSON
print("services.json created") echo "services.json created"
'
# Generate torrc — use /var/lib/tor/ for hidden services (AppArmor-safe) # Generate torrc — use /var/lib/tor/ for hidden services (AppArmor-safe)
cat > /etc/tor/torrc <<TORRC cat > /etc/tor/torrc <<TORRC
@ -1437,7 +1450,7 @@ boxline() {
# TUI helpers — Claude Code-inspired status display # TUI helpers — Claude Code-inspired status display
STEP=0 STEP=0
TOTAL_STEPS=7 TOTAL_STEPS=8
step() { step() {
STEP=$((STEP + 1)) STEP=$((STEP + 1))
echo "" echo ""
@ -1863,27 +1876,32 @@ if [ -t 0 ] && [ -z "$ARCHIPELAGO_WELCOMED" ]; then
sleep 2 sleep 2
done done
O='\033[38;5;208m'
D='\033[38;5;242m'
N='\033[0m'
clear clear
echo "" echo ""
echo " a r c h i p e l a g o" echo -e " ${O}█▀█ █▀▄ █▀▀ █ █ █ █▀▄▀█ █▀▀ █ █▀█ █▀▀ █▀█${N}"
echo " ━━━━━━━━━━━━━━━━━━━━━" echo -e " ${O}█▀█ █▀▄ █ █▀█ █ █ ▀ █ ██▀ █ █▀█ █ █ █ █${N}"
echo " bitcoin node os" echo -e " ${O}▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀${N}"
echo -e " ${D}bitcoin node os${N}"
echo "" echo ""
if [ -n "$IP" ]; then if [ -n "$IP" ]; then
echo " Web UI: http://$IP" echo -e " ${D}web ui${N} http://$IP"
echo " SSH: ssh archipelago@$IP" echo -e " ${D}ssh${N} archipelago@$IP"
echo " Password: archipelago (SSH) / password123 (Web)" echo -e " ${D}password${N} archipelago (SSH) / password123 (Web)"
else else
echo " Waiting for network..." echo -e " ${D}Waiting for network...${N}"
fi fi
echo "" echo ""
if [ -b /dev/mapper/archipelago-data ]; then if [ -b /dev/mapper/archipelago-data ]; then
echo " Storage: LUKS2 encrypted" echo -e " ${D}storage${N} LUKS2 encrypted"
fi fi
if systemctl is-active archipelago-kiosk.service >/dev/null 2>&1; then if systemctl is-active archipelago-kiosk.service >/dev/null 2>&1; then
echo " Display: Kiosk active (Ctrl+Alt+F1 for terminal)" echo -e " ${D}display${N} Kiosk active (Ctrl+Alt+F1 for terminal)"
else else
echo " Display: Console (Ctrl+Alt+F7 for kiosk)" echo -e " ${D}display${N} Console (Ctrl+Alt+F7 for kiosk)"
fi fi
echo "" echo ""
fi fi
@ -2540,6 +2558,28 @@ echo ""
# Suppress kernel messages on console (SquashFS errors when USB is pulled) # Suppress kernel messages on console (SquashFS errors when USB is pulled)
echo 1 > /proc/sys/kernel/printk 2>/dev/null || true echo 1 > /proc/sys/kernel/printk 2>/dev/null || true
# Copy reboot script to tmpfs so it survives USB removal
cat > /tmp/archipelago-reboot.sh <<'REBOOTSCRIPT'
#!/bin/bash
# This script runs from tmpfs — safe after USB removal
TW=$(tput cols 2>/dev/null || echo 60)
[ "$TW" -gt 120 ] && TW=120
cc() { local s=$(echo -e "$1" | sed 's/\x1b\[[0-9;]*m//g'); local p=$(( (TW - ${#s}) / 2 )); [ $p -lt 0 ] && p=0; printf "%*s" "$p" ""; echo -e "$1"; }
cc "\033[1;33m>>> REMOVE THE USB DRIVE NOW <<<\033[0m"
echo ""
cc "\033[37mPress Enter to reboot (or wait 30 seconds)\033[0m"
# Wait for Enter or timeout
read -t 30 -s 2>/dev/null || true
echo ""
cc "\033[37mRebooting...\033[0m"
sleep 1
reboot -f
REBOOTSCRIPT
chmod +x /tmp/archipelago-reboot.sh
# Lazy-unmount live filesystem BEFORE telling user to pull USB # Lazy-unmount live filesystem BEFORE telling user to pull USB
exec 2>/dev/null exec 2>/dev/null
umount -l /run/live/medium 2>/dev/null || true umount -l /run/live/medium 2>/dev/null || true
@ -2553,13 +2593,8 @@ if [ -n "$BOOT_DEV" ]; then
fi fi
exec 2>&1 exec 2>&1
cc "${YELLOW}>>> REMOVE THE USB DRIVE <<<${NC}" # Hand off to tmpfs-based script — survives USB removal
echo "" exec /bin/bash /tmp/archipelago-reboot.sh
cc "${DIM}Rebooting in 10 seconds...${NC}"
sleep 10
# Force reboot — skips systemd clean shutdown (which fails on missing USB)
reboot -f
INSTALLER_SCRIPT INSTALLER_SCRIPT
# For unbundled builds, patch the completion message to reflect no pre-loaded apps # For unbundled builds, patch the completion message to reflect no pre-loaded apps
@ -2601,7 +2636,7 @@ set default=0
# Load font for graphical menu # Load font for graphical menu
if loadfont ($root)/boot/grub/font.pf2; then if loadfont ($root)/boot/grub/font.pf2; then
set gfxmode=auto set gfxmode=1024x768,auto
insmod gfxterm insmod gfxterm
insmod png insmod png
terminal_output gfxterm terminal_output gfxterm
@ -2620,12 +2655,12 @@ else
fi fi
menuentry "Install Archipelago" --hotkey=i { menuentry "Install Archipelago" --hotkey=i {
linux ($root)/live/vmlinuz boot=live components quiet splash linux ($root)/live/vmlinuz boot=live components quiet splash loglevel=0 rd.systemd.show_status=false vt.global_cursor_default=0
initrd ($root)/live/initrd.img initrd ($root)/live/initrd.img
} }
menuentry "Install Archipelago (verbose)" --hotkey=v { menuentry "Install Archipelago (verbose)" --hotkey=v {
linux ($root)/live/vmlinuz boot=live components console=ttyS0,115200 console=tty0 linux ($root)/live/vmlinuz boot=live components loglevel=4 console=ttyS0,115200 console=tty0
initrd ($root)/live/initrd.img initrd ($root)/live/initrd.img
} }
@ -2688,13 +2723,13 @@ DEFAULT install
LABEL install LABEL install
MENU LABEL Install Archipelago MENU LABEL Install Archipelago
KERNEL /live/vmlinuz KERNEL /live/vmlinuz
APPEND initrd=/live/initrd.img boot=live components quiet APPEND initrd=/live/initrd.img boot=live components quiet loglevel=0 rd.systemd.show_status=false vt.global_cursor_default=0
MENU DEFAULT MENU DEFAULT
LABEL install-verbose LABEL install-verbose
MENU LABEL Install (verbose output) MENU LABEL Install (verbose output)
KERNEL /live/vmlinuz KERNEL /live/vmlinuz
APPEND initrd=/live/initrd.img boot=live components APPEND initrd=/live/initrd.img boot=live components loglevel=4
LABEL local LABEL local
MENU LABEL Boot from local disk MENU LABEL Boot from local disk

View File

@ -4,7 +4,8 @@
<SplashScreen v-if="showSplash" @complete="handleSplashComplete" /> <SplashScreen v-if="showSplash" @complete="handleSplashComplete" />
<!-- Main App Content - only show after splash and routing is complete --> <!-- 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) --> <!-- Spotlight command palette (Cmd+K / Ctrl+K) -->
<SpotlightSearch /> <SpotlightSearch />
@ -211,10 +212,11 @@ onMounted(async () => {
showSplash.value = true showSplash.value = true
} else { } else {
// Already seen intro, direct route, or boot mode (boot screen handles intro) // Already seen intro, direct route, or boot mode (boot screen handles intro)
showSplash.value = false // Set isReady BEFORE hiding splash to prevent flash of partial content
document.body.classList.add('splash-complete')
await router.isReady() await router.isReady()
isReady.value = true 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 // Up/Down: exit field, navigate spatially
e.preventDefault() e.preventDefault()
const dir = e.key === 'ArrowDown' ? 'down' as const : 'up' as const 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) 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 return
} }
// Left/Right: stay in field (cursor movement). Escape: handled below. // 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] const next = items[nextIdx]
if (next && next !== activeEl) { if (next && next !== activeEl) {
focusEl(next) focusEl(next)
// Auto-navigate sidebar links // Auto-navigate sidebar links (not buttons — Logout etc. require Enter)
if (next.tagName === 'A') { if (next.tagName === 'A') {
const href = (next as HTMLAnchorElement).getAttribute('href') const href = (next as HTMLAnchorElement).getAttribute('href')
if (href?.startsWith('/')) router.push(href).catch(() => {}) if (href?.startsWith('/')) router.push(href).catch(() => {})
@ -493,10 +501,14 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
function autoFocusMain() { function autoFocusMain() {
const active = document.activeElement as HTMLElement | null 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 (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')) return
if (document.querySelector('[role="dialog"]')) return if (document.querySelector('[role="dialog"]')) return
if (isInZone(active, 'sidebar')) return
requestAnimationFrame(() => { requestAnimationFrame(() => {
// Re-check sidebar after RAF — user may still be navigating
if (isInZone(document.activeElement as HTMLElement, 'sidebar')) return
const remembered = recallFocus('main') const remembered = recallFocus('main')
if (remembered) { remembered.focus({ preventScroll: true }); return } if (remembered) { remembered.focus({ preventScroll: true }); return }
const containers = getContainers() const containers = getContainers()

View File

@ -56,7 +56,7 @@
autocomplete="off" 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" 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')" :placeholder="t('login.enterPasswordSetup')"
@keyup.enter="handleSetupWithSound" @keydown.enter="handleSetupWithSound"
:disabled="loading || formDisabled" :disabled="loading || formDisabled"
/> />
</div> </div>
@ -72,7 +72,7 @@
autocomplete="off" 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" 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')" :placeholder="t('login.confirmPasswordPlaceholder')"
@keyup.enter="handleSetupWithSound" @keydown.enter="handleSetupWithSound"
:disabled="loading || formDisabled" :disabled="loading || formDisabled"
/> />
</div> </div>
@ -156,7 +156,7 @@
autocomplete="off" 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" 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')" :placeholder="t('login.enterPasswordPlaceholder')"
@keyup.enter="handleLoginWithSound" @keydown.enter="handleLoginWithSound"
:disabled="loading || formDisabled" :disabled="loading || formDisabled"
/> />
</div> </div>
@ -424,6 +424,14 @@ async function handleLogin() {
setTimeout(() => totpInputRef.value?.focus(), 100) setTimeout(() => totpInputRef.value?.focus(), 100)
return 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() stopSynthwave()
whooshAway.value = true whooshAway.value = true
playLoginSuccessWhoosh() playLoginSuccessWhoosh()

View File

@ -15,7 +15,7 @@
<!-- Content Area --> <!-- 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="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"> <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> <p v-else-if="errorMessage" class="text-red-400 text-sm">{{ errorMessage }}</p>
<!-- Passphrase Input --> <!-- Passphrase Input -->
<div class="path-option-card cursor-default px-4 py-4 sm:px-6 sm:py-6"> <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 --> <!-- Content Area -->
<div class="flex flex-col items-center gap-6 mb-6"> <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> <p v-else-if="errorMessage" class="text-red-400 text-sm">{{ errorMessage }}</p>
<!-- Sign Button (if not verified yet) --> <!-- Sign Button (if not verified yet) -->
<button <button
@ -127,7 +127,7 @@ async function signChallenge() {
if (isRetryable) { if (isRetryable) {
serverStarting.value = true serverStarting.value = true
} else { } 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 { } else {
await new Promise((r) => setTimeout(r, 1000 * (attempt + 1))) await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)))

View File

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