diff --git a/CHANGELOG.md b/CHANGELOG.md index a97152c30..9f8f1a4c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ All notable changes to Bambuddy will be documented in this file. - **PyJWT CVE-2025-45768 (PYSEC-2025-183 / GHSA-65pc-fj4g-8rjx): permanently ignored in pip-audit** — Advisory is disputed by the PyJWT maintainers, with the advisory description literally noting *"this is disputed by the Supplier because the key length is chosen by the application that uses the library."* `fix_versions=[]` on the advisory confirms no PyJWT patch exists or will exist. Bambuddy is not affected: `backend/app/core/auth.py:184` auto-generates secrets via `secrets.token_urlsafe(64)` (~86 chars of entropy, far above any sane minimum) and the file-loaded path at `:177` rejects secrets shorter than 32 chars. Added a permanent `--ignore-vuln CVE-2025-45768` to `.github/workflows/security.yml` with an inline comment citing the file:line evidence so a future maintainer reviewing the ignore list sees why it's load-bearing. Also dropped the stale `--ignore-vuln CVE-2026-4539` for Pygments — Pygments has since shipped a patched version and the ignore is no longer load-bearing (verified: `pip-audit --ignore-vuln CVE-2025-45768` alone reports clean). ### Fixed +- **SliceModal: process / filament dropdowns now filter by nozzle diameter too, not just printer model (#1325 follow-up #2, reported by @IndividualGhost1905)** — With the @BBL name fallback in place, the reporter saw that an X2D 0.4 selection still mixed 0.2 / 0.6 / 0.8 nozzle process variants into the main list. The fallback's regex stripped any trailing ` nozzle` suffix from both sides before comparing, so `"Bambu Lab X2D 0.4 nozzle"` and `"0.40mm Strength @BBL X2D 0.8 nozzle"` both reduced to `"X2D"` and matched. The bundle path was already nozzle-correct (a `.bbscfg` is scoped to one printer-preset-name including its nozzle, so the bundle-side exact-match was nozzle-aware); only the name fallback needed fixing. **Fix**: `extractPrinterPresetModel` and `extractBblToken` now each return `{ model, nozzle }`. The nozzle is the parsed string ("0.4" / "0.6" / etc.) or `null` when the name has no suffix. `classifyByBambuName` treats a `null` process nozzle as `"0.4"` — Bambu's convention is to omit the suffix on 0.4 (the default) and include it for 0.2 / 0.6 / 0.8, exactly as the reporter described. Both `model` and `nozzle` must compare equal for a `'match'`; differing nozzles fall into the existing "Other printers" group, no new group label needed. If the selected printer preset name has no parseable nozzle (non-Bambu / hand-typed), the matcher degrades to model-only — Bambu printer presets always include nozzle in practice, so this is defensive. **Tests**: 9 new in `slicerPrinterMatch.test.ts` covering the matrix (0.4 printer vs no-suffix / 0.6 / 0.8 process; 0.6 printer vs 0.6 / no-suffix-=-0.4; explicit 0.4-suffix-on-process still matches 0.4 printer; same rule on filament presets; wrong-model dominates over matching-nozzle; no-nozzle printer name degrades to model-only); one existing test reframed (the case that previously asserted a 0.6-nozzle process matched a 0.4 printer — the exact bug — now asserts mismatch). 46 slicerPrinterMatch + 34 SliceModal tests green; frontend build clean. - **Timelapse now attaches to the archive after a backend restart mid-print (#1485 follow-up, reported by @pwostran)** — With the duplicate-archive fix from #1485 in place, a restart mid-print stopped creating ghosts — but the resulting archive came back without its timelapse video (only the finish snapshot was attached). Cause is a side-effect of the #1304 first-push guard: on the first MQTT push after Bambuddy starts (`_previous_gcode_state = None`), `is_new_print` is deliberately False so `on_print_start` doesn't fire — which prevents duplicate archive creation **but also** prevents the timelapse-baseline capture, since both live behind the same callback. At PRINT COMPLETE, `_scan_for_timelapse_with_retries` finds an empty `_timelapse_baselines` for the printer and falls into the "take baseline now" fallback in `main.py`. By that point the printer has already uploaded the in-flight MP4, so the snapshot includes it. Every retry then reports "N files found / no new files since baseline" and the scan gives up. The reporter's support bundle is the smoking gun — pre-reboot baseline of 7 files, post-reboot fallback baseline of 8 files (including the just-uploaded one), 4 retries all unable to see the diff. **Fix**: `bambu_mqtt.py` now fires a sibling `on_print_running_observed` callback inside the "Now tracking RUNNING state" branch when the first-push guard suppresses `on_print_start`. `main.py` wires it to a thin handler that fetches the printer row from DB and calls the existing `_capture_timelapse_baseline_at_start`. The callback only fires the first time we observe RUNNING per session (gated on the same `not self._was_running` branch the timelapse-flag restore already lives in), so a normal print start path is unaffected. The handler is also idempotent: if a baseline already exists for that printer, it returns without touching it. Safe because the printer doesn't upload the timelapse until *after* PRINT COMPLETE, so a baseline captured any time during the in-flight print is still pre-upload — no narrow window. The plumbing (`set_print_running_observed_callback` setter, in-`connect_printer` wrapper, constructor pass-through) mirrors the existing `on_print_start` / `on_print_complete` callback chain in `printer_manager.py`. **Tests**: 7 new in `TestPrintRunningObservedCallback` in `test_bambu_mqtt.py` (fires on first RUNNING after startup, doesn't double up with `on_print_start`, fires only once per session, skips on non-RUNNING / missing file / no-callback-set, payload shape mirrors `on_print_start`); 3 new in a dedicated `test_timelapse_baseline_restart_recovery.py` (handler captures the printer's existing-videos snapshot into `_timelapse_baselines`, skips when a baseline already exists, skips when the printer row was deleted between push and handler). 336 MQTT + print-start + timelapse tests green; backend ruff clean. - **SliceModal: process / filament dropdowns now filter for users who haven't uploaded slicer bundles (#1325 follow-up, reported by @IndividualGhost1905)** — The original #1325 fix replaced a stale hardcoded `@BBL ` allow-list with bundle-based compatibility: a process / filament preset was classified against the selected printer by consulting the user's uploaded Slicer Bundles (.bbscfg). That works perfectly for users who have uploaded bundles for every printer their cloud catalogue covers — and silently no-ops for everyone else: every cloud preset resolves to `'unknown'`, nothing moves into "Other printers", and the dropdown looks identical to the pre-fix state. **Fix**: restored the `@BBL ` name fallback as a third tier *below* the bundle path, but with the token-to-printer mapping driven by **the backend's canonical `PRINTER_MODEL_MAP`** (`backend/app/utils/printer_models.py`) instead of a duplicated frontend table. A new `GET /api/v1/slicer/printer-models` route ships the mapping unmodified; `slicerPrinterMatch.buildCompatibilityIndex` accepts it as a second arg, inverts it into a short-code → display-fragment table (`X1C` → `X1 Carbon`, `P2S` → `P2S`, `A1 Mini` → `A1 mini`, …), and `presetCompatibility` uses it only after `compatible_printers` and the bundle index have already returned `'unknown'`. The match is case- and whitespace-insensitive (`"A1 mini"`, `"A1 Mini"` and `"a1mini"` all compare equal). When the registry doesn't list a token, the matcher falls back to comparing the raw token against the printer-preset model fragment — so a brand-new "Q1" printer with `@BBL Q1`-tagged presets matches without any code change. Adding a new model only requires updating the existing backend `PRINTER_MODEL_MAP` (already the single source of truth for `is_dual_nozzle_model`, the rod-type/ethernet registries, and 3MF metadata normalisation) — no frontend table to keep in sync. **Tests**: 2 new in `test_slicer_presets.py` (`/printer-models` returns the full `PRINTER_MODEL_MAP`; the route hands back a copy, not the live module dict); the existing 25 `slicerPrinterMatch.test.ts` cases were extended to 36 covering: registry-driven X1C vs X1 Carbon match, A1 vs A1 mini disambiguation, H2D vs H2D Pro disambiguation, the previously-missing P2S / H2C / H2S / X2D, raw-token fallback for unregistered models, graceful degradation when the registry fetch hasn't resolved yet, the `compatible_printers`-wins-over-name rule, and the bundle-wins-over-name rule. 38 slicer-presets + 36 slicerPrinterMatch tests green; backend ruff clean; frontend build clean. - **Cloudflare-fronted Bambuddy no longer needs an `unsafe-inline` override to load (#1460 follow-up, reported by @Soopahfly)** — A Bambuddy instance behind Cloudflare logged an inline-script CSP violation on every page load: Cloudflare's bot-detection script (`/cdn-cgi/challenge-platform/scripts/jsd/main.js`) is injected into the HTML on the edge with a hash that changes per request, so it can never be allowlisted by `script-src` hash. The contributor's workaround was to relax `script-src` to `'unsafe-inline'` in their Nginx Proxy Manager — which works but defeats most of the CSP. **Fix**: the SPA CSP now stamps a fresh per-request **nonce** into `script-src` (`'self' 'nonce-'`). Per [Cloudflare's documented behaviour](https://developers.cloudflare.com/cloudflare-challenges/challenge-types/javascript-detections/#if-you-have-a-content-security-policy-csp), when a nonce is present in the CSP header Cloudflare clones the same nonce onto its injected ` +