diff --git a/core/security/launch_lock.py b/core/security/launch_lock.py index 8b8ce52a..2a0a75af 100644 --- a/core/security/launch_lock.py +++ b/core/security/launch_lock.py @@ -30,6 +30,9 @@ from __future__ import annotations _ALLOWED_GET = frozenset({ '/api/profiles', # profile picker list (multi-profile launch) '/api/profiles/current', # how the frontend detects the lock state + '/api/setup/status', # #842: first-run check runs before the PIN screen; + # blocking it made the frontend think setup wasn't + # done and re-launch the wizard on every visit. }) # POST endpoints that drive selection + unlock. Selecting a profile only sets diff --git a/tests/test_launch_lock_gate.py b/tests/test_launch_lock_gate.py index 1e897815..9dc062c6 100644 --- a/tests/test_launch_lock_gate.py +++ b/tests/test_launch_lock_gate.py @@ -77,6 +77,12 @@ def test_locked_allows_unlock_flow(): assert L('/api/profiles/logout', 'POST') is False +def test_locked_allows_setup_status(): + # #842: the first-run check runs before the PIN screen. Blocking it made the + # frontend think setup was incomplete and relaunch the wizard every visit. + assert L('/api/setup/status', 'GET') is False + + def test_locked_allows_keyauthed_public_api(): # The public REST API carries its own @require_api_key, so a launch-locked # UI must not break a legitimate headless key holder. diff --git a/webui/static/init.js b/webui/static/init.js index fe4779c5..57af415c 100644 --- a/webui/static/init.js +++ b/webui/static/init.js @@ -2072,10 +2072,16 @@ document.addEventListener('DOMContentLoaded', async function () { if (!forceSetup) { try { const setupResp = await fetch('/api/setup/status'); - const setupData = await setupResp.json(); - if (!setupData.setup_complete) { - showWizard = true; - localStorage.removeItem('soulsync_setup_complete'); + // Fail-safe (#842): only launch the wizard when the server DEFINITIVELY + // says setup isn't done. A non-OK response (e.g. 401 while the launch + // PIN is locked) must NOT trigger the wizard — otherwise a PIN-gated + // returning user gets the full setup flow every visit. + if (setupResp.ok) { + const setupData = await setupResp.json(); + if (setupData.setup_complete === false) { + showWizard = true; + localStorage.removeItem('soulsync_setup_complete'); + } } } catch (e) { console.warn('Setup status check failed, continuing normal init:', e);