# Changelog All notable changes to Bambuddy will be documented in this file. ## [0.2.5b1] - Unreleased ### Changed - **VP access code is now auto-derived from the target printer in non-proxy modes (Discord report)** — A user on Discord set up a Queue-mode VP with a different access code than the real target printer and couldn't get the slicer to connect, even after the cert-trust path was sorted. Root cause: the live target-printer mirror that landed earlier in the 0.2.5 cycle forwards the slicer's MQTT/RTSPS auth bytes through to the real printer — the slicer holds **one** code in its profile (the one it bound the VP with), and that code has to pass two checks (VP listener, then real printer). If the codes diverge the bridge silently fails at the second hop and the slicer abandons the connection (e.g. opens 8883, FINs before sending a ClientHello). The wiki *did* document a code-match requirement but framed it as a camera-only concern (`MQTT and FTP work either way; only the camera path needs the match`) — wrong, all bridged protocols inherit. **The fix removes the foot-gun rather than re-document it.** When a target printer is selected on a non-proxy VP (Archive / Review / Queue), the access-code field in the VP card switches to a read-only display showing the target's code with an Eye-toggle reveal, and the backend auto-inherits the value on every `create` / `update` (any explicit `access_code` submitted alongside a target is silently overridden — belt-and-braces for non-UI clients). When no target is set, the field stays editable as before. The same `inheritsAccessCodeFromTarget` predicate gates a small "Inherited from target" badge in place of the existing `isSet` / `notSet` status pill. Changing the target after the slicer has already bound triggers an info toast ("Access code now matches the new target — re-add this device in your slicer") because the slicer's stored code is now stale. **One-shot startup migration** in `core/database.py` corrects any pre-existing mismatched VPs on first boot after the upgrade: SELECTs the diverged rows for an INFO log per VP (`VP 'Workshop Queue' (id=3) access code synced from target printer 'X1C #2'` — audit trail for anyone digging through logs), then UPDATEs via correlated subquery (idempotent — the WHERE clause excludes already-synced rows, so re-running is a no-op; portable across SQLite and Postgres). No user-facing banner because there's no action for the user to take — the fix is done, and a previously-stuck bridge now works. **Wiki**: `features/virtual-printer.md` line 1189 flipped from the wrong MQTT/FTP-work-either-way claim to "the bridge forwards slicer auth bytes through; Bambuddy auto-derives so the codes can't diverge", the line-84 tip's "for camera" framing replaced with the broader rule, and the port-table row for RTSP `:322` annotated with "transparent passthrough to the real printer's `:322`, same end-to-end TLS as proxy mode" so the dedicated-bind-IP-vs-passthrough-to-printer apparent contradiction reads as one consistent model. **i18n**: 5 new keys (`accessCode.inheritedFromTarget`, `accessCode.derivedFromTargetHint`, `accessCode.reveal`, `accessCode.hide`, `toast.targetCodeChangedRebind`) translated in all 11 locales (de/en/es/fr/it/ja/ko/pt-BR/tr/zh-CN/zh-TW), no English fallbacks per the project's hard rule. ### Fixed - **Home-page filament assign no longer leaves the slicer unaware of PFCN cloud presets (#1648, reported by @ferch-G)** — Reporter on an H2D with a Polymaker spool noticed that assigning the spool from the Dashboard left the slicer's filament dropdown showing "unknown" / generic, but clicking Configure right after made the slicer recognize it correctly — "Configure" felt like a mandatory follow-up step rather than a refinement. **Root cause: PFCN-prefix cloud preset IDs were never handled.** Bambu's cloud uses three preset-ID shapes: `GFS…` (official Bambu), `PFUS…` (cloud user-created), and `PFCN…` (cloud shared / partner-uploaded — e.g. Polymaker's "(Custom)" Bambu Lab H2D variants like the reporter's `PFCN80e80c1f79db85`). `apply_spool_to_slot_via_mqtt` only routed `GFS` and `PFUS` through the cloud-detail lookup that extracts the real `filament_id`. PFCN slipped past the cloud-lookup branch, fell into the local-preset `int()` parse path, raised ValueError, dropped into `normalize_slicer_filament` which returns any `P`-prefix unchanged, and the raw PFCN landed in `tray_info_idx` — which the printer's calibration table can't index, so the slicer rendered "unknown". The Configure modal rescued each assign because it does its own `getCloudSettingDetail` lookup and writes the resolved `filament_id`. **Fix:** extend the cloud-detail-lookup branch (`inventory.py:129`) and the discard safety net (`inventory.py:223`) to include `PFCN` alongside `GFS`/`PFUS`. After the fix, the same three paths work: cloud-authenticated → real `filament_id` from `detail["filament_id"]` ships as `tray_info_idx` (Polymaker PLA Matte resolves to `GFL05`); cloud unavailable → raw PFCN discarded, slot reuses an existing valid P-prefix preset if material matches; nothing else available → falls through to the spool's generic material id (`PLA → GFL99`). Source comment now lists all three cloud-ID shapes so the next time Bambu invents a new prefix (PFXX, PFYY, …) the maintainer doesn't have to re-derive the structure from a bug report. **Tests:** 3 new integration cases in `test_inventory_assign.py::TestAssignSpoolPfcnCloudPreset` — falls back to generic when cloud unavailable (and pins the no-PFCN-leak invariant), reuses an existing slot's valid P-prefix preset when material matches, and the happy-path cloud lookup that produces a resolved `filament_id` while preserving the original PFCN as `setting_id`. Existing 28 assign-flow tests stay green. - **Bambu cloud A1 Mini filament / process profiles no longer hidden in AMS slot picker (#1649, root-caused by @technopaw)** — Reporter on an A1 Mini observed that the AMS slot Configure dropdown showed no Bambu / Generic filament profiles; only user-authored profiles surfaced. Mirror in the Profiles tab: filtering by "A1 Mini" left only A1 (non-mini) results. **Root cause: Bambu rolled out a profile rename mid-2026.** The `@BBL ` suffix on cloud profiles shifted from the long display form to a terse model code — `Bambu PLA Basic @BBL A1 Mini ...` is now `Bambu PLA Basic @BBL A1M ...` across 106 cloud profiles. User-authored profiles still use the long form (which is why the reporter's custom A1 Mini profile worked, and Bambu PLA Basic happened to render via the `localPreset` always-shown path). Bambuddy's filter compared the extracted token verbatim against the display name (`"A1M".toUpperCase() === "A1 MINI"` is false), so the rename silently stripped every newly-renamed profile from the picker. **Fix: centralized alias-aware match in `frontend/src/utils/slicerPrinterMatch.ts`.** New `PRINTER_MODEL_SUFFIX_ALIASES` table holds the bidirectional `A1 Mini` ⇄ `A1M` mapping (uppercase-normalised, narrow on purpose — wide-net aliasing like `X1` ⇄ `X1C` would silently group truly distinct printers); exported `matchesPrinterModelSuffix(presetSuffix, printerModel)` helper does the case-insensitive compare with alias fallback. Both consumer sites switched to the helper: `ConfigureAmsSlotModal.tsx:586,607` (the AMS slot picker, hit directly + reached from SpoolBuddy's AMS page via `mapModelCode(printer?.model)`), and `slicerPrinterMatch.ts:classifyByBambuName` (the SliceModal Process / Filament compatibility check). Backend `PRINTER_MODEL_MAP` also gains a `Bambu Lab A1M` → `A1 Mini` entry so server-side 3MF printer-model normalization stays consistent if a future 3MF embeds the short form. The structure stays open: when Bambu introduces the next rename, it's a single new row in the alias table — `/api/v1/cloud/settings` is the place to grep, called out in the source comment. **Tests**: 7 new unit cases in `slicerPrinterMatch.test.ts` pin the alias helper (canonical, case-insensitive both directions, A1M ↔ A1 Mini in both orientations, A1M does NOT collapse to A1, A1 does NOT collapse to A1 Mini, unrelated models reject) plus 3 integration cases in `presetCompatibility` (cloud filament `@BBL A1M` matches A1 Mini, cloud process `@BBL A1M` matches A1 Mini, `@BBL A1M` does NOT match A1). 2 new component-level cases in `ConfigureAmsSlotModal.test.tsx`: `@BBL A1M` cloud preset surfaces when picker is for A1 Mini (with `@BBL A1` correctly filtered out), and `@BBL X1C` stays filtered out when picker is for A1 Mini (sanity check against accidental widening). All existing 2062 vitest cases stay green. - **VP Queue / Archive / Review: Bambu Studio 2.7.x stayed stuck at "Downloading" after Send (#1658, reported by @IndividualGhost1905)** — Reporter on Bambu Studio 2.7.1.57 + X1C reported that sending a model to a Queue-mode VP (with Auto-Dispatch off) left the slicer's send modal stuck at "Downloading" forever; clicking Delete on the queued item didn't release it, and even Auto-Dispatch ON + a successful real print didn't release it. Only toggling the VP off/on cleared the slicer. The deleted-from-queue framing is a red herring — the slicer was stuck *before* deletion, the user just noticed it most when they deleted. **Root cause: the #1280 fix assumed the wrong event order.** The original assumption was MQTT `project_file` → FTP upload → set `gcode_state=FINISH`, and the slicer's "Downloading" UI releases on FINISH. Bambu Studio 2.7.x flipped the Send sequence to FTP `verify_job` → FTP `.3mf` → MQTT `project_file`, so on_file_received's `set_gcode_state("FINISH", …)` fires *first*, then the synthetic `_send_print_response` ack runs and overwrites `_gcode_state` back to `"PREPARE"`. From that point the 1 Hz cached-as-base push stream carries PREPARE forever, the slicer waits for the FINISH transition it'll never see, and the modal sits stuck. **Auto-Dispatch ON is the same bug**: the real printer's gcode_state goes PREPARE → RUNNING → FINISH on its bridge, but `_send_status_report` overrides the cached push's `gcode_state` with the local `_gcode_state` (still PREPARE), so the real state changes never reach the slicer. The fix re-fires `set_gcode_state("FINISH", filename, prepare_percent="100")` from `on_print_command` 1.5 s after the synthetic ack, for every non-proxy mode (queue / archive / review). The 1.5 s window is long enough for the slicer's modal to see at least one PREPARE push on the 1 Hz cycle (so the transition reads as PREPARE → FINISH, matching what the slicer expects) and short enough that the modal feels responsive. Proxy mode is exempt — there the real printer drives the bridge state and a synthetic FINISH would clobber a real PREPARE/RUNNING transition. The scheduler cancels any in-flight timer when a new project_file lands so a slicer that retries doesn't end with two competing FINISH timers. **Tests**: 6 new cases in `test_virtual_printer.py` — schedules on archive (and by extension queue/review), proxy mode does NOT schedule, no-MQTT skip is silent, second `project_file` cancels the first timer, delayed run sets the expected `(state, filename, prepare_percent)` triple, empty filename does not schedule. - **Finish photo no longer shows the bed already dropped (#1397, reported by @rtadams89, @Jeff-GebhartCA, @MA2ZAK)** — Bambu's end-gcode lowers the build plate as soon as the print completes. Bambuddy's existing finish-photo path captured a fresh camera frame at `gcode_state=FINISH`, by which time the bed was already at the bottom of the chamber — the photo showed the top of the print well below the camera's natural framing, badly framed and sometimes invisible. Earlier capture attempts (at `layer_num >= total_layer_num` while still RUNNING) hit motion-blur because the toolhead was still parking; capturing through the window kept the wrong frame because the latest was always ~2s before FINISH, mid-bed-drop. **The fix sources the photo from a brief Bambu timelapse Bambuddy records on every dispatched print instead.** Firmware stops timelapse recording AFTER the toolhead parks but BEFORE the bed-drop end-gcode runs, so the last frame frames the finished print correctly — verified on N=2 H2C prints by extracting the last frame of two real timelapses (`spoolbuddy_v2.1` and `case_SpoolBuddy`); both showed the print clearly with the toolhead parked off-frame upper-left and the bed at print height, no motion blur. The post-park-pre-drop window is at least ~2 seconds wide on both, so `-sseof -1.0` (seek to last second, skip the literal last frame) is safe against any encoder tail artifact. **Implementation: force-on at dispatch + cleanup after extraction.** `BackgroundDispatchService._resolve_effective_timelapse(db, archive, job)` reads the `capture_finish_photo` setting before each `start_print` call (reprint + library-file flows both wired) and, when the user did NOT opt in to timelapse for this print, overrides `timelapse=True` on the MQTT command + marks the new `PrintArchive.bambuddy_forced_timelapse` column True. User-opted-in timelapses pass through unchanged (no override needed). Migration adds the column branched on `is_sqlite()` for the boolean default (`DEFAULT 0` on SQLite, `DEFAULT FALSE` on Postgres — PG rejects `DEFAULT 0` for BOOLEAN). New module-level `extract_video_last_frame(video_path, output_path)` in `services/camera.py` runs a single `ffmpeg -sseof -1.0 -i