mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
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.
This commit is contained in:
@@ -18,6 +18,8 @@ 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
|
||||
- **File Manager no longer polls the printer over FTPS every 30 seconds while open (#1480, reported by @OscarsWorldTech)** — The reporter's P1S churned through MQTT disconnect/reconnect cycles and timelapse downloads silently failed. The support bundle showed the real picture: during the churn windows, MQTT (`Connection stale - no message for 60.2s`), FTPS (`_ssl.c:1015: The handshake operation timed out`) and the camera all timed out *together* and recovered together — the P1S's embedded controller saturating, not a network fault (wifi -44 dBm, Docker host networking). A visible contributor on Bambuddy's side: `FileManagerModal.tsx` ran its `getPrinterFiles` query with `refetchInterval: 30000`, so every 30 s while the File Manager modal sat open it opened a *fresh* FTPS connection — full TLS handshake — to re-list the current directory. A printer's file list doesn't change on its own; it only changes on upload / delete (the modal's mutations already `invalidateQueries`) or when a print finishes. The blind 30 s poll was pure load, and on a fragile controller like the P1S it was enough to tip MQTT and FTP into the timeouts above. **Fix**: the `refetchInterval` is removed. The listing still refreshes on modal open, on directory / tab change (the path is in the query key), after every upload / delete, and via the existing manual Refresh button — so nothing stops updating, the printer just isn't hammered. Reduces steady-state FTPS connection load while the modal is open from one handshake every 30 s to zero. 19 FileManagerModal tests green; frontend build clean.
|
||||
- **STL thumbnail generation failures now log a full traceback** — Surfaced by the #1480 support bundle: every STL in the reporter's library failed thumbnail generation with `unsupported operand type(s) for /: 'str' and 'str'`, but `generate_stl_thumbnail`'s `except` handler logged only the bare exception message — no traceback, no line number. The fault could not be reproduced from a clean STL across path shapes (`#` and spaces in the path), `str` vs `Path` arguments, or large meshes that exercise `simplify_quadric_decimation`, so it is data- or environment-specific and the message alone is not enough to locate it. `stl_thumbnail.py` now passes `exc_info=True` on that warning, so the next support bundle carries the traceback and the exact failing line. No behaviour change to thumbnail generation itself.
|
||||
- **Slicer: the Process / Filament dropdowns now filter by printer using the uploaded Slicer Bundles instead of guessing from preset names (#1325, reported by @IndividualGhost1905)** — After the printer-preset pre-selection landed, the reporter found the Process Profile dropdown still showed a flat mix of `@BBL X1C` and `@BBL P2S` presets with the printer set to X1C — P2S presets that should have dropped into the trailing "Other printers" group sat in the main list. **Root cause**: `frontend/src/utils/slicerPrinterMatch.ts` resolved each cloud / standard preset's printer by parsing the `@BBL <model>` suffix of its name against a hard-coded `KNOWN_MODEL_CODES` allow-list. That list (`X1C, X1E, X1, P1S, P1P, A1M, A1, H2D, H2S`) was missing `P2S` (and `H2C`, `X2D`), so every `@BBL P2S` preset parsed to an empty model-code set, `presetCompatibility` returned `unknown`, and the dropdown keeps `unknown` presets in the main list (only `mismatch` moves to "Other printers"). It was a maintenance trap by construction: every new Bambu model silently broke filtering until someone edited the list. **Fix**: the name-suffix heuristic and both hard-coded model tables (`KNOWN_MODEL_CODES`, `PRINTER_NAME_PATTERNS`) are removed. Compatibility is now read from ground truth — the user's uploaded Slicer Bundles (`.bbscfg`). Each bundle is scoped to one printer and lists the process / filament presets it ships, so "process P works with printer X" holds exactly when some uploaded bundle for printer X contains P. `buildCompatibilityIndex` builds a `presetName → {printer names}` index per slot from `GET /slicer/bundles` (already fetched by the modal), and `presetCompatibility` consults it — still preferring an imported preset's own `compatible_printers` list when present. A newly released Bambu model is covered the moment its bundle is uploaded, with no code change. Presets no bundle covers stay in the main list (`unknown` is never hidden), so a user with no bundle imported sees the un-filtered list rather than a wrong one. `printerPresetCode` / `presetModelCodes` are gone; `SliceModal` passes the bundle-derived index to `PresetDropdown`, `pickProcessDefault`, and `pickFilamentForSlot` in place of the old model code. **Tests**: `slicerPrinterMatch.test.ts` rewritten — 12 tests covering `buildCompatibilityIndex` (per-printer mapping, multi-bundle union, `# ` user-clone-prefix stripping, empty-printer skip) and `presetCompatibility` (imported-tier `compatible_printers` exact match, bundle-driven match / mismatch / unknown, the #1325 P2S-into-X1C repro, no-bundles and no-printer-selected cases). 32 SliceModal tests green; frontend build clean; backend ruff clean.
|
||||
- **PWA: Bambuddy can now be installed as an app on Android, and the font is self-hosted (#1460, reported by @Soopahfly)** — Reporter could install Bambuddy as a PWA on desktop but not on an Android phone (Pixel 9 Pro XL, failing in both Chrome and Edge). A thorough remote-DevTools trace confirmed the manifest was valid, all icons/screenshots present, and the service worker activated, running, and controlling the page — DevTools reported no installability blockers — yet no install prompt ever appeared. **Two root causes, both Bambuddy-side.** **(1) No in-app install trigger.** Chrome for Android removed the automatic install mini-infobar in Chrome 108 (2022); since then a site must either listen for `beforeinstallprompt` and surface its own button, or the user must dig into the browser's ⋮ menu. Desktop Chrome still auto-shows the omnibox install icon, which is exactly why desktop "worked" and Android didn't. Bambuddy had no `beforeinstallprompt` handler at all, so on Android there was no discoverable install path. New `frontend/src/components/InstallAppButton.tsx` captures the `beforeinstallprompt` event (calling `preventDefault()` so the button is the single predictable entry point), re-fires it on click, shows a success toast on accept, and clears the captured prompt afterwards (a prompt can only be used once). It renders nothing when there is no pending prompt — already installed, unsupported browser, or iOS Safari (no programmatic install) — so it never shows a dead button. Added to both the expanded and compact sidebar-footer rows in `Layout.tsx`, next to the GitHub link. New `nav.installApp` / `nav.installAppSuccess` i18n keys with real translations in all 9 locales (en/de/es/fr/it/ja/pt-BR/zh-CN/zh-TW). **(2) Inter font loaded from the Google Fonts CDN.** `frontend/src/index.css` pulled Inter via `@import url('https://fonts.googleapis.com/css2?...')`. For a local-first, offline-capable PWA this is wrong: it leaks a request to Google on every load, breaks the UI font when offline, and — as the reporter's trace showed — triggered CSP console errors (`connect-src` doesn't allow `fonts.googleapis.com`). The service worker made it worse: a request to `fonts.googleapis.com/css2` has path `/css2` (no `.css` extension), so it missed the CSS branch, fell through to the catch-all HTML branch, and on failure was answered with the cached `index.html` — which is why the font request came back as `text/html`. **Fix**: the two Inter variable woff2 files (latin + latin-ext, one file covers every weight via the variable axis) are now bundled in `frontend/public/fonts/` and declared with `@font-face` in `index.css`, served same-origin. The service worker now (a) skips all cross-origin requests entirely — letting the browser handle them so a failed cross-origin fetch can never be answered with `index.html` again — (b) caches the font files in `STATIC_ASSETS` and via a `.woff2` / `/fonts/` match in the cache-first branch so the UI renders offline, and (c) bumps its cache version. With Inter self-hosted, `fonts.googleapis.com` and `fonts.gstatic.com` were dropped from the SPA and gcode-viewer CSP directives in `backend/app/main.py` (the `/docs` ReDoc/Swagger CSP keeps them — that third-party UI genuinely loads Google Fonts). Frontend build clean, i18n parity green across 9 locales, backend ruff clean, 17 security-header tests green.
|
||||
- **Flow Calibration now actually runs when the print option is enabled (#1478, reported by @andreirusu99)** — Reporter on an H2S saw poor extrusion around corners (classic too-high K factor); the printer's flow-dynamics calibration step never appeared in the pre-print checklist even with **Flow Calibration** toggled on in the Re-print dialog, while the same 3MF printed from Bambu Studio did calibrate. **Root causes, both in the `project_file` command built by `start_print` (`backend/app/services/bambu_mqtt.py`)**: (1) `extrude_cali_flag` was hardcoded to `0`. A BambuStudio request-topic capture from a real H2D (plus X1C and P2S captures) shows BambuStudio always sends `1` (run flow-dynamics calibration) or `2` (skip, reuse the stored PA value), paired with `flow_cali`, and **never** `0` — so the printer skipped calibration regardless of the toggle. (2) An earlier revision integer-encoded the calibration/leveling fields (`timelapse`, `bed_leveling`, `flow_cali`, `vibration_cali`, `layer_inspect`) for the H2 family (H2D/H2S/H2C/X2D) on the belief that H2 firmware required `0/1`; the same H2D capture disproves this — BambuStudio sends plain JSON booleans for every model. The "integer required" claim conflated these fields with `use_ams`, which genuinely must stay boolean (H2D Pro reads an integer `use_ams` as a nozzle index — the actual #1386 cause). **Fix**: `extrude_cali_flag` is now `1 if flow_cali else 2`, and the five calibration/leveling fields are sent as JSON booleans for all models, matching BambuStudio's wire format exactly. The H-family integer-conversion branch (`is_h_family`) is removed. `use_ams` is unchanged. This affects only the outbound print command; the virtual-printer inbound coercion of slicer integer `0/1` flags (#1403) is a separate path and untouched. **Tests**: in `test_bambu_mqtt.py`, the two tests that asserted the integer format (`test_x2d_uses_integer_format_for_calibration_fields`, `test_h2s_keeps_integer_format_for_calibration_fields`) are corrected to assert booleans and renamed; all three model tests (X2D/H2S/P2S) now also assert `extrude_cali_flag` is `1` when flow cali is on and `2` when off. 381 mqtt + virtual-printer tests green; backend ruff clean.
|
||||
|
||||
@@ -142,5 +142,10 @@ def generate_stl_thumbnail(
|
||||
logger.warning("STL thumbnail generation unavailable (missing dependencies): %s", e)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning("Failed to generate STL thumbnail for %s: %s", stl_path, e)
|
||||
# Log the traceback, not just the message: a bare
|
||||
# "unsupported operand type(s) for /: 'str' and 'str'" gives no clue
|
||||
# which line failed, and the fault is data-/environment-specific
|
||||
# enough that it can't be reproduced from a clean STL — the traceback
|
||||
# in the next support bundle is what pinpoints it (#1480).
|
||||
logger.warning("Failed to generate STL thumbnail for %s: %s", stl_path, e, exc_info=True)
|
||||
return None
|
||||
|
||||
@@ -300,10 +300,15 @@ export function FileManagerModal({ printerId, printerName, onClose }: FileManage
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [onClose]);
|
||||
|
||||
// No auto-poll: every refetch opens a fresh FTPS connection (TLS handshake
|
||||
// and all) to the printer, and a 30s interval saturated fragile printer
|
||||
// controllers like the P1S — MQTT, FTP and the camera all timed out
|
||||
// together while this modal sat open (#1480). A printer's file list only
|
||||
// changes on upload / delete (the mutations below invalidate the query)
|
||||
// or when a print finishes; the manual Refresh button covers the rest.
|
||||
const { data, isLoading, refetch } = useQuery({
|
||||
queryKey: ['printerFiles', printerId, currentPath],
|
||||
queryFn: () => api.getPrinterFiles(printerId, currentPath),
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
|
||||
const { data: storageData } = useQuery({
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -26,7 +26,7 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-B2ysIPjO.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-B7aP7po9.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CBuiHfeb.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user