mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
96a5f953f0d7e80b852ea220f4635cfe7b4c36ae
2895
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
96a5f953f0 |
ci(windows-installer): drop Inno Setup install step
windows-latest runners ship Inno Setup 6.7.1 pre-installed under the same path we already hardcode for ISCC.exe; the choco install was trying to downgrade to 6.2.2 and failing on the version mismatch. |
||
|
|
45337cd540 | ci: add Windows installer workflow | ||
|
|
ef25c5f2db | Updated README | ||
|
|
802faf119c | Updated README | ||
|
|
e0ace602fb | Updated README | ||
|
|
96168c2d03 |
fix(restore): pause timer-based DB writers before swap (Postgres deadlock)
close_all_connections() only disposes the engine's connection pool —
asyncio tasks like print_scheduler.run() and the smart-plug snapshot
loop wake on their 30 s cadence and lazily reopen pool connections
holding RowExclusiveLock on print_queue / smart_plug_energy_snapshots.
The restore's DROP TABLE ... CASCADE pass needs AccessExclusiveLock on
every public table, producing an AB/BA deadlock that rolls back the
entire restore transaction.
Reproduced 2026-06-09 restoring a native install's backup into a fresh
Docker+Postgres deploy:
asyncpg.exceptions.DeadlockDetectedError: deadlock detected
Process X waits for AccessExclusiveLock on relation 109940
Process Y waits for RowExclusiveLock on relation 110182
Fix:
- Layer 1: pause print_scheduler / smart_plug_manager /
notification_service / background_dispatch via their existing stop
affordances before close_all_connections(), with a 1.0 s sleep for
in-flight loop iterations to release sessions. Restore handler
already requires a container restart on success, so the paused
services come back via the next lifespan startup.
- Layer 2: prepend SET LOCAL lock_timeout = '10s' to the begin-block
in _import_sqlite_to_postgres so any reactive writer (per-printer
MQTT, hourly AMS history recorder) that slips through the pause
window fails fast and visibly instead of producing a new deadlock.
v0.2.4.6
|
||
|
|
e789b2374a |
chore(deps): pin aiohttp >=3.14.0 for CVE-2026-34993 + CVE-2026-47265
pywebpush brings aiohttp in transitively with no version bound, so the resolver kept installing 3.13.5. Both CVEs are fixed in 3.14.0; direct floor pin here, same shape as the existing idna / urllib3 / starlette transitive pins. Our usage in services/external_camera.py is unaffected by 3.14.0 (ClientSession, ClientTimeout, ClientError, iter_chunked all unchanged); 29 external_camera tests pass on 3.14.1; pip-audit clean. |
||
|
|
c193db0acf |
Merge pull request #1699 from maziggy/0.2.4.6
**Bambuddy 0.2.4.6**
**⚠ Upgrade Notes — Read Before Updating**
0.2.4.6 is a fix-led patch release on the same 0.2.4 code base — no schema breaks beyond auto-migrated column additions (dialect-branched for SQLite and Postgres), no Docker entrypoint changes. 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.
Five behavior-change callouts to know about before you upgrade:
- /api/v1/inventory/spools/{id}/reset-usage renamed to /reset-consumed-counter (breaking for external API consumers). The old name implied the endpoint zeroed weight_used; in practice it only stamps the baseline so the consumed-counter widget reads 0 going forward while remaining weight is preserved. Same for the bulk variant and the Spoolman-mode mirror. No compat shim — the old name actively misled callers. If you scripted against the old path, the migration is a one-line URL swap. Frontend, Bambuddy UI, and SpoolBuddy kiosk all migrated in-tree.
- Slicer-API sidecar now ships as pre-built images on GHCR + Docker Hub (#1657). The old slicer-api/docker-compose.yml used build: { context: https://github.com/maziggy/orca-slicer-api.git#... } which required git inside the Docker BuildKit worker — Container Station, Synology DSM, and QNAP don't ship it there, so the build failed outright. New compose uses image: ghcr.io/maziggy/orca-slicer-api:${SIDECAR_TAG:-latest}. After pulling this release:
cd slicer-api
docker compose pull
docker compose up -d
- No more --no-cache --pull build dance. SIDECAR_TAG env var defaults to latest; pin to bambuddy-X.Y.Z to lock to the sidecar image that shipped with a specific Bambuddy release. Both images are linux/amd64 only; ARM64 hosts (Pi 4/5, Apple Silicon Linux) should run the sidecar on a separate x86_64 box and point Bambuddy at it via the Sidecar URL field.
- VP MQTT bridge net.info[].ip rewrite: FQDN-configured printers now work without manual reconfiguration (#1429). If you added a printer to Bambuddy by hostname (p1s.fritz.box, a router-provided DNS name, a Tailscale MagicDNS name, etc.) on 0.2.4+, the bridge couldn't rewrite the slicer-facing IP and Send quietly went to the real printer instead of the Bambuddy archive whenever the printer was powered on. After upgrade the bridge resolves the configured hostname to IPv4 at arm time and the rewrite path engages automatically — no UI action required. The unresolvable-input case (typo, dead DNS) is now logged with a specific reason instead of silently no-opping. If you were on the IPv4 workaround you can switch back to hostnames after upgrade.
- VP access codes auto-derived from the target printer in non-proxy modes. A startup migration syncs any pre-existing diverged Queue / Archive / Review VPs to match their target printer's code on first boot, with one INFO log per VP for audit. If the codes had previously diverged and your slicer was failing to connect because of it, that bridge starts working automatically after this upgrade. No user action; you may need to re-add the device in your slicer if the code changed.
- Docker bridge-mode default port mapping narrowed from 50000-51000 (1001 ports) to 50000-50029 (3 VPs worth) by default (#1646). The wide range from 0.2.4.5 spawned ~2000 docker-proxy host processes consuming ~3.5 GB host RAM on the reporter's box. New compose file maps just the slices for the first 3 VPs by default; bridge-mode users with more than 3 VPs widen the mapping by editing the published comment (50000-500N9 where N = vp_count - 1). Host-mode Linux and proxy-mode VPs are unchanged. Per-VP slicing is non-overlapping by design; collisions beyond 100 VPs fall back to the per-session retry the pre-#1646 code already had.
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 unless you map the VP passive FTP port range (see Upgrade Notes above) or you run the slicer-API sidecar (see Upgrade Notes above).
**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
requirements.txt picks up one new optional dependency this release — curl_cffi, used by the firmware-update check to clear Bambu Lab's Cloudflare JA3 challenge (#1666). If the wheel fails to install on your platform Bambuddy falls back to httpx automatically; only the in-app firmware download URL stops resolving and the wiki-based version-detection badge keeps working.
Native install — INSTALL_PATH under /home
If your install lives under /home/... rather than the default /opt/bambuddy/, the systemd unit's ProtectHome=true directive previously prevented ExecStart from resolving the venv binary (#1685). The new install.sh detects INSTALL_PATH == /home/* and emits ProtectHome=read-only automatically. Existing /opt/... installs keep the stricter ProtectHome=true.
**Windows install**
0.2.4.6 ships the same Windows installer as 0.2.4.5. Existing installs upgrade in place via the Service / Update entries on the installer.
---
**Highlights**
0.2.4.6 is a reporter-credited fix release on top of the 0.2.4 line. Two big-ticket adds: Orca Cloud profile sync end-to-end — read/list/slice with profiles from your Orca Cloud account alongside the existing Bambu Cloud integration, with four sign-in providers (Google, Apple, GitHub, email+password), a 4-tier preset picker (Orca Cloud → Bambu Cloud → local → standard), and full AMS + SpoolBuddy surface integration — and native CSV import / export for the inventory (#1576, PR #1659 by @samedyuksel), backed by a 18-column round-trippable schema with formula-injection hardening and a per-row dry-run preview.
The VP surface got another sweep. The hostname/FQDN side of #1429's net.info[].ip rewrite is finally closed (root-caused by @Mape6 — every IPv4 codepath now resolves the configured hostname before parsing). #1548 round 2 fixes the underlying reason OrcaSlicer dropped idle MQTT connections to the VP — the slicer never sends PINGREQ at all, so spec-compliant keep_alive * 1.5 disconnects fired against a slicer that holds idle sessions indefinitely against real Bambu firmware; the fix drops the application-level read timeout in favour of TCP SO_KEEPALIVE for dead-connection detection. #1646's FTP passive-port pool is now per-VP-sliced, dropping bridge-mode host RAM by ~16× on the reporter's single-VP setup. #1658 closes the BambuStudio 2.7.x "Downloading" hang — the slicer flipped the Send sequence to FTP-before-MQTT in 2.7.x, so the set_gcode_state(FINISH) synthesis fired before the real _send_print_response overwrote it back to PREPARE; the fix re-fires FINISH 1.5 s after the synthetic ack.
K-profile + AMS-slot work: #1688 / #1689 (both reported and diagnosed by @Spionkiller01 with H2C testing) fix two related symptoms with one shared root cause — spool preset ids (GFSG98_09) and K-profile filament_ids (GFG98) look different but normalise to the same value, and previously the matchers used name parsing only. The follow-up to #1689 (Spionkiller01's diff, applied verbatim) closes the residual unconfigured-but-loaded slot case where the original cali_idx safety net was unreachable. #1694 surfaces "?" instead of "Empty" on loaded-but-unconfigured AMS slots (matches the slicer's own convention). #1680 fixes the Assign-Spool toast that claimed success on the deferred-configure-on-insert path.
Reliability and dispatch: #1679 fixes the false-cancel-and-duplicate-archive bug when Bambuddy restarts mid-print (the connected-edge reconciliation fired against state.state="unknown" before the printer's first real push_status landed). #1678 fixes the print-queue wedge when a printer accepts project_file but never starts (the watchdog returned success on subtask_id advance alone; new Phase B waits up to 180 s for the active-state transition before reverting). #1697 fixes multi-plate filament over-counting — a 190 g lid from a multi-plate 3MF was getting debited the whole file's 273 g. #1397 closes the long-standing "bed already dropped" finish-photo regression by extracting the last frame of a brief timelapse Bambuddy now force-records on every dispatched print (post-park, pre-bed-drop window) — verified on N=2 H2C prints, bed-slinger field verification welcome.
Cloud / auth: #1666 routes the Bambu Lab firmware-download fetch through curl_cffi to clear the Cloudflare JA3 challenge that started 403-ing plain Python TLS — the HTTP-layer User-Agent stays Bambuddy/1.0 honest per the 2026-05-12 compliance commitment, only the TLS handshake bytes match Chrome (pinned by a test that fails the build if the UA override is ever dropped). #1698 fixes the silently-zombie tabs after JWT expiry — setAuthToken(null) from the API client wasn't reaching AuthContext.user, so ProtectedRoute kept rendering with no token and every subsequent request 401'd silently. Fix (mirrored from @TCL987's working fork patch) dispatches an auth:expired event the AuthContext listens to.
---
**New Features**
- Orca Cloud profile sync — end-to-end (slicer + AMS + SpoolBuddy). Bambuddy now reads, lists, and slices with profiles from your Orca Cloud account alongside the existing Bambu Cloud integration. OrcaSlicer 2.4.0-alpha shipped its own Supabase-backed cloud (auth.orcaslicer.com / api.orcaslicer.com); this integrates using a standard PKCE handshake. Four sign-in providers: Google, Apple, GitHub (paste-flow PKCE, since Orca's Supabase project only allowlists localhost for redirect_to), and email+password (direct grant). UI: the Cloud Profiles tab is now two — "Bambu Cloud" (existing, unchanged) and "Orca Cloud" (new, same rich layout). Slicer integration surfaces Orca Cloud as the top tier in a 4-tier preset picker (Orca Cloud → Bambu Cloud → local → standard); AMS slot configuration accepts orca_cloud as a 4th preset source; SpoolBuddy modal fetches Bambu + Orca filaments in parallel via Promise.allSettled. Persistent storage: 5 new columns on users (token / refresh / expiry / provider / email) plus 3 transient PKCE state columns with 10-min TTL, dialect-branched for SQLite and Postgres. Auth-disabled mode falls back to the global Settings table. Refresh tokens are single-use (Supabase behaviour) and rotated just-in-time with <5 min leeway; new pair persisted before the downstream call so a mid-flight crash doesn't strand the user. New explicit orca_cloud:auth permission flag folded into the existing can_access_cloud API-key scope. Filed OrcaSlicer/OrcaSlicer#14028 upstream to widen the redirect_to allowlist so future Bambuddy versions can ship clean OAuth instead of paste-flow. 32 backend unit + 6 preset-resolver + 6 sidecar-fetch + 6 frontend tests; ~35 new i18n keys translated across all 10 non-English locales.
- Native CSV import / export for the inventory (#1576, PR #1659 by @samedyuksel). Bulk-add spools without manually clicking through the form, and back up / migrate the local inventory in a single round-trip. Export downloads bambuddy-spools-YYYY-MM-DD.csv with one row per active spool; Import shows a per-row preview classifying each row as valid / error / skipped / duplicate-warn before anything hits the database, then a confirm click persists only the valid rows in one transaction. Fixed 18-column schema includes weight_used / last_used / storage_location / category / low_stock_threshold_pct for a lossless round-trip. Colour resolution: explicit rgba wins, otherwise brand + color_name resolves against the Color Catalog in a single in-memory pass. Hardening: 5 MB upload cap with bounded 64 KB chunked read (handles chunked uploads where file.size is None), spreadsheet formula-injection guard on export with the inverse strip on import for lossless round-trip, soft duplicate-warn flag when an active spool with the same material+brand+color exists. Local inventory only — in Spoolman mode the buttons render disabled with a tooltip pointing at Spoolman's own CSV import/export. 25 backend integration tests + 3 frontend modal tests; full i18n in all 11 locales. Companion wiki PR maziggy/bambuddy-wiki#41.
- Archives page banner: reactive install-step-4 nudge for the slicer-side "Store sent files on external storage" setting. Companion to the new external_storage diagnostic check (below) — catches the slicer-side variant of the same setting that never reaches the printer on older BambuStudio / OrcaSlicer. New GET /archives/no-3mf-warning endpoint returns true iff any archive in the last 30 days has extra_data.no_3mf_available=True and isn't soft-deleted; banner is amber, dismissible, and one-shot via localStorage (persistent across browser restarts — "you've been told" should outlive a session). 5 backend integration tests; 4 i18n keys in all 11 locales.
- Connection diagnostic now verifies install step 4 ("Store sent files on external storage"). Many users miss this setting when adding their first printer; without it BambuStudio / OrcaSlicer never leave a .gcode.3mf on the printer's SD card, every archive falls back to no-thumbnail / no-metadata, and the cause is invisible. New external_storage check reads state.store_to_sdcard (the MQTT home_flag bit 11 Bambuddy already parses). Pass when the printer reports the bit on, fail when off, skip when no live state or the field has never been populated. Slot in the check list sits between port_ftps and mqtt_auth. The skip text explicitly calls out the older-slicer limitation so users on that path know to verify manually. Wiki updated on the System and Troubleshooting pages.
- Connection diagnostic now verifies the printer is publishing on its report topic (#1622). A printer with a wrong-cased serial — or one that isn't publishing for any reason — would previously pass mqtt_auth because the broker accepts the subscription regardless. User-visible symptom was "AMS / K-profiles / custom filaments missing on the slicer side". New printer_publishing check turns the existing log warning into a structured diagnostic result. Bounded 10 s polling at 0.5 s intervals; exits the moment a message arrives, with an elapsed-seconds counter on the modal so the spinner doesn't look hung. The support-package gathering path stays fast (instant pass/fail, no wait).
- "Open in Slicer" desktop target is now configurable separately from the API sidecar slicer (#1329, reported by @hasmar04). Reporter wanted to slice via the Bambu Studio sidecar but open files locally in OrcaSlicer; the existing preferred_slicer setting drove both, so picking one forced the other. New open_in_slicer setting ('bambu_studio' | 'orcaslicer' | null) drives only the desktop "Open in Slicer" URI handoff. Default is null — existing installs fall back to preferred_slicer and behave identically until a user changes it. Five call sites across ArchivesPage, MakerworldPage, ModelViewerModal migrated. MakerWorld's "Slice in {{slicer}}" button label additionally branches on useSlicerApi so the text always matches what the button does.
- Queue items + Print modal now show the build plate type, per-plate accurate (#1281, reported by @CMW-ISS). Multi-printer farms with 40+ plate runs need to know which physical plate each queued job needs. New extract_bed_type_from_3mf(file_path, plate_id) helper alongside the existing filament-usage extractor; PrintQueueItemResponse gains bed_type; the /archives/{id}/plates (and library equivalent) include per-plate bed_type for the modal's plate selector. Per-plate accuracy matters because the archive-level capture stores only the first plate's value — a 40-plate 3MF mixing PEI + Engineering returns "PEI" for every plate at the archive level, but plate 17 may actually need Engineering. 8 unit cases on the extractor.
- Print Log page: per-row delete (#1687 part 1, reported by @IndividualGhost1905). Every row in Archives → Print Log now has a trash icon next to the filament cell, gated on archives:delete_own / archives:delete_all. Filament / time / cost contribution drops out of Quick Stats in the same response cycle (stats aggregate over PrintLogEntry). The matching archive (if any) stays untouched — the log row is a sibling, not a child.
- Print Log page: per-row failure-cause classification (#1687 part 4, reported by @IndividualGhost1905). Reporter clarified after part 1 shipped that what he actually wanted for failure-cause grouping on the log (spaghetti, jam, bed-adhesion, etc.) was different from archive tags (which describe the model). The storage and aggregation were already there; the gaps were the GET serialiser silently dropping failure_reason from PrintLogEntrySchema, and orphan log entries (no archive —
dispatch errors, aborts, manual entries) having no edit path. New PATCH /print-log/{entry_id}, vocabulary validated against the same 11-key canonical set the Archive Edit modal uses. Pencil icon beside the trash icon; modal saves invalidate both print-log and archives-stats query keys.
- Add Printer: scan a custom subnet for printers behind a router on a different L3 segment (#1564, reported by @MartinNYHC, root-caused by @IndividualGhost1905). SSDP multicast (239.255.255.250:2021) doesn't traverse routers, so the existing Discover pass couldn't find printers on a different subnet. New "Custom subnet..." sentinel in the AddPrinterModal picker reveals a CIDR text input. When custom is picked, discovery routes through POST /discovery/scan with the typed CIDR (which already does the unicast probe). Last custom CIDR persisted to localStorage. No backend changes — SubnetScanner.scan_subnet() already caps at /22 (1024 hosts) with batch-50 concurrency.
- VP MQTT bridge surfaces why net.info[].ip rewrite didn't arm (#1429 defensive). The bridge had 4 silent early-return paths in _refresh_ip_encoding. When the rewrite silently no-op'd, the only signal was the absence of the armed INFO line. Each path now emits one specific-reason INFO line, throttled by a dedup field so an idle unarmed bridge doesn't spam at 30 s tick. This is exactly the surface that pinpointed #1429's hostname/FQDN root cause on the reporter's bundle.
---
**Changes**
- Slicer-API sidecar ships as pre-built images on GHCR + Docker Hub (#1657, reported by @d3nn3s08). Removes the build-from-source git dependency that broke installs on QNAP, Synology DSM, and Container Station. See Upgrade Notes for the new docker compose pull workflow.
- VP access code is now auto-derived from the target printer in non-proxy modes (Discord report). With the live target-printer mirror that landed earlier in the 0.2.5 cycle forwarding slicer auth bytes through to the real printer, the slicer's stored code has to clear both checks (VP listener + real printer). If the codes diverged the bridge silently failed at the second hop. The fix removes the foot-gun: when a target printer is selected, the VP card's access-code field becomes read-only with an
Eye-toggle reveal showing the target's code. One-shot startup migration syncs any pre-existing diverged VPs with one INFO log per VP. Wiki corrected (previously framed code-match as a camera-only concern — wrong, all bridged protocols inherit).
- File Manager sidebar: "All Files" now scopes to your own uploaded files; new "External" entry holds the combined linked-folder view (#1621, reported by @kcw96). Restores the pre-external semantics so long-time users get their muscle memory back. The combined "everything across every external mount" view moves to a sibling sidebar entry that only renders when at least one external folder is linked (zero-cost on installs without the feature). New internal_only / external_only query flags
on /api/v1/library/files. Empty-state copy distinguishes "no internal files yet" from "no external files" so a user staring at an empty External view doesn't think their NAS is broken.
- Empty AMS units no longer trigger hourly humidity / temperature notifications (#1619). The hourly recorder fanned out alarms for every AMS unit above threshold without checking whether the unit was actually loaded. New _ams_has_filament() helper inspects the firmware-reported tray_exist_bits bitmap (with a fallback to the tray array's tray_type strings for early-pushall shapes). Sensor history still records regardless of the gate so the System page humidity / temperature charts stay continuous — only the outbound notification is suppressed.
- Inventory: /reset-usage renamed to /reset-consumed-counter; UI label is now "Reset counter". Old name implied the endpoint zeroed weight_used; in practice it only stamps the baseline. See Upgrade Notes for the breaking-change scope. Behaviour is unchanged in both internal and Spoolman modes — only the path and the UI label moved.
- docker-compose.yml: bridge-mode warning about the FTP passive range + docker-proxy RAM footprint (#1646, reported by @TheFou). Default port mapping narrowed from 50000-51000 (1001 ports / ~3.5 GB RSS in bridge mode) to 50000-50029 (3 VPs). Comment explains how to widen for more VPs. See Upgrade Notes.
- AMS drying now enabled for H2C starting at firmware 01.02.00.00. Moved from _DRYING_UNSUPPORTED_MODELS to _DRYING_MIN_FIRMWARE with the same 01.02.00.00 floor as H2S / P2S. Both SSDP model codes the H2C advertises (O1C, O1C2) get the same gate.
---
**Fixed**
**UI / rendering**
- AMS drying popover's "Start Drying" button no longer hidden behind iOS Safari's bottom URL bar on iPhone (#1669). Popover sizing switched from 100vh → 100dvh; popoverPosition now defaults viewportHeight from window.visualViewport?.height so the flip-above decision uses the actually-visible area.
- Project edit modal couldn't be scrolled on short screens, so Save / Cancel were unreachable (#1642, reported by @klevin92). Standard flex-modal-scroll fix: max-h-[calc(100vh-2rem)] + sticky footer with border-t separator.
- Label printing produced two identical PDFs per click (#1628). window.open(url, '_blank', 'noopener,noreferrer') returns null even on success — the fallback <a download> click fired on every click. Dropped noopener,noreferrer (same-origin blob URL, passive PDF preview, no script context).
- Service-worker activate handler no longer hangs first-install browsers (demo-site stuck spinner + Firefox Corrupted-Content). The forced client.navigate(client.url) added for kiosk-reload-on-deploy fired on every fresh origin, not just upgrades. Reload logic moved from sw.js to sw-register.js gated on hadController = !!navigator.serviceWorker.controller captured at script load — only fires when a previous SW was controlling the document.
**Inventory / AMS**
- K-profile matching now prefers filament_id over parsed names — surfaces custom profiles in the spool form AND fixes Configure Slot showing "default 0.020" for an actively-bound K-profile (#1688 + #1689, both reported and diagnosed by @Spionkiller01 with H2C testing; #1689 also reported by @IndividualGhost1905). Spool preset ids (GFSG98_09) and K-profile filament_ids (GFG98) normalise to the same value via the new toFilamentId helper (drops _NN variant suffix, strips the S in GFS); generic GFx99 ids excluded from id-match so they don't over-match. Both surfaces — isMatchingCalibration (PA profile suggester) and matchingKProfiles (AMS slot modal) — switched to id-match-first with name fallback. SpoolBuddy kiosk surfaces inherit via shared components.
- Configure Slot now keeps the active K-profile on reopen for assigned-but-unconfigured slots (#1689 follow-up, reported and patched by @Spionkiller01). Residual case where the original cali_idx safety net was unreachable because selectedPresetInfo was null for slots with tray_type="" / no slot_preset_mappings row. Fix is Spionkiller01's diff verbatim with the existing extruder guard.
- AMS slots with a spool loaded but no material configured now show "?" instead of "Empty" (#1694, reported by @kleinwareio). Compact label below the slot circle was rendering tray.tray_type || "Empty", falling back to Empty whenever the firmware hadn't been told which material is in the slot. New branch on getEmptySlotKind matches the slicer's own convention. SpoolBuddy kiosk's AmsUnitCard carried the same bug and got the same fix.
- "Assign Spool" no longer claims the AMS slot was configured when it wasn't (#1680, reported by @kleinwareio). Backend defers the ams_filament_setting MQTT publish for empty slots (Bambu firmware drops the push) and stores the assignment with pending_config=true; the toast was always "Spool assigned and AMS slot configured" regardless. Fix branches on pending_config and surfaces a new "Assigned. Slot will configure when you insert the spool." message.
- Home-page filament assign no longer leaves the slicer unaware of PFCN cloud presets (#1648, reported by @ferch-G). Polymaker's "(Custom)" Bambu Lab H2D variants use PFCN... preset IDs (alongside GFS... and PFUS...); the cloud-detail lookup branch in apply_spool_to_slot_via_mqtt only routed the first two prefixes. Fix extends the cloud-detail-lookup branch + the discard safety net to include PFCN.
- Bambu cloud A1 Mini filament / process profiles no longer hidden in AMS slot picker (#1649, root-caused by @technopaw). Bambu shifted the @BBL suffix from A1 Mini to A1M across 106 cloud profiles mid-2026; Bambuddy's filter compared the token verbatim against the display name. New PRINTER_MODEL_SUFFIX_ALIASES table holds the bidirectional A1 Mini ⇄ A1M mapping. Same helper covers SliceModal Process/Filament compatibility. Aliases narrow on purpose — wide-net aliasing like X1 ⇄ X1C would silently group truly distinct printers.
**Archive / stats / metadata**
- Filament usage no longer over-counts when printing one plate from a multi-plate 3MF (#1697, reported by @volodymyr-doba). Reporter printed a single 190 g lid from a 5-box+5-lid file and got debited for the whole file's 273 g. Two completion-time recorders (internal _track_from_3mf and store_print_data for Spoolman mode) called the extractor with no plate_id and summed every plate. Fix threads plate_id end-to-end via a new PrintSession.plate_id field captured from on_print_start + a parallel _print_plate_ids dict for the direct-Print path (which bypasses the queue). 9 new tests across test_usage_tracker.py, test_spoolman_tracking.py, and test_print_start_expected_promotion.py.
- VP archive/queue names with & no longer render as &amp; (#1658 follow-up, reported by @IndividualGhost1905). ThreeMFParser._parse_3dmodel was parsing <metadata> payloads without html.unescape(); raw & landed in the DB and React re-escaped on render. Fix applies the same loop-until-stable unescape pattern the sibling ProjectPageParser already had. Same drop: tooltip rewritten in all 11 locales to spell out the BambuStudio 2.7.x reality that BS unconditionally overwrites the user-typed Send-dialog name with the slugified 3MF Title field — there's no MQTT field carrying the original.
- Finish photo no longer shows the bed already dropped (#1397, reported by @rtadams89, @Jeff-GebhartCA, @MA2ZAK). Bambu's end-gcode lowers the build plate at print completion; capturing at gcode_state=FINISH showed the print well below the camera's natural framing. Fix sources the photo from a brief Bambu timelapse Bambuddy now force-records on every dispatched print — firmware stops recording AFTER the toolhead parks but BEFORE the bed-drop end-gcode runs, so the last frame frames the finished print correctly. BackgroundDispatchService overrides timelapse=True on the MQTT command + marks PrintArchive.bambuddy_forced_timelapse; new extract_video_last_frame ffmpeg helper extracts the last frame; _cleanup_forced_timelapse deletes both the locally-attached file and the on-printer copy when the user didn't ask for one. Verified on N=2 H2C prints; bed-slinger field verification welcome. Round-2 fix in the same drop: extractor switched from -sseof -1.0 to -update 1 (frame-per-layer recording produces sub-second videos on small prints, where the 1-second seek-from-end went before the start of the file); resolver wired into print_scheduler.py so queue-dispatched prints get the override too (not just background_dispatch.py).
**Virtual printer**
- VP MQTT no longer disconnects idle OrcaSlicer at keep_alive * 1.5 (#1548 round 2, reported by @hollajandro). Round 1 shipped the MQTT §4.4-compliant idle disconnect; the reporter's follow-up pcap proved the spec compliance was itself the regression — OrcaSlicer sends zero MQTT packets after the initial burst (no PINGREQ), so any spec-compliant server disconnects. Real Bambu firmware doesn't enforce §4.4 (verified against the reporter's identical Orca install holding idle sessions against real hardware on the same network). Fix drops the application-level read timeout after CONNECT/auth and sets SO_KEEPALIVE on the underlying socket so the OS TCP stack detects truly dead connections.
- VP MQTT bridge net.info[].ip rewrite never armed when the printer was added by hostname/FQDN (#1429, root-caused by @Mape6, also hit @TrickShotMLG02). _ip_to_uint32_le and the host-interface picker both assume dotted-quad IPv4 and bailed on the FQDN string. New _resolve_target_to_ipv4 resolves via socket.getaddrinfo(target, None, family=socket.AF_INET) (IPv4-only filter — the net.info[*].ip field is uint32 LE). Returns None on transient DNS hiccups so the encoding doesn't break permanently. The configured FQDN is preserved into the armed log line as configured→resolved so a bad-DNS regression stays legible.
- FTP passive-port pool now sliced per-VP (10 ports each) so bridge-mode Docker drops from ~3.5 GB to ~210 MB host RAM (#1646, reported by @TheFou). Reporter measured 2002 docker-proxy host processes from the 1001-port range. Class-level PASSIVE_PORT_MIN/MAX constants are gone; new module-level compute_passive_port_slice(vp_id) returns a 10-port window allocated by VP id. Result for the reporter (single VP): ~70 MB instead of ~3.5 GB. Wrap-around behaviour at 100 VPs falls back to the per-session retry the pre-#1646 code already had. Docs corrected in the same drop (userland-proxy: false framing tightened).
- VP Queue / Archive / Review: Bambu Studio 2.7.x stayed stuck at "Downloading" after Send (#1658, reported by @IndividualGhost1905). BambuStudio 2.7.x flipped the Send sequence to verify_job → .3mf → project_file, so on_file_received's set_gcode_state("FINISH") fired first and the synthetic _send_print_response ack overwrote it back to PREPARE. The slicer waited for a FINISH transition it'd never see. Fix re-fires set_gcode_state(FINISH) from on_print_command 1.5 s after the synthetic ack, for every non-proxy mode. Proxy mode is exempt — there the real printer drives bridge state.
- VP settings card now shows the target printer's serial in proxy mode. Runtime services (SSDP, MQTT bind identity, certificate subject) all use the target printer's actual serial; _vp_to_dict always returned the self-generated suffix-based serial, breaking the visual "one identity per VP" mental model. Fix queries the target's serial when vp.mode == proxy and target_printer_id; falls back to the self-generated serial when the target row is missing (printer deleted, race) so the card still renders.
**Print queue / dispatch**
- Restarting Bambuddy mid-print no longer marks the live archive as "cancelled / aborted" + duplicates it + double-counts filament (#1679, reported by @IndividualGhost1905, corroborated by @Arn0uDz on watchtower-driven restarts). On startup, _on_connect broadcast on_state_change(self.state) immediately after the broker accepted the connection — BEFORE _request_push_all round-tripped real status. The reconcile saw state.state="unknown" + state.subtask_name="" and synthesised an aborted PRINT COMPLETE for every in-flight archive. When the real PRINT COMPLETE arrived, _active_prints didn't have the entry, so a brand-new archive row was created. Fix: two-layer guard — on_printer_status_change now gates reconcile spawn on state.state being a real value (parsed firmware enum); _is_active_archive_stale returns (False, "") when state.state is empty / "unknown" / None.
- Print queue no longer wedges in "Currently Printing" when a printer accepts project_file but never starts (#1678, reported by @kleinwareio). The _watchdog_print_start returned success as soon as subtask_id advanced — added for H2D's #1078 FINISH→PREPARE delay — but subtask_id advance is "command landed" not "actually printing". When the printer accepted the file but wedged (cloud+LAN re-auth dance after a power cycle, old firmware), the queue row stayed status='printing', in-memory _expected_prints kept the entry, and every subsequent queue item was blocked. Fix splits the watchdog into Phase A (existing 90 s) + Phase B (new 180 s, ~3.5× the worst observed H2D FINISH→PREPARE delay): if Phase A exited via subtask_id-alone, Phase B keeps watching for the active-state transition before reverting. Phase B explicitly does NOT force-reconnect (subtask_id advance proves the publish landed; force-reconnect mid-parse triggers 0500_4003 per #1150).
- Print Log "User" column now shows the user for prints started from the Queue (#1670, reported by @JmanB52D). Two-link gap on the Queue→manual-start path: POST /queue/{id}/start discarded the user dep, and PrintScheduler._start_print never called set_current_print_user. Fix writes item.created_by_id on the route (first-claim-wins so UI-added attribution isn't overwritten), and forwards the resolved username into printer_manager.set_current_print_user from the scheduler.
- Print queue require_previous_success no longer cascades indefinitely after a user-cancelled print (#1667, fully root-caused by @599w6c26tv-droid). Reporter saw a single user-cancelled print block 18 downstream queue items over 3 days, with reproducible logs + DB dumps proving two distinct bugs: the lookback query excluded cancelled (so user-cancellations were never found as the recent predecessor — the query walked past) AND included skipped (so one bug-cascaded skip became the next item's "failed predecessor"). Fix swaps the lookback list to ["completed", "failed", "cancelled", "aborted"] and broadens the success check to prev_item.status in ("completed", "cancelled"). Conservative one-shot recovery migration in run_migrations resets only the skipped items whose immediate real predecessor was cancelled — narrow enough not to disturb skipped items whose true predecessor was a real failure.
- Print modal now exposes a "Nozzle Offset Calibration" toggle for dual-nozzle printers (#1682, reported by @louiskleiman). The nozzle_offset_cali field was hardcoded to 2 (skip) in the MQTT project_file payload. New end-to-end plumbing with a hard MQTT-layer gate on dual-nozzle (is_dual_nozzle_model() + runtime _is_dual_nozzle flag) — even a stale queue item from when the printer was misidentified gets downgraded to 2 at dispatch. New default_nozzle_offset_cali setting (default True, matches BambuStudio behaviour); Settings row + QueuePage bulk-edit toggle only render when at least one registered printer is dual-nozzle.
**Cloud / external**
- Firmware-update check no longer 403s against Bambu Lab's Cloudflare-gated download page (#1666, reported by @arekm). CF upped bot protection on bambulab.com to a JA3 / TLS-fingerprint challenge; plain Python TLS handshakes (httpx, requests, urllib) don't match Chrome's ClientHello bytes. Fix uses curl_cffi for the two bambulab.com fetches only. The HTTP-layer User-Agent stays Bambuddy/1.0 honest per the 2026-05-12 compliance commitment — only TLS handshake bytes match Chrome. Pinned by a test (test_bambulab_curl_cffi_session_keeps_honest_user_agent) that fails the build if the UA override is ever dropped. Soft dependency: if the wheel doesn't install on the host platform, the service logs a startup warning and falls back to httpx (wiki-based version detection continues to work; only the in-app firmware download URL stops resolving). wiki.bambulab.com and the CDN path stay on httpx — neither sits behind the same JA3 gate.
- MakerWorld URL imports into a writable external folder wrote bytes to internal storage, not the NAS (#1645, reported and root-caused by @needo37). save_3mf_bytes_to_library accepted folder_id but never loaded the folder or inspected is_external / external_path — hardcoded the destination to internal storage and left the row with is_external=False. Same class of bug as #1112 (fixed for multipart-upload + move paths, never applied to byte-import). Fix mirrors the multipart-upload path: load the target LibraryFolder, feed it through _resolve_upload_destination, persist via _stored_file_path(dest, is_external).
**Auth / session**
- Tabs no longer go silently zombie after the JWT expires — auth-expiry now redirects to /login on the same tab (#1698, reported by @TCL987, fix mirrored from reporter's fork). When a 401 with a token-invalidating message landed in client.ts, the handler called setAuthToken(null) to drop the token, but AuthContext.user is a React state value that doesn't react to module-level setters. ProtectedRoute only redirects when user === null, so the protected tree kept rendering, every request went out with no Authorization header, and the UI silently looked empty. Fix dispatches a window.dispatchEvent(new CustomEvent('auth:expired')) after setAuthToken(null); AuthContext listens and calls setUser(null). Generic 401 Authentication required (no token-invalidating message) still doesn't fire the event — preserves the existing transient-401 tolerance.
**System / install**
- System page now reports the container's uptime / boot time, not the host's (#1690, reported by @IndividualGhost1905). psutil.boot_time() reads /proc/stat:btime, which on shared-kernel containers is the host's boot time. Fix reads psutil.Process(1).create_time() — PID 1 in a container is the entrypoint, on bare metal / VMs it's effectively the host init. Defensive fallback to boot_time() if /proc/1/stat is unreadable.
- Native systemd install no longer fails when INSTALL_PATH is under /home (#1685, reported by @Geoff-S). bambuddy.service shipped with ProtectHome=true, which makes /home/* invisible to the service namespace; ReadWritePaths=$INSTALL_PATH doesn't reliably re-expose subpaths for executable resolution. Fix detects INSTALL_PATH == /home/* and emits ProtectHome=read-only for that case; the default /opt/bambuddy/ install keeps ProtectHome=true.
- X2D archives lose 3MF metadata because FTPS handshake fails on firmware 01.01.00.00 (#1638, reported by @vasmarfas). Same shape as the P2S 01.02.00.00 FTPS bug from #1401 — Python 3.13's TLS-1.3 default rejected by the X2D's implicit-FTPS server. Fix adds an X2D entry to the per-model ftp_profiles.py registry with cap_tls_v1_2=True and an N6 → X2D SSDP alias. Honest caveat: hypothesis-driven trial. The TLS-1.2 cap is the most likely cure given symptom family resemblance to #1401; if it doesn't clear the error, the registry slot stays useful as a tuning anchor for the next round of diagnostics.
**Settings**
- Profile editor filament type dropdown now lists PLA-CF and the other Bambu CF / GF / specialty materials (#1686, reported by @Bgabor997). The filament_type select shipped with 11 base materials; expanded to 25 BambuStudio-aligned options grouped by family (PLA + CF/GF/AERO, PETG + CF, ABS + GF, ASA + CF/GF, PC, PCTG, PA family + CF/PAHT-CF/PA6-CF/PA6-GF, PET-CF, TPU, PPS family + CF/GF for X1E, PVA, HIPS).
- Scheduled local backup time is now interpreted as local time, not UTC (#1602 follow-up). Pre-fix the picker stored HH:MM and _calculate_next_run replaced into datetime.now(timezone.utc) — a UTC+3 user who wanted 21:00 local had to enter 18:00. Post-fix uses the container's local timezone resolved from TZ via zoneinfo.ZoneInfo. UTC fallback when TZ is unset or unrecognised. UI now renders the resolved zone name next to the time field. One-time behaviour change: users who entered a UTC-offset workaround will see the first scheduled cycle after upgrade run at their local-TZ offset earlier than expected.
**Internal / observability**
- Background asyncio tasks no longer get garbage-collected mid-flight (#1648 follow-up). Support-bundle review surfaced 94 Task was destroyed but it is pending! warnings in 8 days of v0.2.4.5 — asyncio holds only a weak reference to the result of create_task, so "fire and forget" call sites that don't store the returned task got GC'd before completion. The warning gives no traceback, so the originating exception (if any) vanished silently. New spawn_background_task(coro, *, name=None) helper stores a strong reference, attaches a done-callback that auto-removes on completion AND logs uncaught exceptions with the originating traceback. 16 truly-orphan create_task call sites migrated; other sites already keeping strong refs are unchanged.
---
**Credits**
External contributors and reporters who drove this release: @samedyuksel (PR #1659 CSV import / export), @Spionkiller01 (root-caused + patched #1688 / #1689 / #1689 follow-up with H2C testing), @TCL987 (#1698 JWT zombie tabs — fix mirrored from their fork), @needo37 (#1645 MakerWorld external folder, plus the original #1593 multi-plate diagnosis), @volodymyr-doba (#1697 multi-plate filament tracking), @kleinwareio (#1680 / #1694 / #1678), @IndividualGhost1905 (#1679 / #1658 / #1690 / #1687 / #1670), @hasmar04 (#1329), @CMW-ISS (#1281), @hollajandro (#1548 round 2), @klevin92 (#1642), @kcw96 (#1621), @TheFou (#1646 + Docker docs corrections), @louiskleiman (#1682), @ferch-G (#1648), @technopaw (#1649), @arekm (#1666 with the working bypass demonstrated), @JmanB52D (#1670), @599w6c26tv-droid (#1667 fully root-caused), @Bgabor997 (#1686), @Geoff-S (#1685), @vasmarfas (#1638), @MartinNYHC (#1564), @d3nn3s08 (#1657), @rtadams89, @Jeff-GebhartCA, @MA2ZAK (#1397), @vmhomelab's Windows installer carries forward unchanged.
Thank you!
|
||
|
|
ecdf577cfc |
test(security-fixtures): nosec B104/B108 on new 0.2.4.6 test code
Two adversarial-input fixtures added on the 0.2.4.6 branch were
missing the # nosec annotation that
|
||
|
|
e7574fa67c | Merge remote-tracking branch 'origin/main' into 0.2.4.6 | ||
|
|
3d35482c0c | Bumped version | ||
|
|
53b318c797 |
fix(ams): surface active K-profile when no slot preset is resolvable (#1689 follow-up)
Configure Slot dropped to "default 0.020" on reopen for slots that were physically loaded but unconfigured (tray_type="", no slot_preset_mappings row). The #1689 cali_idx safety net was unreachable from that path — matchingKProfiles early-returned [] on !selectedPresetInfo before the safety net ran. Split the early return so the cali_idx fallback survives the no-preset case: when selectedPresetInfo is null but slotInfo.caliIdx > 0, return the active profile as a single-item list (extruder-matched when known). Strictly additive; caliIdx == 0/null still returns [], existing matcher unchanged when a preset is resolvable. |
||
|
|
f87b5bb3b8 |
feat(diagnostic, archives): install step 4 — proactive check + reactive banner
Two complementary surfaces for the most-missed install step ("Store sent
files on external storage"):
1. Connection diagnostic check (printer-side variant)
- Reads state.store_to_sdcard, parsed from MQTT home_flag bit 11.
- Pass / fail / skip; instant, no I/O.
- Catches the newer-firmware variant where the toggle moved onto the
printer itself (P2S 01.02 / Studio 2.6+).
An FTP upload-and-verify probe was tried first and rejected. /cache
is always writable from Bambuddy regardless of the slicer setting;
only BambuStudio's own behaviour changes when the toggle flips.
Empirically confirmed against X1C + H2D with the slicer option
toggled off: probe still succeeded, home_flag bit 11 stayed True.
2. Archives-page banner (slicer-side variant)
- The slicer-side toggle is invisible to the printer — older
BambuStudio doesn't push the change to the printer. The diagnostic
can't see it.
- Symptom is deterministic: archiver creates rows with
extra_data.no_3mf_available=True (main.py:2770) when it can't pull
the 3MF from /cache after a slicer-initiated print.
- New endpoint GET /archives/no-3mf-warning returns whether any
archive in the last 30 days has the flag (excluding soft-deleted).
- Amber dismissible banner at the top of /archives; one-shot
localStorage dismissal (matches Layout.tsx update-banner pattern,
but persistent across sessions).
- React-Query disabled after dismissal so the endpoint isn't polled
once the user has been told.
|
||
|
|
60e31634b8 |
feat(diagnostic): add "Store sent files on external storage" check (install step 4)
Detects the printer-side variant of install step 4 — many users (esp. on clean installs) forget to enable this and only notice when their archive cards have no thumbnails. The diagnostic now catches it upfront. Detection: read state.store_to_sdcard, which Bambuddy already parses from MQTT push_status home_flag bit 11 (bambu_mqtt.py:153). Instant, no I/O. An FTP upload-and-verify probe was tried first and rejected. /cache is always writable from Bambuddy regardless of the slicer setting — only BambuStudio's own behaviour changes when the toggle flips, not the printer's acceptance policy. Confirmed empirically against X1C + H2D with the slicer option toggled off: probe succeeded, home_flag bit 11 stayed True. So the only reliable signal is what the printer actually reports about its own state. Limitation: the printer-side variant only exists on newer firmware (P2S 01.02 / Bambu Studio 2.6+). On older versions the toggle lives only in the slicer and the printer never hears about it, so this check will pass even when the user is missing step 4 in BambuStudio. The skip-text and the wiki call this out explicitly. A reactive banner on the no-3MF archive-fallback path is planned as a follow-up to cover that case. Statuses: - pass: state.store_to_sdcard is True - fail: state.store_to_sdcard is False (-> overall escalates to problems) - skip: no live state, disconnected, or field never populated |
||
|
|
bebdb38e41 |
feat(print-log): per-row classification editor + fix silent-drop in GET
(#1687 part 4, reported by @IndividualGhost1905) Reporter clarified after part 1 shipped that point 2 wasn't about archive `tags` (which describe the model — home decor, toys), but about failure-cause classification on the *log* row itself: spaghetti, jam, bed-adhesion, etc. Different surface, different lifetime. The data field he wanted already existed. PrintLogEntry.failure_reason is a String(100); the Failure Analysis widget already groups by it; the Archive Edit modal already mirrors archive.failure_reason into the most recent log entry (archives.py:1421, shipped with #1444). The only gaps were: 1. The GET endpoint silently dropped failure_reason (and archive_id and created_by_id) from PrintLogEntrySchema construction even when set in the DB — so the Print Log table couldn't render what the Failure Analysis widget grouped by. Fixed independently of the editor; regression test added. 2. Orphan log entries (no archive — dispatch errors, aborts before archive creation, manual entries) had no edit path at all because the Archive Edit modal cannot reach them. The new endpoint is the only way to classify those rows. Changes: - Backend: new PATCH /print-log/{entry_id} taking {failure_reason, status}, gated on require_ownership_permission( ARCHIVES_UPDATE_ALL, ARCHIVES_UPDATE_OWN) — same ownership shape as the per-row DELETE. Validates against the same 11-key failure vocabulary and 5-key status set the Archive Edit modal uses; unknown values return 400 rather than getting stored as raw text (the i18n layer maps the value back through the vocabulary, unrecognised values would render as literal strings). Empty-string failure_reason stores back as NULL so the column's nullable=True intent is preserved end-to-end. GET endpoint now surfaces failure_reason, archive_id, created_by_id. - Frontend: FAILURE_REASON_KEYS moved to an export from EditArchiveModal.tsx so the new editor reuses the exact same vocabulary — backend and frontend stay in lockstep. Pencil icon beside the existing trash icon on every Print Log row, opens a compact two-field modal (status + failure reason). Save invalidates print-log and archives-stats query keys so the Failure Analysis widget reflects the re-classification on the same response cycle. Failure reason rendered as a sub-label under the status badge, matching PrintLogTable.tsx's convention. - i18n: 10 new keys (editEntryTitle, editEntryDescription, entryUpdated, entryUpdateFailed, archives.permission.noEdit, plus a 5-key statuses block) translated across all 11 locales. No English fallbacks. - Wiki: features/print-log.md gains per-row actions section, updated permissions table, PATCH/single-DELETE endpoint docs. |
||
|
|
85fbd7fc35 |
fix(queue): persist "Print Anyway" so scheduler stops re-flagging it
When the user clicked Print Anyway on a filament-deficit warning, the
acknowledgement was one-shot. The route cleared manual_start and
filament_short, then the next scheduler tick re-ran
compute_deficit_for_queue_item against identical spool state, found
the same deficit, and re-set both flags. The item bounced between
"user said anyway" and "scheduler re-blocked" — every Play click
returned 409, every confirm got rolled back on the next tick.
Add a persistent acknowledgement flag on the queue item:
- New column `skip_filament_check` on print_queue. SQLite + Postgres
migration branched on is_sqlite() so Postgres doesn't reject
DEFAULT 0 on BOOLEAN.
- PrintQueueItemCreate + PrintQueueItemResponse schemas + the
TypeScript types carry the field.
- POST /print-queue/{id}/start with skip_filament_check=true now
ALSO sets item.skip_filament_check = True (not just clearing
manual_start / filament_short).
- PrintScheduler._block_on_filament_deficit short-circuits to
False — no compute, no flag-setting, no notification — when
item.skip_filament_check is True. We trust the operator's
decision and stop fighting them.
- PrintModal at queue-creation time threads
skip_filament_check=true into the create payload when the user
clicks Print Anyway on the frontend deficit warning, so a print
that was warned-then-acknowledged at add-to-queue time goes in
pre-acknowledged — scheduler never blocks it on first tick.
Flag is not auto-cleared on spool swap by design: if remaining is
now sufficient, the check returns no deficit anyway, so the flag
is moot. Auto-clearing would add lifecycle complexity without
changing behaviour.
AMS Backup awareness (the other half of the discussion) intentionally
NOT included — verified the H2D's bit-26 of print.cfg toggles with
the printer-side AMS Backup setting, but the X1C's cfg has a
different shape entirely and verifying every model family isn't
realistic. Silently under-warning would be worse than always
per-slot. The check stays single-slot for now.
|
||
|
|
fdcc063d9f |
fix(k-profile): match by filament_id, surface active profile in Configure Slot (#1688 + #1689)
Two related bugs in K-profile matching, same root cause. #1688 — spool form's PA-profile suggester (PAProfileSection via isMatchingCalibration in spool-form/utils.ts) matched K-profiles by parsing the profile NAME for material/brand/variant. Spools already store slicer_filament (the slicer preset id) and K-profiles already carry filament_id, but both were ignored — so a user's custom K-profile whose name doesn't agree with the slicer preset got silently dropped from suggestions even when the underlying filament_id was identical. #1689 — ConfigureAmsSlotModal's matchingKProfiles ran the same name-only logic on the slot's selected preset. A spool assigned under "Generic PLA" with a custom K-profile actively bound on the printer landed in the modal as "K profile not assigned, default 0.020 will be used", while the printer-card hover-card correctly showed the active profile. Two paths, only one was filtering by name. Shared root: spool preset ids and K-profile filament_ids look different but are equivalent after normalising. Spools store slicer_filament as the cloud setting_id form ("GFSG98_09" — _09 is the variant suffix, the S infix marks setting_id form); K-profiles store filament_id as the bare form ("GFG98"). Plain === doesn't work; both need normalising. This conversion already existed in the other direction at buildFilamentOptions (filament_id → "GFS" + filament_id.slice(2)), so the inverse toFilamentId helper is just the matching reverse, not new ground. Fix — one shared helper, two surfaces: - spool-form/utils.ts: new exports toFilamentId(id) (drops "_NN" variant suffix and strips the "S" in "GFS", so GFSG98_09 → GFG98) and isGenericFilamentId(id) (flags Bambu's generic GFx99 ids which are shared across many filaments and must NOT id-match — the name fallback handles those correctly). - isMatchingCalibration: gains slicer_filament?: string in formData, tries id-match (with generic exclusion) before the existing name parse. PAProfileSection already passes the full formData so no caller edit needed. Strictly additive precedence. - ConfigureAmsSlotModal.selectedPresetInfo: resolves a filamentId field (toFilamentId(cp.setting_id) for cloud presets, toFilamentId(builtinFilamentId) for builtin; empty for local / orca paths which fall through to name match). - ConfigureAmsSlotModal.matchingKProfiles: id-match check at the top of the per-profile predicate (preferred when both sides agree after normalisation), then the existing name-parse logic, then ALWAYS unshifts the slot's currently-active K-profile by slot_id === slotInfo.caliIdx — gated on activeIdx > 0 (so caliIdx 0/null doesn't leak unrelated profiles in), extruder-matched when slotInfo.extruderId is known. This is Spionkiller01's #1689 patch verbatim with the activeIdx > 0 guard added. SpoolBuddy: both kiosk K-profile surfaces reuse the shared components. SpoolBuddyWriteTagPage renders PAProfileSection; SpoolBuddyAmsPage renders ConfigureAmsSlotModal. Verified — fixes propagate automatically, no kiosk-specific edits. What this does NOT change: spools without slicer_filament, K-profiles without filament_id, and generic GFx99 ids all fall through to the existing name-based matching path. Strictly additive precedence; no input shape that matched under the old logic fails to match under the new. The #1053 cloud-preset PFUS* path is preserved because the toFilamentId regex /^GFS/ doesn't match a "PFU" prefix. |
||
|
|
e9b6fefe95 |
fix(auth): redirect to /login when JWT expires mid-session (#1698)
When the JWT expired on an open tab, the next API request hit a 401 with
"Token has expired"; client.ts cleared the token from storage but
AuthContext.user stayed populated from the original mount. ProtectedRoute
only redirects when user === null, so the protected tree kept rendering
and every subsequent request silently failed with no Authorization
header — the UI looked like every list was empty until a manual refresh
remounted AuthProvider.
The 3 other setAuthToken(null) sites live inside AuthContext itself and
already pair with setUser(null), so only the client.ts cross-module
site needed a React-tree signal.
- client.ts: after setAuthToken(null) on a token-invalidating 401,
window.dispatchEvent(new CustomEvent('auth:expired')). Guarded on
`typeof window !== 'undefined'` for SSR / test safety. Generic
"Authentication required" 401s still don't clear the token or fire
the event — treated as transient timing issues per the pre-existing
comment at client.ts:155.
- AuthContext.tsx: mount useEffect adds a window listener that calls
setUser(null) under the mountedRef guard; cleanup removes the
listener so unmount → remount doesn't double-bind.
Mirrors the patch the reporter shipped on their fork (deec96d1).
|
||
|
|
72044e3a53 |
fix(usage): scope 3MF filament tracking to dispatched plate (#1697)
When a print targets a single plate from a multi-plate 3MF, both the
internal Filament Inventory tracker and the Spoolman-mode tracker parsed
the 3MF without a plate filter and summed every plate's filament — so a
single lid print debited the spool the entire file's grey + black totals.
The 3MF parser already supports plate_id (queue pre-flight uses it at
print_queue.py:254/:286). Plumbed it through both dispatch paths:
Queue path:
- PrintSession gains a plate_id field; on_print_start queries the
printer's currently-printing queue row and records queue_item.plate_id
onto the session.
- _track_from_3mf accepts plate_id and passes it to the extractor.
- store_print_data moves its existing queue-item lookup above the
extract and uses queue_item.plate_id as the plate filter.
Direct-Print path (reprintArchive / printLibraryFile — never goes
through the queue):
- _print_plate_ids dict added in main.py, parallel to _print_ams_mappings.
- register_expected_print accepts plate_id and stores it; the 2 sites in
background_dispatch.py and the 1 site in print_scheduler.py now pass
it (resolve was already happening, just needed reordering before the
register call so the value is available).
- Expected-print promotion in main.py injects _print_plate_ids[archive_id]
into the session, guarded so a queue capture wins over the dict.
- _get_start_plate_id helper feeds plate_id into all 3
_store_spoolman_print_data call sites; spoolman_tracking.store_print_data
takes the caller value first, falls back to queue_item.plate_id.
PrintArchive.filament_used_grams stays file-level summed by design
(#1593's contract — the archive describes the file, not the run); only
the per-run usage attribution becomes plate-aware. Single-plate direct
prints resolve to plate_id=1 → plate 1 = whole file, identical to the
prior no-filter behaviour.
|
||
|
|
529155413f |
fix(ams): show "?" for loaded-but-unconfigured AMS slots (#1694)
Reporter on a 3-AMS P1S saw slots labelled "Empty" even though spools
were physically loaded - OrcaSlicer's Device view showed the same
slots as loaded.
Root cause: the compact label below the AMS slot circle rendered
tray.tray_type || t('ams.slotEmpty'), falling back to "Empty" whenever
the printer firmware hadn't been told which material is in the slot.
getEmptySlotKind already distinguishes 'physical' (firmware confirmed
empty via state 9/10) from 'reset' (tray_type absent but firmware
hasn't confirmed empty - spool loaded, just unassigned). The hover
card and circle border already used that distinction; the compact
label did not.
Fix: label branches on emptyKind - 'physical' keeps "Empty", 'reset'
shows "?" matching the slicer's own convention. External / VT tray
label is unchanged (external trays have no "configured/unconfigured"
distinction - they're either loaded or not). SpoolBuddy AmsUnitCard
carried the same bug and got the same fix plus a tooltip.
|
||
|
|
7a9c78c32c |
● fix(vp): stop enforcing MQTT keepalive 1.5x to match real Bambu firmware (#1548)
Round 1 ( |
||
|
|
58d1b1eb74 |
fix(system): use PID 1 create_time for uptime/boot time on containers (#1690)
System -> Uptime / Boot Time read psutil.boot_time(), which on shared-kernel containers (Docker, LXC, Proxmox containers) is /proc/stat:btime - the host kernel's boot time, not the container's. Reporter on Proxmox LXC saw the Proxmox node's uptime instead of the Bambuddy container's. PID 1 is the container's entrypoint (or the host init on bare metal), and its create_time is the POSIX wall-clock timestamp of when it started. Switching to psutil.Process(1).create_time() reports the right value on containers and matches host boot within a sub-second on bare metal. Defensive fallback to psutil.boot_time() on psutil.Error / OSError so the endpoint still returns 200 with the best-available answer if /proc/1/stat is unreadable (locked-down container, custom seccomp policy). |
||
|
|
40729da013 |
feat(print-log): per-row delete on Print Log page (#1687 part 1)
Reporter noted the existing "Also remove this print from Quick Stats"
toggle at archive delete is one-shot: if you kept stats then, there was
no later way to drop the row; and rows without a backing archive
(errors, aborts, manual entries) had no delete affordance at all.
Backend: DELETE /print-log/{entry_id} mirrors delete_archive's
ownership flow via require_ownership_permission(ARCHIVES_DELETE_ALL,
ARCHIVES_DELETE_OWN). Owners drop their own rows; admins drop any row;
missing IDs return 404 rather than 200-silently. /archives/stats
aggregates over PrintLogEntry, so the filament / time / cost / count
contribution drops out of Quick Stats in the same response cycle. The
linked archive (if any) is untouched - the log row is a sibling, not a
child.
Frontend: trash icon next to the filament cell on every row, gated on
the same permission shape as the archive trash. Confirm modal -> row
gone. Mutation invalidates both print-log and archives-stats query
keys so the totals re-render without a manual refresh.
#1687 also asks for per-row tagging (already covered by
EditArchiveModal's tags field) and per-row filament-usage-history
edits (deferred - "restore deducted grams" is only consistent for the
most recent usage row per spool; needs a separate design call).
|
||
|
|
bfbf5d59af |
fix(profiles): add PLA-CF + Bambu CF/GF/specialty variants to filament_type select (#1686)
The Profiles page edit modal (shared by BL Cloud / Orca Cloud / Local Profiles) renders filament_type from backend/app/data/filament_fields.json via GET /cloud/fields/filament. The curated 11-option list pre-dated Bambu's CF/GF lineup expansion, so PLA-CF and most carbon/glass-fiber variants were unselectable — saved presets carried the wrong material code at dispatch. Expanded to 25 BambuStudio-aligned options grouped by family: PLA (+ CF/GF/ AERO), PETG (+ CF), ABS (+ GF), ASA (+ CF/GF), PC, PCTG, PA family (+ CF/ PAHT-CF/PA6-CF/PA6-GF), PET-CF, TPU, PPS family (+ CF/GF for X1E), PVA, HIPS. K-profiles editor unaffected (picks filament_id, not filament_type). |
||
|
|
fb1e9a917e |
fix(install): use ProtectHome=read-only for /home-rooted installs (#1685)
bambuddy.service shipped with ProtectHome=true, which makes /home/* invisible to the service namespace. Installing into /home/bambuddy/ (instead of the default /opt/bambuddy/) made ExecStart=/home/bambuddy/venv/bin/uvicorn fail with status=203/EXEC because systemd couldn't resolve the binary path. ReadWritePaths=$INSTALL_PATH does not reliably re-expose /home/* subpaths for exec resolution. install/install.sh now detects /home/* INSTALL_PATH and emits ProtectHome=read-only; default /opt/bambuddy installs keep ProtectHome=true. The manual deploy template defaults to read-only with a comment on when to tighten it. read-only keeps /home immutable to the service - no security regression, since ReadWritePaths still gates writes to the install/data/log dirs only. |
||
|
|
68877c639e |
feat(settings): split API slicer + Open-in-Slicer preferences (#1329)
Reporter wanted to slice via the Bambu Studio sidecar but open files
locally in OrcaSlicer. preferred_slicer drove both the in-app
SliceModal sidecar selection AND the desktop "Open in Slicer" URI
handoff, so picking one forced the other.
New open_in_slicer setting (str | None) drives only the desktop URI;
null inherits from preferred_slicer so existing installs behave
identically. Storage in the existing app_settings key/value table;
GET normalises the "None" string back to null mirroring the
default_printer_id convention.
Frontend: Settings -> Slicer card adds a second dropdown ("Open in
Slicer" with "Same as API slicer" / Bambu Studio / OrcaSlicer);
ArchivesPage, MakerworldPage, ModelViewerModal switch desktop-URI
call sites to open_in_slicer ?? preferred_slicer. MakerworldPage's
"Slice in {{slicer}}" label additionally branches on useSlicerApi
so the label matches what the button actually dispatches.
|
||
|
|
432080d500 |
feat(queue): show build plate type on queue items + print modal (#1281)
Reporter on a multi-printer farm with 40+-plate runs needed to walk to
the printer with the right physical plate, but the queue and the
scheduling modal didn't surface curr_bed_type the way the archive card
already did.
New utils/threemf_tools.extract_bed_type_from_3mf helper (per-plate) so
both the queue API and the /plates endpoint can return per-plate values.
PrintQueueItemResponse.bed_type populated from archive.bed_type /
library_file.file_metadata['bed_type'] as the default, then overridden
per-plate via the helper when item.plate_id is set. /archives/{id}/plates
(and library equivalent) include bed_type in each plate object.
Per-plate accuracy matters because archive.bed_type is captured at
ingest as only the first plate's value (services/archive.py:235) -
a 40-plate 3MF mixing PEI + Engineering returns PEI at the archive
level for every plate. The helper re-reads the 3MF and returns the
truth.
Frontend: queue card meta row + PlateSelector per-plate row + PrintModal
header all render bed icon + canonical label via the existing
getBedTypeInfo() helper, same as the archive card.
|
||
|
|
7b4c5b3c1f |
fix(vp): show target printer's serial on proxy-mode VP card
The runtime services (SSDP, MQTT bind identity, cert subject) already advertise the target printer's serial via target_printer_serial or self.serial in proxy mode, but the API response that drives the VP settings card always returned the self-generated suffix-based serial. The card therefore displayed a serial that didn't match what slicers see, breaking the "one identity per VP" mental model. _vp_to_dict now resolves vp.target_printer_id -> Printer.serial_number when mode == VP_MODE_PROXY and substitutes the result into the response serial field. Archive / queue / review keep the self-generated serial (those modes never speak the target's identity). Orphaned target falls back to self-generated so the card still renders. |
||
|
|
294b926327 | Updated BACKERS | ||
|
|
e22c7f5d64 | Updated BACKERS | ||
|
|
5fbbcb6077 | Post work PR #1659 | ||
|
|
66c09dff2d | feat(inventory): CSV import/export for the inventory page (#1576) (#1659) | ||
|
|
2c2725cb53 |
fix(print): expose nozzle_offset_cali toggle for dual-nozzle printers (#1682)
Bambuddy's project_file MQTT payload hardcoded "nozzle_offset_cali": 2 (skip), giving users on H2D / H2D Pro / H2C / X2D no way to control the same toggle BambuStudio exposes. Critical for diamond-nozzle setups that must keep the calibration off. start_print() now takes a nozzle_offset_cali kwarg; the value is encoded as 1 (run) or 2 (skip) and gated on is_dual_nozzle so single-nozzle machines always send 2 even if a stale flag arrives. The kwarg threads through printer_manager, both background_dispatch sites, and print_scheduler so every dispatch path respects the per-item setting. print_queue gains a nozzle_offset_cali column (DEFAULT TRUE, is_sqlite() branch for Postgres BOOLEAN). Settings default key default_nozzle_offset_cali defaults to TRUE to match BambuStudio. Schemas updated across print_queue, library FilePrintRequest, archive ReprintRequest, settings. PrintModal renders the new toggle only when the selected printer is dual- nozzle (printer-mode: nozzle_count===2; model-mode: DUAL_NOZZLE_MODELS). SettingsPage default-print-options row + QueuePage bulk-edit tri-state both hide unless any registered printer is dual-nozzle. Labels reuse the existing settings.default* keys so the only new i18n strings are settings.defaultNozzleOffsetCali / Desc and queue.bulkEdit.nozzleOffsetCali - real translations in all 11 locales. |
||
|
|
8957cfaeef |
fix(inventory): assign-spool toast no longer claims AMS configured when slot was empty (#1680)
Backend correctly defers MQTT configuration when the AMS slot is empty
at assign time (state ∈ {9, 10}) — firmware drops the push silently;
on_ams_change replays the config once a spool is detected. The
response carries pending_config=true to communicate that. The
printer-card AssignSpoolModal was ignoring the flag and always
showing "Spool assigned and AMS slot configured" — the SpoolBuddy
modal has handled this since it shipped. Now mirrors the same
branch and shows "Assigned. Slot will configure when you insert
the spool." when pending_config is true.
|
||
|
|
d0d6a659e9 |
fix(reconcile): don't synthesize aborted PRINT COMPLETE on the bare-connect edge before push_status arrives (#1679)
on_printer_status_change fires from MQTT _on_connect BEFORE the first push_status round-trips, when PrinterState is still on construction defaults (state="unknown", subtask_name=""). The connected-edge reconcile was treating that degenerate state as evidence and synthesising aborted PRINT COMPLETE for every in-flight archive on every Bambuddy restart. The reactive PRINT COMPLETE then created a duplicate archive (lookup misses on cleared _active_prints), so filament got deducted twice. Two-layer guard: gate the reconcile spawn on a real state.state, and make _is_active_archive_stale return not-stale on unknown/empty input as defensive fallback. #1542 behaviour preserved — real stale archives still get caught the moment a real push_status arrives. |
||
|
|
fdaff37975 |
fix(queue): two-phase dispatch watchdog so a printer that accepts project_file but never starts doesn't wedge the queue (#1678)
_watchdog_print_start now treats subtask_id-advance as Phase A "command landed", not as final success. Phase B (180s) keeps watching for the active-state transition; if it never arrives — printer accepted the file but stalled (cloud+LAN re-auth after a power cycle on old firmware was the reported trigger) — revert the queue item to 'pending' instead of leaving it stuck in 'printing' until container restart. Phase B skips force_reconnect because subtask_id-advance proves the project_file landed; a reconnect mid-parse would trigger 0500_4003 (#1150). Phase A's H2D 50s FINISH→PREPARE tolerance (#1078) is preserved by Phase B's 180s headroom. |
||
|
|
a104ccc95c |
fix(pwa): don't force-navigate first-install clients in SW activate
Repro on every fresh demo subdomain: visitor lands on Printers OK, first
sidebar click sticks on a spinner (Chromium) or trips "Corrupted Content
Error" with sw.js stuck `activating` (Firefox). Manual reload recovers.
Root cause is the `client.navigate(client.url)` call added to the activate
handler in
|
||
|
|
1aaa3dfc7d | Updated BACKERS.md and README.md | ||
|
|
54ac359081 | Updated BACKERS.md and README.md | ||
|
|
96ce403554 |
fix(archive): HTML-unescape 3MF Title metadata; correct VP name tooltip (#1658)
ThreeMFParser._parse_3dmodel left XML-escaped values raw, so a Title of "PCB Vise & Solder Station" landed in the DB as the literal "&" and React re-escaped it on render to "&amp;". Apply the same loop-until-stable html.unescape() the sibling ProjectPageParser already uses, uniformly across all <metadata> values. Same drop: rewrite the VP archive-name-source tooltip in all 11 locales. BambuStudio 2.7.x (PrintJob.cpp:314-325) overwrites the user-typed Send-dialog name with the slugified 3MF Title field whenever one is present, so the previous "handy if you renamed the job in the send dialog" copy was false. New text spells out the actual behavior; both Filename and Metadata modes often produce the same string for that reason. |
||
|
|
d597d36d5b |
fix(vp): slice FTP passive ports per VP, drop bridge-mode RAM by 95% (#1646)
The 50000-51000 docker-compose port range spawned ~2000 docker-proxy host processes (~3.5 GB RSS) under Docker's default userland-proxy. The 1001-port pool was symptom treatment — collisions only matter for multi-VP-on-shared-bind, but the cost was paid by every install. Each VP now gets a non-overlapping 10-port slice computed from its id (VP 1 -> 50000-50009, VP 2 -> 50010-50019, ...). Class constants are gone; VirtualPrinterFTPServer takes passive_port_min/max instance args. Wraps modulo PASSIVE_MAX_SLOTS = 100, with the existing 10-attempt random retry as same-slot collision fallback. Compose default narrowed to 50000-50029 (3 VPs). Proxy-mode VPs forward the real printer's full range and stay on the separate TCPProxy constants. Compose comment rewritten to acknowledge Linux multi-service hosts as a primary bridge-mode audience and drop an over-stated "confirmed by reporter" claim about userland-proxy=false. |
||
|
|
47fe30c5ad |
fix(queue): credit user who clicks /start in print log when auth on (#1670)
The print log's User column came from printer_manager.get_current_print_user,
but only background_dispatch (Archive Print, Library Print) ever populated
that dict. The Queue manual-start path went straight from
POST /queue/{id}/start into PrintScheduler._start_print, neither end
recording the clicker — so any print started from the queue landed in
PrintLogEntry with created_by_username NULL even with auth enabled.
Two-sided fix: /start now writes user.id to item.created_by_id when no
prior owner is set (preserves UI-added items' original uploader), and
PrintScheduler gains _propagate_owner_to_printer_manager called from
_start_print to hand the owner into set_current_print_user before the
print command goes out.
|
||
|
|
ea4d5eb794 |
fix(ui): drying popover footer no longer clipped by iOS Safari URL bar (#1669)
100vh and window.innerHeight report the layout viewport on iOS Safari, so the popover's Start Drying button rendered behind the bottom toolbar overlay. Switch the popover maxHeight to 100dvh and default computePopoverPosition's viewportHeight to visualViewport.height (with innerHeight fallback). The flip-above decision and the body-scroll fallback now both run against the actually-visible area, so the footer button stays reachable on iPhone. |
||
|
|
edaf7c4559 |
fix(queue): cancelled prints no longer block require_previous_success chain (#1667)
Two bugs in PrintScheduler._check_previous_success: - Lookback excluded 'cancelled' so user cancellations were walked past - Lookback included 'skipped', so one skip cascaded indefinitely Swap to ['completed', 'failed', 'cancelled', 'aborted'] and accept both 'completed' and 'cancelled' as predecessor success. Real 'failed' / 'aborted' still gate. One-shot migration in run_migrations resets only the skipped items whose true predecessor was cancelled — surgical reversal of the exact bug fingerprint, leaves genuine failure-gated skips alone. Portable across SQLite and Postgres, idempotent on re-run. |
||
|
|
4bcab89eee |
fix(firmware-check): bypass Cloudflare TLS-fingerprint gate via curl_cffi (#1666)
Cloudflare on bambulab.com now serves cf-mitigated=challenge to plain Python TLS handshakes. Use curl_cffi.AsyncSession with impersonate="chrome" for the two bambulab.com fetches (index page + per-model JSON); wiki and CDN paths stay on httpx. HTTP User-Agent stays honest "Bambuddy/1.0" — only TLS-handshake bytes match Chrome, per the compliance commitment. Soft dependency — falls back to httpx with a startup warning when curl_cffi isn't importable; wiki-based version detection still works. |
||
|
|
b362f159ac |
docs(slicer-api): switch sidecar to pre-built images on GHCR + Docker Hub (#1657)
Local build via Docker git context required `git` in the BuildKit worker,
which QNAP Container Station and Synology DSM don't provide. Both sidecar
images are now published to ghcr.io/maziggy/{orca-slicer-api,bambu-studio-api}
and docker.io/maziggy/{orca-slicer-api,bambu-studio-api}; compose pulls
:latest by default, SIDECAR_TAG=bambuddy-X.Y.Z pins per release.
Wiki, slicer-api README, .env.example, and CHANGELOG aligned. Build-from-source
path kept under "Building from source (advanced)" for forks / dev work.
Both images are linux/amd64 only — OrcaSlicer ARM64 on hold pending upstream
fix; BambuStudio doesn't publish ARM64.
|
||
|
|
f243e4e598 |
fix(asyncio): track strong refs on orphan create_task sites
asyncio holds only a weak reference to tasks returned by ``create_task``. Fire-and-forget callers that discard the return value let the event loop GC the task before it finishes, logging ``Task was destroyed but it is pending!`` with no traceback. The #1648 support-bundle review surfaced 94 such warnings in 8 days of v0.2.4.5 -- the silently-vanished exceptions reach support bundles as opaque GC notices instead of actionable errors. New backend/app/core/tasks.py::spawn_background_task(coro, *, name=None) is the one place in the codebase that calls asyncio.create_task. It stores the task in a module-level set, attaches a done-callback that auto-removes on completion AND surfaces any uncaught exception via the logger with the originating traceback, and accepts name= so a leak source is traceable through /tracebacks and the log line. Cancelled tasks don't log (a shutting-down service is not an error). Migrated the 16 truly-orphan create_task call sites to the helper: main.py (8): reconcile-stale, cooldown-poweroff, energy calc, smart-plug, maintenance-check, photo-then-notify, layer-timelapse, scan-timelapse, print-scheduler, notify-no-archive (the last one was hand-rolling the same pattern with task + no-op done_callback) printers.py:3123 apply-pa-after-refresh print_queue.py:1034 queue cooldown-poweroff firmware_update.py:261 firmware upload archive.py:1514 timelapse mp4 convert print_scheduler.py:2199 watchdog print-start library.py:1614 STL backfill smart_plugs.py:259 tasmota scan discovery.py:159 subnet scan smart_plug_manager.py x3 plug auto-off-pending background_dispatch.py x2 (lambda-wrapped inside loop.call_soon_threadsafe) upload progress Sites that already kept strong refs are unchanged: self._tasks.append(asyncio.create_task(...)) -- VP manager, tcp_proxy, mqtt_server self._x_task = asyncio.create_task(...) on service instances -- mqtt_bridge, obico_detection, github_backup, archive_purge, local_backup, library_trash, discovery service Locally assigned + awaited/gathered -- tcp_proxy bidirectional pumps, camera_fanout, slice_dispatch, slicer_api progress_task, manager._finish_release_task, main.py module-level cleanup loops |
||
|
|
d82f4e032f |
fix(inventory): handle PFCN cloud preset IDs in assign-via-MQTT (#1648)
Reporter on an H2D + Polymaker PLA Matte spool noticed that assigning
the spool from the Dashboard left the slicer's filament dropdown
showing "unknown", but clicking Configure right after made the
slicer recognize it correctly. "Configure" felt like a mandatory
follow-up step rather than a refinement.
Bambu cloud uses three preset-ID shapes:
GFS… — Bambu official cloud preset
PFUS… — cloud user-created preset
PFCN… — cloud shared / partner preset (Polymaker's "(Custom)"
Bambu Lab H2D variants ship this prefix)
apply_spool_to_slot_via_mqtt only routed GFS and PFUS through the
cloud-detail lookup that extracts the underlying filament_id. PFCN
slipped past the cloud-lookup branch, fell into the local-preset
int() parse path, raised ValueError, dropped into
normalize_slicer_filament which returns any P-prefix unchanged, and
the raw PFCN landed in tray_info_idx. The printer's calibration
table can't index that, so the slicer rendered "unknown". The
Configure modal rescued every assign because it does its own
getCloudSettingDetail and writes the resolved filament_id.
Extend the cloud-detail-lookup branch (inventory.py:129) and the
discard safety net (inventory.py:223) to include PFCN alongside
GFS/PFUS. Three behaviours fall out:
* Cloud-authenticated: the real filament_id from
detail["filament_id"] ships as tray_info_idx (Polymaker PLA
Matte resolves to GFL05).
* Cloud unavailable: raw PFCN discarded, the slot reuses an
existing valid P-prefix preset if material matches.
Source comment now lists all three cloud-ID shapes so the next time
Bambu invents a new prefix the maintainer doesn't have to re-derive
the structure from a bug report.
|
||
|
|
b8916ac3de |
fix(presets): match Bambu cloud @BBL A1M as A1 Mini (#1649)
Reporter on an A1 Mini saw the AMS slot Configure dropdown render no
Bambu / Generic filament profiles, and saw the Profiles tab strip
A1 Mini results when filtering by that model. Bambu rolled out a
profile rename mid-2026: the @BBL <code> suffix on 106 cloud profiles
shifted from the long display form to a terse model code -- e.g.
"Bambu PLA Basic @BBL A1 Mini ..." is now
"Bambu PLA Basic @BBL A1M ...". User-authored profiles still use the
long form. Bambuddy's filters did a verbatim uppercase compare
("A1M" vs "A1 MINI"), so every renamed cloud profile silently
disappeared from the picker.
Centralize the alias check in slicerPrinterMatch.ts. New
PRINTER_MODEL_SUFFIX_ALIASES table maps "A1 Mini" <-> "A1M"
bidirectionally; exported matchesPrinterModelSuffix() does the
case-insensitive compare with the alias fallback. Two consumer
sites swap to the helper:
* ConfigureAmsSlotModal.tsx (Orca cloud and Bambu cloud filter
branches) -- the AMS slot picker, hit directly and reached from
SpoolBuddy's AMS page via mapModelCode(printer?.model)
* slicerPrinterMatch.ts:classifyByBambuName -- the SliceModal
Process / Filament compatibility check
Backend printer_models.py also gets a "Bambu Lab A1M" -> "A1 Mini"
entry so server-side 3MF model normalization stays consistent if a
3MF ever embeds the short form.
Kept the alias table narrow on purpose. Wide-net aliasing (e.g.
"X1" <-> "X1C") would silently collapse physically distinct
printers. When Bambu introduces the next rename, it is one new row
in the table -- /api/v1/cloud/settings is the place to grep, called
out in the source comment.
|
||
|
|
e895d8350a |
fix(vp): re-fire FINISH after project_file ack so slicer releases (#1658)
Reporter on Bambu Studio 2.7.1.57 + X1C saw the Send modal stuck at
"Downloading" after sending to a Queue-mode VP. Delete-from-queue and
even Auto-Dispatch ON + a successful real print didn't release it.
Root cause: BS 2.7.x flipped the Send sequence from
MQTT project_file -> FTP upload -> done
to
FTP verify_job -> FTP .3mf -> MQTT project_file.
The #1280 fix sets gcode_state=FINISH in on_file_received (after the
FTP upload). Under the new order, the synthetic project_file ack in
_send_print_response then runs and overwrites _gcode_state back to
PREPARE. The 1 Hz cached-as-base push stream carries PREPARE forever,
the slicer never sees the FINISH transition it waits for, and the
modal sits stuck. Auto-Dispatch ON shares the cause: the real
printer's PREPARE->RUNNING->FINISH on the bridge gets masked by the
local _gcode_state override in _send_status_report.
Re-fire set_gcode_state("FINISH", filename, prepare_percent="100")
1.5 s after the project_file ack for every non-proxy mode (queue /
archive / review). The 1.5 s window lets the slicer see at least one
PREPARE push on the 1 Hz cycle so the transition reads as
PREPARE -> FINISH, matching what the slicer expects. Proxy mode is
exempt -- there the real printer drives the bridge state and a
synthetic FINISH would clobber a real PREPARE/RUNNING transition.
The scheduler cancels any in-flight timer when a new project_file
arrives so a retrying slicer doesn't end with two competing FINISH
timers. The pending timer is also cancelled on stop_server.
|