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:
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user