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:
96
docs/BETA-ISSUES-20260328.md
Normal file
96
docs/BETA-ISSUES-20260328.md
Normal 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** |
|
||||
117
docs/INSTALL-SCREENS-DESIGN.md
Normal file
117
docs/INSTALL-SCREENS-DESIGN.md
Normal 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
|
||||
@@ -5,17 +5,18 @@
|
||||
title-text: ""
|
||||
desktop-color: "#0a0a0a"
|
||||
desktop-image: "background.png"
|
||||
desktop-image-scale-method: "stretch"
|
||||
|
||||
+ boot_menu {
|
||||
left = 25%
|
||||
left = 15%
|
||||
top = 40%
|
||||
width = 50%
|
||||
height = 30%
|
||||
width = 70%
|
||||
height = 35%
|
||||
item_color = "#aaaaaa"
|
||||
selected_item_color = "#f7931a"
|
||||
item_height = 36
|
||||
item_spacing = 8
|
||||
item_padding = 16
|
||||
item_height = 40
|
||||
item_spacing = 10
|
||||
item_padding = 20
|
||||
scrollbar = false
|
||||
}
|
||||
|
||||
|
||||
@@ -280,6 +280,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
xfonts-base \
|
||||
plymouth \
|
||||
plymouth-themes \
|
||||
zstd \
|
||||
&& apt-get clean \
|
||||
&& 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.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
|
||||
RUN systemctl enable NetworkManager || true && \
|
||||
systemctl enable ssh || true && \
|
||||
@@ -409,12 +417,18 @@ NGINXCONF
|
||||
echo " Using archipelago-update.service + timer from configs/"
|
||||
fi
|
||||
|
||||
# Copy container doctor and reconciliation timers
|
||||
# Copy container doctor and reconciliation timers + scripts
|
||||
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.timer" "$WORK_DIR/archipelago-doctor.timer"
|
||||
cp "$SCRIPT_DIR/configs/archipelago-reconcile.service" "$WORK_DIR/archipelago-reconcile.service"
|
||||
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/"
|
||||
fi
|
||||
|
||||
@@ -1106,21 +1120,20 @@ LOG="/var/log/archipelago-tor.log"
|
||||
mkdir -p "$ARCHY_TOR_DIR"
|
||||
|
||||
# Write services.json for the backend to read
|
||||
python3 -c '
|
||||
import json
|
||||
services = [
|
||||
{"name": "archipelago", "local_port": 80, "enabled": True},
|
||||
{"name": "bitcoin", "local_port": 8333, "enabled": True},
|
||||
{"name": "electrumx", "local_port": 50001, "enabled": True},
|
||||
{"name": "lnd", "local_port": 9735, "enabled": True},
|
||||
{"name": "btcpay", "local_port": 23000, "enabled": True},
|
||||
{"name": "mempool", "local_port": 4080, "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)
|
||||
print("services.json created")
|
||||
'
|
||||
cat > "\$ARCHY_TOR_DIR/services.json" <<TORJSON
|
||||
{
|
||||
"services": [
|
||||
{"name": "archipelago", "local_port": 80, "enabled": true},
|
||||
{"name": "bitcoin", "local_port": 8333, "enabled": true},
|
||||
{"name": "electrumx", "local_port": 50001, "enabled": true},
|
||||
{"name": "lnd", "local_port": 9735, "enabled": true},
|
||||
{"name": "btcpay", "local_port": 23000, "enabled": true},
|
||||
{"name": "mempool", "local_port": 4080, "enabled": true},
|
||||
{"name": "fedimint", "local_port": 8175, "enabled": true}
|
||||
]
|
||||
}
|
||||
TORJSON
|
||||
echo "services.json created"
|
||||
|
||||
# Generate torrc — use /var/lib/tor/ for hidden services (AppArmor-safe)
|
||||
cat > /etc/tor/torrc <<TORRC
|
||||
@@ -1437,7 +1450,7 @@ boxline() {
|
||||
|
||||
# TUI helpers — Claude Code-inspired status display
|
||||
STEP=0
|
||||
TOTAL_STEPS=7
|
||||
TOTAL_STEPS=8
|
||||
step() {
|
||||
STEP=$((STEP + 1))
|
||||
echo ""
|
||||
@@ -1863,27 +1876,32 @@ if [ -t 0 ] && [ -z "$ARCHIPELAGO_WELCOMED" ]; then
|
||||
sleep 2
|
||||
done
|
||||
|
||||
O='\033[38;5;208m'
|
||||
D='\033[38;5;242m'
|
||||
N='\033[0m'
|
||||
|
||||
clear
|
||||
echo ""
|
||||
echo " a r c h i p e l a g o"
|
||||
echo " ━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " bitcoin node os"
|
||||
echo -e " ${O}█▀█ █▀▄ █▀▀ █ █ █ █▀▄▀█ █▀▀ █ █▀█ █▀▀ █▀█${N}"
|
||||
echo -e " ${O}█▀█ █▀▄ █ █▀█ █ █ ▀ █ ██▀ █ █▀█ █ █ █ █${N}"
|
||||
echo -e " ${O}▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀${N}"
|
||||
echo -e " ${D}bitcoin node os${N}"
|
||||
echo ""
|
||||
if [ -n "$IP" ]; then
|
||||
echo " Web UI: http://$IP"
|
||||
echo " SSH: ssh archipelago@$IP"
|
||||
echo " Password: archipelago (SSH) / password123 (Web)"
|
||||
echo -e " ${D}web ui${N} http://$IP"
|
||||
echo -e " ${D}ssh${N} archipelago@$IP"
|
||||
echo -e " ${D}password${N} archipelago (SSH) / password123 (Web)"
|
||||
else
|
||||
echo " Waiting for network..."
|
||||
echo -e " ${D}Waiting for network...${N}"
|
||||
fi
|
||||
echo ""
|
||||
if [ -b /dev/mapper/archipelago-data ]; then
|
||||
echo " Storage: LUKS2 encrypted"
|
||||
echo -e " ${D}storage${N} LUKS2 encrypted"
|
||||
fi
|
||||
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
|
||||
echo " Display: Console (Ctrl+Alt+F7 for kiosk)"
|
||||
echo -e " ${D}display${N} Console (Ctrl+Alt+F7 for kiosk)"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
@@ -2540,6 +2558,28 @@ echo ""
|
||||
# Suppress kernel messages on console (SquashFS errors when USB is pulled)
|
||||
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
|
||||
exec 2>/dev/null
|
||||
umount -l /run/live/medium 2>/dev/null || true
|
||||
@@ -2553,13 +2593,8 @@ if [ -n "$BOOT_DEV" ]; then
|
||||
fi
|
||||
exec 2>&1
|
||||
|
||||
cc "${YELLOW}>>> REMOVE THE USB DRIVE <<<${NC}"
|
||||
echo ""
|
||||
cc "${DIM}Rebooting in 10 seconds...${NC}"
|
||||
sleep 10
|
||||
|
||||
# Force reboot — skips systemd clean shutdown (which fails on missing USB)
|
||||
reboot -f
|
||||
# Hand off to tmpfs-based script — survives USB removal
|
||||
exec /bin/bash /tmp/archipelago-reboot.sh
|
||||
INSTALLER_SCRIPT
|
||||
|
||||
# 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
|
||||
if loadfont ($root)/boot/grub/font.pf2; then
|
||||
set gfxmode=auto
|
||||
set gfxmode=1024x768,auto
|
||||
insmod gfxterm
|
||||
insmod png
|
||||
terminal_output gfxterm
|
||||
@@ -2620,12 +2655,12 @@ else
|
||||
fi
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -2688,13 +2723,13 @@ DEFAULT install
|
||||
LABEL install
|
||||
MENU LABEL Install Archipelago
|
||||
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
|
||||
|
||||
LABEL install-verbose
|
||||
MENU LABEL Install (verbose output)
|
||||
KERNEL /live/vmlinuz
|
||||
APPEND initrd=/live/initrd.img boot=live components
|
||||
APPEND initrd=/live/initrd.img boot=live components loglevel=4
|
||||
|
||||
LABEL local
|
||||
MENU LABEL Boot from local disk
|
||||
|
||||
@@ -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