mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
4fac9ff12cfc5a52661eec29aa388ca85152dd25
2727
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4fac9ff12c |
fix(test): stop sys.modules-deleting backend.app.main in test_code_quality
+ ci: shard backend tests 4-way + drop -v for ~3.5x wall-clock speedup Root cause of the 4 CI failures on PR #1514 (all in test_print_start_assigns_printer_id_to_vp_archive.py + test_timelapse_baseline_restart_recovery.py): test_all_modules_importable in test_code_quality.py was deleting backend.app.main from sys.modules and re-importing it via importlib.import_module. That created NEW module-level dicts (_timelapse_baselines, _expected_prints, _active_prints, …) and re-ran root_logger.addHandler — hence the duplicate log lines at the same microsecond in captured stderr. Any sibling test that bound those names via "from backend.app.main import _timelapse_baselines" before the reimport now held a reference to the OLD dict; production code (reached via "from backend.app.main import on_print_start") resolved the symbol through the NEW module instance. Production mutated the new dict, the test read the old one, the assertion saw None / un-mutated mock_archive. Locally with -n 30, xdist load-balanced test_code_quality.py to a different worker process so the collision never happened (which is why the suite was green for me). CI's -n auto = -n 2 on ubuntu-latest made the collision deterministic. Fix: drop the "del sys.modules[name]" step. importlib.import_module already returns the cached module if cached, or runs the import machinery if not — either way, any import-time error surfaces. The "fresh import" framing was theatre; in practice every module in the list is already imported by other tests/fixtures before this test runs, so we were never actually getting a fresh import anyway — just destruction. CI workflow tightening (separate concern, same PR since both touch the test infrastructure): - Dropped -v from the pytest invocation. 5300+ "PASSED foo::bar" lines per worker were eating ~30-60s of stdout I/O on 2-vCPU runners. --tb=short is sufficient for failure context. - Sharded backend-tests into a 4-way matrix via pytest-split (new dev dep). Each shard runs ~1326 tests in ~95s on a 2-vCPU runner; all 4 run in parallel so wall-clock drops from 362s -> ~100s. - fail-fast: false on the matrix so a single failing shard doesn't hide failures in the other three — PRs see the complete failure picture in one push. |
||
|
|
af03a6f384 |
fix(test): snapshot _timelapse_baselines inside the patch context to dodge CI race
test_running_observed_captures_baseline_on_restart_recovery was reading _timelapse_baselines.get(1) after the patch() with-block exited. Locally and under low parallelism this works fine — the dict still holds what _capture_timelapse_baseline_at_start wrote. CI under xdist's default load-balancing scheduling intermittently saw the dict empty by the time the top-level assert ran, even though the production code logged "Baseline at print start: 3 video files for printer 1" right before returning. The duplicate log line at the same microsecond in the captured stderr is the tell — module state is being re-touched between the handler completing and the test asserting, almost certainly via the session-scoped event_loop fixture in conftest.py interacting badly with the per-file autouse _clear_baselines teardown of a sibling test on the same worker. The test is verifying the handler captured the baseline at the moment it returned, so capture the relevant value at exactly that point — inside the with-block, immediately after the await. That's immune to whatever happens to the module-level dict afterward. |
||
|
|
f385936c58 |
Merge pull request #1514 from maziggy/0.2.4.3
**Bambuddy v0.2.4.3** **⚠ Upgrade Notes — Read Before Updating** Almost everyone is upgrading from 0.2.4.2. 0.2.4.3 is a patch release on the same 0.2.4 code base — no schema breaks, no Docker entrypoint changes, no Vite/proxy quirks. The in-app Apply Update button in Settings → System → Updates works for Docker and for any native install already on 0.2.4.1 or later. Make a backup before upgrading via Settings → Backup → Create Backup. Native install with update.sh snapshots the database automatically and rolls back on failure. Docker and fully-manual paths don't. ### Docker docker compose pull docker compose up -d docker-compose.yml doesn't need refreshing — none of the entrypoint, volume, or env-var conventions changed since 0.2.4. ### Native install — recommended path sudo BRANCH=main /opt/bambuddy/install/update.sh Snapshots the database first and rolls back on failure. ### Native install — manual path sudo systemctl stop bambuddy cd /opt/bambuddy sudo -u bambuddy git fetch --prune --tags --force origin sudo -u bambuddy git checkout main sudo -u bambuddy git reset --hard origin/main sudo /opt/bambuddy/venv/bin/pip install -r requirements.txt sudo systemctl start bambuddy **Behaviour changes to know about** - "Prefer Lowest Remaining Filament" now reads from your inventory, not the printer's RFID counter. Slots bound to a Bambuddy or Spoolman spool sort by your remaining-grams figure first; MQTT-only slots fall back to the previous logic. If you use the preference and have bound spools, the dispatch will start picking a different slot than before — the one that actually has less filament left in your inventory. (#1508) - Camera live view: ffmpeg's RTSP socket-timeout flag is now probed at runtime. Native-Ubuntu installs with a transitional-era ffmpeg were hitting Address already in use because that ffmpeg repurposed -timeout into a listen-mode option. No user action — but if you'd manually patched your install to drop the flag, you can revert. (#1504) - Cloudflare-fronted installs: CSP script-src is now nonce-based. If you'd added 'unsafe-inline' to your reverse-proxy CSP as a workaround for Cloudflare's auto-injected Rocket Loader / email-obfuscation script, remove it — it's no longer needed. (#1460 follow-up) - The 0.2.4 feature-freeze still holds for 0.2.5-bound work. A handful of feat: items shipped in this release because they were already in flight before the freeze and would have stranded the original reporters until the 0.2.5 cycle (Spanish locale #1243, the slicer-filter series #1325, cross-printer re-slicing). Everything else from now on is fix-only on the 0.2.4 train. --- **Highlights** 0.2.4.3 closes two long-running issue clusters and lands the self-service triage infrastructure the support queue has been asking for. The #1325 slicer-filter series is finally complete — Process / Filament dropdowns in the Slice dialog now filter by your selected printer and nozzle diameter, with the right answer whether you've uploaded a Slicer Bundle or are relying on the @BBL preset-name fallback. The #1395 camera reliability series picks up P2S stream stability, ffmpeg stderr capture on stalls, and the cross-ffmpeg-version RTSP timeout flag that was breaking live view on native Ubuntu (#1504). Around them: a new Connection Diagnostic for "printer won't connect" triage, a Log Health Scanner that surfaces self-fixable issues before they hit the support queue, and a Virtual Printer Setup Diagnostic with one-click slicer-cert export — all three are now auto-attached (sanitized) to support bundles and bug reports. Plus the largest fix tally of any 0.2.4.x patch — re-slicing correctness across nozzle classes and multi-plate sources (#1493 + the slice-all toggle), "Prefer Lowest Filament" now reads from your inventory instead of the printer's RFID counter (#1508), timelapse re-attaches after a backend restart mid-print (#1485 follow-up), and a full Spanish locale (#1243). Security: idna >= 3.15 for CVE-2026-45409 (ReDoS) and starlette >= 1.0.1 for PYSEC-2026-161 (malformed Host header). --- **New Features** - Connection Diagnostic — self-service triage for "printer won't connect / won't print" — Triage review of recently-closed issues found roughly a third were user-side setup errors (wrong IP after DHCP renew, LAN-mode toggle off, access code mismatch, port-block by a router-level guest network). The new per-printer diagnostic runs the full layer-1-through-MQTT check in one click — TCP reachability, MQTT TLS handshake, status-message receipt, IP sanity, SSDP visibility — and renders a ranked layer-8 root-cause list with a "Copy to clipboard for bug report" affordance. - Log Health Scanner + Add/Edit-Printer setup pre-flight — Passive scanner that walks your last N hours of logs against a curated catalog of known-issue signatures (#1480 FTP loop, #1395 ffmpeg drops, #1462 drying-power-off, …) and surfaces matches on the System page. Add/Edit-Printer also runs a pre-save pre-flight that warns before you commit a misconfiguration (e.g. trailing whitespace in the access code, swapped IP/serial). - Virtual Printer Setup Diagnostic + one-click slicer-certificate export — VP card now exposes a per-VP "Diagnose" button that walks the slicer-side misconfig list (cert not installed in BambuStudio's trust root, FTPS port collision with another service on the bind IP, MQTT-mode mismatch with the target printer). The matching "Export slicer certificate" button writes the right .crt for whichever VP you're testing, eliminating the manual openssl invocation that ate half the VP-related support tickets. - Slicer: process & filament profiles now filter by the selected printer (#1325, requested by @IndividualGhost1905) — Picking an X1C in the server-side Slice dialog no longer floods the Process / Filament dropdowns with H2D-only presets. When you've uploaded a Slicer Bundle for that printer, filtering is exact-match against the bundle's contents; without a bundle, falls back to a name-based @BBL heuristic on model + nozzle. - Slicer: cross-printer re-slicing across nozzle classes + multi-plate slice-all toggle — Re-slicing an X1C archive for an H2D, or re-slicing a parted-statue multi-plate 3MF in one click. The slice-all toggle produces a single output 3MF whose Metadata/plate_N.gcode entries cover every plate, with the filament dropdowns showing the union of slot usage across plates so paint slots from plate 2 aren't invisible. - Spanish (es) locale (#1243, requested by @MiguelAngelLV) — Full European Spanish translation across all 4899 keys. Joins the existing en / de / fr / it / ja / pt-BR / zh-CN / zh-TW locales; parity check enforces no English-leak. - Currency: Belize Dollars (BZD) (#1454, requested by @PLGuerraDesigns) — Added to the Settings → Cost currency dropdown. - Event-loop stall watchdog — makes a frozen backend self-diagnose (#1486 groundwork) — Several "container hangs after adding a printer" reports share a signature that leaves no actionable evidence in the logs. The watchdog logs a full traceback when the asyncio event loop is blocked for longer than the threshold, so the next time it hangs we get a real stacktrace instead of "it just stopped responding". --- **Changes** - Support bundle + bug-report submission now include the live diagnostic snapshot. Connection Diagnostic / Virtual Printer Setup Diagnostic / Log Health Scanner results are now persisted into the downloadable support ZIP and the submitted GitHub issue, with IPs / printer names / serials / access codes sanitized via the existing log-sanitizer plus an IPv4 regex fallback. A 4-step progress checklist replaces the spinner so the longer wait is communicated honestly. - Settings → SpoolBuddy: CPU load tile on the device card. Pulled from the existing system_stats.load_avg heartbeat — no new round-trip. - Filament inventory: grouped rows now show group totals (#1368) — Collapsed group row now shows the rolled-up Remaining / Total values for the group instead of the values from a single member spool. - Bug-report panel: connection diagnostic collapses for multi-printer setups so the panel doesn't overflow when you have eight printers. - Color Catalog sync now identifies itself as Bambuddy to filamentcolors.xyz. Honest User-Agent — Bambuddy/<version> instead of the bare python-httpx/x.y.z default. Same compliance-audit pass that produced today's Bambu Lab outreach. --- **Fixed** Slicer / slicing - Slicer process / filament dropdowns now filter by nozzle diameter too (#1325 follow-up #2, reported by @IndividualGhost1905) — With the @BBL name fallback in place, an X2D 0.4 selection was still admitting 0.2 / 0.6 / 0.8 nozzle process variants. The regex now parses both model AND nozzle from preset names; differing nozzles fall into the "Other printers" group. - Slicer process / filament dropdowns now filter for users without uploaded Slicer Bundles (#1325 follow-up, reported by @IndividualGhost1905) — Original #1325 fix relied on the user having uploaded a .bbscfg; reporters running without one saw no filtering at all. The name-based @BBL fallback now produces sensible filtering even when no bundle is present. - Slicer: process / filament dropdowns now filter by printer using uploaded Slicer Bundles (#1325, reported by @IndividualGhost1905) — Root fix for #1325 — replaces the preset-name guessing heuristic with bundle-backed exact match. - Re-slicing across the single-nozzle / dual-nozzle boundary now actually works (#1493) — Re-slicing a model sliced for a single-nozzle printer (X1C, P1S, A1, P2S, …) onto a dual-nozzle printer (H2D, X2D) — or vice versa — now produces a correct output 3MF. Per-plate loop handles the bed-bound consolidation that BS CLI's --arrange would otherwise reject. - Failed slice now opens an error modal instead of a toast that vanishes before it can be read. - Re-slicing for a different printer no longer silently produces a file for the original printer — Cross-model re-slice was carrying the source's printer_id through, so the resulting archive looked like it belonged to the wrong printer in the UI. - Re-sliced archive now shows the printer it was sliced for, not the source's printer. - Sliced files no longer report "0 g" filament usage — Archive card and slice-result both showed filament_used_g: 0 for real multi-hour prints. - Flow Calibration now actually runs when the print option is enabled (#1478, reported by @andreirusu99) — H2S reporter saw poor extrusion around corners; root cause was the Flow Calibration option being silently skipped because the wrong project_file field was being passed to the printer. - File Manager: list-view actions no longer clipped; preview modal Slice button now respects the Slicer API setting. Camera - X1/H2/P2 live camera no longer fails with Address already in use on transitional ffmpeg builds (#1504, reported by @rage03usa, confirmed by @PawseHaxor) — Native-Ubuntu reporter was retrying indefinitely with Unable to open RTSP for listening … Address already in use because that ffmpeg version repurposed -timeout into a listen-mode option. Runtime probe now picks -stimeout or -timeout based on what ffmpeg advertises, covering the full pre-deprecation / transitional / modern range without regressing either end. - Camera: P2S RTSP stream no longer drops every frame after the first (#1395, reported by @Tschipel) - Camera: ffmpeg stderr is now captured when an RTSP stream stalls (#1395, reported by @Tschipel) — Previously stderr was only captured when ffmpeg actually crashed, so silent stalls left no diagnostic trace. - Camera diagnostic (stethoscope) is now visible in the pop-out camera window (#1395, reported by @Tschipel) Inventory / scheduler - "Prefer Lowest Remaining Filament" now uses Bambuddy's inventory weight, not just the printer's RFID counter (#1508, reported by @kleinwareio) — Reporter had a cloned spool in slot 1 (950 g) and the original in slot 4 (50 g), the preference enabled, and the dispatch consistently picked slot 1. Root cause: scheduler was reading the printer's tray.remain (which is -1 for non-RFID spools and ties every slot at the sentinel). Now bound-spool slots sort by Bambuddy / Spoolman remaining-grams first; MQTT-only slots fall through to the previous logic. - Scheduler: queue items with force_color_match filament overrides now produce a correct AMS mapping at dispatch (#1437, fixed by external PR #1440 from @Person2099) — When a queue item had force_color_match overrides but no specific plate (plate_id=None), the scheduler was dispatching with ams_mapping: null and the printer was picking AMS slots by filament type only, ignoring the required colour entirely. Contributor fix walks <plate> elements first in filament_requirements.py and falls back to legacy _collect_filaments only when the modern format is absent. - Archive filament colour now reflects the assigned inventory spool, not the slicer's 3MF (#1494, reported by @IndividualGhost1905) — Adding a #000000 black filament was being overridden by the 3MF's embedded colour. - Insufficient-filament pre-print warning now fires on every dispatch path (#1496, reported by @needo37) — Pre-print check was only firing for one of the three dispatch entry points. Spoolman - OpenSpoolman-tagged spools are now selectable in the AMS-slot assignment picker (#1122, reported by @mithkr) — Reporter runs Bambuddy alongside OpenSpoolman; OpenSpoolman writes a different tag shape into spool.extra, which the assignability check was rejecting. Now decided from the slot-assignment ledger instead of the tag field. - Missing-spool-assignment notification no longer false-fires on every Spoolman-mode print (#1473, reported and root-caused by @ojimpo) — Check was only querying one of the two assignment tables. - Per-print weight reporting now works for tag-less spools assigned via the Bambuddy UI (#1459, reported by @Moskito99 — follow-up to #1119) — Postgres + Spoolman reporter saw weight updates skip every non-tag spool assigned through the UI rather than via NFC scan. - AMS hover card and SpoolBuddy fill-bar no longer surface a stale spool after re-assigning a non-RFID slot (#1457, reported by @Menthe11) — P1S reporter with generic non-RFID spools saw the old assignment hang around in both UI surfaces after switching the slot to a different spool. Archives / timelapse - Timelapse now attaches to the archive after a backend restart mid-print (#1485 follow-up, reported by @pwostran) — Companion fix to the duplicate-archive guard from #1485. The same first-push guard that prevents the duplicate also prevented the timelapse-baseline capture, so the post-reboot scan couldn't find new files. Baseline is now captured on the first observed RUNNING state when the start callback is suppressed. - A backend restart mid-print no longer duplicates the job in the archive (#1485, reported by @pwostran) - Timelapse auto-attach works for VP-queue / dispatch prints (#1403 follow-up, reported by @pwostran) — The snapshot-diff strategy that picks the right MP4 was incorrectly attributing files to the wrong archive for dispatched prints. File Manager / Library - "All Files" view now shows files inside subfolders (#1499) — Was previously returning only top-level files. - Library files now display the filename, not the embedded 3MF Title (#1489, reported by @needo37) — File Manager cards / search / sort were keying off file_metadata.print_name, which mismatched what users actually search by. - 3D preview no longer freezes the page on complex multi-part 3MFs (#1412, reported by @anthonyma94) — Opening the 3D preview on a multi-colour parted MakerWorld statue hung the browser tab. - STL thumbnail generation failures now log a full traceback — Surfaced by the #1480 support bundle. Printers / connectivity - File Manager no longer polls the printer over FTPS every 30 seconds while open (#1480, reported by @OscarsWorldTech) — P1S reporter's printer was churning through MQTT disconnect/reconnect cycles every 30s because the File Manager was opening idle FTPS sessions to the printer for no functional reason. - Printer serial numbers normalized on input; stale connection that never receives a status report now logs an actionable hint (#1465, reported by @jmneely94) - Smart-plug "Auto Off after Drying" no longer kills the printer seconds into a drying cycle (#1462, reported by @Kyobinoyo) — X2D reporter set a 1-hour dry cycle and the smart plug cut power 8 seconds in. False "drying complete" detection on power-up state. - AMS drying popover now scrolls instead of clipping the Start button on short viewports (#1458, reported by @kleinweby) - Add Printer no longer hangs the container on P1S (#1445, reported by @psybernoid, confirmed by @thomassjogren) — Regression introduced in 0.2.4.2 by the fix(printers) cleanup. - FTP: P2S upload truncates / 426 "Failure reading network stream" on Python 3.13 (#1401, reported and root-caused by @iitazz) — P2S firmware 01.02.00.00 reporter on Python 3.13 saw uploads truncate at exactly 1.8 MB. SpoolBuddy - Spool ID surfaced everywhere a spool's identity is rendered + Write-Tag page honours Spoolman mode (#1439, reported + partially prototyped by @flom89) Stats - Print Activity heatmap buckets prints by local date, not UTC date (#1446, reported and root-caused by @needo37) — CDT (UTC-5) reporter noticed prints finished in the evening appeared on the wrong calendar day. - Failure Analysis widget no longer shows "Unknown" for archives classified after the fact (#1444, reported and root-caused by @needo37) UI / PWA - PWA: in-app install button + self-host the Inter font (#1460, reported by @Soopahfly) — Reporter could install Bambuddy as a PWA on desktop but not on Android, and the font was being fetched from rsms.me on every page load. - Self-hosted Inter font now actually loads — /fonts/*.woff2 was not served (#1460 follow-up) — The browser console logged downloadable font: rejected by sanitizer because the static mount didn't include the fonts subdirectory. - Cloudflare-fronted Bambuddy no longer needs an unsafe-inline override to load (#1460 follow-up, reported by @Soopahfly) — CSP script-src switched to nonce-based so Cloudflare's auto-injected email-obfuscation / Rocket Loader script passes without the override. - Local Profiles: search bar no longer disappears when a query matches nothing (#1470, reported by @pwostran) - Failure Detection: Status panel Low / High thresholds now reflect the selected sensitivity (#1469, reported by @JohnMacOB) Security - idna >= 3.15 — clears CVE-2026-45409 (ReDoS in idna.encode() with crafted Unicode payloads). - starlette >= 1.0.1 — clears PYSEC-2026-161 (malformed Host header crash in request.url construction). - PyJWT CVE-2025-45768 permanently ignored in pip-audit — Advisory is disputed by the PyJWT maintainers; Bambuddy auto-generates 86-char secrets via secrets.token_urlsafe(64), file-loaded path rejects secrets shorter than 32 chars. --- **Thanks** **@Person2099** ([PR #1440](https://github.com/maziggy/bambuddy/pull/1440) — `force_color_match` AMS-mapping fix). Thanks for the upstream patch. |
||
|
|
02e119ea45 |
fix(test): use /nonexistent/ instead of /tmp/ to satisfy Bandit B108
The test_returns_empty_when_3mf_missing test sets a deliberately
non-existent file_path on a PrintArchive to verify
compute_deficit_for_queue_item handles the missing-3MF branch
gracefully. The path just needs to fail an existence check — the
/tmp/ prefix was incidental.
Bandit B108 ("insecure temp file usage") regex-matches /tmp/,
/var/tmp/, and /dev/shm/. Dropping /tmp/ in favour of /nonexistent/
keeps the test behaviour identical (still a guaranteed-missing
path, still triggers the missing-file branch) while clearing the
GitHub Advanced Security finding on PR #1514 without adding a
# nosec annotation.
|
||
|
|
d64c0dcd08 | Merge remote-tracking branch 'origin/main' into 0.2.4.3 | ||
|
|
07ea69c3f0 |
test(docker): include gcode_viewer/ in the test image so the packaging assertion actually runs
Dockerfile.test only COPYed backend/ and pyproject.toml, so the integration test at tests/integration/test_gcode_viewer.py:63 silently pytest.skip'd in every Docker run with "gcode_viewer/ index.html not present at /app/gcode_viewer/index.html". That was deliberate fallback behaviour for unit-test environments where the assets are intentionally absent, but in CI it meant the #1218 packaging regression (3D Preview returning {"detail":"Not Found"} because the embedded PrettyGCode viewer wasn't bundled into the prod image) had no test guarding against a recurrence — the test that was supposed to catch it was the one being skipped. Add COPY gcode_viewer/ ./gcode_viewer/ to the backend-test stage, matching the path the production Dockerfile uses (static_dir.parent / "gcode_viewer" = /app/gcode_viewer/) so the assertion runs against the same layout the app sees at runtime. Path-anchored comment in the Dockerfile so a future maintainer doesn't strip the COPY as unused. |
||
|
|
f513c2c1db | Bumped version | ||
|
|
a64df5a922 | Updated CHANGELOG | ||
|
|
b9d51ffd80 |
chore(deps): floor-pin starlette>=1.0.1 against PYSEC-2026-161
pip-audit reported starlette 1.0.0 in the dev venv. starlette is transitive via fastapi, whose range still admits 1.0.0, so the resolver was silently picking the vulnerable build. Same floor-pin strategy as the existing idna/urllib3 entries — direct pin in requirements.txt with a why-comment so it isn't mistaken for an unused line and dropped later. Verified clean: pip-audit reports "No known vulnerabilities found" after the upgrade (starlette 1.0.0 → 1.1.0 locally). |
||
|
|
3b9633a178 |
● feat(support): include sanitized connection / VP / log-health diagnostics in support bundle and bug report (#1506 follow-up)
The three diagnostic surfaces shipped earlier this month ( |
||
|
|
cc468ddd42 | Updated BACKERS.md | ||
|
|
17602774f3 | Updated BACKERS.md | ||
|
|
03896d1af0 |
fix(scheduler): use inventory weight for "Prefer Lowest Filament" sort (#1508)
Reporter has a P1S with an inventory spool cloned to slot 1 and the
original (much further used) in slot 4, the preference enabled, and
the dispatch picked slot 1 every time. The sort's been blind to
Bambuddy inventory weights — it reads MQTT `tray.remain`, the
printer firmware's RFID-decremented value, which has two limitations:
- Bambu RFID only. Non-RFID spools report -1 and get clamped to a
sentinel; multiple non-RFID trays then tie in the sort and Python's
stable sort collapses to AMS-slot insertion order, so slot 1 wins.
- Even when set, it's the printer's counter, not Bambuddy's
`label_weight - weight_used` (internal) or Spoolman's
`remaining_weight`. The two diverge whenever the user re-spools,
swaps cardboard, or runs a print outside Bambuddy.
The reporter is on internal-inventory mode with non-RFID spools — both
failure modes apply, hence slot 1 every time.
Fix: when a slot is bound to an inventory spool the inventory record's
remaining weight becomes the sort signal. New async helper
`_build_inventory_remain_overrides(db, printer_id, loaded)` returns
`{global_tray_id: remaining_grams}` for bound slots — internal mode
joins SpoolAssignment → Spool once per dispatch; Spoolman mode joins
SpoolmanSlotAssignment then reuses `_spoolman_remaining_grams` from
filament_deficit.py for parity.
New `_prefer_lowest_sort_key` does a two-tier comparison: inventory-
tracked spools sort BEFORE MQTT-only spools, then ascending by
remaining within each tier, then ascending by ams_id*4+tray_id as the
deterministic slot tie-breaker. The tier flag dominates so grams
(inventory) and percent (MQTT) never get cross-compared — no unit
conversion needed.
MQTT-only behaviour is preserved exactly: remain=-1 still maps to the
101 sentinel and slot order still decides on ties. Users who haven't
bound any inventory spool see no change. The DB lookup runs only when
prefer_lowest_filament is enabled.
External / VT slots are skipped (tracked separately from AMS bindings).
|
||
|
|
eae96da56e |
fix(camera): probe ffmpeg for the right RTSP socket-timeout flag (#1504)
A previous attempt swapped `-timeout` → `-stimeout` unconditionally to
fix EADDRINUSE on the reporter's transitional ffmpeg. That broke every
install on a modern ffmpeg (5+/6+/7+) — current Debian/Ubuntu/Homebrew
— where `-stimeout` was removed and `-timeout` is back to meaning
socket I/O. Verified locally: `ffmpeg -stimeout ...` errors
"Unrecognized option 'stimeout'" on ffmpeg 7.1.
install on a modern ffmpeg (5+/6+/7+) — current Debian/Ubuntu/Homebrew
— where `-stimeout` was removed and `-timeout` is back to meaning
socket I/O. Verified locally: `ffmpeg -stimeout ...` errors
"Unrecognized option 'stimeout'" on ffmpeg 7.1.
ffmpeg has shipped THREE arrangements of this option over time and
Bambuddy supports the full range:
- Pre-deprecation (early 4.x and earlier): `-timeout` is socket I/O.
- Transitional (~late-4.x, Jammy-era): `-timeout` is deprecated and
repurposed to RTSP listen-mode timeout; any non-zero value implies
`-listen`, which makes ffmpeg bind the TLS-proxy port and fail with
EADDRINUSE. `-stimeout` is the replacement socket I/O option.
- Modern (5.x / 6.x / 7.x): `-stimeout` REMOVED. `-timeout` is back to
socket I/O — the original meaning.
So no single literal is correct on all installs.
Fix: `rtsp_socket_timeout_flag()` in services/camera.py probes
`ffmpeg -h demuxer=rtsp` once and picks `-stimeout` when ffmpeg
advertises it (covers transitional + older builds that kept it as an
alias), else `-timeout` (modern + pre-deprecation). Cached at module
level for the process lifetime — ffmpeg doesn't swap mid-run.
The function returns the option name without a leading dash; callers
prepend it themselves so a formatting bug can't pass an empty flag.
Wired into both RTSP ffmpeg call sites in lockstep: routes/camera.py
(printer camera) and services/external_camera.py (external RTSP),
which use the same TLS-proxy + ffmpeg pattern and would hit the same
regression on either ffmpeg cohort.
Tests: 8 in test_ffmpeg_rtsp_timeout_flag.py — 6 probe unit tests
(prefers stimeout when advertised, falls back to timeout on modern,
defaults to timeout when ffmpeg missing or probe raises, caches across
calls, trailing-space substring guard against `-listen_timeout`
false-positives), 2 parametrised guards against either RTSP ffmpeg
argv re-hard-coding a literal instead of consuming the probe. 37
probe + existing external-camera tests green.
|
||
|
|
6591fc011f |
fix(slicer): filter process / filament presets by nozzle diameter too (#1325 follow-up #2)
After the @BBL name fallback landed, IndividualGhost1905 reported that
an X2D 0.4 selection still mixed 0.2 / 0.6 / 0.8 nozzle process variants
into the main dropdown. The fallback's two extractors —
extractPrinterPresetModel and extractBblToken — both ended their regex
with `\s+[\d.]+\s*nozzle\s*$` and discarded the match, reducing
"Bambu Lab X2D 0.4 nozzle" and "0.40mm Strength @BBL X2D 0.8 nozzle"
to the same "X2D" string. Match was model-only; nozzle ignored.
Bambu's naming convention: 0.4 is the default and DROPS the suffix; 0.2
/ 0.6 / 0.8 carry an explicit "<size> nozzle" segment. So a process
preset with no suffix is implicitly 0.4 — not "any nozzle".
Have both extractors return { model, nozzle }, parsing the suffix out
instead of stripping. classifyByBambuName then requires both model AND
nozzle to compare equal; a null process nozzle counts as "0.4" per the
convention above. Differing nozzles fall into the existing "Other
printers" group — no new group label.
The bundle path was already nozzle-correct: a .bbscfg is scoped to one
printer-preset-name including its nozzle, and the bundle-side exact
match is therefore nozzle-aware. Only the @BBL name fallback needed
fixing. The `compatible_printers` tier is also unaffected (Bambu's
bundled `compatible_printers` lists include the full printer-preset
name with nozzle, so disambiguation already works).
If the selected printer preset name has no parseable nozzle (non-Bambu
/ hand-typed), the matcher degrades to model-only. Bambu printer
presets always carry one in practice; this is defensive.
9 new tests cover the matrix:
- 0.4 printer ↔ no-suffix process: match
- 0.4 printer ↔ 0.6 / 0.8 process: mismatch
- 0.6 printer ↔ 0.6 process: match
- 0.6 printer ↔ no-suffix process (=0.4): mismatch
- same rule applied to filament presets
- explicit "0.4 nozzle" suffix on process still matches 0.4 printer
- wrong-model still mismatches even when nozzles agree
- no-nozzle printer name degrades to model-only match
One existing test ("handles a trailing nozzle-size suffix on the @BBL
tag") had asserted that a 0.6-nozzle process matched a 0.4 printer —
the exact reporter complaint. Reframed: matching 0.4-suffix still
matches, 0.6-suffix now mismatches.
|
||
|
|
ed08ed3787 |
fix(timelapse): capture baseline on restart-recovery so post-reboot timelapses attach (follow up issue #1485)
When Bambuddy is restarted mid-print, the first MQTT push from the printer carries `_previous_gcode_state = None`. The #1304 guard deliberately suppresses on_print_start on that first push to prevent duplicate archive creation — but on_print_start is also where _capture_timelapse_baseline_at_start runs, so the in-memory _timelapse_baselines dict stays empty for the resumed session. At PRINT COMPLETE, _scan_for_timelapse_with_retries finds no baseline and falls into its "take baseline now" fallback. By that point the printer has already uploaded the in-flight MP4, so the snapshot includes the new file. Every "Found N files / no new files since baseline" retry then fails to detect a diff, and the archive ends up with no timelapse attached — pwostran's report (#1485 follow-up): card shows the finish snapshot but no video. Add a sibling callback on_print_running_observed that bambu_mqtt fires in the "Now tracking RUNNING state" branch when on_print_start was suppressed. main.py wires it to a thin handler that looks up the printer row and calls the existing _capture_timelapse_baseline_at_start. Idempotent — skips if a baseline already exists (handles the rare same-session race where on_print_start also fires for some reason). The printer doesn't upload the timelapse until after PRINT COMPLETE, so a baseline captured any time during the print is still pre-upload — no narrow window to hit. Verified against the in-the-field logs in #1485 (pwostran's 2026-05-23 support bundle): pre-reboot: baseline = 7 files reboot post-reboot completion: fallback baseline = 8 files (includes new MP4) -> all 4 retry attempts report "no new files since baseline" 10 new tests cover both the MQTT-side fire decision (fires when suppressed, doesn't fire when on_print_start handles it, once per session, payload shape mirrors on_print_start) and the main.py handler (snapshot capture, double-capture guard, missing-printer-row guard). |
||
|
|
c0c07bc509 |
fix(archives): cross-model re-slice no longer carries source printer_id
When the user re-sliced an H2D archive for X1C, the new archive card and
reprint modal both showed the source printer's name (e.g. "Workshop H2C")
even though sliced_for_model on the same row correctly read "X1C".
slice_and_persist_as_archive copied source_archive.printer_id verbatim
onto the new PrintArchive row. Both the archive card
(ArchivesPage.tsx:3571) and the reprint modal read printer_id first and
only fall back to sliced_for_model when it's None. With printer_id set
to the source's H2D printer, neither could see that the new file is for
a different model.
Same shape as the sliced_for_model fix already in the same function — a
cross-model re-slice means the source's physical printer isn't where
this output prints anymore. When the slicer-baked target model differs
from source_archive.sliced_for_model, drop printer_id so both surfaces
fall back to the sliced_for_model badge ("X1C"). Same-model re-slices
keep printer_id so the reprint modal still pre-selects the source
printer.
Edge case: when source_archive.sliced_for_model is None (older archives
that predate that column being populated), we can't tell whether this
is a cross-model re-slice. Fail open and preserve printer_id rather
than spuriously nulling it.
slice_and_persist (the library-file path) doesn't have this bug —
LibraryFile has no printer_id column.
Tests in TestSliceArchiveResliceModel cover all three branches: cross-
model nulls printer_id; same-model keeps it; unknown source model
preserves it.
Surfaced via the bambuddy demo doing H2D -> X1C re-slices after the
.bbscfg System-tier slicing fix landed.
|
||
|
|
5f473801f8 |
fix(file-manager): list-view actions clipped + preview modal slice button now respects Slicer API setting
Two small bugs reported in chat:
1. List-view actions column was clipped off the right edge. The grid
template fixed the actions column at 80px - a sliced 3MF row renders
7 icon buttons (Print, Schedule, Slice, 3D Preview, Download, Rename,
Delete) which need ~220px. The wrapper's overflow-hidden (there for
rounded corners) then swallowed everything past column 80px.
Switched the trailing column from 80px to min-content so it sizes to
the widest action strip across all rows, and replaced overflow-hidden
with overflow-x-auto so narrow viewports get a scrollbar instead of
vanished icons. Rounded corners survive because the border-radius is
on the wrapper itself, not on the overflow box.
2. The 3MF preview modal's "Open in Slicer" button always launched the
external slicer (BambuStudio / Orca) regardless of Settings -> Workflow
-> Slicer -> Use Slicer API. With the API enabled, the file-row Cog
already opens the in-app Bambuddy SliceModal - the preview-modal
button should match.
Added an optional onSliceWithBambuddy callback to ModelViewerModal.
When set AND settings.use_slicer_api is true AND we're in library mode
on a sliceable type (.3mf/.stl/.step/.stp), the header button becomes
a Cog labelled "Slice" and calls the in-app handler. Otherwise it
falls back to the existing ExternalLink "Open in Slicer" button.
FileManagerPage wires the callback to close the viewer and open the
SliceModal with the same file, gated on the same isSliceableFilename
+ library:upload permission checks the row's Cog already uses. No new
i18n keys - reuses slice.action ("Slice") that the file row already
ships in all 9 locales.
|
||
|
|
3058c5789b |
fix(slice): @BBL name fallback for users without slicer bundles (#1325 follow-up)
The first cut of #1325 swapped a stale hardcoded model table for bundle-based compatibility matching: a cloud / standard process or filament preset was classified by consulting the user's uploaded Slicer Bundles (.bbscfg). That works perfectly when bundles cover every printer in the user's cloud catalogue, and silently no-ops otherwise - every cloud preset resolves to 'unknown', nothing moves into "Other printers", and the dropdowns look identical to the pre-fix state. The reporter saw exactly this on a clean install with no bundles uploaded. Restored BambuStudio's `@BBL <token>` name convention as a third tier below the bundle path, but driven by the canonical backend PRINTER_MODEL_MAP - exposed via a new GET /slicer/printer-models route - rather than a manually-maintained frontend table. The matcher inverts the registry into short-code -> printer-fragment ("X1C" -> "X1 Carbon"), normalises whitespace + case so "A1 mini" and "A1 Mini" compare equal, and falls back to raw-token compare for models not yet in the registry (so a future "Q1" matches without a code change). Adding a new Bambu model still touches exactly the one backend file already listed in the Bambu Model Codes registry. Tests: 2 new in test_slicer_presets.py (route returns the full PRINTER_MODEL_MAP, route returns a copy not the live dict); 11 new in slicerPrinterMatch.test.ts covering registry-driven X1C vs X1 Carbon, A1 vs A1 mini, H2D vs H2D Pro, P2S / H2C / H2S / X2D (which the original hardcoded list was missing), raw-token fallback, registry-not-loaded-yet degradation, and the precedence rules between compatible_printers / bundle / @BBL name. 38 slicer-presets + 36 slicerPrinterMatch + 34 SliceModal tests green; backend ruff clean; frontend build clean. |
||
|
|
4096d8d6bd |
fix(csp): nonce-based script-src so Cloudflare-injected scripts pass (#1460 follow-up)
Behind Cloudflare, the bot-detection script CF injects into every HTML response carries a hash that rotates per request, so it can never be allowlisted by hash. Reporters with CF in front had to relax their NPM CSP to 'unsafe-inline' as a workaround. Per Cloudflare's documented behaviour, when a nonce is present in the page's script-src, CF clones it onto its injected <script>. The SPA CSP now stamps a fresh per-request nonce via secrets.token_urlsafe(16), keeping 'self' for our own scripts (index.html has had no inline scripts since the SW registration moved to /sw-register.js in the original #1460 PR), so no HTML body rewriting is needed. Also folded in: /manifest.json, /sw.js and /sw-register.js now accept HEAD as well as GET, so `curl -I` and uptime scanners stop returning 405 on those routes - a separate red herring during this issue's debugging. Tests: 3 new in test_security_headers.py - 'nonce-' token stamped into SPA script-src while 'self' remains and 'unsafe-inline' does not; nonce is fresh per request across 5 sequential calls; HEAD on the three PWA routes never returns 405. 22/22 security-header tests green; backend ruff clean. |
||
|
|
4686d108ef |
feat(slice): cross-printer re-slicing across nozzle classes + multi-plate slice-all
Re-slicing a 3MF authored for a single-nozzle printer (X1C, P1S, A1, P2S)
onto a dual-nozzle printer (H2D / H2D Pro) — or vice versa — previously
failed with "G-code in unprintable area of multi-extruder printers" (the
source's bed-coordinate layout lands in the H2D's per-nozzle dead zone)
or, on multi-color projects, a hard SIGSEGV inside the slicer's ZFiller
polygon-clipping. Earlier shipped a fail-fast 400 guard; this drop lifts
it and actually does the conversion by forwarding the sidecar's existing
--arrange flag when the source and target nozzle classes differ. BS
itself reconciles the embedded project_settings.config against the new
printer that way, the same way the GUI's "Switch Printer" operation
does. The guard becomes a kept-for-compat no-op.
Slice-all-plates added to the SliceModal: a checkbox for multi-plate
sources sends plate=0 to the backend, which forwards --slice 0 to the
BS CLI. Same-class slice-all produces one multi-plate output 3MF in a
single sidecar call. Cross-class slice-all loops per plate (BS's
--arrange is project-wide and would otherwise consolidate every plate's
objects onto one bed) and merges the per-plate outputs into one
multi-plate 3MF locally via the new merge_plate_3mfs helper. The toast
shows "Plate 2 of 5 — Generating G-code (47%)" through the loop.
Three side fixes surfaced during testing:
- substitute_unused_plate_filaments overwrites unused-slot filaments
with the slot-1 selection before slicing so BS's loaded-filament
temperature validator doesn't reject a PLA print whose unused slot 2
defaulted to ABS in the dropdown
- re-sliced archive thumbnail now prefers the source's per-plate
render (Metadata/plate_N.png) over the project-wide MakerWorld cover
art, because BS CLI with --arrange skips writing a fresh per-plate
preview
- re-sliced archive bed_type now lifts from the sliced output's
curr_bed_type onto the PrintArchive column the card actually reads
Schema: SliceRequest.plate range relaxed from ge=1 to ge=0 to admit
the "all plates" sentinel; SlicerApiService.slice_with_profiles /
slice_with_bundle take an `arrange` parameter.
Tests: 26 in test_slicer_3mf_convert (count / merge / substitute /
extract), 3 in test_slicer_api (arrange wire format), 9 in
test_library_slice_api (guard no-op, bed_type lift, thumbnail
fallback, new cross-class slice-all loop integration test), 2 in
test_archive_service (Auxiliaries fallback), 4 in SliceModal.test
(plate=0 toggle), 2 in SliceJobTrackerContext.test (multi-plate toast
prefix). 659 backend + 42 frontend green; backend ruff clean,
frontend build clean, i18n parity green at 4984 keys × 9 locales.
|
||
|
|
ebba1385d3 |
feat(spoolbuddy-settings): show CPU load on the device card
The SpoolBuddy daemon already reports load_avg (1/5/15 min) and cpu_count in its heartbeat, but the Settings -> SpoolBuddy card only showed CPU temp / memory / disk / system uptime. Adds a fifth tile rendering the 1-min load with core count and percent-of-cores -- e.g. "1.20 / 4 (30%)" -- next to the existing CPU temp tile. Falls back to a bare load number when cpu_count is missing and hides the tile when the daemon doesn't emit load_avg. settings.spoolbuddy.cpuLoad translated across all 9 locales. |
||
|
|
7ea4410b21 |
fix(queue): insufficient-filament warning now fires on every dispatch path (#1496)
The pre-print deficit warning from #720 only ran inside the PrintModal submit flow. Both the green ▶ button on a staged queue row (POST /queue/{id}/start) and the Virtual Printer queue-mode intake bypassed it — auto_dispatch=True VP intakes would dispatch unsupervised onto spools that physically can't complete the print. Extracted the deficit check into backend/app/services/filament_deficit.py (single source of truth, both internal inventory and Spoolman modes). POST /queue/{id}/start returns 409 with a structured deficit payload unless ?skip_filament_check=true. The dispatch scheduler runs the same check before each _start_print; a deficit promotes the item to manual_start + sets a new filament_short flag (idempotent migration on print_queue). The flag clears automatically on the next tick when the operator swaps a spool to one with enough material. Frontend ▶ catches the 409 and opens a confirm modal showing each shorted slot's required vs remaining grams; the row now renders a yellow "Insufficient filament" badge when filament_short is set. Translated across all 9 locales. |
||
|
|
32fcd85827 |
fix(library): "All Files" view now shows files inside subfolders (#1499)
The File Manager sidebar's "All Files" entry was passing include_root=true to GET /library/files because the boolean was derived from `selectedFolderId === null`. The backend's include_root flag means *root files only* when folder_id is null, so a library where every file lived in a subfolder rendered as empty. Pass include_root=false from "All Files" so the backend returns every active file across folders. Adds a regression test that mounts the page, mocks the endpoint to distinguish include_root=true vs false, and asserts both a root file and a nested file appear. |
||
|
|
6d673d0626 | Updated BACKERS | ||
|
|
6b87cdefd7 | Updated BACKERS | ||
|
|
e222a0ef0e |
feat(system): log-health scanner + Add/Edit-Printer setup pre-flight
Adds a passive log-health check that complements the active Connection Diagnostic. Scans Bambuddy's recent app log against a curated allowlist catalog of known failure signatures (rejected access code, FTPS :990 timeout, FTPS TLS failure, flapping MQTT, unreachable camera, SQLite "database is locked" contention), dedupes and classifies each finding as layer8/environment/bug, and deep-links to the troubleshooting wiki. Sample log lines are sanitized before they leave the process. Exposed via GET /system/health and surfaced on two surfaces sharing one SystemHealthPanel component: a System Health section on the System page, and inline in the bug reporter when the form opens. The Add-Printer and Edit-Printer dialogs gained a setup-time pre-flight: saving runs the connection diagnostic and, on a failed check, warns with a "save anyway" escape hatch instead of silently saving a printer that will immediately show offline. Log read/parse/sanitize primitives extracted from routes/support.py into a shared services/log_reader.py (behaviour-preserving); affected support tests repointed accordingly. Tests: test_log_health.py (11), test_system_api.py (2 new), SystemHealthPanel + BugReportBubble + AddPrinterPreflight + EditPrinterPreflight (8 frontend). All strings translated across the 9 locales. Backend ruff clean, full unit suite green, frontend build + eslint clean, i18n parity green. |
||
|
|
ab8e07618f |
fix(inventory): archive filament colour follows the assigned spool, not the 3MF (#1494)
An archive's filament_color was parsed verbatim from the print job's 3MF (filament_colour in project_settings.config) — the slicer's filament-slot colour, which a user picks independently of the exact hex they curate on the Bambuddy inventory spool. So a print from a #000000 inventory spool showed #161616 (the slicer's near-black) in the archive card and the Color Distribution graph, even though usage tracking correctly decremented the right spool. Once usage tracking has resolved the print's filament slots to inventory spools, the spool colours are authoritative. _track_from_3mf (built-in inventory) and report_usage (Spoolman mode) now overwrite the archive's filament_color with the slot-ordered, de-duplicated colours of the matched spools. The rewrite is all-or-nothing: it only applies when every used slot resolved to a spool carrying a colour, so a partially-mapped multi-colour print keeps the 3MF colour rather than silently dropping the unmatched slots. Shipped for both inventory modes: built-in spools read Spool.rgba, Spoolman spools read the spool's filament.color_hex (fetched via get_spool for tag-less slot-assignment matches). New helpers _spool_color_to_hex / _archive_colors_from_spools in usage_tracker.py, reused by spoolman_tracking.py via _apply_spool_colors_to_archive. Tests: 12 new in test_usage_tracker.py (hex normalisation, the all-or-nothing rule across single/multi/partial/no-colour/AMS-fallback cases, end-to-end rewrite), 4 in test_spoolman_tracking.py (Spoolman rewrite + empty/partial/missing-archive no-ops). 70 tracking tests green; backend ruff clean. |
||
|
|
6d7a92c024 |
fix(camera): P2S RTSP stream dropped every frame after the first (#1395)
A fresh P2S support bundle showed ffmpeg's reason for the stall: `frame=1 time=00:00:00.06 dup=0 drop=526 speed=0.0037x`. ffmpeg connects and frames arrive (drop counter climbs ~15/s), but it emits one output frame and the output clock freezes. The streaming command ends with `-r 15`, putting ffmpeg in CFR mode: it drops/dupes input frames to hit 15 fps based on the source's timestamps. P2S firmware 01.02.00.00 sends an RTSP stream whose RTP timestamps don't advance, so CFR treats every frame after the first as a same-timestamp duplicate and drops it. Snapshot capture works on the same printer because that path has no `-r` (no CFR conversion); X1/H2 are unaffected because their firmware timestamps are correct. The earlier probesize fix was masking this second bug. Add `-use_wallclock_as_timestamps 1` to the P2S camera profile via the existing extra_ffmpeg_input_args hook. ffmpeg rebuilds each packet's PTS from arrival wall-clock time, the output clock advances, and CFR conversion works. No dataclass change, no other model touched. Tests: 2 new in test_camera_profiles.py (P2S splices the flag+value pair; default profile keeps extra_ffmpeg_input_args empty so the override never leaks to X1/H2). |
||
|
|
6bc6a1d683 |
feat(virtual-printer): setup diagnostic + one-click slicer-certificate export
Two recurring virtual-printer support pains, both on the Virtual Printers
settings page.
Setup check: a stethoscope action on each VP card runs a pass/fail/warn/skip
checklist — VP enabled, services running, bind interface still exists, access
code set, target printer (proxy mode), and a live TCP probe of the FTP / MQTT
/ discovery ports on the bind IP. start_server swallows per-service bind
errors, so a service object can exist while nothing is listening; probing the
bind IP from outside is the only reliable signal and it catches the common
"VP not visible in the slicer" bind-IP-conflict and stale-interface cases.
Slicer certificate: virtual printers present a TLS cert signed by a shared CA
the slicer must trust. Until now users had to docker exec in and cat
bbl_ca.crt. A "Slicer certificate" row on the settings card now offers Copy
and Download (bambuddy-virtual-printer-ca.crt) plus the SHA-256 fingerprint.
GET /virtual-printers/ca-certificate returns only the public certificate; the
CA private key never leaves the backend. The CA is generated on demand so the
button works before the first VP is enabled.
Backend:
- services/virtual_printer/diagnostic.py — run_vp_diagnostic + port probes
- schemas/virtual_printer.py — VPDiagnosticResult
- CertificateService.get_ca_certificate_info() + manager helper
- routes: GET /virtual-printers/ca-certificate, /{vp_id}/diagnostic
Frontend:
- VirtualPrinterDiagnosticModal.tsx; stethoscope button on VirtualPrinterCard
- caCert row on VirtualPrinterList; utils/clipboard.ts (shared copy w/
non-secure-context fallback + downloadTextFile), de-duplicating the
existing FQDN-copy logic
- vpDiagnostic.* + virtualPrinter.caCert.* across all 9 locales
9 backend unit tests + 4 route integration tests + 6 frontend tests.
Backend ruff clean, frontend build clean, i18n parity green.
|
||
|
|
ed31b8f4a4 |
fix(ui): collapse bug-report connection diagnostic for multi-printer setups
The "Report a Bug" panel scans every configured printer on open and shows
connection problems inline. The first cut rendered a full ~6-row checklist
per problem printer, stacked — on a large fleet that pushed the description
box and Submit button below the fold in the max-w-md / max-h-80vh panel.
The diagnostic section is now a compact summary: one "N of M printers have
connection issues" line plus the affected printers as collapsed rows
(healthy printers count toward M, render no detail). Each row expands on
demand to that printer's full checklist via the shared Collapsible widget;
a single problem auto-expands since that's the case where inline detail is
wanted without a click. Panel height is now fixed regardless of fleet size.
- BugReportBubble.tsx: diagnostic query pairs each result with the printer
name (new DiagnosticEntry type); render block uses Collapsible rows
- i18n: new bugReport.diagnosticSummary ({{problems}}/{{total}}) replaces
static diagnosticHeading; diagnosticIntro reworded count-neutral — all 9
locales
- 2 new BugReportBubble tests (collapsed-then-expand; single auto-expand)
|
||
|
|
4925b4c830 |
fix(slice): re-slice correctness — model label, honest errors, filament usage, nozzle guard
Five follow-up fixes to cross-printer re-slicing, all surfaced while
testing archive re-slices.
1. Re-sliced archive now records the printer it was sliced FOR.
slice_and_persist_as_archive copied sliced_for_model from the source
archive, so re-slicing X1C->H2D still showed "X1C sliced". Read it
from the freshly-sliced 3MF's parsed metadata instead, falling back
to the source only when absent.
2. Real slicer rejections are surfaced instead of silently masked.
_run_slicer_with_fallback retried with the 3MF's embedded settings on
any sidecar 5xx — including genuine content rejections (object off
the bed, incompatible filament temps), which "succeeded" only by
re-slicing for the source's original printer. A new
_slicer_rejection_message detects the slicer's own error string and
surfaces it as a 400; the embedded-settings fallback is kept only for
true CLI crashes.
3. A failed slice opens an error modal, not a 3s toast. The slicer's
reason is actionable and a toast hides it before it can be read. New
AlertModal (acknowledge-only); SliceJobTrackerContext shows it on a
failed job. New slice.failedTitle key in all 9 locales.
4. Sliced files no longer report "0 g" filament usage. The sidecar
doesn't always populate the X-Filament-Used-* headers;
ThreeMFParser._parse_gcode_header now also reads the slicer's own
"total filament weight/length" from the G-code header, and both
slice-persist paths fall back to it when the sidecar reports 0.
5. Nozzle-class re-slice guard. Re-slicing across the single-nozzle <->
dual-nozzle boundary (e.g. X1C -> H2D) fails BambuStudio's
multi-extruder validation; both slice routes now reject it up front
with a clear 400. The dual-nozzle model classification — previously
an inline tuple duplicated across start_print and the K-profile
routes — is centralized into DUAL_NOZZLE_MODELS / is_dual_nozzle_model
in printer_models.py, consumed by all three sites and the guard.
Full cross-nozzle-class re-slicing (dual-nozzle project_settings
reconciliation) remains separately tracked.
Tests: _slicer_rejection_message, _canonical_printer_model,
guard_nozzle_class_reslice, is_dual_nozzle_model, the G-code-header
filament parse, AlertModal, and end-to-end slice-API coverage including
an X1C-archive-to-H2D 400. Backend ruff + i18n parity clean; frontend
build clean.
|
||
|
|
16da533c9a |
fix(static): serve /fonts/*.woff2 — self-hosted Inter font (#1460 follow-up)
The browser console logged "downloadable font: rejected by sanitizer" for inter-latin.woff2 on every load. The #1460 PWA fix added @font-face rules pointing at /fonts/inter-latin.woff2 and bundled the woff2 files into static/fonts/, but main.py only mounts /assets, /img and /icons as static directories. With no /fonts mount, /fonts/*.woff2 fell through to the SPA catch-all and returned index.html with 200 OK; the browser's OpenType sanitizer rejected the HTML-as-a-font. Add a /fonts StaticFiles mount alongside /img and /icons. The woff2 files themselves are valid (verified — Inter variable, latin and latin-ext subsets). Also bump the service worker STATIC_CACHE version (v26 -> v27). sw.js lists the two font URLs in STATIC_ASSETS, and cache.addAll() treats the 200 OK HTML as a successful fetch — so it had cached index.html under the font URLs and served it cache-first. The version bump makes the activate handler purge the poisoned cache and re-fetch the real fonts. |
||
|
|
71e58e6cf1 |
fix(library): show the filename, not the embedded 3MF Title (#1489)
File Manager cards, search and sort keyed off file_metadata.print_name, which ThreeMFParser lifts from the 3MF's <metadata name="Title">. That title is the in-app project title — generic "Exported 3D Model" for any Bambu Studio "Save As", a marketing title for a MakerWorld download — and almost never the filename the user saved as. A card for Whatever.3mf showed "Exported 3D Model"; correcting it needed a rename round-trip, since the Rename dialog disables Save while the name is unchanged. The slicer-output write path already dropped print_name for this exact reason; the four other paths that store parsed 3MF metadata onto a LibraryFile did not — external-folder scan, managed multipart upload, the multi-file ZIP-upload branch, and MakerWorld import. Add a shared _without_print_name() helper and apply it at all four import paths; switch the slicer path to it so there is one rule. A LibraryFile's display name is its filename — only PrintArchive carries a real print_name, which is untouched. Remove the now-redundant filename->print_name mirroring in the rename route. Add a one-time idempotent data migration (_migrate_drop_library_print_name, SQLite json_remove / PostgreSQL jsonb key-removal branched on is_sqlite()) so libraries imported before the fix correct themselves without the rename workaround. No frontend change: print_name || filename yields the filename once print_name is gone. Tests: 6 new in test_library_print_name.py cover _without_print_name and the migration (incl. idempotency, siblings preserved, null metadata). SQLite migration branch verified by test; PostgreSQL branch verified against a real Postgres instance. |
||
|
|
056f06a396 |
fix(camera): capture ffmpeg stderr when an RTSP stream stalls (#1395)
A P2S support bundle on 0.2.5b1 — per-model probesize fix already applied — showed the camera still failing: ffmpeg connects, stays alive 30+ seconds, emits zero JPEG bytes, the 30s stdout.read times out, reconnect loop repeats. No ffmpeg stderr appeared anywhere in the log to explain why. The cause was a diagnostic bug, not the camera path. _read_ffmpeg_stderr called process.stderr.read() — read-to-EOF. A stalled-but-still-alive ffmpeg (the P2S RTSP failure mode) never closes stderr, so the read blocked until the 2s wait_for timeout and returned None, discarding the banner + stream-analysis lines ffmpeg had already printed. ffmpeg stderr was captured only when it fully exited; once the probesize bump turned the earlier crash into a hang, the diagnostic went dark. Drain stderr incrementally in bounded 8KB chunks (64KB cap), returning whatever ffmpeg printed so far whether or not it has exited. Also log the resolved per-model probesize/analyzeduration on the info-level "Starting RTSP camera stream" line, and log the full ffmpeg argv at debug level with only the credential-bearing camera URL redacted instead of hiding the entire command. No behaviour change to streaming — this makes the unresolved P2S RTSP stall diagnosable in the next support bundle. |
||
|
|
51b0d28b05 |
fix(camera): show diagnostic in window-mode camera page (#1395)
The #1395 camera-diagnostic follow-up (stethoscope control-bar icon, Diagnose button in the stream-error state, CameraDiagnoseModal) shipped wired into EmbeddedCameraViewer.tsx only — the embedded camera mode. CameraPage.tsx, the standalone window opened at /camera/{id} when camera_view_mode is "window" (the default), never got it. A reporter on window mode could not see the stethoscope regardless of container rebuilds or cache clears: the camera.diagnose strings are in the bundle but come from EmbeddedCameraViewer, which never renders in window mode. Switching to overlay mode surfaced it instantly. Port the diagnostic into CameraPage.tsx: Stethoscope control-bar button between Refresh and Fullscreen, a Diagnose button next to Retry in the streamError block, and the CameraDiagnoseModal render. No new i18n keys — camera.diagnose.* already exist in all 9 locales. The backend per-model camera-profile fix from the same issue is view-mode-agnostic and unaffected. |
||
|
|
774eba73c8 |
feat(diagnostics): event-loop stall watchdog to catch silent backend freezes
Several "container hangs after adding a printer" reports (#1486) share a signature with nothing to act on: HTTP goes silent, /health hangs, the process may ignore SIGTERM, and the log just stops mid-stream - a frozen asyncio loop cannot log a thing. loop_watchdog re-arms faulthandler.dump_traceback_later() from an async heartbeat. While the loop ticks the timer is cancelled and re-armed before it can fire; if the loop stalls for 30s the heartbeat can't re-arm and faulthandler's C-level timer thread dumps every thread's stack to stderr, so the blocked frame shows up in `docker compose logs`. |
||
|
|
745ed847e6 |
fix(archive): stop duplicating the job on a backend restart mid-print (#1485)
A restart during an active print duplicated the running job in the
archive, and every further restart spawned another. on_print_start
re-attaches by subtask_id, falling back to a name match plus a 4-hour
staleness cutoff that cancelled + recreated any name-matched 'printing'
archive older than 4h - destroying the live archive of every long print.
Two fixes:
- start_print records the minted subtask_id (last_dispatch_subtask_id);
on_print_start falls back to it when the printer hasn't echoed one
yet, so queue/scheduled archives persist a restart-stable id.
- Replace the 4h cutoff with a progress-aware check: a name-matched
'printing' archive resumes whenever the printer reports real (or
unknown) progress; it is stale only when the printer shows a
freshly-started print (<1%) on an archive over 2h old.
|
||
|
|
3286ccd7d2 |
fix(inventory): send honest Bambuddy User-Agent on FilamentColors.xyz sync
The Color Catalog sync built its httpx.AsyncClient with no User-Agent, so it leaked httpx's default python-httpx/x.y string - the only outbound client that did; bambu_cloud, makerworld and firmware_check all send Bambuddy/1.0 (+https://github.com/maziggy/bambuddy). It now sends the same honest UA. Found while investigating an issue - a Cloudflare 403 on the sync that turned out to be the reporter's network/IP reputation, not Bambuddy. The UA leak was a separate inconsistency found in passing; this change does not by itself resolve a Cloudflare IP block. |
||
|
|
50d1984820 |
fix(printer): stop the File Manager polling the printer over FTPS every 30s (#1480)
FileManagerModal ran its file-listing query with refetchInterval 30000, opening a fresh FTPS connection (full TLS handshake) to the printer every 30s while the modal was open. On fragile controllers like the P1S that load tipped MQTT, FTP and the camera into simultaneous timeouts. Drop the interval; the listing still refreshes on open, directory change, after upload/delete, and via the manual Refresh button. Also log STL thumbnail failures with a traceback (exc_info) so a data-specific "str / str" TypeError can be located from a support bundle. |
||
|
|
e0247fc6a6 |
fix(slicer): filter process/filament presets by uploaded bundles, not preset names (#1325)
The process dropdown still mixed @BBL P2S presets into an X1C list: slicerPrinterMatch matched cloud/standard presets by parsing the @BBL <model> name suffix against a hardcoded model-code allow-list that was missing P2S, H2C and X2D — so those presets resolved to "unknown" and stayed in the main list instead of "Other printers". Drop both hardcoded model tables. Compatibility now comes from the user's uploaded Slicer Bundles: a bundle is scoped to one printer and lists the presets it ships, so a preset matches a printer exactly when some bundle for that printer contains it. New models are covered the moment their bundle is uploaded. |
||
|
|
17e39921bb |
fix(pwa): add in-app install button and self-host the Inter font (#1460)
Bambuddy installed as a PWA on desktop but not on Android. Two causes:
- Chrome for Android removed the automatic install banner in Chrome 108.
With no beforeinstallprompt handler, Android had no install path. New
InstallAppButton captures the event and re-fires it from the sidebar.
- index.css pulled Inter from fonts.googleapis.com: breaks offline, trips
CSP, and the service worker answered the failed cross-origin request
with cached index.html. Inter is now self-hosted; the SW skips all
cross-origin requests and caches the font; CSP drops the Google hosts.
|
||
|
|
379b1c14bc |
fix(printer): Flow Calibration was silently skipped — wrong project_file fields (#1478)
The H2S never ran flow-dynamics calibration even with the print option
enabled, because start_print built the project_file command wrong:
- extrude_cali_flag was hardcoded to 0. A BambuStudio request-topic
capture from a real H2D (plus X1C/P2S captures) shows it is always 1
(run calibration) or 2 (skip, reuse stored PA), paired with flow_cali,
never 0 — so the printer skipped calibration regardless of the toggle.
- flow_cali and the other calibration/leveling fields were integer-
encoded for the H2 family on a mistaken belief that H2 firmware
requires 0/1. The same capture sends plain JSON booleans for every
model; the belief conflated these fields with use_ams (which does
need to stay boolean — the actual #1386 cause).
Fix: extrude_cali_flag = 1 if flow_cali else 2; send timelapse,
bed_leveling, flow_cali, vibration_cali and layer_inspect as booleans
for all models; drop the is_h_family integer-conversion branch.
use_ams is unchanged.
Corrected the two tests that asserted the integer format and renamed
them; all three model tests now also assert extrude_cali_flag.
|
||
|
|
7aad3fb395 |
fix(inventory): show group totals on collapsed grouped rows (issue #1368)
With "Group similar" enabled, the collapsed group row showed a single member's values — a group of five 1 kg spools displayed 1000 g instead of 5 kg (#1368). The group header now aggregates across all members: the table view's Label / Net / Gross / Used / Remaining columns and the grid card's weight figure show group totals. Identity columns (Material, Brand, Colour) and the Cost/kg rate stay per-spool-correct since they are the group key; per-spool-only fields with no meaningful total (dates, location, note, tag ID) keep the representative member's value. The expanded per-spool rows are unchanged. New aggregateGroupSpool helper with 4 unit tests; the table group component's header prop is renamed representative -> headerSpool. Frontend-only — all data was already in the spool list. |
||
|
|
e738645b0d |
feat(slicer): filter slice profiles by printer + default from the 3MF (issue #1325)
The Slice dialog listed every process / filament preset regardless of the chosen printer, and always defaulted to the first listed preset rather than what the 3MF was prepared with (#1325). Matching uses the slicer's own compatible_printers list for imported (local) presets and falls back to the "@BBL <model>" name suffix for cloud / standard presets. Compatibility-unknown presets are never hidden. Defaults: the printer and process dropdowns default to the preset names embedded in the source 3MF's project_settings.config when those presets are available; the per-slot filament and process pre-picks prefer a printer-compatible preset, and switching the printer re-picks any selection left incompatible. - UnifiedPreset gains compatible_printers, exposed for the local tier - plates endpoints return embedded_printer / embedded_process - new frontend util slicerPrinterMatch.ts; extract_embedded_presets_from_3mf - slice.otherPrinters added across all 9 locales |
||
|
|
7eba29624b |
feat(slicer): filter process & filament profiles by selected printer (issue #1325)
The Slice dialog listed every process / filament preset regardless of the chosen printer (#1325). Picking a printer profile now filters both dropdowns to compatible presets; presets resolving to a different Bambu model move into a trailing "Other printers" group. Matching uses the slicer's own compatible_printers list for imported (local) presets, and falls back to the "@BBL <model>" name suffix for cloud / standard presets where no compatibility metadata is available, so all three tiers are covered. Compatibility-unknown presets (custom or untagged) are never hidden. The pre-pick and printer-switch paths follow the same rule. - UnifiedPreset gains compatible_printers, exposed for the local tier - new frontend util slicerPrinterMatch.ts with the matching logic - slice.otherPrinters added across all 9 locales |
||
|
|
a51d59eabf |
feat(i18n): add Spanish (es) locale
Full European Spanish translation — frontend/src/i18n/locales/es.ts covers all 4899 keys with placeholders, plural forms, and inline markup preserved. Registered in i18n/index.ts (resources, supportedLngs, availableLanguages) and selectable as "Español". check-i18n-parity.mjs auto-discovers the new file; added an ES_COGNATES allow-list for genuine Spanish cognates and brand/format tokens so Check 4 does not flag them as untranslated leaks. Brings the supported-language count to 9. |
||
|
|
be669a7aea | Updated .github/ISSUE_TEMPLATE/bug_report.yml and .github/ISSUE_TEMPLATE/config.yml | ||
|
|
76e327f4a1 |
feat: connection diagnostic for "printer won't connect" triage
A triage review of the last 200 closed issues found ~1/3 were
user-side setup errors — printer not in LAN developer mode, blocked
ports, Docker bridge networking, wrong access code, cross-subnet —
each costing a multi-round-trip support exchange.
Add a Connection Diagnostic that runs those checks automatically:
- backend/app/services/printer_diagnostic.py: TCP probes of MQTT
8883 / FTPS 990 / RTSPS 322, LAN developer mode, Docker network
mode, printer/host subnet match, MQTT credential class; each
check returns pass/fail/warn/skip with a localized fix.
- Routes: GET /printers/{id}/diagnostic (saved printer) and
POST /printers/diagnostic (pre-save Add-Printer flow).
- ConnectionDiagnostic.tsx: modal + shared checklist, surfaced from
the printer card actions menu, an offline-printer quick button,
the Add-Printer dialog, and a new System-page section.
- The in-app bug reporter scans configured printers when the form
opens and always shows the result inline — a healthy confirmation,
or the detected problem and its fix.
- config.yml troubleshooting link repointed to the rendered wiki
page; bug_report.yml gains a diagnostic checkbox.
Diagnostic strings translated across all 8 locales. Backend service
unit tests (15) + frontend modal tests (3). Ruff clean, frontend
build clean, i18n parity green.
|
||
|
|
e1a236e408 |
fix(spoolman): decide spool assignability from the slot-assignment ledger, not extra.tag (#1122)
GET /spoolman/spools/unlinked hid any spool with a non-empty extra.tag from the AMS-slot assignment picker. extra.tag is only an RFID/NFC matching key -- OpenSpoolman writes its own NFC tag value into that same Spoolman field -- so every OpenSpoolman-tagged spool became un-assignable in Bambuddy even when it occupied no slot. get_unlinked_spools now determines assignability from the spoolman_slot_assignments table (the documented source of truth for slot assignments) and ignores extra.tag entirely. Both link_spool and the AMS auto-sync upsert a row there for every occupied slot, so the ledger is complete. get_linked_spools and find_spool_by_tag still use extra.tag -- they are genuine tag-match maps and are unaffected. Internal-inventory mode needs no parallel change: it stores tags in its own DB with no Spoolman extra collision. Updates test_get_unlinked_spools_success and adds test_get_unlinked_spools_excludes_slot_assigned. |