fix: auth, container resilience, ISO build, gamepad polish

- 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:
Dorian
2026-03-30 13:35:02 +01:00
parent 68f2a9c5bf
commit fdd69ce1b5
16 changed files with 218 additions and 88 deletions

View File

@@ -78,11 +78,22 @@ class RPCClient {
}
throw new Error('Session expired')
}
// CSRF 403: retry twice after delay (cookie may have been
// updated by a concurrent Set-Cookie response not yet visible to JS)
if (response.status === 403 && attempt < maxRetries - 1) {
await new Promise((r) => setTimeout(r, 500))
continue
// 403: read body to distinguish CSRF (retryable) from RBAC (permanent)
if (response.status === 403) {
let reason = ''
try {
const body: RPCResponse<unknown> = await response.json()
reason = body.error?.message || ''
} catch { /* body parse failed */ }
const isCsrf = !reason || reason.toLowerCase().includes('csrf')
if (isCsrf && attempt < maxRetries - 1) {
// CSRF mismatch — cookie may have been updated by a concurrent
// Set-Cookie response not yet visible to JS. Retry after delay.
await new Promise((r) => setTimeout(r, 500))
continue
}
throw new Error(reason || `HTTP 403: Forbidden`)
}
const err = new Error(`HTTP ${response.status}: ${response.statusText}`)
const isRetryable = response.status === 502 || response.status === 503