fix: production onboarding, CI tests, container security, keyboard nav
Some checks failed
Build Archipelago ISO / build-iso (push) Has been cancelled

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 b684c2972e
commit 127a36c5c8
16 changed files with 1134 additions and 552 deletions

View File

@@ -6,7 +6,7 @@ vi.stubGlobal('fetch', mockFetch)
// FileBrowserClient reads window.location.origin in constructor, so stub it
Object.defineProperty(window, 'location', {
value: { origin: 'http://localhost', protocol: 'http:', hostname: 'localhost' },
value: { origin: 'http://localhost', protocol: 'http:', hostname: 'localhost', pathname: '/app/filebrowser' },
writable: true,
})
@@ -34,9 +34,17 @@ function jsonResponse(body: unknown, status = 200): Response {
}
}
/** Set up authenticated state — bypasses jsdom cookie path restrictions */
function setAuthenticated() {
;(fileBrowserClient as any)._authenticated = true
document.cookie = 'auth=test-token'
}
describe('FileBrowserClient', () => {
beforeEach(() => {
mockFetch.mockReset()
;(fileBrowserClient as any)._authenticated = false
document.cookie = 'auth=; expires=Thu, 01 Jan 1970 00:00:00 GMT'
})
describe('login', () => {
@@ -75,9 +83,7 @@ describe('FileBrowserClient', () => {
describe('listDirectory', () => {
it('lists items in a directory', async () => {
// Ensure authenticated first
mockFetch.mockResolvedValueOnce(jsonResponse('"token"'))
await fileBrowserClient.login()
setAuthenticated()
const mockItems = {
items: [
@@ -98,8 +104,7 @@ describe('FileBrowserClient', () => {
})
it('adds leading slash if missing', async () => {
mockFetch.mockResolvedValueOnce(jsonResponse('"token"'))
await fileBrowserClient.login()
setAuthenticated()
mockFetch.mockResolvedValueOnce(jsonResponse({ items: [], numDirs: 0, numFiles: 0, sorting: { by: 'name', asc: true } }))
@@ -110,8 +115,7 @@ describe('FileBrowserClient', () => {
})
it('throws on non-OK response', async () => {
mockFetch.mockResolvedValueOnce(jsonResponse('"token"'))
await fileBrowserClient.login()
setAuthenticated()
mockFetch.mockResolvedValueOnce(jsonResponse(null, 404))
@@ -135,8 +139,7 @@ describe('FileBrowserClient', () => {
describe('upload', () => {
it('uploads a file to the correct path', async () => {
mockFetch.mockResolvedValueOnce(jsonResponse('"token"'))
await fileBrowserClient.login()
setAuthenticated()
mockFetch.mockResolvedValueOnce(jsonResponse(null, 200))
const file = new File(['hello'], 'test.txt', { type: 'text/plain' })
@@ -151,8 +154,7 @@ describe('FileBrowserClient', () => {
})
it('throws on upload failure', async () => {
mockFetch.mockResolvedValueOnce(jsonResponse('"token"'))
await fileBrowserClient.login()
setAuthenticated()
mockFetch.mockResolvedValueOnce(jsonResponse('Disk full', 507))
const file = new File(['data'], 'big.bin')
@@ -163,8 +165,7 @@ describe('FileBrowserClient', () => {
describe('createFolder', () => {
it('creates a folder at the correct path', async () => {
mockFetch.mockResolvedValueOnce(jsonResponse('"token"'))
await fileBrowserClient.login()
setAuthenticated()
mockFetch.mockResolvedValueOnce(jsonResponse(null, 200))
@@ -176,8 +177,7 @@ describe('FileBrowserClient', () => {
})
it('throws on failure', async () => {
mockFetch.mockResolvedValueOnce(jsonResponse('"token"'))
await fileBrowserClient.login()
setAuthenticated()
mockFetch.mockResolvedValueOnce(jsonResponse(null, 500))
@@ -187,8 +187,7 @@ describe('FileBrowserClient', () => {
describe('deleteItem', () => {
it('sends DELETE request for the item', async () => {
mockFetch.mockResolvedValueOnce(jsonResponse('"token"'))
await fileBrowserClient.login()
setAuthenticated()
mockFetch.mockResolvedValueOnce(jsonResponse(null, 200))
@@ -200,8 +199,7 @@ describe('FileBrowserClient', () => {
})
it('throws on failure', async () => {
mockFetch.mockResolvedValueOnce(jsonResponse('"token"'))
await fileBrowserClient.login()
setAuthenticated()
mockFetch.mockResolvedValueOnce(jsonResponse(null, 403))
@@ -211,9 +209,7 @@ describe('FileBrowserClient', () => {
describe('getUsage', () => {
it('returns usage summary for root directory', async () => {
// Login first
mockFetch.mockResolvedValueOnce(jsonResponse('"token"'))
await fileBrowserClient.login()
setAuthenticated()
const mockData = {
items: [
@@ -235,8 +231,7 @@ describe('FileBrowserClient', () => {
})
it('returns zeros on failed request', async () => {
mockFetch.mockResolvedValueOnce(jsonResponse('"token"'))
await fileBrowserClient.login()
setAuthenticated()
mockFetch.mockResolvedValueOnce(jsonResponse(null, 500))
@@ -264,8 +259,7 @@ describe('FileBrowserClient', () => {
describe('rename', () => {
it('sends PATCH request with new destination', async () => {
mockFetch.mockResolvedValueOnce(jsonResponse('"token"'))
await fileBrowserClient.login()
setAuthenticated()
mockFetch.mockResolvedValueOnce(jsonResponse(null, 200))
@@ -278,8 +272,7 @@ describe('FileBrowserClient', () => {
})
it('throws on rename failure', async () => {
mockFetch.mockResolvedValueOnce(jsonResponse('"token"'))
await fileBrowserClient.login()
setAuthenticated()
mockFetch.mockResolvedValueOnce(jsonResponse(null, 409))