mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
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.
This commit is contained in:
+1
-1
@@ -25,7 +25,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- **Dependency Updates for Published Advisories** — Bumped two dependencies flagged by vulnerability scanners. `python-multipart` 0.0.22 → 0.0.26 closes CVE-2026-40347 (GHSA-mj87-hwqh-73pj), a denial-of-service triggered by large preamble or epilogue data around a multipart boundary — the 0.0.26 release now skips the preamble before the first boundary and silently discards the epilogue after the closing one. Bambuddy uses `python-multipart` transitively through FastAPI/Starlette for form and file-upload parsing, so any authenticated endpoint accepting `multipart/form-data` (e.g. backup restore, project thumbnail upload) was exposed. `dompurify` 3.3.3 → 3.4.0 picks up the fix for GHSA-39q2-94rc-95cp (the function-form `ADD_TAGS` could bypass `FORBID_TAGS`); Bambuddy's two call sites (`ProjectDetailPage`, `ProjectPageModal`) only use array-form `ALLOWED_TAGS`/`ALLOWED_ATTR`, so the specific bypass was not reachable, but the bump still hardens the sanitizer against future misconfiguration and clears the audit warning.
|
||||
|
||||
### Fixed
|
||||
- **SpoolBuddy Kiosk Shows Login Page in Full-Mode Install** — Installing SpoolBuddy via the `spoolbuddy/install/install.sh` script in `full` mode (both Bambuddy and SpoolBuddy on the same host) left the kiosk launching at `http://localhost:<port>/spoolbuddy?token=CHANGE_ME_AFTER_SETUP` — a placeholder the script wrote into `spoolbuddy/.env` because the install runs before Bambuddy has an admin to create a real API key. The frontend's `AuthContext` validated that placeholder against the server, got rejected, and redirected to the login page; disabling authentication let the kiosk through (only symptom users noticed), so the bug only surfaced with auth enabled. Standalone mode was unaffected because the user supplies a valid key from an existing Bambuddy server before install proceeds. Added `backend/app/cli.py` with a `kiosk-bootstrap` subcommand that creates an API key row directly in the DB (scoped read-only: `can_read_status=True`, `can_queue=False`, `can_control_printer=False`) and prints it to stdout. `install.sh` full-mode now runs the CLI as the bambuddy service user right after `create_bambuddy_service`, captures the key, and `sed`-replaces the placeholder in `spoolbuddy/.env`. On first reboot the kiosk picks up a real token and logs in automatically — no manual "create an API key and edit the env" step is required anymore. The CLI is idempotent with `--force` to rotate on re-install.
|
||||
- **SpoolBuddy Kiosk Unusable After Full-Mode Install** — A bundled Bambuddy + SpoolBuddy install via `spoolbuddy/install/install.sh --mode full` produced an unusable kiosk on first boot: Chromium raced ahead of uvicorn and showed "can't connect to localhost"; after a manual reload the kiosk URL `/spoolbuddy?token=…` was hijacked by Bambuddy's first-run wizard (`AuthContext` force-redirects to `/setup` whenever `requires_setup=true`, regardless of the target path); the wizard asks for admin credentials, but a touch-only Pi has no on-screen keyboard; if the user skipped auth the browser landed at `/` instead of the kiosk, and if they tried to enable auth they were stranded. Standalone mode was unaffected because it runs against an already-configured remote Bambuddy. Fixed in three parts: (a) new `backend/app/cli.py` with a `kiosk-bootstrap` subcommand that in a single DB transaction creates a scoped API key (`can_read_status=True`, `can_queue=False`, `can_control_printer=False`) and upserts `setup_completed=true`, so the first-run wizard never triggers and the kiosk URL loads the SpoolBuddy page directly; users can still enable authentication later from the admin UI and the pre-provisioned key keeps working. (b) `install.sh` full-mode now runs the CLI as the bambuddy service user immediately after `create_bambuddy_service` and `sed`-replaces the `CHANGE_ME_AFTER_SETUP` placeholder in `spoolbuddy/.env`. (c) The generated `spoolbuddy-kiosk-launch` now polls `${backend_url}/health` with a 60 s timeout before exec'ing Chromium, so cold boots wait for uvicorn instead of flashing the connection-refused error. The CLI is idempotent with `--force` for re-installs.
|
||||
- **Bambu Lab X2D Support** ([#988](https://github.com/maziggy/bambuddy/issues/988)) — Added X2D to the Add Printer and Edit Printer model dropdowns (both were missing the new model, so manual printer setup had no X2D option — auto-discovery via SSDP was unaffected). The newly released X2D (dual-nozzle, enclosed, hardened steel rod gantry, AMS 2 Pro compatible) identifies itself as internal model code `N6` via SSDP/MQTT, and serials begin with `20P9`. Because neither the code nor the prefix existed in any of Bambuddy's model tables, multiple paths silently fell back to wrong defaults: the camera service routed to the chamber-image protocol on port 6000 (which the X2D doesn't speak) instead of RTSP on port 322 — the reporter saw `Chamber image: data is not a valid JPEG` spam and no stream; the K-profile edit/delete path conditioned its in-place `cali_idx` write on the H2D serial prefix `094` and would therefore have treated X2D as a single-nozzle printer even though its dual-extruder layout matches H2D; the firmware-update check logged `Unknown printer model: N6`; and the virtual-printer model registry had no way to emulate X2D. Added the `N6 → X2D` mapping across every registry (`PRINTER_MODEL_ID_MAP`, `PRINTER_MODEL_MAP`, `ETHERNET_MODELS`, `STEEL_ROD_MODELS`, `CHAMBER_TEMP_SUPPORTED_MODELS`, firmware-check API keys and wiki path, virtual-printer SSDP product names and serial prefix, DB migration `vp_model_fixes`), extended `supports_rtsp()` to match `X2` display names and the `N6` internal code (camera now goes to port 322), expanded the dual-nozzle serial prefix check in `kprofiles.py` and the K-profile delete command in `bambu_mqtt.py` to also accept `20P9` so the H2D-style `cali_idx` in-place edit path runs on X2D, added X2D to the `is_h2d` model-family gate that selects the integer-format `timelapse`/`bed_leveling`/`flow_cali`/`vibration_cali`/`layer_inspect` fields in the MQTT print command, and added X2D to the frontend's door-badge and airduct-mode whitelists, `mapModelCode` lookups on both the Printers page and Spoolbuddy AMS page, and the MaintenancePage wiki-URL resolver (X2D inherits P2S's steel-rod lubrication, belt-tension, nozzle cold-pull and PTFE wiki pages, since its hardware is closer to P2S than to H2). Credit to @krautech for the report and the debug bundle, and to @legend813 for the initial PR (#989) that seeded most of the registry changes — the classification was corrected (X2D uses hardened steel rods like P2S, not carbon rods) and the dual-nozzle/K-profile gaps were added on top.
|
||||
- **Print Speed Icon Not Updating Live When Changed on Printer** ([#993](https://github.com/maziggy/bambuddy/issues/993)) — Changing the print speed mode from the printer's own panel (instead of from Bambuddy) did not update the speed icon on the Printers page card; the new value only appeared after a full page reload. The MQTT parser was already tracking `spd_lvl` and updating `state.speed_level` correctly, but the WebSocket serializer (`printer_state_to_dict`) was missing the field — so live status pushes never carried `speed_level`, and the frontend's merge-over-old-cache update left the icon stuck on its previous value. The REST `/status` endpoint used on initial page load already included it, which is why reloads worked. Added `speed_level` to the WebSocket payload. Thanks to @chesterakl for reporting.
|
||||
- **Camera Popup Shows "Valid camera stream token required" With Auth Enabled** ([#979](https://github.com/maziggy/bambuddy/issues/979)) — When Camera View Mode was set to "Window" and authentication was enabled, clicking the camera button opened a popup that immediately failed with `"Valid camera stream token required"`, while the embedded overlay kept working. Two root causes: (1) `window.open(...)` passed `noopener` in the popup features, which severed the opener link and prevented the browser from copying sessionStorage (where the auth token lives) into the popup — so the new window booted unauthenticated and the `POST /printers/camera/stream-token` fetch returned 401, leaving the `<img>` src without the required `?token=` query param; (2) even once the token arrived, `CameraPage` computed its URL from the module-level stream-token cache on render and never re-rendered when the cache was updated in a `useEffect`, so the first paint locked in a tokenless URL that the backend kept rejecting. Fixed by dropping `noopener` from the camera popup features (same-origin, trusted window) so sessionStorage is inherited, subscribing `CameraPage` to the `camera-stream-token` React Query so it re-renders the moment the token resolves, and appending the token directly from the reactive query value instead of the effect-synced module cache — the `<img>` src stays empty until the token is ready, so no tokenless request ever leaves the popup. Embedded-overlay mode was unaffected. Thanks to @VREmma for the reproducer.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 \
|
||||
|
||||
Reference in New Issue
Block a user