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.
Full-mode install wrote CHANGE_ME_AFTER_SETUP as SPOOLBUDDY_API_KEY because
no admin exists yet to create a real one. On reboot the kiosk launched with
that placeholder, AuthContext rejected it, and the user hit the Bambuddy
login page instead of the kiosk. Standalone mode was unaffected — users
paste a real key from their existing Bambuddy before install.
Adds backend/app/cli.py with a kiosk-bootstrap subcommand that creates a
scoped APIKey row directly in the DB (can_read_status=True, everything else
false) and prints the full key to stdout. install.sh full-mode runs it as
the bambuddy service user after create_bambuddy_service, captures the key,
and sed-replaces the placeholder in spoolbuddy/.env. Idempotent with
--force for re-installs.
Drops the outdated "create an API key and edit .env" next-step block since
the kiosk is now provisioned automatically.
Follow-up to the SpoolBuddy LCD power-off fix. Field-testing on a
Raspberry Pi OS Bookworm kiosk showed the watchdog appearing absent
from `ps ax | grep spool` — actually it had already `exec`'d into
`swayidle`, but with no logging there was no way to confirm that
without manually re-running the script.
- spoolbuddy-idle.sh now redirects stdout+stderr to
~/.cache/spoolbuddy-idle.log and prints WAYLAND_DISPLAY,
XDG_RUNTIME_DIR, PATH, the resolved timeout, and the final
`swayidle` command line on every start.
- Auto-detect WAYLAND_DISPLAY by scanning $XDG_RUNTIME_DIR for a
wayland-* socket (10s retry loop) so the script survives the race
where labwc launches autostart before exporting its env.
- Default XDG_RUNTIME_DIR to /run/user/$(id -u) if unset.
The SpoolBuddy kiosk's "screen blank timeout" setting only painted a
black CSS overlay over the browser window — the HDMI panel's backlight
stayed on indefinitely, wasting power and risking burn-in on
OLED/LED panels.
Move blanking down to the OS layer:
- install.sh now installs swayidle + wlopm + jq and rewrites labwc's
autostart to launch a new spoolbuddy-idle.sh watchdog instead of the
old `wlr-randr --on` keep-alive loop.
- The watchdog sources /opt/bambuddy/spoolbuddy/.env, derives device_id
from the first non-loopback MAC (same algorithm as daemon/config.py),
fetches the configured blank_timeout from the backend once on boot,
and execs `swayidle -w timeout $T 'wlopm --off HDMI-A-1' resume
'wlopm --on HDMI-A-1'`. Touch/keypress wakes via labwc's input event
path. timeout=0 skips swayidle entirely so existing installs that
never picked a timeout keep their current always-on behavior.
- New GET /api/v1/spoolbuddy/devices/{id}/display endpoint returns the
current brightness + blank_timeout. Gated on INVENTORY_UPDATE (same
level the daemon heartbeat key already uses) so existing SpoolBuddy
API keys work without extra permissions.
- SpoolBuddyLayout drops blanked state, the blank timer, activity
listeners, resetActivity, and the CSS overlay. Runtime updates to
the timeout take effect on next kiosk/browser restart; default for
newly-enabled blanking is 300 seconds.
Swipe down from the top of the SpoolBuddy display to open a quick-access
menu for toggling printer smart plugs and managing the device (restart
daemon, restart browser, reboot, shutdown). All destructive actions
require confirmation.
Backend: new POST /spoolbuddy/devices/{id}/system/command endpoint
queuing reboot/shutdown/restart_daemon/restart_browser commands.
Daemon: handles commands via subprocess (sudo reboot, systemctl restart).
Frontend: SpoolBuddyQuickMenu component, swipe-down gesture detection,
i18n keys for all 7 locales.
The touchscreen display blanked right after boot, requiring a touch
to wake. Two issues: no consoleblank=0 in cmdline.txt (kernel blanks
the console during Plymouth→labwc transition), and the wlr-randr
anti-blank loop slept 60s before its first run.
- Add consoleblank=0 to kernel cmdline in install.sh
- Move sleep after wlr-randr in labwc autostart so it fires immediately
- Round scale weight to integer before sending to backend (Pydantic
rejects non-whole floats for int fields), move modal close to finally
block, add error toast with actual API message
- Fix null-field crash in SpoolInfoCard prop construction: pick one
source object instead of per-field ?? fallbacks that crash when
displayedSpool has null subtype/brand/rgba and matchedSpool is null
- Add React ErrorBoundary to App so crashes show error instead of
black screen
- Remove --max-old-space-size=128 and --enable-low-end-device-mode
from kiosk Chromium flags (crashed renderer/display)
- Append kiosk flags to Pi GPU defaults instead of resetting them
- Add wlr-randr keep-alive and screenBlankTimeout=0 to prevent
display blanking on labwc 0.9.x
- Fix tests: add ToastProvider to Dashboard test wrapper, update
StatusBar tests for removed animate-pulse class
Frontend: replace expensive idle dashboard animations (3x animate-ping
with scale transforms, blur-2xl glow, continuous animate-pulse on
status dots) with static NFC rings and slow 5s color-cycling spool.
Chromium: add --disable-extensions, --disable-background-timer-throttling,
--memory-pressure-off, --disable-renderer-backgrounding, --disable-breakpad,
and --js-flags=--max-old-space-size=128. Install script: mask stripped
services (not just disable) to prevent socket/dbus reactivation; use
/etc/systemd/user/ global overrides for user services instead of
unreliable su-based systemctl --user. Remove chromium/upower from
strip_packages since kiosk reinstalls them immediately.
Add Chromium flags to cut overhead on Pi: disable extensions, crash
reporter, background timer throttling, renderer backgrounding, and cap
V8 heap at 128MB. Mask (not just disable) stripped system services to
prevent socket/dbus reactivation, and add xdg-permission-store to the
disable list. Remove chromium and upower from strip_packages since the
kiosk needs them — they were being uninstalled then immediately
reinstalled on every run.
reporter, background timer throttling, renderer backgrounding, and cap
V8 heap at 128MB. Also mask (not just disable) stripped system services
to prevent socket/dbus reactivation, and add xdg-permission-store to
the disable list.
Replace Chromium with cog (WPE WebKit) for the kiosk browser. Cog is
purpose-built for embedded kiosk displays with a fraction of Chromium's
CPU and memory footprint on Pi hardware.
Add React Query `select` to SpoolBuddyLayout and SpoolBuddyDashboard
printer status queries so only `connected` is extracted. Temperature,
fan, and progress changes no longer trigger re-renders on every MQTT
tick.
Expand service/package stripping to disable pipewire audio stack, CUPS
printing, rpcbind, upower, polkit, accounts-daemon, xdg-desktop-portal,
and mpris-proxy. Add user-level service masking for pipewire/portals.
Update SSH update cache clearing to handle both WPE WebKit and legacy
Chromium cache paths.
Override Debian's default Chromium flags via /etc/chromium.d/spoolbuddy-kiosk
to disable GPU rasterization, enable low-end device mode, and disable smooth
scrolling/background networking. The system default --enable-gpu-rasterization
conflicted with per-launch flags — the new config replaces all system defaults
so kiosk flags take effect cleanly.
Expand service/package stripping to disable pipewire audio stack, CUPS
printing, rpcbind, upower, polkit, accounts-daemon, xdg-desktop-portal,
and mpris-proxy. Add user-level service masking for pipewire/portals
that system-level disable misses.
Add Chromium performance flags (disable-gpu-rasterization,
enable-low-end-device-mode, disable-smooth-scrolling,
disable-background-networking, disable-dev-shm-usage) to reduce
CPU load from ~54% to manageable levels on Pi 4B.
Expand service/package stripping to disable pipewire audio stack,
CUPS printing, rpcbind, upower, polkit, accounts-daemon,
xdg-desktop-portal, and mpris-proxy. Add user-level service
masking for pipewire/portals that system-level disable misses.
New splash shows only the SpoolBuddy logo with green glow bloom,
radial gradient, light rays, and vignette. Removed Bambuddy branding.
Includes generator script for easy customization.
Defers initramfs rebuild during install until after Plymouth theme
is configured, avoiding redundant rebuilds from apt hooks.
New splash shows only the SpoolBuddy logo with green glow bloom,
radial gradient, light rays, and vignette. Removed Bambuddy branding.
Includes generator script for easy customization.
Defers initramfs rebuild during install until after Plymouth theme
is configured, avoiding redundant rebuilds from apt hooks.
Reverts the fim/fbi experiment — Plymouth is the only splash tool
that reliably handles Pi KMS/DRM from early boot. install.sh is
restored to the original Plymouth setup. The new polished splash
image and generator script are kept.
Plymouth ran as a persistent daemon throughout boot, consuming memory
and competing for framebuffer allocation. fim renders via DRM (Pi KMS
doesn't expose a usable legacy framebuffer), displays the image, and
exits — zero ongoing resource cost.
New splash image shows only the SpoolBuddy logo with baked-in glow,
radial gradient, light rays, and vignette effects (66KB vs 205KB).
Install script auto-purges Plymouth on existing installs in a single
pass to avoid redundant initramfs rebuilds.
Plymouth ran as a persistent daemon throughout boot, consuming memory
and competing for framebuffer allocation. fbi writes pixels directly
to the framebuffer and exits — zero ongoing resource cost.
New splash image shows only the SpoolBuddy logo with baked-in glow,
radial gradient, light rays, and vignette effects (66KB vs 205KB).
Install script auto-purges Plymouth on existing installs in a single
pass to avoid redundant initramfs rebuilds.
Plymouth ran as a persistent daemon throughout boot, consuming memory
and competing for framebuffer allocation. fbi writes pixels directly
to the framebuffer and exits — zero ongoing resource cost.
New splash image shows only the SpoolBuddy logo with baked-in glow,
radial gradient, light rays, and vignette effects (66KB vs 205KB).
Install script auto-removes Plymouth on existing installs.
Install script now runs apt-get upgrade -y after installing system
packages. A WiFi safeguard (APT hook + helper script) is installed
first, backing up NetworkManager connections before dpkg and restoring
them if wiped — prevents headless Pis from losing WiFi during upgrades.
Runs apt-get upgrade -y after installing system packages and the WiFi
safeguard hook. Ensures the Pi is fully up to date before deploying
SpoolBuddy, and the WiFi safeguard protects NM connections during
the upgrade.
APT hook backs up NetworkManager WiFi connections before dpkg runs
and restores them if they get wiped. Prevents headless SpoolBuddy
Pis from losing WiFi after apt upgrade (observed with Bookworm
kernel/raspi-config updates clearing system-connections/).
After updates, the kiosk browser showed stale frontend assets from
Chromium's disk cache even after restarting. Added --disk-cache-size=0
to the launch flags — the kiosk loads a single page from the local
network so caching provides no benefit.
The getty@tty1 autologin had no network dependency, so the labwc/Chromium
kiosk chain started before connectivity was up — showing a connection
error for 10-15 seconds. Added After=network-online.target to the
autologin override so the browser has network when it launches.
The daemon's self-update mechanism (git fetch/reset on its own code) was
fragile: .git permission errors, self-modifying code mid-run, hardcoded
main branch. Bambuddy now SSHes into the SpoolBuddy Pi and drives the
update remotely — matching its own branch, with step-by-step progress
via WebSocket. After updating the daemon, the kiosk browser is also
restarted so it loads the updated frontend.
SSH key pairing is automatic: Bambuddy generates an ED25519 keypair and
returns the public key in the registration response. The daemon deploys
it to authorized_keys on first connect — no manual setup needed.
Changes:
- New: backend/app/services/spoolbuddy_ssh.py
- Rewritten: trigger_daemon_update endpoint (SSH instead of pending_command)
- New: GET /spoolbuddy/ssh/public-key endpoint
- Auto SSH key deployment via registration response + daemon
- Removed: daemon _perform_update() and cmd=="update" handler
- Install script: bash shell, sudoers for daemon + kiosk restart, .ssh/ setup
- Dockerfile: added openssh-client
- Frontend: SSH key display, force update button
- Fixed: update check compares APP_VERSION, not GitHub releases
- Fixed: kiosk browser restart after update
The daemon's self-update mechanism (git fetch/reset on its own code) was
fragile: .git permission errors, self-modifying code mid-run, hardcoded
main branch. Bambuddy now SSHes into the SpoolBuddy Pi and drives the
update remotely — matching its own branch, with step-by-step progress
via WebSocket.
SSH key pairing is automatic: Bambuddy generates an ED25519 keypair and
returns the public key in the registration response. The daemon deploys
it to authorized_keys on first connect — no manual setup needed.
- New: backend/app/services/spoolbuddy_ssh.py (keypair, SSH commands, update orchestration)
- Rewritten: trigger_daemon_update endpoint uses SSH instead of pending_command
- New: GET /spoolbuddy/ssh/public-key endpoint for manual pairing
- Removed: daemon _perform_update() and cmd=="update" heartbeat handler
- Updated: install.sh — bash shell, sudoers for systemctl restart, .ssh/ setup
- Updated: Dockerfile — added openssh-client
- Updated: frontend — SSH key display, force update button
- Fixed: update check now compares against APP_VERSION, not GitHub releases
The daemon's self-update mechanism (git fetch/reset on its own code) was
fragile: .git permission errors, self-modifying code mid-run, hardcoded
main branch. Bambuddy now SSHes into the SpoolBuddy Pi and drives the
update remotely — matching its own branch, with step-by-step progress
via WebSocket. Install script updated with SSH access, sudoers entry,
and --ssh-pubkey flag for pairing.
- Redesign settings page with tabbed layout (Device, Display, Scale, Updates)
- Add screen blank timeout: blanks after touch inactivity, tap to wake
- Add CSS brightness filter for HDMI displays (no sysfs on HDMI)
- Add backend `language` field to app settings for server-side persistence
- Sync UI language from backend on kiosk load (separate Chromium instance)
- Top bar clock respects user's time format setting (system/12h/24h)
- Add SpoolBuddy settings translations for all 6 languages (en/de/fr/ja/it/pt-BR)
- Disable Chromium swipe-to-navigate in kiosk install script
- Add `video` group for DSI backlight access
Root cause: The daemon used wlopm for screen blanking, but wlopm was never installed. Additionally, the daemon runs as the spoolbuddy system user which has no access to
the Wayland socket, so Wayland-based tools can't work.
Fix in display_control.py:
- Replaced wlopm --off/--on with vcgencmd display_power 0/1 — this is a Raspberry Pi firmware-level command that's pre-installed and works without Wayland socket access
- Added shutil.which("vcgencmd") check at init to avoid repeated failures on non-RPi hardware
- Added explicit PermissionError handling for brightness writes with a helpful message about the video group
Fix in install.sh:
- Added video group to the spoolbuddy service user's groups (was: gpio, spi, i2c → now: gpio, spi, i2c, video), which grants access to both vcgencmd and sysfs backlight
files
Scale Tab Numpad
Root cause: On the 1024x600 kiosk screen (~376px available content height), the weight info card + numpad + action buttons exceeded the space, causing tiny buttons and
overlapping.
Fix in SpoolBuddySettingsPage.tsx:
- Hide the weight info card during weight entry step (calStep !== 'weight'), reclaiming ~70px
- Compact inline weight reading in the step header (small dot + monospace text) so users can still see the live scale value
- Larger numpad buttons: min-h-[56px] with text-lg font size (was no min-height, text-sm)
- Added active:scale-95 for tactile touch feedback
- mt-auto on action buttons to push them to the bottom, preventing overlap
- Removed the wrapping card around the calibration flow to save vertical padding
When Bambuddy auth is enabled, the SpoolBuddy kiosk gets redirected to
the login page because ProtectedRoute requires a user from GET /auth/me,
which only handled JWT tokens. The kiosk daemon already has an API key
but couldn't use it to satisfy the frontend auth check.
- Backend: /auth/me now accepts API keys (Bearer bb_xxx or X-API-Key)
and returns a synthetic admin UserResponse with all permissions
- Frontend: AuthContext reads ?token= from URL on first load, stores in
localStorage, and strips from URL (prevents history/referrer leakage)
- Install script: kiosk URL now includes ?token=${API_KEY}
- Tests: 3 new integration tests (Bearer API key, X-API-Key header,
invalid key rejection)