Two related queue issues surfaced when scheduling an ASAP print with
quantity > 1 on an H2D:
1. Double-dispatch — both items in the batch ended up in 'printing'
status on the same printer, logged as "BUG: Multiple queue items in
'printing' status for printer N". The scheduler seeded its busy
set empty each tick and relied on _is_printer_idle() reading live
MQTT state, but H2D / P1 series lag several seconds between the
print command and IDLE → RUNNING, so the next check_queue() tick
saw IDLE and dispatched the second batch item onto the already-
running printer. check_queue() now seeds busy_printers with every
printer_id that has a row in 'printing' status before iterating,
so any printer with an outstanding dispatched job is excluded
regardless of what MQTT currently reports.
2. Progress bar flashed 100% — immediately after dispatch the queue
item's per-row progress bar showed the prior print's final mc_percent
for a few seconds, then snapped back to 0% when the new print
started ticking. QueuePage.tsx now gates progress / remaining_time /
layer fields on status.state being RUNNING or PAUSE; in any other
state (FINISH from the prior print, IDLE, PREPARE while heating)
the bar renders at 0% with no stale ETA or layer count.
Regression coverage added in test_phantom_print_hardening.py
(TestBusyPrinterSeedingFromPrintingItems, 3 tests): seeding query
returns only printers with 'printing' rows, empty when none exist,
and end-to-end check_queue() does not call _start_print for a pending
item whose printer already has a 'printing' row even when
_is_printer_idle() is forced True.
Reshapes the embedded PrettyGCode viewer (landed in #963) into a focused
archive-preview tool, matching Bambuddy's data model instead of the
OctoPrint-style "connected-printer + library file picker" flow it shipped
with. Reached only from the Archives page 3D-preview button; URL
/gcode-viewer?archive=<id>[&plate=<N>].
Backend:
- /archives/{id}/gcode accepts ?plate=N and resolves the filename by
parsing the suffix as int, so zero-padded names like plate_01.gcode
are found when the plates endpoint reports index 1.
- /archives/{id}/plates gains top-level has_gcode: bool. Source-only
3MFs (PNG/JSON fallback path) surface the flag so the frontend can
skip the picker instead of sending the user into a dead viewer.
- printer_state_to_dict injects name + model into every WS snapshot so
consumers render proper labels on the initial tick without racing a
separate /printers fetch.
- /gcode-viewer (no trailing slash) dropped from the backend so reloads
fall through to the SPA catch-all and keep the layout shell; only
/gcode-viewer/ (trailing slash) and /gcode-viewer/<path> remain for
the iframe + static assets.
Frontend:
- PlatePickerModal shown only for multi-plate archives with sliced
gcode, grid layout with thumbnails matching the Re-print modal.
- Source-only archives show a noGcode toast instead of the empty
viewer.
- ArchivesPage navigate path swapped to /gcode-viewer?archive=<id> with
no trailing slash; GCodeViewerPage iframe forwards
window.location.search so the archive reference survives both the
initial navigate and a full-page reload.
- Viewer iframe's auth path: fetch intercept injects Bearer; a 401
redirects to / so the SPA handles login.
Viewer adapter:
- Stripped the printer selector, WebSocket subscription, library file
picker, tryAutoLoadPrintingFile, BAMBU_BED_SIZES, and updatePrinter-
Selector. The viewer no longer observes live printer state.
- Bed size derived from /archives/{id}/capabilities.build_volume
(extracted from the 3MF's printable_area/printable_height), so H2D,
H-family, and any future printer render on the correct bed without
a hardcoded map.
- loadArchiveById accepts a plate param; fetch intercept rewrites
__bambuddy_archive_<id>[_plate<N>] to /archives/<id>/gcode[?plate=N].
Nav + locale cleanup:
- Sidebar "GCode Viewer" nav entry removed (viewer is archive-scoped
now, not a destination page).
- 32 orphaned gcodeViewer locale keys deleted across all 8 locales.
- platePicker.{title, hint, plateLabel, objectCount, noGcode} keys
added in all 8 locales.
ArchivesPage: the now-unreachable ModelViewerModal render paths + its
showViewer state removed. ModelViewerModal itself stays — File Manager
still uses it for library file previews (plate picker + .3mf 3D model).
pre-commit:
- gcode_viewer/ excluded from trailing-whitespace + end-of-file-fixer
so vendored third-party JS libs don't drift away from upstream.
Incidental sweeps picked up by pre-commit and kept (unrelated but
benign):
- NotificationsPage.tsx: single trailing-whitespace line removed.
- spoolbuddy/scripts/pn5180_diag.py: dead `import gpiod` dropped —
the pn5180 driver module imported at line 27 does its own
`import gpiod` and `gpiod.Chip()` calls, so the diag script's
top-level import was never referenced.
Tests:
- 6 new cases in test_gcode_viewer.py for the backend plate / has_gcode
behaviour (plate=N resolution, zero-padded filenames, missing-plate
404, no-plate fallback, plate=0 rejection, has_gcode true/false).
- 3 new cases in test_printer_manager.py for name/model WS injection.
- PlatePickerModal.test.tsx — 6 frontend cases covering render,
plate-name composition, onSelect payload, backdrop close, and
thumbnail fallback.
* feat: add embedded GCode viewer
Adds PrettyGCode as a built-in GCode visualiser embedded directly in the
Bambuddy layout, so users can preview and inspect GCode files without
leaving the dashboard.
In expanded view, PrinterQueueWidget rendered its own "Clear Plate & Start
Next" button inside a yellow-bordered card whenever the plate-clear gate
was up and an auto-dispatch item was queued. PR #939 added the card-level
"Mark plate as cleared" button that already covers that state — and every
other state (staged-only queue, empty queue, etc.) — so both buttons hit
the same /clear-plate endpoint with identical optimistic-update semantics.
Two controls, one action, visible together in one specific state.
Remove the widget's button and its entire needsClearPlate render branch.
The widget becomes a passive "Next in queue" preview linking to /queue;
the card-level button remains the single plate-clear entry point.
Also drop:
- now-dead awaitingPlateClear / requirePlateClear / printerState props
from PrinterQueueWidgetProps and the matching call site
- orphaned queue.clearPlate / queue.plateReady translations from all eight
locale files (queue.clearPlateSuccess stays — used by the card button's
success toast)
- PrinterQueueWidgetClearPlate.test.tsx (654 lines) — every test asserted
the behaviour of the now-gone button; PrinterQueueWidget.test.tsx still
covers the passive-link path
Deliberately *not* changed: plate-status pill stays inside the Status box
(lines 2664/2671/2736/2783 of PrintersPage.tsx). Compact-view (Size S)
pill and icon-only clear button at :2664/:2671/:2673 untouched.
_watchdog_print_start reverted queue items to "pending" at 45 s if
gcode_state hadn't changed, assuming the MQTT project_file was swallowed
by a half-broken session (#887/#967). H2D Pro firmware (01.01.00.00)
routinely keeps state=FINISH for 48-55 s after actually accepting the
command before transitioning to PREPARE. The watchdog reverted items
the printer had already started physically printing; the archive updated
normally via _active_prints, but the queue item was now "pending" again,
and the next scheduler tick after plate-clear re-dispatched the same
item as if it had never run. With one item left in the queue that looked
like a reprint of the just-finished job; with multiple items the
symptom was masked by item N+1 getting dispatched during the race.
Add a second "command landed" signal: subtask_id advancing past the
pre-dispatch value. Bambuddy already mints a unique submission_id per
project_file publish (#1042) and the printer echoes it back on the next
push_status as soon as it starts processing the command - well before
gcode_state transitions on slow-transition models. _start_print now
captures pre_subtask_id alongside pre_state and passes both to the
watchdog, which exits early on either a state change or a subtask_id
advance.
Raise default timeout 45 s → 90 s as belt-and-braces for printers that
neither flip state nor echo subtask_id inside the polling window.
Genuinely half-broken sessions (both signals unchanged across the full
90 s) still revert + force-reconnect exactly as before.
Transient subtask_id=None during reconnect is not mis-detected as a
change. pre_subtask_id=None falls back to state-only checking so the
fix is safe for printers that haven't reported a subtask_id yet.
New test_scheduler_watchdog.py pins the eight behaviours that matter:
pickup via state change; pickup via subtask_id change with state still
FINISH (the exact #1078 case); revert when neither signal changes;
default timeout is 90 s; pre_subtask_id=None state-only fallback;
current subtask_id=None not treated as change; printer disconnect
mid-watchdog leaves DB untouched; item that already moved on is not
clobbered.
PR #939 added the awaiting_plate_clear gate but stored it on
PrinterManager, not on PrinterState. printer_state_to_dict() — which
builds every WebSocket printer_status payload — never emitted the flag,
so the frontend's WS merge preserved the stale false value. The only
path that surfaced true was the 30s HTTP fallback poll, and incoming WS
ticks kept bumping React Query's dataUpdatedAt, pushing the refetch out
further on chatty printers.
Emit awaiting_plate_clear from printer_state_to_dict by reading
printer_manager.is_awaiting_plate_clear(printer_id) directly; returns
False when no id is passed. No frontend change needed — the existing WS
merge carries the flag end-to-end and the button now appears the instant
the printer transitions to FINISH.
Regression tests assert the WS dict always contains the key and surfaces
True when the manager has the flag set for that printer_id.
Affects every printer (A1/H2D/X1C) equally — transport-agnostic path.
Opening the sidebar's Change Password modal while on the Printers page
caused the "Search printers" input to render as a masked password field
and stay that way after closing the modal.
Root cause: the modal had three type=password inputs but no accompanying
username anchor, so password-manager extensions (1Password, Bitwarden,
browser built-ins) hunted the DOM for a matching text input and latched
onto the unlabelled Printers-page search bar.
- Layout.tsx: add hidden autocomplete=username anchor at the top of the
Change Password modal form. Also ensures saved new passwords are
correctly keyed to the logged-in user.
- PrintersPage.tsx: harden the search input with type=search,
name=printer-search, autoComplete=off, data-1p-ignore, data-lpignore
so heuristic autofill skips it regardless.
Bambu Cloud returns filament_id=null for user presets that only override
fields of a generic base (e.g. "Sting3D ABS" inheriting from
"Generic ABS @BBL H2D"). ConfigureAmsSlotModal fell back to
convertToTrayInfoIdx(base_id), which strips "S" and the version suffix
from "GFSB99_07" to "GFB99" — Generic ABS's filament_id. The printer
accepted and echoed back GFB99, so OrcaSlicer / BambuStudio Sync
Filaments resolved the slot to "Generic ABS" and the custom preset
never appeared on the printer LCD.
The preceding default already set tray_info_idx to the PFUS*/PFSP*
setting_id unchanged, and the rest of the stack round-trips that
format (configure_ams_slot, inventory Assign Spool, and print
scheduler slot-matching on P* short-form IDs). The base_id branch
overwrote the correct default.
Remove the base_id fallback. When cloud detail returns a distinct
filament_id we still prefer it; otherwise the setting_id default
stands. BambuStudio Sync now resolves the custom preset cleanly.
OrcaSlicer falls back to the inherited generic because OrcaSlicer
user-preset JSONs don't carry a filament_id field — that is an
OrcaSlicer limitation and behaviour is strictly not worse than before.
Regression tests (frontend):
- filament_id=null keeps PFUS* as tray_info_idx
- concrete filament_id wins over the default
- GFS* path skips the cloud-detail fetch entirely
- fetch failure degrades gracefully to the PFUS* default
Regression tests (backend):
- test_configure_pfus_preserves_setting_id_pair: HT slot endpoint
forwards both tray_info_idx=PFUS… and setting_id=PFUS… untouched
Thanks to @mrnoisytiger for the browser-console / network / backend-log
data that isolated the fallback path and the OrcaSlicer preset JSON
that showed the missing filament_id field.
A single legacy spool with a 7-char rgba ('FFFFFFF', missing one F)
caused GET /api/v1/inventory/spools to 500 with a pydantic
ResponseValidationError, leaving the reporter with a blank Filaments
page and "Add Spool" silently failing. Root cause spans three layers:
1. Write path: SpoolUpdate.rgba had no pattern constraint (only
SpoolCreate did), so PATCH could plant malformed values in the DB.
2. Frontend: ColorSection hex input's `val.length <= 6 ? 'FF' : ''`
emitted 7-char rgba for 5-char input (XXXXX + FF = 7) and for
7-char typed input (no alpha appended).
3. Read path: SpoolResponse inherited the write-side pattern, so a
single bad row 500'd the entire list endpoint instead of being
tolerated through serialize.
SpoolUpdate.rgba now carries the same ^[0-9A-Fa-f]{8}$ pattern as
SpoolCreate. The hex input emits a fully-formed 8-char RRGGBBAA on
every keystroke — 8-char paste passes through, 7-char drops the
stray, shorter input pads RGB with '0' and appends FF alpha.
SpoolResponse.rgba is now Optional[str] with no pattern — write-side
validation is the right place for format rules; responses must
tolerate historical rows.
Tests: 16 schema tests (SpoolCreate/Update reject, SpoolResponse
tolerate), 7 frontend tests covering every input length 0–8 plus
non-hex strip. A user who already has a bad row in their DB now sees
it render with a default color instead of having to hand-edit SQLite.
The "Print" button on a printer card (and drag-drop-onto-card) used
FileUploadModal to persist the file as a LibraryFile, then dispatched
through POST /library/files/{id}/print. The LibraryFile row + disk file
were left behind after every one-off print, polluting File Manager with
entries the user never asked to save.
FilePrintRequest.cleanup_library_after_dispatch (default False) opts
into post-dispatch cleanup. When set, _run_print_library_file stages
db.delete(lib_file) in the same transaction as archive_print so a
mid-flight FTP / start_print failure rolls both back cleanly, commits
together, then unlinks the library disk file + thumbnail after commit
succeeds. External library files (is_external=True) are never touched.
Only the Printers-page Direct-Print PrintModal sets the flag. Every
other api.printLibraryFile caller (File Manager Print, Project Detail
Print) leaves it unset — their entries are there by user intent.
Also moves formatPrintName out of PrintersPage.tsx into a new
utils/printName.ts module — fa1c46d9 (#881) exported it inline so its
test could import it, tripping react-refresh/only-export-components.
The 0.2.3.1 fix (f03d0c4c) added current_user to the library print
endpoint and plumbed it into the dispatch job object, but the job
runner never read it back out. _run_print_library_file called
ArchiveService.archive_print() without created_by_id and never called
set_current_print_user, so archives created by the printer-card "Print"
button, File Manager prints, and Library prints all landed with
created_by_id=NULL and were invisible to the per-user statistics filter.
The post-print user-targeted notification also had no recipient.
Forward job.requested_by_user_id to archive_print() at creation time
and register the current-print user after start_print succeeds,
matching _run_reprint_archive (lines 686-691).
Reprint-from-Archive still shows NULL for archives whose original
created_by_id was NULL — the reprint path reuses the source row as-is
and only refreshes started_at. Tracked as a separate follow-up.
When two printers were running different plates of the same multi-plate
3MF, the Printers page cards displayed the same file name on both and
there was no way to tell them apart. The Queue view already had this
information by cross-referencing the archive's plate list; the card
didn't have the linkage.
Expose `current_archive_id` (resolved by matching the MQTT `subtask_id`
against `PrintArchive.subtask_id` — the bridge introduced in #972 for
restart-resume) and `current_plate_id` (parsed from `gcode_file` by a
new shared `parse_plate_id` helper) on the status endpoint. The helper
is also called from the WebSocket push path so plate transitions
reflect within 100 ms instead of waiting 30 s for the next REST poll;
the archive id itself stays REST-only since it's stable for the life
of a print and shouldn't make the push path touch the DB.
The card fetches plate metadata via the same `api.getArchivePlates()`
call QueuePage uses — shared React Query cache keeps it cheap across
polls — and renders the actual plate name (or a "Plate N" fallback)
only when `is_multi_plate` is true. Single-plate prints stay clean.
Falls back to the previous `plate_N.gcode` regex path when there's no
archive linkage (e.g. prints started directly from the printer LCD).
Tests cover the plate-id extraction across Bambu Studio path shapes
(backend parse_plate_id, printer_state_to_dict wiring) and the label
override precedence in formatPrintName (frontend).
The strict CSP shipped in 0.2.3b4 / 0.2.3.1 whitelisted only `https:`
for `frame-src`, so the Filament tab's Spoolman iframe was blocked
on the typical self-host setup where Spoolman runs on plain HTTP on
a LAN. Reporter saw a blank Filament page with a brief Spoolman
flash on reload and a browser-console CSP violation pointing at
`http://<host>:7912/spool`.
Allow `http:` as well, matching the `connect-src 'self' ws: wss:`
pattern already used for WebSockets. `frame-ancestors 'none'` still
prevents Bambuddy itself from being framed cross-origin, which is
the protection that actually matters for clickjacking defense.
After configuring an AMS-HT slot with a custom cloud preset, the slot
card and Configure modal kept showing "Generic PLA" even though the
printer and slicer had the correct preset. The `/slot-presets` response
keyed HT entries at `ams_id * 4 + tray_id = 512`, but frontend lookups
used `ams_id` directly (128 on PrintersPage via getGlobalTrayId, 64 on
SpoolBuddy via a one-off formula). All three agreed for regular AMS, so
the mismatch only surfaced on HT — the saved preset never reached the
UI and the render fell through to `tray.tray_type`.
Backend now keys via a helper that mirrors frontend `getGlobalTrayId`.
SpoolBuddy's AMS page switches to the shared helper. Regression test
covers regular, HT, and external slot keys.
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.