Follow-up to the previous commit that swapped the `ssh`/`ssh-keygen`
subprocesses for asyncssh. asyncssh.connect() internally calls
getpass.getuser() to resolve the *local* username for ~/.ssh/config
host matching, regardless of the explicit `username=` we pass for the
remote login. Under an arbitrary Docker PUID with no /etc/passwd
entry, getpass.getuser() tries LOGNAME/USER/LNAME/USERNAME (all unset
in python:3.13-slim) and falls back to pwd.getpwuid(), which raises
KeyError. asyncssh rewraps that as "Unknown local username: set one
of LOGNAME, USER, LNAME, or USERNAME in the environment" — which
surfaced in the UI as "ssh connection failed: no username set in the
environment".
Fix is two-part:
- _ensure_local_username_env() runs at module import. If getpass
.getuser() already works, or any of LOGNAME/USER/LNAME/USERNAME is
set, it is a no-op. Otherwise it sets LOGNAME=bambuddy so asyncssh
can proceed. Native installs are untouched.
- asyncssh.connect() is now called with config=[] to skip the
default ~/.ssh/config load, which relies on a resolvable home
directory that may not exist under arbitrary Docker PUIDs.
Three new unit tests cover the env-var fallback, including the case
where the operator has set USER but the passwd lookup still fails.
Commit 67749565 eliminated ssh-keygen from the SpoolBuddy remote-update
flow, but the update path still shelled out to the OpenSSH `ssh` client
for every command. Like ssh-keygen, the `ssh` binary calls
getpwuid(getuid()) during startup and aborts with "No user exists for
uid <N>" when the container runs under an arbitrary PUID that isn't in
/etc/passwd (python:3.13-slim only ships a root entry, so any
`user: "1000:1000"` compose setup trips the same error).
detect_current_branch() had a related problem: when the git repo is
bind-mounted into the container, .git exists inside Docker, so the code
tried to run `git rev-parse`. Git isn't in the image, so the subprocess
silently fell back to the GIT_BRANCH env var — and if git ever were
added, it could hit the same getpwuid trap.
The entire update path is now subprocess-free:
- _run_ssh_command uses asyncssh (pure-Python, built on the already
installed cryptography library). Connection errors map to rc=255 to
match `ssh`'s convention; asyncio.timeout handles the timeout path.
- detect_current_branch reads .git/HEAD directly (handling git-worktree
`gitdir:` pointer files too), keeping the same GIT_BRANCH → "main"
fallback chain.
- shutil and the inline `import subprocess` are gone from the module.
Regression tests assert that neither keypair creation, branch
detection, nor command execution spawns any subprocess. Native installs
are unaffected.
The sidebar <img> tag in Layout.tsx fetched custom external-link icons
via a raw /api/v1/external-links/{id}/icon URL. That endpoint is
protected by the shared camera-stream token (passed as ?token=xxx
because <img> tags cannot send Authorization headers), so the request
came back 401 with the "Valid camera stream token required" message.
The edit dialog already routed through api.getExternalLinkIconUrl(),
which wraps the URL via withStreamToken(); the sidebar now does the
same in both the open-in-new-tab and NavLink branches.
The Shortest Job First toggle badge was rendered inside the Pending
Queue section header, which only mounts when pendingItems.length > 0
and the list view is selected. Clicking the toggle often lined up
with the scheduler picking up the last pending item, which unmounted
the whole section and took the toggle with it.
Moved the toggle into the queue page header next to the list/timeline
view switcher so it stays visible regardless of pending-item count,
filters, or view mode. On mobile the view-mode switcher remains
hidden (as before) but the SJF button is visible icon-only.
Fixes#929. The breadcrumb in ProjectDetailPage used the non-existent key
`navigation.projects`. Changed to the correct key `nav.projects` which is
defined in all locale files and resolves to e.g. 'Projects' in English.
The SpoolBuddy remote-update flow shelled out to `ssh-keygen` to create
its update keypair on first use. Inside the Docker container the process
runs under an arbitrary PUID that is not listed in /etc/passwd, so
ssh-keygen aborted at the getpwuid() home-directory lookup with
"no user exists for uid 1001" and the update button failed.
Generate the ed25519 keypair in-process via the `cryptography` library
(already a dependency) and serialize it in OpenSSH format. No subprocess,
no /etc/passwd lookup. Native installs are unaffected.
Added a regression test that asserts no subprocess is spawned during
keypair creation so this can't come back.
Two bugs surfaced while investigating camera reconnect behaviour in #925.
The camera page briefly displayed "Reconnecting attempt 6 of 5" before
giving up, because the attempt counter could be incremented to the
maximum while the reconnect banner was still rendering. The displayed
value is now clamped to the configured maximum.
Every failed ffmpeg spawn logged the full ~20-line ffmpeg version,
configuration, and lib* banner, producing hundreds of lines of noise
per failed camera click (one reported click produced 555 log lines
across 30 retries). A new _summarize_ffmpeg_stderr helper strips the
banner and caps output at the last 10 meaningful lines, applied at
all three stderr log sites (immediate-failure, stream-ended,
read-timeout). Covered by unit tests for empty input, banner
stripping, line cap, blank-line filtering, and banner-only input.
The underlying "camera service stops accepting connections after
prolonged uptime" behaviour in the X1C firmware is still under
investigation — these two fixes are independent of that root cause.
Two related LDAP authentication changes.
Fix: POSIX primary group membership was ignored. authenticate_ldap_user
only searched for posixGroup entries via memberUid (supplementary
groups). A user's primary group — referenced by the gidNumber attribute
on the user object matching gidNumber on a posixGroup — was never
resolved, so users whose role came from their primary group landed
without the expected permissions. The authenticator now runs a second
search for posixGroup entries whose gidNumber matches the user's
primary gidNumber, then dedupes DNs case-insensitively before passing
the list to resolve_group_mapping (LDAP DNs are case-insensitive by
spec).
New feature: ldap_default_group setting. Settings → Authentication →
LDAP → Advanced has a new "Default group" selector. When an LDAP user
authenticates but is not listed in any mapped LDAP group, they are
assigned to this fallback group instead of being left with no groups
(and therefore no permissions). A warning is logged each time the
fallback is applied so admins can spot missing group assignments.
Empty setting preserves the old behavior.
Tests: added 4 mocked authenticate_ldap_user tests covering primary
gidNumber lookup, dedupe of overlapping memberUid+primary gid matches,
case-insensitive DN dedupe, and the guard when a user entry has no
gidNumber attribute. Also extended the existing parse_ldap_config tests
to cover the new default_group field.
Backend: ldap_service.py (primary group + dedupe + default_group
field), schemas/settings.py (schema field), api/routes/auth.py
(fallback wiring in _provision_ldap_user / _sync_ldap_user).
Frontend: LDAPSettings.tsx default-group dropdown in the Advanced
collapsible, api/client.ts type field, new i18n keys in all 7 locales
(defaultGroup, defaultGroupNone, defaultGroupHint).
The debug support bundle included virtual_printer_remote_interface_ip
unmasked in support-info.json. The setting key didn't match any
sensitive-key filter substring. Added "_ip" to the filter set so IP
address settings are excluded. Log file content was already redacted
by the existing IPv4 regex.
The on_ams_change auto-sync callback set locations for new spools but
never called clear_location_for_removed_spools(), leaving stale locations
that caused double-booked slots. Also pass synced_spool_ids in the
single-printer sync route to match the sync-all endpoint behavior.
The clearPlateMutation.isSuccess state from React Query persisted after
the first successful plate clear. When the next print finished, the
stale isSuccess rendered the static confirmation instead of the clickable
button. Reset mutation state when printer leaves FINISH/FAILED.
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.
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.
Users can authenticate against an LDAP/AD server with configurable
server URL, bind DN, search base, and user filter. Supports StartTLS
and LDAPS — plaintext is not allowed. Both Active Directory (memberOf)
and POSIX groups (memberUid) are mapped to BamBuddy groups on each
login. Auto-provisioning creates local accounts on first LDAP login.
Local admin accounts remain as fallback when LDAP is unreachable.
Password management is disabled for LDAP users.
When auto_archive was disabled but the print was dispatched by BamBuddy
(queue/reprint), on_print_start discarded the expected print entry and
returned early. The archive was never promoted to _active_prints, so at
completion archive_id and ams_mapping were both None — all tracking paths
failed silently. Now detects expected prints before the auto-archive
early-return and falls through to the normal promotion path. Also injects
the stored ams_mapping into the usage tracker session for printers where
MQTT request topic subscription fails (P1S, A1).
Spool CRUD endpoints (create, bulk create, update, delete, archive,
restore) did not emit websocket events, so SpoolBuddy Dashboard and
other tabs relying on event-driven cache invalidation never refreshed.
All inventory mutation endpoints now broadcast an `inventory_changed`
websocket event. Frontend handles it by invalidating `inventory-spools`.
When multiple smart plugs were assigned to the same printer, only the
first plug's automation triggered. All automation paths (print start
auto-on, print complete auto-off, queue auto-off, scheduler power-on)
now iterate every plug linked to the printer. Also fix queue auto-off
hardcoded to Tasmota instead of using the correct service for the plug
type.
Queue status update (printing → completed) failed silently when SQLite
was locked by another writer, leaving ghost jobs permanently stuck in
printing status. Add run_with_retry() for SQLite lock retries and split
runtime tracker into per-printer commits to reduce lock hold time.
The dev mode probe (ams_filament_setting to ext slot) fired on every
auto-reconnect, which destabilized some firmware MQTT brokers (A1/P1)
causing a reconnect-probe-disconnect feedback loop. Now caches the
probe result across reconnects and only probes once on first connect,
with a 5s delay to let the session stabilize.
When auto-archive was off, archive_id was None at print completion so
the entire 3MF tracking path was skipped. AMS remain% fallback also
failed on printers reporting remain=-1. Now searches library files and
previous archives by filename to locate the 3MF without an archive,
and captures the AMS slot-to-tray mapping at print start so it's
available at completion regardless of archive state.
Per-model start/end G-code snippets configurable in Settings (Workflow
tab). Queue items get "Inject G-code" toggle — scheduler injects
snippets into a temp 3MF copy before FTP upload. Supports Farmloop,
SwapMod, AutoClear, Printflow 3D and similar bed-clearing systems.
Original files are never modified.
Usage tracking failed silently when FTP download failed (fallback archive
with no 3MF), when printing from external spool holder (VT tray not
iterated by AMS fallback), and notifications showed "Unknown" for time
and filament. Now resolves 3MF from library/previous archives, tracks
VT tray remain% deltas, enriches notifications with usage tracker
results, and captures print time from MQTT for fallback archives.
External folder scan now mirrors disk subfolder structure into the folder
tree instead of flattening all files into root. Hidden directories are
filtered, orphaned subfolders are cleaned up on rescan. Fixes#890.
File manager delete endpoints (folder, file, bulk) now commit before
returning the response — previously relied on post-response auto-commit,
causing a race where the frontend refetch arrived before the commit.
New dedicated MQTT methods, API endpoints, and UI buttons for
loading/unloading filament from the external spool holder without
requiring an AMS. Includes state guards to prevent load when filament
is already loaded and unload when nothing is loaded.
Add timeout and retry to the developer mode probe. After a keep-alive
timeout, paho auto-reconnects but the session can be half-broken: the
printer sends status but ignores commands. The probe had no recovery —
one unanswered probe permanently blocked retries. Now times out after
10s with one retry; two consecutive failures force-close the socket for
a clean reconnect.
Skip AMS remain% weight sync in on_ams_change while a print session is
active. The MQTT FINISH message triggers both the AMS weight sync (SET
from remain%) and the usage tracker (ADD from 3MF data) in the same
event loop cycle, causing double deduction. The active-session check is
snapshotted before any await to prevent a race with on_print_complete
popping the session during interleaved execution.
Camera snapshot, test, and plate detection endpoints created temporary
JPEG files with default 0644 permissions. Switch from NamedTemporaryFile
to mkstemp with explicit 0600 permissions.
Set X-Content-Type-Options, X-Frame-Options, and Referrer-Policy on all
responses. CSP omitted (React inline styles would require unsafe-inline,
negating protection). HSTS omitted (LAN app commonly accessed over HTTP).
An API key with printer_ids=[] was treated the same as null (global
access) due to a falsy check. Now None means global access and []
means no printer access. Added a startup migration to normalize any
existing [] rows to NULL so they retain their intended global access.
Also fixed the webhook /queue endpoint which used the same falsy
check, allowing []-scoped keys to see all printers.