Просмотр исходного кода

fix(install): make SpoolBuddy kiosk usable on first boot in full-mode install

  Full-mode install booted into an unusable kiosk:
  - Chromium opened before uvicorn → "can't connect to localhost"
  - After reload, requires_setup=true hijacked /spoolbuddy → /setup
  - Touch-only Pi has no keyboard to complete the setup wizard
  - Declining auth left the user at / instead of the kiosk

  Fixes, bundled:

  1. backend/app/cli.py kiosk-bootstrap now, in one DB transaction:
     - creates a scoped API key (can_read_status=True, rest false)
     - upserts setup_completed=true
     so AuthContext never redirects and the kiosk URL loads directly. Users
     who want auth can still enable it from the admin UI; the provisioned
     key keeps working.

  2. install.sh full-mode runs the CLI as the bambuddy service user after
     create_bambuddy_service and sed-replaces the CHANGE_ME_AFTER_SETUP
     placeholder in spoolbuddy/.env.

  3. The generated spoolbuddy-kiosk-launch polls ${backend_url}/health for
     up to 60s before exec'ing chromium, so cold boots wait for uvicorn
     instead of flashing ERR_CONNECTION_REFUSED.

  Standalone mode was unaffected — users supply a real key from their
  existing Bambuddy before install.
maziggy 1 месяц назад
Родитель
Сommit
464d56ea0d
4 измененных файлов с 65 добавлено и 2 удалено
  1. 1 1
      CHANGELOG.md
  2. 11 0
      backend/app/cli.py
  3. 40 0
      backend/tests/unit/test_cli.py
  4. 13 1
      spoolbuddy/install/install.sh

Разница между файлами не показана из-за своего большого размера
+ 1 - 1
CHANGELOG.md


+ 11 - 0
backend/app/cli.py

@@ -17,7 +17,9 @@ from sqlalchemy.ext.asyncio import async_sessionmaker
 
 from backend.app.core.auth import generate_api_key
 from backend.app.core.database import async_session as default_session_maker, init_db
+from backend.app.core.db_dialect import upsert_setting
 from backend.app.models.api_key import APIKey
+from backend.app.models.settings import Settings
 
 DEFAULT_KIOSK_KEY_NAME = "spoolbuddy-kiosk"
 
@@ -68,6 +70,15 @@ async def kiosk_bootstrap(
             expires_at=None,
         )
         db.add(row)
+
+        # Mark first-run setup as completed so the kiosk URL loads directly
+        # instead of being force-redirected to /setup by AuthContext. Without
+        # this, a bundled SpoolBuddy/Bambuddy install boots into the Bambuddy
+        # first-run wizard (touch-only Pi has no keyboard to complete it).
+        # Users who want authentication enable it later from the admin UI; the
+        # API key we just created is already valid so the kiosk keeps working.
+        await upsert_setting(db, Settings, "setup_completed", "true")
+
         await db.commit()
         return full_key
 

+ 40 - 0
backend/tests/unit/test_cli.py

@@ -13,6 +13,7 @@ from backend.app.cli import DEFAULT_KIOSK_KEY_NAME, KioskBootstrapError, kiosk_b
 from backend.app.core.auth import _validate_api_key
 from backend.app.core.database import Base
 from backend.app.models.api_key import APIKey
+from backend.app.models.settings import Settings
 
 
 @pytest_asyncio.fixture
@@ -113,6 +114,45 @@ async def test_bootstrap_force_rotates_existing_key(session_maker):
         assert validated.name == DEFAULT_KIOSK_KEY_NAME
 
 
+@pytest.mark.asyncio
+@pytest.mark.unit
+async def test_bootstrap_marks_setup_completed(session_maker):
+    """Bootstrap must set setup_completed=true so AuthContext doesn't redirect the kiosk to /setup."""
+    await kiosk_bootstrap(
+        DEFAULT_KIOSK_KEY_NAME,
+        force=False,
+        session_maker=session_maker,
+        ensure_schema=False,
+    )
+
+    async with session_maker() as db:
+        setting = (await db.execute(select(Settings).where(Settings.key == "setup_completed"))).scalar_one()
+        assert setting.value == "true"
+
+
+@pytest.mark.asyncio
+@pytest.mark.unit
+async def test_bootstrap_setup_idempotent_on_rotate(session_maker):
+    """Re-running with --force must not duplicate the setup_completed row."""
+    await kiosk_bootstrap(
+        DEFAULT_KIOSK_KEY_NAME,
+        force=False,
+        session_maker=session_maker,
+        ensure_schema=False,
+    )
+    await kiosk_bootstrap(
+        DEFAULT_KIOSK_KEY_NAME,
+        force=True,
+        session_maker=session_maker,
+        ensure_schema=False,
+    )
+
+    async with session_maker() as db:
+        rows = (await db.execute(select(Settings).where(Settings.key == "setup_completed"))).scalars().all()
+        assert len(rows) == 1
+        assert rows[0].value == "true"
+
+
 @pytest.mark.asyncio
 @pytest.mark.unit
 async def test_bootstrap_custom_name(session_maker):

+ 13 - 1
spoolbuddy/install/install.sh

@@ -984,7 +984,7 @@ setup_kiosk() {
         dpkg-divert --local --rename --add /usr/sbin/update-initramfs >/dev/null 2>&1 || true
         ln -sf /bin/true /usr/sbin/update-initramfs
     fi
-    run_with_progress "Installing kiosk packages" apt-get install -y labwc chromium plymouth wlr-randr swayidle wlopm jq
+    run_with_progress "Installing kiosk packages" apt-get install -y labwc chromium plymouth wlr-randr swayidle wlopm jq curl
     # Restore real update-initramfs
     if dpkg-divert --list /usr/sbin/update-initramfs 2>/dev/null | grep -q local; then
         rm -f /usr/sbin/update-initramfs
@@ -1198,6 +1198,18 @@ else
     kiosk_url="\$FALLBACK_URL"
 fi
 
+# Wait for the Bambuddy backend to be reachable before launching Chromium.
+# Without this the browser opens before uvicorn has bound to the port on a
+# cold boot and the user sees an ERR_CONNECTION_REFUSED splash until they
+# manually reload. Probe /health (no auth, no body) with a short timeout.
+probe_url="\${backend_url:-http://localhost}/health"
+for _i in \$(seq 1 60); do
+    if curl -sf --max-time 2 "\$probe_url" >/dev/null 2>&1; then
+        break
+    fi
+    sleep 1
+done
+
 exec chromium --kiosk --no-first-run --disable-infobars \
     --disable-session-crashed-bubble --disable-features=TranslateUI \
     --noerrdialogs --disable-component-update \

Некоторые файлы не были показаны из-за большого количества измененных файлов