The bed-jog "not homed" warning modal was gated on a session-scoped
"warned" flag set only by the "Move anyway" button. Clicking "Auto
Home" sent the G28 and closed the modal but never set the flag, so the
next jog click in the same session re-prompted — even though the
printer was now homed.
The homeAxes mutation's onSuccess handler now flips the same
`bambuddy.bedJog.warned.<printerId>` sessionStorage flag. The warning
still fires once per printer per session (intended safety guard,
cleared on restart), but not repeatedly after a successful auto-home.
Critical safety fix. The bed-jog dialog's "Home Z" button sent a bare
`G28 Z` over gcode_line. On Bambu printers where the Z endstop is at
the top (bed moves UP into it — H2C, H2D, H2S, X1 family), `G28 Z`
skips the toolhead-park step that a full `G28` runs first, so the bed
rises at full speed with nothing getting out of the way. The reporter
only escaped damage because the toolhead happened to be parked on the
purge chute.
The /printers/{id}/home-axes endpoint and BambuClient.home_axes() now
always send bare `G28` regardless of the axes argument, triggering the
firmware's safe multi-step routine (park toolhead → home XY → home Z).
The axes argument is kept for API compat but ignored; invalid values
still return 400.
Frontend retitles the button "Auto Home" and updates the dialog copy
in all 7 locales so users aren't surprised when X/Y motion happens
before Z. Parameterized regression test asserts z/xy/all all produce
bare G28.
Three related fixes reported together:
(1) After resetting an AMS slot, the printer card showed "Empty Slot"
with no Configure or Assign Spool actions while SpoolBuddy's AMS page
still let the user re-configure the same slot. Commit c9efa4b8 (#784)
added a `tray?.state === 10` gate to the EmptySlotHoverCard actions,
intended to hide them on physically-empty slots (state=9). In practice
firmware often reports state=9 (or omits state entirely) after a
user-initiated reset even when a spool is still present, so the gate
hit the wrong case. The gate was redundant anyway — EmptySlotHoverCard
only renders when tray_type is empty — so it's removed at both the
standard-AMS and AMS-HT render paths.
(2) After configuring a slot with a Generic profile, the Assign Spool
modal hid manually-added inventory spools even when material matched,
unless the user flipped "Show all spools". The filter required exact
slicer_filament_name equality, which manually-added spools don't
populate. Filter now prefers exact slicer-profile match when both
sides have one, and falls back to partial material match in either
direction (so a "PLA" spool shows up for a "PLA Basic" slot).
(3) On assign, the mismatch dialog fired on every Generic spool
because Bambu Studio / OrcaSlicer profile names carry an @printer
nozzle (variant) qualifier while the tray stores the bare base name.
Both the filter and checkProfileMatch now strip everything from @
onward before comparing.
Adds 3 regression tests covering each path.
The parity gate expansion in 8f9eb0d4 started printing the full
missing-key and placeholder-mismatch lists for every informational
locale on every test run, which was noisy given CI only cares about
strict locales. Collapse info reports to one line per category
(`fr: missing keys vs en: 74`) and keep the full lists available via
VERBOSE_INFO=1 for when someone is actually catching up a locale.
The mini thumbnail wrapped its src with withStreamToken() (appends the
short-lived camera-stream token, needed because <img> can't send an
Authorization header), but the enlarged lightbox <img> used a bare
${status.cover_url}?view=top. On auth-enabled instances the backend
rejected the unauthenticated request and the browser showed the
broken-image icon. Wrap the enlarged src with withStreamToken() too.
Users integrating a Shelly plug through an external MQTT broker
(ioBroker, Zigbee2MQTT, HA's MQTT broker, etc.) lost the plug's
power/state/energy readings after every Bambuddy restart. The only
fix was opening Settings → Smart Plugs, renaming the topic to a dummy
value, saving, renaming back, and saving again.
Root cause: three code paths configure an MQTT smart plug's
subscriptions — the startup restore in main.py, the create route,
and the update route — and they had drifted. The create/update
routes used the newer per-type model (mqtt_power_topic /
mqtt_energy_topic / mqtt_state_topic with per-type paths,
multipliers and mqtt_state_on_value) while the startup restore was
still on the legacy single-topic model. Worse, the restore loop
short-circuited on `if plug.mqtt_topic:`, skipping any plug whose
topics were only set in the new per-type fields — exactly the shape
of a Shelly-via-ioBroker config, which publishes power and state on
separate topics. The "rename, save, rename back" workaround routed
through the update endpoint and re-established the subscription the
correct way.
Extracted the topic-resolution + service.subscribe() call into
subscribe_plug_to_mqtt() in mqtt_smart_plug.py and routed all three
paths through it so the schema can't drift again. The helper keeps
the legacy `mqtt_topic` field working as a fallback for all three
data types — matching the behaviour the startup restore used to
have via subscribe()'s internal `effective_*_topic or topic`
collapsing, and matching the change-detection dict already used
during updates.
Regression tests cover: per-type topics restored without a legacy
topic, legacy single-topic backward compat, per-type multipliers
overriding legacy, per-type winning when both are set, the
empty-config skip case, and topic-list de-duplication.
On bare-metal Raspberry Pi OS bookworm / armv7l / Python 3.11, 3MF
files larger than a few megabytes arrived complete via the
virtual-printer FTP server but the copy into data/archives/ was
silently truncated. The archive row was still written, the printer
card looked fine, and the problem only surfaced later when opening
the archive — the subsequent zipfile.ZipFile() in
GET /archives/{id}/plates raised BadZipFile and the UI came up blank
with no thumbnail, plate list, or filament data.
Two things conspired:
1. archive_print() used shutil.copy2, which takes Python's sendfile()
fast path on Linux. On the reporter's kernel/fs combination
sendfile returned a short count on the first call for the upload
sizes hit in practice and the destination ended up truncated.
Small files completed in one syscall and were fine.
2. ThreeMFParser.parse() caught the resulting BadZipFile in a bare
`except Exception: pass`, so the archive pipeline kept going with
empty metadata and left the bad file on disk — nothing in the
logs hinted anything had gone wrong until a support bundle came
in and the "Failed to parse plates" warning fired much later.
The archive copy is now an explicit chunked read/write with fsync —
sendfile is not in the path. After the copy, if the source was a
valid ZIP but the destination isn't, we refuse to create the archive
row, remove only the truncated file (and the archive directory if
empty — archive_dir is created with exist_ok=True so rmtree would be
unsafe if a same-second same-filename collision happened), and log
both sizes at ERROR so the condition is obvious in future support
bundles. The parser's silent catch now logs at WARNING for the same
reason.
All nine archive_print() call sites already check `if archive:` or
`if not archive:`, so returning None for corrupted ZIPs propagates
cleanly without behaviour changes elsewhere.
Regression tests cover single-chunk and multi-chunk copies, mtime
preservation via copystat, overwrite of an existing destination, a
ZIP roundtrip through a multi-megabyte 3MF, the new parser WARNING,
and a truncation sentinel verifying that zipfile.is_zipfile() flips
to False on a half-written ZIP — the exact post-condition
archive_print now trusts.
On auth-enabled instances, logging out and back in left the File Manager
(and occasionally the Archives page) full of broken thumbnails until a
manual page reload. Thumbnail URLs are gated by a short-lived camera
stream token that <img> tags cannot send via Authorization headers, so
the token is appended as ?token=… at render time.
Two races broke this after sign-in:
1. The token query was keyed on ['camera-stream-token'] alone and fired
while the user was still on the login page. It 401'd, React Query
cached the failure with a 50-minute staleTime, and nothing invalidated
it after login — the token never arrived.
2. Even when the token did arrive, the module-level variable holding it
was not reactive, so pages that had already rendered kept serving
image URLs with no token in them.
Fixes:
- Include user.id in the query key and gate with
`enabled: authEnabled ? !!user : true`. A new sign-in produces a new
key and triggers a fresh fetch; no anonymous fetch is cached.
- When the token transitions from null to a value, walk the DOM once
and update src on every <img>/<video> pointing at /api/v1/ without
the current token so already-rendered pages reload in place.
- Mirror the query key/gate in CameraPage so it shares the cache entry.
The DOM-rewrite logic is extracted into rewriteMediaSrcWithToken() with
unit tests covering: appending to a query-less URL, & separator with an
existing query, skipping URLs that already carry the current token,
replacing a stale token (trailing and middle positions), leaving
non-/api/v1/ URLs alone, updating <video>, and URL-encoding tokens with
special characters.
The wiki scraper silently returned no versions for P2S and X2D, causing
Bambuddy to fall back to the Bambu Lab download page, which still listed
01.01.01.00 as "latest" even though 01.02.00.00 shipped on 2026-04-09.
Two regex mismatches in _fetch_all_versions_from_wiki():
1. Heading anchor ids require an optional dash between version bytes and
date. H2D/X1/H2C/H2S use "h-01020000-20260409"; P2S and X2D publish
"h-0102000020260409" (no dash).
2. The text fallback only matched ASCII parens around release dates, but
P2S, X2D, A1 and A1-mini render dates in full-width parens (YYYYMMDD)
(U+FF08/U+FF09).
Anchor regex now accepts an optional dash; fallback accepts both paren
styles. Added regression tests for both shapes.
Both fields have existed on the model and been shown in the File
Manager for some time, but nothing ever wrote to them — every file in
every library appeared to have never been printed.
Now on_print_complete's queue-status update path calls a small
_bump_library_file_usage_if_completed() helper that increments
print_count and stamps last_printed_at on the source library file
whenever a queued print completes successfully. Failed, cancelled and
user-aborted prints are intentionally skipped so the fields represent
successful usage rather than attempt count.
Unblocks sorting the File Manager by last-printed date and is a
prerequisite for the scheduled-purge feature requested in #1008,
which is held until we see whether manual sort+bulk-delete covers the
use case.
Add CVE-2026-6385, CVE-2026-30997 and CVE-2026-6192 to .trivyignore.
All three are marked "vulnerable / postponed" in both bookworm and
trixie by the Debian Security Tracker with no upstream fix yet, so
the Trivy container scan will keep re-raising them on every run.
None of the vulnerable code paths are reachable in Bambuddy:
* CVE-2026-6385 (ffmpeg DVD subtitle heap OOB write) — ffmpeg here
only ingests printer-camera RTSP and MJPEG/H.264/H.265 streams,
never DVD/VOB files with subtitle tracks.
* CVE-2026-30997 (ffmpeg AV1 decoder OOB read → DoS) — Bambu
printer cameras emit H.264/H.265/MJPEG, not AV1.
* CVE-2026-6192 (openjpeg JPEG 2000 integer overflow) —
libopenjp2-7 is pulled in transitively by ffmpeg but Bambuddy
never decodes JPEG 2000 files.
Not caused by the recent bookworm → trixie runtime image switch;
both releases carry the same "postponed" status. Rationale captured
inline next to each CVE for future auditors.
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.
Picks up ffmpeg 5 → 7 (HEVC/AV1 improvements), OpenSSL 3.0 → 3.3, and
two more years of APT package freshness. Frontend-builder stays on
Bookworm until the Node.js image team publishes Trixie variants.
- Bandit B108: mark 3 dummy /tmp paths in test fixtures as nosec
- CodeQL py/ldap-injection: already RFC 4515 escaped via _ldap_escape()
- CodeQL py/incomplete-url-substring-sanitization: test-only assertions
- GitGuardian: replace sample passwords with <placeholder> strings in
notification-template preview data
Previously the script only inspected en/zh-CN/zh-TW, leaving de/fr/it/ja/pt-BR
drift invisible. Now locales are auto-discovered from src/i18n/locales/, and a
STRICT list (de, zh-CN, zh-TW — currently in parity) gates CI while the rest
report informationally until their drift is caught up. ja notably has 27 real
placeholder bugs worth fixing before promotion to strict.
Facebook and some other OAuth providers issue authorization codes that
exceed 512 characters. Pydantic rejected these with 422 string_too_long.
The OAuth spec defines no maximum code length; 2048 aligns with common
provider limits.
Also adds three integration tests to verify 512-char and 2048-char codes
are accepted while 2049-char codes are correctly rejected.
OrcaSlicer's Linux BBLNetworkPlugin publishes MQTT payloads with the
C-string null terminator included in the length, so decoded messages
arrived as `{…}\x00`. The strict json.loads() raised JSONDecodeError
and the publish handler silently returned — pushall, get_version, and
project_file were never answered, and the slicer hit its 60 s sync
timeout. Print_queue mode only (proxy mode tunnels MQTT). The b069b521
serial-adaptation fix was correct but ran past this earlier silent
failure.
_handle_publish now strips trailing \x00/whitespace before parsing and
logs the raw payload on any remaining decode failure so future silent
variants are visible in support bundles.
feat(cloud): support China region for token-based login
The /cloud/token endpoint always used the global Bambu API endpoint,
so users with China-region access tokens could not validate their
token. The password login flow already exposes a region selector; this
brings the token flow to parity.
Archive reprints and library-file prints built the MQTT project_file
command with hardcoded project_id="0", subtask_id="0", task_id="0".
Printers key per-job state (including gcode_start_time) on those IDs,
so reprints looked like continuations of the same job and third-party
MQTT observers (OctoEverywhere) reported compounding durations across
repeat replays — a 40 min job reprinted from archive showed ~1h40m,
and a second reprint of the same file showed ~4h. BambuStudio mints
fresh IDs per submission; bambu_mqtt.start_print() now does the same
using an epoch-millisecond timestamp for all three fields. md5 is
deliberately left empty to avoid activating firmware md5-validation
against a digest we can't compute without re-reading the upload.
Added 6 regression tests in TestStartPrintUniqueIdentityFields
covering non-zero IDs, md5 stays empty, uniqueness across successive
submissions, numeric-string format, and blast-radius guard on
unrelated payload fields. Updated CHANGELOG.
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.
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.