fix: auth, container resilience, ISO build, gamepad polish
Some checks failed
Some checks failed
- fix: login disconnect — verify session before WebSocket connect - fix: 403 on app install — distinguish CSRF vs RBAC errors, only retry CSRF - fix: health monitor now watches ALL containers (removed skip list for backend services like nbxplorer, databases, UI containers) - fix: server.get-state added to CSRF-exempt list (read-only) - fix: ISO build includes container-specs.sh and lib/common.sh in rootfs so reconcile actually works on fresh installs - fix: gamepad nav — improved Server tab zone nav, focus styles, autofocus - chore: move L484 web-only apps to Services tab - chore: install store for cross-view install tracking Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -436,6 +436,9 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
|
||||
focusEl(retryContainers[0])
|
||||
} else if (attempts >= 10) {
|
||||
clearInterval(poll)
|
||||
// No containers on this page (e.g. Settings) — focus first focusable element
|
||||
const z = document.querySelector('[data-controller-zone="main"]') as HTMLElement | null
|
||||
if (z) { const f = getFocusableElements(z); if (f[0]) focusEl(f[0]) }
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
@@ -475,16 +478,30 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
|
||||
}
|
||||
|
||||
if (dir === 'down') {
|
||||
// Down from nav bar → first container
|
||||
// Down from nav bar → jump to containers (remember tab for Up return)
|
||||
rememberFocus('navBar', activeEl)
|
||||
const containers = getContainers()
|
||||
const nearest = findNearestInDirection(activeEl, containers, 'down')
|
||||
if (nearest) { rememberFocus('main', nearest); focusEl(nearest); return }
|
||||
// Fallback: just focus first container
|
||||
if (containers[0]) { rememberFocus('main', containers[0]); focusEl(containers[0]) }
|
||||
if (containers[0]) { rememberFocus('main', containers[0]); focusEl(containers[0]); return }
|
||||
// Containers not rendered yet — poll until they appear
|
||||
let attempts = 0
|
||||
const poll = setInterval(() => {
|
||||
attempts++
|
||||
const retryContainers = getContainers()
|
||||
if (retryContainers[0]) {
|
||||
clearInterval(poll)
|
||||
rememberFocus('main', retryContainers[0])
|
||||
focusEl(retryContainers[0])
|
||||
} else if (attempts >= 10) {
|
||||
clearInterval(poll)
|
||||
}
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
|
||||
// Up from nav bar → nothing
|
||||
// Up from nav bar → nothing (use Escape to go to sidebar)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -500,15 +517,26 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
|
||||
return
|
||||
}
|
||||
|
||||
// Up from top-row container → nav bar (if exists)
|
||||
// Up from top-row container → nav bar, or previous focusable (linear pages like Settings)
|
||||
if (dir === 'up') {
|
||||
const remembered = recallFocus('navBar')
|
||||
if (remembered) { focusEl(remembered); return }
|
||||
const navItems = getNavBarItems()
|
||||
if (navItems.length) {
|
||||
const nearest = findNearestInDirection(activeEl, navItems, 'up')
|
||||
if (nearest) { focusEl(nearest); return }
|
||||
// Fallback: first nav bar item
|
||||
const first = navItems[0]
|
||||
if (first) focusEl(first)
|
||||
if (first) { focusEl(first); return }
|
||||
}
|
||||
// No nav bar items — try any focusable element above (linear page nav)
|
||||
const zone = document.querySelector('[data-controller-zone="main"]') as HTMLElement | null
|
||||
if (zone) {
|
||||
const allFocusable = getFocusableElements(zone).filter(el =>
|
||||
el.hasAttribute('data-controller-container') ||
|
||||
!el.closest('[data-controller-container]')
|
||||
)
|
||||
const above = findNearestInDirection(activeEl, allFocusable, 'up')
|
||||
if (above) { rememberFocus('main', above); focusEl(above) }
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -524,12 +552,15 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
|
||||
return
|
||||
}
|
||||
|
||||
// At grid edges: try all focusable elements in main zone as fallback
|
||||
// (prevents dead ends when spatial nav between containers fails)
|
||||
// At grid edges: try containers + nav bar items as fallback
|
||||
// (prevents dead ends, but never jumps into container inner controls)
|
||||
if (dir === 'down' || dir === 'right') {
|
||||
const zone = document.querySelector('[data-controller-zone="main"]') as HTMLElement | null
|
||||
if (zone) {
|
||||
const allFocusable = getFocusableElements(zone)
|
||||
const allFocusable = getFocusableElements(zone).filter(el =>
|
||||
el.hasAttribute('data-controller-container') ||
|
||||
!el.closest('[data-controller-container]')
|
||||
)
|
||||
const fallback = findNearestInDirection(activeEl, allFocusable, dir)
|
||||
if (fallback) {
|
||||
rememberFocus('main', fallback)
|
||||
@@ -550,7 +581,11 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
|
||||
const target = activeTab ?? sidebar[0]
|
||||
if (target) { rememberFocus('main', activeEl); focusEl(target) }
|
||||
} else {
|
||||
const all = getFocusableElements()
|
||||
// Exclude container inner buttons to prevent focus getting lost
|
||||
const all = getFocusableElements().filter(el =>
|
||||
el.hasAttribute('data-controller-container') ||
|
||||
!el.closest('[data-controller-container]')
|
||||
)
|
||||
const next = findNearestInDirection(activeEl, all, dir)
|
||||
if (next) focusEl(next)
|
||||
}
|
||||
@@ -607,6 +642,7 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
|
||||
|
||||
watch(() => route.path, () => {
|
||||
zoneFocusMemory.delete('main')
|
||||
zoneFocusMemory.delete('navBar')
|
||||
setTimeout(autoFocusMain, 150)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user