Add a "Collapse" toggle in the File Manager sidebar header next to
"Wrap". When enabled, the folder tree opens with only top-level
folders visible on every page load; disabled restores the previous
fully-expanded default. Toggling the preference also immediately
re-collapses or re-expands the current tree via a key-remount trick
on each top-level FolderTreeItem, so the change takes effect without
a page reload. Preference persists to localStorage under
library-collapse-folders, matching the existing library-* convention.
Backwards-compatible: FolderTreeItem gains an optional
defaultExpanded prop defaulting to true, so no callers see a
behavior change. Missing localStorage key coerces to false, so
existing users keep the old expanded-by-default behavior until they
flip the toggle.
New strings added to all 8 locales under fileManager.*. Wiki
"File Manager" page gains a "Folder sidebar preferences" section
that documents both Wrap and Collapse toggles. Four vitest cases
cover default, preloaded-collapsed, click-to-collapse, and
click-to-expand paths.
The 0.2.3b4 #1003 "fix" POSTed JPEG bytes as multipart form data,
but Obico's /p/ endpoint is declared methods=['GET'] upstream and
reads ?img=URL from the query string. Every POST was 405'd by
Flask's router before any handler ran, which is why the Obico
container logs were silent while Bambuddy kept reporting
"ML API call failed for printer N:" with a blank suffix —
raise_for_status() on the 405 produced an exception whose str()
rendered empty.
Restored the pre-#1003 nonce-URL approach (commit 3e434458):
capture locally with a 20s timeout we control, stash the JPEG
under a single-use 32-byte nonce, hand Obico a
GET /api/v1/obico/cached-frame/{nonce} URL that resolves in
<50ms so its hardcoded 5s read timeout never races RTSP.
Also guards against future silent exceptions: the error format
now falls back to type(exc).__name__ when str(exc) is empty.
Detection also early-returns with an explicit error if
external_url is unset instead of handing Obico a URL it can't
resolve.
The #1003 reverse-proxy scenario (Authelia/Authentik/CF Access
in front of Bambuddy) is addressed by documenting that the
/api/v1/obico/cached-frame/ path must be whitelisted from
external auth at the proxy layer — it is already public on
Bambuddy's side.
Backend: services/obico_detection.py, api/routes/obico.py,
main.py (PUBLIC_API_PATTERNS).
Frontend: FailureDetectionSettings banner + client.ts type +
all 7 locales restored.
Tests: 15 unit + 5 integration tests pass.
The ML API previously called back into Bambuddy to fetch snapshots,
which failed behind reverse proxies with external auth (Authelia, etc.).
Now the detection loop captures the JPEG locally and POSTs it directly
as multipart form data — no callback URL, no nonce cache, no
external_url dependency.
Both the Add Printer and Edit Printer modals had hardcoded model lists
missing the X2D — manual printer setup had no way to select the new
model. Auto-discovery via SSDP and virtual printer model selection
(dynamic from backend) were unaffected.
The Bambu Lab X2D (launched April 2026, dual-nozzle, enclosed, hardened
steel rod gantry, AMS 2 Pro compatible) identifies itself as internal
model code N6 via SSDP/MQTT, and real serials begin with 20P9. None of
these identifiers existed in Bambuddy's registries, so the camera
service fell back to the chamber-image protocol on port 6000 (X2D
doesn't speak it), firmware-check logged "Unknown printer model: N6",
and the dual-nozzle K-profile paths — gated on the H2D serial prefix
"094" — would have treated X2D as single-nozzle.
Backend:
- Register N6 → X2D across every registry (PRINTER_MODEL_ID_MAP,
PRINTER_MODEL_MAP, STEEL_ROD_MODELS, ETHERNET_MODELS,
CHAMBER_TEMP_SUPPORTED_MODELS, firmware-check API keys + wiki path,
virtual-printer SSDP/product/serial tables, DB vp_model_fixes).
- supports_rtsp(): match the X2 display-name prefix and the N6 internal
code; camera now routes to RTSP on port 322.
- Dual-nozzle serial prefix check in bambu_mqtt.delete_kprofile and
kprofiles.set_kprofile broadened to ("094", "20P9") — X2D now takes
the H2D-style cali_idx in-place edit path.
- is_h2d model gate in bambu_mqtt.start_print extended with "X2D" so
timelapse / bed_leveling / flow_cali / vibration_cali / layer_inspect
are sent as integers and external-spool ams_id 254/255 routing is
preserved (H2D-style deputy-nozzle addressing).
X2D uses hardened steel rods like P2S — it is intentionally placed in
STEEL_ROD_MODELS, not CARBON_ROD_MODELS. A regression-guard test pins
the classification.
Frontend:
- mapModelCode in PrintersPage and SpoolBuddyAmsPage handle N6 and X2D.
- Enclosure-door badge and airduct-mode whitelists include X2D.
- MaintenancePage.getMaintenanceWikiUrl routes X2D to P2S wiki URLs for
steel-rod lubrication, belt tension, cold-pull, and PTFE tube
(exported to enable direct unit testing).
Tests:
- test_printer_models.py: TestX2DModel (10 assertions).
- test_bambu_mqtt.py: X2D in start_print ams_mapping and is_h2d gate;
TestDeleteKProfileDualNozzleDetection across H2D, X2D, P2S, X1C.
- MaintenancePageWikiUrls.test.tsx: 15 assertions covering X2D, P2S
regression, X1C/H2D/A1Mini regression, and model-name normalisation.
Docs:
- README: added X2 series to the supported printers table.
- CHANGELOG: new entry under 0.2.3b4 Fixed.
Credit to @krautech for the report and debug bundle, and to @legend813
for PR #989 which seeded most of the registry changes — rod-type
classification was corrected (steel, not carbon) and the dual-nozzle /
K-profile / is_h2d gaps were added on top.
python-multipart 0.0.26 closes CVE-2026-40347 (GHSA-mj87-hwqh-73pj), a
DoS triggered by large preamble/epilogue data around a multipart
boundary. Bambuddy consumes python-multipart transitively through
FastAPI/Starlette for form and file-upload parsing, so multipart routes
(backup restore, project thumbnail upload, etc.) were exposed.
dompurify 3.4.0 picks up the fix for GHSA-39q2-94rc-95cp (function-form
ADD_TAGS could bypass FORBID_TAGS). Bambuddy's two call sites use only
array-form ALLOWED_TAGS/ALLOWED_ATTR, so the specific bypass was not
reachable, but the bump still hardens the sanitizer and clears the
audit warning.
requirements.txt floor raised to python-multipart>=0.0.26;
frontend/package.json caret pinned to ^3.4.0; npm audit and pip audit
both report zero outstanding advisories after the bumps.
Two root causes in the "Camera View Mode = Window" path when auth is on (#979):
1. PrintersPage opened the popup with `noopener`, which severed the opener
link and prevented the browser from copying sessionStorage (auth token)
into the new window. The popup booted unauthenticated, POST
/printers/camera/stream-token returned 401, and the <img> src went out
with no ?token=. The backend's RequireCameraStreamTokenIfAuthEnabled
then rejected every frame with "Valid camera stream token required".
2. CameraPage computed its stream URL from the module-level stream-token
cache in withStreamToken(). That cache is populated by a useEffect in
useStreamTokenSync that runs after render, so even after the token
resolved the first post-arrival render still produced a tokenless URL
and nothing triggered another render.
Fix:
- Drop `noopener` from the camera popup features (same-origin, trusted).
- Subscribe CameraPage to the `camera-stream-token` React Query so the
page re-renders the moment the token arrives.
- Gate currentUrl on `waitingForStreamToken` and append the token directly
from the reactive query value instead of the effect-synced module cache.
Embedded overlay mode was unaffected. Added CameraPage tests covering both
the auth-enabled (token required, src empty until it arrives, then includes
?token=) and auth-disabled (src rendered immediately without token) paths.
Four attempts at making the printer-card SD badge stable on H2D all failed:
the final straw was powering on an A1 causing every connected H2D to flip to
red simultaneously. Bambu firmware SD signaling is not reliably derivable
from MQTT — the legacy `sdcard` field is sporadic and inconsistently typed,
and home_flag bits 8-9 are cleared on heartbeat pushes regardless of card
state with no clean way to distinguish heartbeats from full status reports.
Remove the badge from the Printers page card and the Printer Info modal,
drop `sdcard` from the frontend PrinterStatus type, and strip all home_flag
derivation and heartbeat-handling code from the MQTT parser.
`state.sdcard` is retained on the backend and populated only from a plain
truthy read of the `sdcard` field, because firmware_update.py uses it as a
precondition before starting firmware installs.
Adds a compact "Bed" badge in the printer-card controls row
between print speed and Stop/Pause. Opens a popover with up/down
arrows and a 1 / 10 / 50 mm step selector.
When the Z axis has not been homed since the last print, the
first jog per session opens a Bambu Studio-style modal with
Home Z / Move anyway / Cancel. "Move anyway" bypasses soft
endstops (M211 S0 ... M211 S1) for a single move and is
remembered for the browser session.
Backend:
- POST /printers/{id}/bed-jog?distance=N[&force=bool]
Emits G91 / G1 ZN F600 / G90 (with optional M211 wrap).
Distance validated server-side (non-zero, |N| <= 200 mm).
- POST /printers/{id}/home-axes?axes=z|xy|all
Emits G28 variants.
Both gated behind Permission.PRINTERS_CONTROL.
Frontend:
- New indigo-themed badge + popover in PrintersPage.
- Not-homed confirmation modal with sessionStorage "warned" flag.
- i18n keys under printers.bedJog.* in all 7 locales.
Tests:
- backend/tests/unit/test_bed_jog.py — 13 tests covering
404 / 400 / 500 / success paths for both endpoints, plus
gcode-payload assertions for force on/off.
Docs:
- README feature list, CHANGELOG (0.2.3b4 Unreleased),
printer-control wiki page, website features.html.
Firmware update modal now shows every version from Bambu's wiki release
history, each badged Usable/Unavailable/Installed. Selecting a usable row
— newer or older than current — swaps the release notes and enables
install for that version, so rollback no longer requires hand-flashing.
Wiki scraper tightened to only read heading-anchor ids (h-XXXXXXXX-YYYYMMDD)
instead of any XX.XX.XX.XX substring, eliminating false positives like an
AMS firmware version mentioned in an H2D changelog being listed as H2D
firmware.
Refs #568
On short viewports the modal exceeded the screen height with no scroll,
hiding fields like Access Code and Save. Overlay now scrolls and the
card caps at calc(100vh-2rem) with internal overflow.
The strict CSP added in 0.2.3b4 blocked three things at once:
external sidebar-link iframes (no frame-src declared, so they fell
back to default-src 'self'), the inline service-worker registration
script in index.html, and the Google Fonts @import used for Inter.
- Add `frame-src 'self' https:` so user-configured HTTPS iframe
targets load; frame-ancestors 'none' still prevents Bambuddy
itself from being framed cross-origin.
- Move the inline SW-registration script into public/sw-register.js
so `script-src 'self'` covers it without 'unsafe-inline' or
per-build hashes.
- Allow fonts.googleapis.com in style-src and fonts.gstatic.com in
font-src so the Inter webfont loads.
Search field at the top of Settings now finds Sidebar Links,
Spoolman, Spool/Color Catalog, all four Failure Detection
sections, Email auth (Advanced + SMTP test), 2FA (TOTP, Email
OTP, Linked Accounts), SSO/OIDC, LDAP Server Config, and the
four Backup sub-cards (GitHub, History, Local, Scheduled).
Replaces the hardcoded searchIndex array in SettingsPage.tsx
with a module-level registry (frontend/src/lib/settingsSearch.ts).
Each settings card calls registerSettingsSearch(...) at module
scope, so adding a new card means adding one colocated line
instead of editing a distant central array. Anchor ids were
added to the corresponding Card elements in the affected
components so scrollIntoView lands on the right section.
Surface four Home Assistant-style controls on the Printers page card:
- SD Card badge in the top status row (green / red, icon-only).
- Enclosure Door badge in the top status row (green / yellow, icon-only).
Detection per printer family — X1/X1C/X1E read home_flag bit 23, all
others read top-level `stat` (hex string) bit 23 — so X1 firmware that
does not flip stat bit 23 stops false-triggering "open". WebSocket
status-change dedup key now includes door_open so toggling the door
alone publishes a push, no 30s REST-poll wait.
- Airduct Mode badge beside the speed control (cooling / heating)
for P2S/H2D/H2C/H2S; one-click dropdown calls the existing
set_airduct MQTT command via a new POST /printers/{id}/airduct-mode
route.
- Force Refresh entry in the kebab menu — calls the existing
/printers/{id}/refresh-status endpoint to request a pushall snapshot
without forcing a reconnect.
Tests: door-open parsing (X1 home_flag, non-X1 stat, ignore mismatched
source, invalid hex) and airduct route (validation, not-connected,
success, failure).
Adds a Failure Detection tab under Settings that wires Bambuddy to a
self-hosted Obico ml_api container — no cloud, no account, no WebSocket.
While a print is running, the detection service periodically hands the
printer's camera snapshot URL to the ML API and smooths scores over
time (30-frame warmup + EWM, alpha=2/13, short/long rolling means) so
one noisy frame can't trigger an action. When the smoothed score
crosses HIGH, the configured action fires exactly once per print:
notify, pause, or pause-and-cut-power (via linked smart plugs).
- Backend: new obico_detection + obico_smoothing + obico_actions
services, /obico/status and /obico/test-connection routes
(SETTINGS_READ / SETTINGS_UPDATE), six obico_* AppSettings fields
with validators for sensitivity/action/enabled_printers.
- Frontend: FailureDetectionSettings component (enable, ML URL + test,
sensitivity, action, poll interval, per-printer monitor list, live
status + detection history), new sidebar tab with service-active
bullet, toast on save.
- Tests: 17 detection unit tests + 15 smoothing unit tests + 4
frontend component tests.
- Docs: README bullet, CHANGELOG entry, wiki page under Analytics,
website features.html entry.
With Auto Off enabled and another job queued, the smart plug cut power when a
print finished and immediately re-powered the printer because the scheduler
saw pending items. The printer booted fresh into IDLE and the next job
auto-dispatched, bypassing the "Clear Plate & Start Next" confirmation.
Root cause: the plate-clear gate lived only in PrinterManager._plate_cleared
(in-memory set) and _is_printer_idle treated IDLE as unconditionally idle. On
power cycle the in-memory flag was lost and the IDLE-on-boot state skipped
the gate entirely.
Fix:
- Replace the in-memory flag with an awaiting_plate_clear column on the
printers table, rehydrated into the PrinterManager at startup.
- Set the flag in on_print_complete for completed/failed prints (not user
cancellations); clear it on ack and on scheduler dispatch.
- _is_printer_idle now short-circuits to not-idle whenever require_plate_clear
is on and the flag is set, regardless of the currently reported state —
so the gate holds through power cycles, Bambuddy restarts, and the printer
booting back into IDLE.
- /printers/{id}/clear-plate no longer requires the printer to report
FINISH/FAILED; it accepts the ack whenever the flag is raised.
- Frontend widgets (PrinterQueueWidget, Layout, BulkPrinterToolbar) gate on
the flag rather than reported state.
Tests: added regression tests for IDLE+awaiting=True (the #961 case) and
full DB round-trip tests for the persistence layer.
NozzleRackCard computed its rack base via min(present_ids), which breaks
when the lowest-ID slot is the one currently mounted to a hotend — the
firmware omits that ID from device.nozzle.info entirely, so min() picks
the next slot up and every remaining nozzle renders one position too
far left, with the "empty" placeholder pushed off the right end.
Use the fixed H2C rack base of 16 (matching
test_h2c_nozzle_rack_populated_with_8_entries in the backend) so the
empty slot stays anchored to its physical position regardless of which
nozzle is currently mounted.
Adds a frontend regression test covering ID 16 missing.
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.
The Printer tab AMS popup and spool auto-provisioner resolved color
names from hardcoded tray_id_name tables with a suffix-code fallback —
and suffix codes like "R1" are not globally unique across material
families. A17-R1 (PLA Translucent Cherry Pink) fell through the
fallback and resolved to "Scarlet Red" (A01-R1, PLA Matte), baking
the wrong name into auto-created inventory spools.
The fix removes the hardcoded tables entirely. Backend resolves color
names via the existing color_catalog table by hex; frontend fetches a
compact {hex: name} map once per session via a new
GET /inventory/colors/map endpoint (auth-gated but not on
inventory:read — read-only views need it too) and stores it in a
ColorCatalogProvider context. A useSyncExternalStore hook cascades a
re-render into pages mounted before the fetch completes so they
refresh from HSL-fallback names once the catalog loads.
Existing auto-provisioned spools keep their stored names; only new
provisioning and live display benefit. Co-Authored-By is intentionally
omitted here per project convention — set it via git config if needed.
The Statistics page reported "Gesamt" (All Time) kWh correctly but showed
zero for Today/Week/Month in total-consumption mode. Two bugs drove it:
1. The starting plug counter was kept in an in-memory dict
`_print_energy_start` that was lost on any backend restart mid-print, so
the per-print `energy_kwh` delta silently never got computed. The stats
endpoint's fallback path `SUM(PrintArchive.energy_kwh)` therefore summed
to zero for users running in total mode.
2. Total-consumption mode has no per-print delta by design — it includes
idle/preheat/standby — so the fallback to archive rows was the wrong
strategy even when the data existed.
Fix, in two parts:
- Persist `energy_start_kwh` on the archive row and read it back from a
fresh session at print end. Deletes `_print_energy_start` and its 5
call sites, replacing them with a single `_record_energy_start()` helper.
Per-print tracking is now restart-resilient regardless of tracking mode.
- Add hourly `smart_plug_energy_snapshots` table + `_snapshot_loop()` in
SmartPlugManager. Rewrote the `/archives/stats` energy branch as
`_sum_snapshot_deltas()` which computes per-plug
`max(0, last-in-range - baseline)` where baseline is the latest snapshot
at or before the range start, falling back to the earliest-ever snapshot
and signalling `energy_data_warming_up` when no pre-range baseline
exists (fresh upgrade). MQTT plugs are skipped from snapshots since they
only report "today" and have no lifetime counter.
Frontend: QuickStatsWidget renders an AlertTriangle next to Energy Used /
Energy Cost with a tooltip when `energy_data_warming_up` is true, so the
"low values right after upgrading" situation is explained in-product.
Fully localised across 7 UI languages.
Tests: new backend unit tests cover the snapshot delta arithmetic
(baseline/endpoint, counter reset clamp, multi-plug, warming-up fallback,
endpoint windowing), per-print restart resilience via expunge_all, and the
snapshot task lifecycle (start idempotent, stop cancels). Frontend tests
assert the warning icon appears only when the flag is set and only on the
energy tiles.
Docs: updated `CHANGELOG.md`, `README.md`, wiki `features/energy.md`,
wiki `features/statistics.md`, and website `features.html` with the new
behaviour and warming-up explanation.
Previously, if a SpoolBuddy daemon crashed during registration it could
end up registered twice. The kiosk UI silently used only the first
device and there was no UI path to remove the orphan — administrators
had to delete the row directly in the database.
Adds a new Settings → SpoolBuddy tab that lists every registered device
with live connection status, system details (firmware, IP, CPU temp,
memory, disk, OS, daemon + system uptime), hardware health flags, and
an Unregister action gated by a confirm modal. A yellow banner appears
whenever more than one device is registered to flag likely crash-
duplicates. Backend adds DELETE /spoolbuddy/devices/{device_id} gated
by inventory:delete and broadcasts spoolbuddy_unregistered over WS so
other tabs refresh immediately.
The tab header shows a device-count pill and a green/gray status bullet
reflecting whether at least one registered device is online. An online
device that is accidentally unregistered re-registers itself on its
next heartbeat. Localized in English, German, and Japanese. The kiosk
layout still uses devices[0] — once the orphan is unregistered, the
remaining device naturally becomes [0].
Audit PRs #920 (printers search/filter) and #932 (print from project
view) for regressions, i18n coverage, test gaps, and docs.
No regressions found: existing callers of the changed signatures
(archive_print, getLibraryFiles, filteredPrinters chain) are unaffected;
i18n is complete in all 7 locales for both features.
Backend tests (4 new, all passing):
- test_list_files_by_project_id — bulk JOIN returns files across all
linked folders, excludes unlinked ones
- test_list_files_folder_id_takes_precedence_over_project_id — guards
the documented precedence folder_id > project_id > include_root
- test_add_to_queue_with_project_id — project_id is persisted on the
queue row for later archive linkage
- test_add_to_queue_invalid_project_id_returns_404 — regression guard
for the validation pre-check on the queue path (mirrors the one on
the direct-print path in library.py)
PR #920 was merged without a CHANGELOG entry and without test coverage
for the location filter dropdown (every other new control — name
search, model search, serial search, whitespace trim, clear button,
status filter, empty states, combined filters — already had tests).
Adds:
- `filters by location via dropdown` — overrides the printers mock so
printer 1 has location 'Workshop' and printer 2 has location
'Office', then verifies that selecting each location shows only the
matching printer and that switching between the two works.
- `hides location filter when no printers have a location` — both
printers get a null location, and the test asserts the status filter
dropdown is still rendered but the location filter dropdown is not.
- CHANGELOG entry under [0.2.3b3] > New Features crediting the
contributor and documenting the search/filter feature, including the
WebSocket-reactive status filter behavior.
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.
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 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.