fix: production onboarding, CI tests, container security, keyboard nav

Install & Onboarding:
- Remove DEV_MODE=true from production ISO service file (auto-created
  users, skipped password setup)
- Auto-install no longer overwrites rootfs service file with bad template
- Login.vue always checks auth.isSetup — shows password creation form
  on fresh install without requiring dev build flag
- Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes
- First-boot-containers sources image-versions.sh, runs podman as
  archipelago user (rootless), enables linger + podman.socket
- Correct volume ownership (100000:100000 for rootless UID mapping)

Container Security:
- FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access
- FileBrowser: add --read-only, /data volume for database, proper cmd args
- First-boot script matches backend config (security hardening + health check)

CI Pipeline:
- Add vue-tsc type check + vitest run to build-iso.yml (runs every push)
- Add post-install-tests.yml workflow (workflow_dispatch, SSH to target)
- Build report: set +eo pipefail, fix rootfs path, add || true guards
- Bundle run-post-install-tests.sh into ISO

E2E Test Suite (scripts/run-post-install-tests.sh):
- Phase 1: Install verification (files, services, podman, linger, DEV_MODE check)
- Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete)
- Phase 3: Container lifecycle (install 3 apps via package.install RPC,
  verify running, stop, verify stopped, restart, verify running, health)
- Phase 4: Log verification (first-boot log, diagnostics, journal errors)
- Correct package.install params: {"id", "dockerImage"}

Frontend:
- Fix backdrop-filter tab-switch bug (keep animations paused during rebuild)
- Dashboard glitch animations paused during tab-hidden
- Gamepad nav: auto-focus first container on route change
- Tab roving: Left/Right on role="tab" cycles and activates sibling tabs
- ContainerApps: data-controller-launch on running app cards
- 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-27 16:16:57 +00:00
parent bf14f9e5ad
commit 7cd4d90ed8
16 changed files with 1134 additions and 552 deletions

View File

@@ -283,6 +283,28 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
isControllerActive.value = gamepadCount.value > 0
}, 3000)
// Tab roving: Left/Right on role="tab" switches to sibling tab and activates it
if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && activeEl?.getAttribute('role') === 'tab') {
const tablist = activeEl.closest('[role="tablist"]') ?? activeEl.parentElement
if (tablist) {
const tabs = Array.from(tablist.querySelectorAll<HTMLElement>('[role="tab"]:not([disabled])'))
const idx = tabs.indexOf(activeEl)
if (idx >= 0) {
const nextIdx = e.key === 'ArrowRight'
? (idx + 1) % tabs.length
: (idx - 1 + tabs.length) % tabs.length
const next = tabs[nextIdx]
if (next) {
playNavSound('move')
next.focus()
next.click()
e.preventDefault()
return
}
}
}
}
const sidebarEls = getElementsInZone('sidebar')
const mainEls = getElementsInZone('main')
const hasZones = sidebarEls.length > 0 && mainEls.length > 0
@@ -460,6 +482,28 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
}
}
/** Auto-focus first main container on route change (game-style: always have something selected) */
function autoFocusMain() {
// Don't steal focus from inputs or modals
const active = document.activeElement as HTMLElement | null
if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')) return
if (document.querySelector('[role="dialog"]')) return
requestAnimationFrame(() => {
const mainZone = document.querySelector('[data-controller-zone="main"]')
if (!mainZone) return
const firstContainer = mainZone.querySelector<HTMLElement>('[data-controller-container]')
if (firstContainer) {
firstContainer.focus({ preventScroll: true })
}
})
}
watch(() => route.path, () => {
// Small delay to let Vue render the new route's DOM
setTimeout(autoFocusMain, 150)
})
onMounted(() => {
checkGamepads()
window.addEventListener('keydown', handleKeyDown, true)
@@ -467,6 +511,8 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
window.addEventListener('gamepadconnected', handleGamepadConnected)
window.addEventListener('gamepaddisconnected', handleGamepadDisconnected)
pollIntervalId = setInterval(handleGamepadInput, 500)
// Initial auto-focus after mount
setTimeout(autoFocusMain, 300)
})
onBeforeUnmount(() => {