Two new project fields: a free-text URL rendered as a one-click
external-link button beside the project name on every card (opens in a
new tab, click is e.stopPropagation()-guarded so it doesn't enter the
project), and a cover photo that replaces the status-icon box with a
square thumbnail.
URL is plumbed through ProjectCreate/Update/Response/ListResponse,
including from-template + create-template flows so it inherits between
a project and its template. Cover photo is not inherited because the
file would be shared on disk between source and copy.
Schema validator rejects anything other than http:// or https://
prefixes -- <a href> rendering would otherwise execute javascript:
/ data: / file: URLs even with React's default escaping. PATCH uses
model_fields_set for the URL field so users can clear it by sending
{"url": null}.
Cover image storage: Project.cover_image_filename references a file
Cover image storage: Project.cover_image_filename references a file
inside the existing archives/projects/{id}/attachments/ dir, but it's
tracked separately from the attachments JSON list so swap/delete on
the cover doesn't perturb the user's other attachments. Three routes
(POST/GET/DELETE /projects/{id}/cover-image) accept only .jpg/.jpeg/
.png/.gif/.webp (no SVG -- SVG can carry script payloads), replace in
place (prior file deleted before the new one lands so repeat uploads
can't accumulate orphans), and self-heal when a DB reference points at
a vanished disk file by clearing the column and 404'ing.
GET cover-image is gated by RequireCameraStreamTokenIfAuthEnabled
(accepts ?token=... query string) -- not the bearer-token gate -- so
<img src> requests work in both auth-on and auth-off configurations.
The frontend wraps getProjectCoverImageUrl with withStreamToken(),
matching the existing pattern from getArchiveThumbnail.
Permissions: PROJECTS_UPDATE for upload/delete/PATCH, PROJECTS_READ
gate is implicit via the stream-token credential. Migration: 2
idempotent ALTER TABLE projects ADD COLUMN. Localised across all 8
UI languages.
Adds an optional slicer-api/ Compose stack and wires Bambuddy's File
Manager, Archives, and MakerWorld pages to a new server-side Slice flow.
Slicing runs as an in-memory background job (POST returns 202 + job_id,
polled via GET /api/v1/slice-jobs/{id}) so a multi-minute slice no
longer pins the modal; result lands as a new .gcode.3mf in the same
folder (or new archive for archive sources) with the embedded
thumbnail extracted.
Backend
- New services: slice_dispatch (in-memory dispatcher, 30min retention
sweep) and slicer_api (HTTP bridge with 4xx/5xx/connection error
split that drives the 3MF embedded-settings fallback retry path).
- New schemas: SliceRequest, SliceResponse, SliceArchiveResponse,
SliceJobEnqueueResponse.
- New routes: POST /library/files/{id}/slice,
POST /archives/{id}/slice, GET /api/v1/slice-jobs/{id} (gated on
LIBRARY_READ since job IDs are sequential and the body leaks source
filenames and result IDs).
- AppSettings + env defaults: use_slicer_api, orcaslicer_api_url,
bambu_studio_api_url. DB-stored values override env defaults.
Frontend
- New SliceModal handles preset gating; enqueues then closes
immediately.
- New SliceJobTrackerProvider polls active jobs at app level, surfaces
a single toast per job (queued -> running -> completed / failed)
and invalidates library/archives queries on terminal status.
- Settings -> Workflow -> Slicer card: preferred slicer dropdown,
Use Slicer API toggle, contextual sidecar URL field.
- File Manager / Archives / MakerWorld get a Slice button gated on
the Use Slicer API setting.
- gcode-viewer adapter learns ?library_file=<id> so sliced library
files preview inline.
i18n
- New slice.* and settings.{useSlicerApi,slicerCard,orcaslicerApiUrl,
bambuStudioApiUrl,slicerApiUrlDescription,useSlicerApiDescription}
+ fileManager.noPermissionSlice keys across all 8 locales (en, de,
fr, it, ja, pt-BR, zh-CN, zh-TW). English fully translated, German
fully translated, the other six seeded with English fallbacks
pending native translation.
Tests
- 10 backend integration tests in test_library_slice_api.py covering
validation (404/400), happy-path enqueue, sidecar-down, 3MF
embedded-settings fallback, STL no-fallback, and preset-error ->
failed job paths.
- New unit tests in test_slicer_api.py for the HTTP bridge.
- 5 new SliceModal frontend tests covering preset gating, library +
archive enqueue paths, error surface, and preset-load failure.
- Existing SettingsPage tests adjusted: slicer dropdown asserts now
switch to the Workflow tab first; added a beforeEach URL reset so
one test's tab click doesn't bleed into sibling tests.
Sidecar
- New slicer-api/ folder is self-contained and optional. Two services
(orca-slicer-api on 3003, bambu-studio-api on 3001 behind --profile
bambu) build via Docker git-build-context from
maziggy/orca-slicer-api@bambuddy/profile-resolver. The fork patches
the OrcaSlicer CLI's profile compatibility quirks (inherits-chain
resolver, from:User -> system rewrite, '# ' clone-prefix strip,
sentinel-value strip) empirically required to slice real GUI
exports without segfaulting the CLI.
Docs
- CHANGELOG entry under [0.2.4b1] - Unreleased Added.
- README File Manager bullet for the new server-side Slice button.
- bambuddy-website features.html: new card under "Configurable Slicer".
- bambuddy-wiki: new page features/slicer-api.md + nav entry +
features index card.
Notes
- Opt-in: with Use Slicer API off, the existing "open in desktop
slicer via URI" flow is the default and unchanged.
- 3MF inputs that segfault the CLI on --load-settings transparently
retry with embedded settings; the resulting job carries
used_embedded_settings: true.
- Sliced files always export as .gcode.3mf so File Manager picks up
the embedded thumbnail; file_type is set to "gcode" (blue badge).
ntfy supports a Priority header (1=min, 2=low, 3=default, 4=high, 5=urgent)
that controls escalation on the receiving device, but every event was being
sent at the server default — so a "50% complete" ping looked identical to
"print failed" or "printer offline". Add a per-event priority dropdown
section in the Add/Edit Notification modal (visible only for ntfy, listing
only enabled events); the backend reads config.event_priorities and emits
the matching Priority header on POST and PUT (image-attachment) paths.
Unmapped events fall through to the ntfy server default. Out-of-range
and non-numeric values are dropped, not clamped, so a misconfigured value
never silently sends at the wrong urgency. Test sends omit the header by
design so the test path can't accidentally page someone at urgent priority.
Backward compatible: existing providers without event_priorities behave
exactly as before. NtfyConfig.event_priorities is optional; the route
stores config as a JSON blob so no migration is needed.
i18n: full translations across all 8 locales (en/de/fr/it/ja/pt-BR/zh-CN/
zh-TW). README, CHANGELOG, and the wiki notifications page updated.
Tests: 6 backend (Priority set on mapped, omitted on unmapped/missing/
no-priorities, ignored for bad values, propagated through attachment
path), 6 frontend (section visible only for ntfy, lists only enabled
events, save round-trip, edit pre-fill, toggle drops row, non-ntfy
never writes the key).
#1108 — Long-lived camera-stream tokens for HA / Frigate / kiosks. Camera-only
V1, hard 365-day cap (no infinite tokens), pbkdf2 hashed at rest, plaintext
shown to user exactly once on creation. New "Camera API Tokens" panel under
Settings → API Keys with self-service create/revoke, styled confirm modal,
admin "All users" view for leak triage. Auth path: /camera/stream tries the
existing 60-min ephemeral table first, falls through to the long-lived path.
Indexed lookup_prefix keeps verify O(1) per token.
Permission audit: gated the existing API-keys-CRUD + Webhook docs + API
Browser content behind api_keys:read so non-admins with camera:view land on
the API Keys tab and see only the Camera Tokens panel they actually have
permission to use. Grid layout collapses to single column for non-admins.
Tests: 29 new backend (15 service + 14 integration covering create/list/
revoke ownership rules, the auth fall-through, scope enforcement, prefix
collisions) + 6 new frontend tests for the section UI including the new
modal flow. All 77 backend tests + 21 frontend camera tests pass. Ruff
clean (lint + format).
Docs: README updated with fan-out + long-lived-token bullets. Wiki gets a
new "Long-Lived Camera Tokens" section under features/camera.md (HA YAML
example, security model, permission requirements, revoke flow). Website
features.html gets the bullet under Camera Streaming.
Also includes #1089 follow-up tweaks already merged in this branch:
_stream_start_times.setdefault for accurate stream_uptime, subscribe()
RuntimeError retry to close the grace-vs-subscribe race, atomic
unsubscribe count via the iter_subscriber on_unsubscribe callback.
* feat(makerworld): URL-paste import and print for MakerWorld models
Add a dedicated /makerworld sidebar page where users paste a MakerWorld
model URL and get the full plate list + one-click "Import to Library" or
"Print Now". Closes the workflow gap that kept LAN-only users on the
Bambu Handy app solely for MakerWorld download-and-send.
The authenticated tier reuses the existing Bambu Cloud token that
Bambuddy already stores for firmware checks and slicer settings --
MakerWorld shares the same auth backend, so the same JWT works there.
No separate OAuth flow, no companion browser extension, no credential
hijack. Anonymous users can still paste a URL and see model metadata;
the 3MF download itself requires the Cloud login.
Print Now hands off to the existing PrintModal (plate picker + AMS
mapping + dispatch) so multi-filament models work via the same code
path as library-file prints. Imported 3MFs are stored through a new
shared save_3mf_bytes_to_library() helper so the multipart upload
route and the MakerWorld import route don't duplicate 3MF parsing +
thumbnail extraction logic.
LibraryFile gains indexed source_type + source_url columns. Re-pasting
a URL for a model already in the library returns the existing row
instead of re-downloading -- dedupe is by canonicalised URL, not SHA256,
because MakerWorld's download URLs are signed and change per request.
Thumbnail proxy (/makerworld/thumbnail) hot-links through the backend
instead of directly to makerworld.bblmw.com -- the SPA's img-src CSP
stays strict and users' IPs don't hit MakerWorld's CDN logs. The
endpoint is intentionally unauthenticated since <img> tags can't carry
a Bearer token; SSRF-guarded by a CDN host allowlist so it can't be
used as a generic proxy.
Search and browse-catalogue are explicitly out of scope. The public
design/search endpoint returns empty results from server-originated
requests (likely needs csrf/session state reproducible only from a
real browser), and the __NEXT_DATA__ HTML fallback is blocked by
Cloudflare. URL-paste covers the realistic discovery pattern (Reddit /
YouTube / shared links).
Headers match kloshi-io/makerworld-api-reverse's production-tested set
(User-Agent: 3d-printing-service/1.0, x-bbl-* client identifiers,
Referer). The /instance/{id}/f3mf call includes ?type=download which
community userscripts use to signal legitimate download intent. 418
responses (MakerWorld's CAPTCHA gate) retry once with backoff and then
surface a clear actionable error with an "Open on MakerWorld" fallback
link; we never try to evade bot detection.
Permissions: new makerworld:view (browse metadata, view thumbnails) and
makerworld:import (save 3MFs to library). Administrators and Operators
get both; Viewers get view-only. Migration grants these to existing
groups based on whether they already have library:upload / library:read.
Disclaimer in the UI and wiki page mirrors kloshi's framing: not
affiliated with or endorsed by MakerWorld or Bambu Lab, interoperability
only, not intended to circumvent access controls.
Tests: 30 backend (service + routes) + 4 frontend. Full backend suite
(1931 tests) clean. Frontend build clean.
* feat(makerworld): ship working URL-paste import via api.bambulab.com iot-service
The MakerWorld integration shipped in 0.2.4b1 dev was broken for most
public models: the makerworld.com/design-service path returns "Please
log in to download models" even with a valid Bambu Cloud bearer,
because it's cookie-gated behind Cloudflare. Published reverse-
engineering projects work around this by pasting browser cookies; we
route around it entirely by using the api.bambulab.com/iot-service
endpoint (documented by Pr0zak/YASTL#51), which accepts the same
bearer Bambuddy already has and returns a presigned S3 URL.
Working flow:
GET api.bambulab.com/v1/design-service/design/{id} → metadata
GET api.bambulab.com/v1/iot-service/api/user/profile/{pid}?model_id=<str>
Authorization: Bearer {cloud_token} → signed S3 URL
urllib.request (no redirects, no query re-encoding) → bytes
Notes on each step:
- The model_id query param is the alphanumeric string from the
design response (e.g. US2bb73b106683e5), NOT the integer designId
from the /models/{N} URL. The import route fetches design metadata
first to get it.
- S3 presigned URLs MUST be fetched with urllib (not httpx/curl_cffi)
because the signature is computed over exact query-string bytes;
any normalising encoder breaks it with SignatureDoesNotMatch 400s
(YASTL#52 hit the same issue). Wrapped in a no-redirect opener so
the .amazonaws.com host allowlist guarantee isn't bypassed by a
302 elsewhere.
- The canonical source_url now includes profile_id so different
plates of the same model get distinct library entries. Older rows
from dev builds keep the model-level URL; the resolve endpoint's
"already imported" check LIKEs both shapes.
UI rebuild:
- Per-plate Save + Save & Slice in Bambu Studio / OrcaSlicer (the
plate is unsliced source, so "Print Now" was misleading and is
replaced by an explicit slicer hand-off).
- Import all plates with sequential progress.
- Folder picker (default: auto-created top-level "MakerWorld"
folder, created on first import, folder tree invalidated so
File Manager shows it immediately).
- Image gallery per plate with keyboard-navigable lightbox.
- Recent imports sidebar (sticky on lg+, vertical list with
jump-to-library / slicer / open-on-makerworld icons).
- Inline follow-up actions on imported plate rows so the user
doesn't scroll back to a top-of-page card.
- Per-plate delete via the standard ConfirmModal (no window.confirm).
- Elapsed-time + phase label during import so the 10-30s synchronous
POST doesn't feel frozen.
- URL-change detection drops the preview when the pasted URL
diverges from the resolved one.
Security hardening (found in review):
- DOMPurify.sanitize on the MakerWorld HTML summary before
dangerouslySetInnerHTML (user-authored content).
- <img> tags in that HTML routed through the thumbnail proxy so
the SPA's img-src 'self' data: blob: CSP isn't widened.
- /makerworld/thumbnail uses follow_redirects=False (the host
allowlist only covers the initial URL).
- 3MF CDN fetch strips the bearer (signed URL is the credential).
- S3 fetch uses a no-op HTTPRedirectHandler for the same reason.
- Upstream filename is os.path.basename'd before persisting.
Tests: 46 backend service unit tests, 19 route tests, 12 frontend
tests — all passing. All user-facing strings localised across the
8 UI languages.
* - frontend/src/App.tsx — removed the 3 stale <AdminRoute> lines (kept the 3 <PermissionRoute> equivalents). TSC + Vite both clean.
- backend/tests/integration/test_auth_api.py — added # pragma: allowlist secret + # noqa: S106 on the test fixture line that GitGuardian flagged.
The Bambu Lab X2D (launched April 2026, dual-nozzle, enclosed, hardened
steel rod gantry, AMS 2 Pro compatible) identifies itself as internal
model code N6 via SSDP/MQTT, and real serials begin with 20P9. None of
these identifiers existed in Bambuddy's registries, so the camera
service fell back to the chamber-image protocol on port 6000 (X2D
doesn't speak it), firmware-check logged "Unknown printer model: N6",
and the dual-nozzle K-profile paths — gated on the H2D serial prefix
"094" — would have treated X2D as single-nozzle.
Backend:
- Register N6 → X2D across every registry (PRINTER_MODEL_ID_MAP,
PRINTER_MODEL_MAP, STEEL_ROD_MODELS, ETHERNET_MODELS,
CHAMBER_TEMP_SUPPORTED_MODELS, firmware-check API keys + wiki path,
virtual-printer SSDP/product/serial tables, DB vp_model_fixes).
- supports_rtsp(): match the X2 display-name prefix and the N6 internal
code; camera now routes to RTSP on port 322.
- Dual-nozzle serial prefix check in bambu_mqtt.delete_kprofile and
kprofiles.set_kprofile broadened to ("094", "20P9") — X2D now takes
the H2D-style cali_idx in-place edit path.
- is_h2d model gate in bambu_mqtt.start_print extended with "X2D" so
timelapse / bed_leveling / flow_cali / vibration_cali / layer_inspect
are sent as integers and external-spool ams_id 254/255 routing is
preserved (H2D-style deputy-nozzle addressing).
X2D uses hardened steel rods like P2S — it is intentionally placed in
STEEL_ROD_MODELS, not CARBON_ROD_MODELS. A regression-guard test pins
the classification.
Frontend:
- mapModelCode in PrintersPage and SpoolBuddyAmsPage handle N6 and X2D.
- Enclosure-door badge and airduct-mode whitelists include X2D.
- MaintenancePage.getMaintenanceWikiUrl routes X2D to P2S wiki URLs for
steel-rod lubrication, belt tension, cold-pull, and PTFE tube
(exported to enable direct unit testing).
Tests:
- test_printer_models.py: TestX2DModel (10 assertions).
- test_bambu_mqtt.py: X2D in start_print ams_mapping and is_h2d gate;
TestDeleteKProfileDualNozzleDetection across H2D, X2D, P2S, X1C.
- MaintenancePageWikiUrls.test.tsx: 15 assertions covering X2D, P2S
regression, X1C/H2D/A1Mini regression, and model-name normalisation.
Docs:
- README: added X2 series to the supported printers table.
- CHANGELOG: new entry under 0.2.3b4 Fixed.
Credit to @krautech for the report and debug bundle, and to @legend813
for PR #989 which seeded most of the registry changes — rod-type
classification was corrected (steel, not carbon) and the dual-nozzle /
K-profile / is_h2d gaps were added on top.
Adds a compact "Bed" badge in the printer-card controls row
between print speed and Stop/Pause. Opens a popover with up/down
arrows and a 1 / 10 / 50 mm step selector.
When the Z axis has not been homed since the last print, the
first jog per session opens a Bambu Studio-style modal with
Home Z / Move anyway / Cancel. "Move anyway" bypasses soft
endstops (M211 S0 ... M211 S1) for a single move and is
remembered for the browser session.
Backend:
- POST /printers/{id}/bed-jog?distance=N[&force=bool]
Emits G91 / G1 ZN F600 / G90 (with optional M211 wrap).
Distance validated server-side (non-zero, |N| <= 200 mm).
- POST /printers/{id}/home-axes?axes=z|xy|all
Emits G28 variants.
Both gated behind Permission.PRINTERS_CONTROL.
Frontend:
- New indigo-themed badge + popover in PrintersPage.
- Not-homed confirmation modal with sessionStorage "warned" flag.
- i18n keys under printers.bedJog.* in all 7 locales.
Tests:
- backend/tests/unit/test_bed_jog.py — 13 tests covering
404 / 400 / 500 / success paths for both endpoints, plus
gcode-payload assertions for force on/off.
Docs:
- README feature list, CHANGELOG (0.2.3b4 Unreleased),
printer-control wiki page, website features.html.
Firmware update modal now shows every version from Bambu's wiki release
history, each badged Usable/Unavailable/Installed. Selecting a usable row
— newer or older than current — swaps the release notes and enables
install for that version, so rollback no longer requires hand-flashing.
Wiki scraper tightened to only read heading-anchor ids (h-XXXXXXXX-YYYYMMDD)
instead of any XX.XX.XX.XX substring, eliminating false positives like an
AMS firmware version mentioned in an H2D changelog being listed as H2D
firmware.
Refs #568
Surface four Home Assistant-style controls on the Printers page card:
- SD Card badge in the top status row (green / red, icon-only).
- Enclosure Door badge in the top status row (green / yellow, icon-only).
Detection per printer family — X1/X1C/X1E read home_flag bit 23, all
others read top-level `stat` (hex string) bit 23 — so X1 firmware that
does not flip stat bit 23 stops false-triggering "open". WebSocket
status-change dedup key now includes door_open so toggling the door
alone publishes a push, no 30s REST-poll wait.
- Airduct Mode badge beside the speed control (cooling / heating)
for P2S/H2D/H2C/H2S; one-click dropdown calls the existing
set_airduct MQTT command via a new POST /printers/{id}/airduct-mode
route.
- Force Refresh entry in the kebab menu — calls the existing
/printers/{id}/refresh-status endpoint to request a pushall snapshot
without forcing a reconnect.
Tests: door-open parsing (X1 home_flag, non-X1 stat, ignore mismatched
source, invalid hex) and airduct route (validation, not-connected,
success, failure).
Adds a Failure Detection tab under Settings that wires Bambuddy to a
self-hosted Obico ml_api container — no cloud, no account, no WebSocket.
While a print is running, the detection service periodically hands the
printer's camera snapshot URL to the ML API and smooths scores over
time (30-frame warmup + EWM, alpha=2/13, short/long rolling means) so
one noisy frame can't trigger an action. When the smoothed score
crosses HIGH, the configured action fires exactly once per print:
notify, pause, or pause-and-cut-power (via linked smart plugs).
- Backend: new obico_detection + obico_smoothing + obico_actions
services, /obico/status and /obico/test-connection routes
(SETTINGS_READ / SETTINGS_UPDATE), six obico_* AppSettings fields
with validators for sensitivity/action/enabled_printers.
- Frontend: FailureDetectionSettings component (enable, ML URL + test,
sensitivity, action, poll interval, per-printer monitor list, live
status + detection history), new sidebar tab with service-active
bullet, toast on save.
- Tests: 17 detection unit tests + 15 smoothing unit tests + 4
frontend component tests.
- Docs: README bullet, CHANGELOG entry, wiki page under Analytics,
website features.html entry.
The Statistics page reported "Gesamt" (All Time) kWh correctly but showed
zero for Today/Week/Month in total-consumption mode. Two bugs drove it:
1. The starting plug counter was kept in an in-memory dict
`_print_energy_start` that was lost on any backend restart mid-print, so
the per-print `energy_kwh` delta silently never got computed. The stats
endpoint's fallback path `SUM(PrintArchive.energy_kwh)` therefore summed
to zero for users running in total mode.
2. Total-consumption mode has no per-print delta by design — it includes
idle/preheat/standby — so the fallback to archive rows was the wrong
strategy even when the data existed.
Fix, in two parts:
- Persist `energy_start_kwh` on the archive row and read it back from a
fresh session at print end. Deletes `_print_energy_start` and its 5
call sites, replacing them with a single `_record_energy_start()` helper.
Per-print tracking is now restart-resilient regardless of tracking mode.
- Add hourly `smart_plug_energy_snapshots` table + `_snapshot_loop()` in
SmartPlugManager. Rewrote the `/archives/stats` energy branch as
`_sum_snapshot_deltas()` which computes per-plug
`max(0, last-in-range - baseline)` where baseline is the latest snapshot
at or before the range start, falling back to the earliest-ever snapshot
and signalling `energy_data_warming_up` when no pre-range baseline
exists (fresh upgrade). MQTT plugs are skipped from snapshots since they
only report "today" and have no lifetime counter.
Frontend: QuickStatsWidget renders an AlertTriangle next to Energy Used /
Energy Cost with a tooltip when `energy_data_warming_up` is true, so the
"low values right after upgrading" situation is explained in-product.
Fully localised across 7 UI languages.
Tests: new backend unit tests cover the snapshot delta arithmetic
(baseline/endpoint, counter reset clamp, multi-plug, warming-up fallback,
endpoint windowing), per-print restart resilience via expunge_all, and the
snapshot task lifecycle (start idempotent, stop cancels). Frontend tests
assert the warning icon appears only when the flag is set and only on the
energy tiles.
Docs: updated `CHANGELOG.md`, `README.md`, wiki `features/energy.md`,
wiki `features/statistics.md`, and website `features.html` with the new
behaviour and warming-up explanation.
Audit PRs #920 (printers search/filter) and #932 (print from project
view) for regressions, i18n coverage, test gaps, and docs.
No regressions found: existing callers of the changed signatures
(archive_print, getLibraryFiles, filteredPrinters chain) are unaffected;
i18n is complete in all 7 locales for both features.
Backend tests (4 new, all passing):
- test_list_files_by_project_id — bulk JOIN returns files across all
linked folders, excludes unlinked ones
- test_list_files_folder_id_takes_precedence_over_project_id — guards
the documented precedence folder_id > project_id > include_root
- test_add_to_queue_with_project_id — project_id is persisted on the
queue row for later archive linkage
- test_add_to_queue_invalid_project_id_returns_404 — regression guard
for the validation pre-check on the queue path (mirrors the one on
the direct-print path in library.py)
Per-model start/end G-code snippets configurable in Settings (Workflow
tab). Queue items get "Inject G-code" toggle — scheduler injects
snippets into a temp 3MF copy before FTP upload. Supports Farmloop,
SwapMod, AutoClear, Printflow 3D and similar bed-clearing systems.
Original files are never modified.
New SJF toggle badge on the queue page. When enabled, the scheduler
picks shorter print jobs before longer ones instead of FIFO. A
starvation guard flags jobs that get skipped once, moving them to
the front on the next cycle so long jobs can't be postponed indefinitely.
- Add print_time_seconds and been_jumped columns to PrintQueueItem
- Cache print duration from 3MF metadata at queue item creation
- SJF query: printer_id, target_model, been_jumped DESC, print_time_seconds ASC, position
- Mark jumped items in-memory after each print start
- Toggle badge on queue page header with live state indicator
- Frontend auto-sorts to match scheduler order when SJF enabled
- Settings schema, boolean parsing, and migration (SQLite + PostgreSQL)
- i18n badge keys for all 7 locales
- 10 integration tests for SJF ordering and starvation logic
- Wiki, website, README, and changelog updated
Bambuddy can now use an external PostgreSQL database via the
DATABASE_URL environment variable. SQLite remains the default.
Dialect-aware helpers handle upserts, PRAGMAs, FTS (FTS5 vs
tsvector+GIN), backup/restore, and health checks. All migration
blocks use savepoints to prevent Postgres transaction poisoning.
Backups are always portable SQLite format regardless of backend.
Cross-database restore imports SQLite backups into PostgreSQL
with automatic boolean/datetime conversion, NOT NULL default
filling, and FK constraint handling.
REST/Webhook smart plugs can now fetch power and energy data from
individual URLs instead of requiring all values in a single status
response. Each value falls back to the shared Status URL when no
separate URL is set, preserving backward compatibility. Added power
and energy multipliers for unit conversion (e.g. 0.001 for Wh→kWh).
When multiple AMS spools match the same type/color criteria, an optional
setting now prefers the spool with the lowest remaining filament. This
helps consume partial spools before starting new ones. Sorting applies
to all matching paths: queue scheduler, print modal, and multi-printer
mapping. Unknown remain values (-1) sort to end.
Select multiple printer cards and apply bulk actions (stop, pause,
resume, clear notifications, clear bed) from a floating toolbar.
Selection shortcuts: Select All, Select by State (printing/paused/
finished/idle/error/offline), Select by Location. Action buttons are
smart-enabled based on selected printers' current states. Confirmation
modals for destructive actions. The status summary bar now shows all
printer states.
Admins can now filter the Statistics page by user via a new
stats:filter_by_user permission. A user dropdown appears in the stats
header showing all users plus "No User (System)" for prints without
attribution. The filter applies to all stats widgets, failure analysis,
and CSV/Excel exports. Backend validates the permission on all 4 stats
endpoints, returning 403 if the filter is used without authorization.
Stagger option now available when printing directly to multiple printers,
not just in queue mode. Prints are automatically queued with staggered
start times using group size/interval from Settings. New "Require
plate-clear confirmation" setting lets farm users disable per-printer
plate confirmations so queued prints start automatically on finished
printers.
Also fixes settings API type parsing for require_plate_clear (boolean),
stagger_group_size and stagger_interval_minutes (integer) — without this,
saved values returned as strings would cause the settings toggle to
always show enabled and trigger a permanent save loop.
Queue page redesign: compact stats bar replaces summary cards, color-
coded left borders for status scanning, collapsible history with
condensed rows, and a new production schedule timeline view (#823)
showing estimated completion times grouped by hour with filter tabs
and day navigation. i18n keys added for all 7 languages.
Fix plate thumbnails returning 401 in print modal when auth is enabled
(missing stream token). Fix schedule calendar picker opening off-screen
(hidden input positioned with sr-only instead of near the date field).
When adding a print to the queue for multiple printers, users can now
enable "Stagger printer starts" to avoid power spikes from simultaneous
bed heating. Configurable group size and interval — first group starts
immediately (ASAP) or at scheduled time, subsequent groups get offset
scheduled_time values. No backend queue/scheduler changes — leverages
existing scheduled_time field.
Also adds a dedicated Queue tab in Settings (stagger defaults + auto-
drying moved from Filament tab), i18n keys for all 7 locales, frontend
and backend tests.
Host directories (NAS, USB, network shares) can now be mounted
into the File Manager without copying files. Files are indexed
into the database on scan but read directly from their original
location. Supports read-only mode, hidden file filtering, and
automatic thumbnail extraction for 3MF/STL/gcode.
- POST /library/folders/external — create with path validation
- POST /library/folders/{id}/scan — discover/sync files
- Block uploads, moves, and deletes for read-only external folders
- Never delete actual files from external paths (DB-only removal)
- Purple folder icon + info bar with rescan button in UI
- i18n for all 7 languages
- 19 backend + 11 frontend tests
Replace fixed 30-second debug log collection with an interactive
3-step flow: start logging, reproduce the issue, stop & submit.
Users now control timing instead of racing a countdown.
Backend: split _collect_debug_logs() into POST /start-logging and
POST /stop-logging endpoints; add debug_logs field to submit request.
Frontend: 3-step progress indicator with elapsed timer, pulsing
active state, and 5-minute auto-stop. Updated all 7 locale files.
Add a "Rotate spool during drying" checkbox to the manual drying popover
for AMS 2 Pro and AMS-HT units. The firmware-level rotate_tray field was
already sent (hardcoded to false) — this makes it user-configurable.
The checkbox defaults to unchecked and resets each time the popover opens.
Firmware silently disables rotation if filament is currently loaded.
The closed-source bambu_networking DLL validates TLS connection parameters
and rejects connections where the certificate doesn't match the printer's
real BBL CA certificate. The TLS-terminating proxy presented Bambuddy's
own certificate, causing X1C/X1 prints to silently fail after verify_job.
Switch to transparent TCP proxying for FTP, FileTransfer, Camera, and FTP
data — only MQTT remains TLS-terminated (required for IP rewriting). The
slicer now gets end-to-end TLS directly with the printer's real certificate.
Changes:
- SlicerProxyManager uses TCPProxy for FTP (990), FileTransfer (6000),
Camera (322), and pre-listens on FTP data ports (50000-50100)
- Only MQTT (8883) uses TLSProxy for IP rewriting
- Remove debug logging from MQTT and FTP proxy code
- Fix install.sh missing AmbientCapabilities=CAP_NET_BIND_SERVICE
- Update module docstring, migration docs, README proxy description
- Add tests verifying transparent proxy architecture
Add a speed control badge to the printer monitoring card controls row
that lets users switch between Silent (50%), Standard (100%), Sport
(124%), and Ludicrous (166%) presets during active prints. The badge
displays a gauge icon with the current speed percentage, always visible
but disabled when idle. Includes backend endpoint, optimistic UI
updates, i18n for all 7 locales, and full test coverage.
Add tests, docs, and ruff fixes for per-user email notifications
- Fix missing timezone import in email_service.py (F821)
- Fix unused lambda arg in main.py asyncio done_callback (ARG005)
- Fix E302 blank line spacing for mark_printer_stopped_by_user
- Fix F821/UP037 forward reference in user_email_pref model
- Fix SettingsPage test for duplicate "Notifications" text
- Add backend unit tests for permissions, schemas, and templates
- Add backend integration tests for user-notifications API
- Add frontend tests for NotificationsPage
- Add user_email_pref model import to test conftest
- Update CHANGELOG and README
Ambient drying: automatically dry filament on idle printers when
humidity exceeds threshold, regardless of queue state. Separate toggle
from queue auto-drying — both can run simultaneously. Uses the same
presets, humidity threshold, and power constraint detection.
Fix: block mode (wait for drying) previously skipped the humidity
auto-stop check for already-drying printers, causing drying to
continue indefinitely. Now only prevents starting new drying.
Queue auto-drying: scheduler automatically starts drying on idle printers
with scheduled queue prints when AMS humidity exceeds the configured
threshold. Uses conservative parameters (lowest temp, longest duration)
for mixed filaments. Drying stops when humidity drops below threshold
(30-minute minimum prevents oscillation), when scheduled items are
removed, or when the feature is disabled. Optional "block queue" mode
delays the next print until drying completes.
Configurable presets: temperature and duration per filament type,
editable in Settings → Print Queue, used by both manual drying popover
and queue auto-drying. Separate presets for AMS 2 Pro (n3f) and AMS-HT
(n3s) reflecting different heating capabilities.
PSU detection: drying button disabled with tooltip when dry_sf_reason
indicates insufficient power. Parses drying status bits and dry_sf_reason
from AMS info hex string via MQTT.
Backend: print_scheduler.py (+316 lines), bambu_mqtt.py, printer_manager,
schemas, settings route. Frontend: PrintersPage drying presets prop,
SettingsPage drying config UI, i18n (7 locales). Tests: 27 new tests in
test_scheduler_auto_drying.py covering conservative params, presets,
state sync, stop logic, minimum drying time, and auto-stop regressions.
Notify users when the first layer finishes printing so they can check
adhesion remotely. Triggers once per print when layer 2 begins
(layer_num >= 2, capped at <= 5 to handle reconnects). Includes a
camera snapshot attachment. Adds the on_first_layer_complete toggle
to all notification providers, with backend/frontend/i18n support
across all 7 locales.
Start, monitor, and stop drying sessions for AMS 2 Pro (n3f) and
AMS-HT (n3s) directly from the Printers page. Flame icon in AMS card
header opens a popover with filament-based temperature/duration presets
from BambuStudio. Live countdown status bar shows time remaining.
Backend: cache module_type from MQTT get_version, expose dry_time and
module_type in both WebSocket and REST paths, add supports_drying()
with firmware gating, add server-side guard on start_drying endpoint.
Frontend: drying button, popover with filament select + temp/duration
sliders, status bar, start/stop mutations. i18n for all 7 locales.
Supported: X1/X1C (fw 01.09+), P1P/P1S (fw 01.08+), H2D (fw 01.02.30+),
H2D Pro, X1E. Not supported: P2S, A1, A1 Mini, H2S, H2C.
Cloud credentials were stored globally — one Bambu Cloud account per
Bambuddy instance. When auth was enabled, any user logging into Cloud
overwrote everyone else's credentials. Credentials are now stored
per-user: each user gets their own independent Cloud login.
Also fixed cloud data endpoints (settings, fields, preset CRUD)
requiring settings:read/settings:update permissions instead of
cloud:auth — users who had "Cloud Auth" enabled but "Settings"
disabled couldn't load profiles after logging in.