mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 03:54:17 +02:00
fix/skip-objects
329
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
56accd24de |
fix(smart-plugs): don't blank printer state when an accessory plug switches off (#2629)
An end-of-print auto-off on a plug that powers a filter fan marked the linked printer offline and forced its state to "unknown". The mark was unrecoverable: connected heals on the next MQTT message but state does not (only frames carrying gcode_state rewrite it, and steady-state push_status frames are partial), so the printer stayed "unknown" until a manual Force Refresh and the queue never dispatched to it again. The offline mark is now an explicit presumption: mark_power_off records the state it overwrites and _on_message undoes it as soon as the printer sends another report on its own topic, since inbound traffic proves the power was never cut. A reconnect discards the saved state, so a genuine power cut is unaffected. Each plug also gains a controls_printer_power flag (default true, backfilled) that gates all five power-off paths, and the queue's power-on step now picks the flagged plug instead of whichever linked plug came first. |
||
|
|
2e45893dd5 |
feat(print-options): add "Auto" state to bed levelling, flow & nozzle-offset calibration
Bed levelling, flow calibration, and nozzle-offset calibration were on/off only, so the sole way to run bed levelling was to force a full level before every print. Bambu Studio has always offered a third "Auto" state that lets the printer skip the calibration when it was done recently -- the state most users actually want. Make these three options tri-state (off/on/auto), defaulting to auto, and leave vibration/layer-inspect/timelapse as on/off (Bambu Studio exposes no auto for those). Wire encoding follows Bambu Studio's source exactly: each option sends a JSON bool (true only for "on") plus a companion int -- off=0, on=1, auto=2. The bool fields stay booleans (the #1478 H2S regression); only the companion int widened from {0,1} to {0,1,2}. #1721's observation that stage 8/39 stays queued when sending 2 is the auto contract (queued, skipped at runtime if recent), not a broken "off". - schemas: TriState = Literal[off/on/auto] with a BeforeValidator coercing legacy bool / 0-1 / true-false so old clients and un-migrated rows validate - model + migration: boolean columns -> String; SQLite via column affinity + data backfill, PostgreSQL via ALTER COLUMN TYPE guarded on information_schema (verified on both dialects); settings rows normalised true/false -> on/off - MQTT: start_print takes the tri-state strings and emits the paired bool+int - Virtual Printer: reconstructs the slicer's auto/on/off from the int companion (auto_bed_leveling / extrude_cali_flag) in both capture paths - frontend: CalibrationMode type; off/auto/on segmented controls in the print dialog, queue bulk-edit, and Settings -> Workflow; calibrationMode_* strings in all 11 locales |
||
|
|
c469aa3407 |
feat(slicer): add "slice as designed" mode honouring a 3MF's embedded settings (#2611)
Server-side slicing always applied the picked printer/process/filament triplet via --load-settings, which overrides the designer's embedded project_settings.config — so a MakerWorld model set up for 5 walls came out at the picked profile's default 2. That override is correct for re-slicing a design onto your own printer/AMS, but there was no way to slice a file the way its author configured it. SliceModal now offers a "Use the file's built-in settings" checkbox when the source 3MF carries embedded settings AND the picked printer matches the design's target model. It routes to the existing embedded-settings slice path (previously only a crash fallback), so walls/infill/filament come from the file. Ticking it locks all four preset dropdowns — printer included, since it's unused on this path and changing it would drop the match and hide the toggle. The printer-match gate stops embedded settings being honoured across models (wrong bed); there is no cross-printer re-targeting on this path. - schema: use_embedded_settings on SliceRequest - route: embedded_mode branch; crash-fallback guarded against re-running - frontend: gated checkbox locking all four dropdowns, resets on mismatch - 2 i18n keys across all 11 locales - tests: backend (flag skips triplet / ignored for STL) + frontend (toggle offered on match, locks dropdowns + sends flag / hidden on mismatch) |
||
|
|
83a7b75b14 |
fix(queue): persist selected plate to the archive; reconcile archive on offline stop (#2603)
A print queued from a specific plate of a multi-plate 3MF showed as Plate 1 in Print History after cancellation: the archive derives its plate from the filename, but a whole multi-plate 3MF uploads under one name with no plate suffix, so the parser defaulted to plate 1 and nothing copied the queue item's plate_id onto the archive (which had no plate field). Add a nullable print_archives.plate_id, copy it from the queue item at dispatch (archive- and library-file paths), expose it in the archive API, and render it in Print History. A startup backfill copies the plate onto existing archives from their linked queue rows. Column add + backfill are identical on SQLite and Postgres. Also fix a related lifecycle bug: stopping a printing item while the printer was offline left the linked archive stuck at "printing" (queue row cancelled, but no MQTT completion ever arrives to reconcile the archive). The offline-stop path now closes the archive out directly; the online path still defers to the MQTT completion event. |
||
|
|
e77e10896f |
feat(ams): name the expected slot when a paused print hits an AMS runout (#2587)
The firmware's runout HMS text says "insert into the same AMS slot", which is wrong under AMS Filament Backup: the firmware won't re-accept the depleted slot and advances to the next compatible one. Bambuddy parsed print.ams.tray_now only and dropped tray_tar/tray_pre, so the expected slot never reached the UI. Capture tray_tar/tray_pre on PrinterState and, while paused, resolve them to global tray IDs (expected_tray/previous_tray) on both the REST and WebSocket status payloads via a shared resolver: single-AMS passthrough, multi-AMS snow-mapping resolution, AMS-HT/external passthrough, and an honest null when the slot can't be placed. The AMS graphic highlights the expected slot (amber) and the ran-out slot (red); the HMS modal re-describes runout codes to name both, falling back to "check the printer" when unresolved. Runout copy translated in all 11 locales. Reporter @Jostxxl confirmed tray_pre=1/tray_tar=2 during the pause (ran out in Slot 2, printer expected Slot 3). |
||
|
|
1555fad539 |
fix(notifications): send Pushover retry/expire for Emergency priority (#2586)
Pushover rejects priority-2 (Emergency) messages unless they carry retry and expire. _send_pushover never sent them, so setting priority 2 always failed with Pushover's "retry and expire are required" error. Now at priority 2 we send retry/expire (default 60s/3600s, clamped to Pushover's 30-10800s range), surfaced as two provider fields shown only when priority is 2. Added PushoverConfig schema fields, i18n labels across all locales, and unit tests. |
||
|
|
00251fe808 |
feat(orca-cloud): pair via RFC 8628 device flow, replacing the paste-based sign-in
OrcaSlicer shipped a first-class external-app pairing API (OAuth 2.0 Device Authorization Grant), so the Supabase-PKCE copy-paste flow is replaced end to end. Connecting is now: click Connect, approve a short code on the Orca Cloud settings page, done — no redirect, no callback paste, no client secret, works from a LAN IP / localhost / behind a proxy. Backend: services/orca_cloud.py rewritten to device-code request + poll (the four RFC outcomes) + refresh_token grant + introspection + external sync pull; routes expose /device/start and /device/poll (device_code kept server-side in the reused orca_cloud_pending_* columns, no migration). Requests sync:read (read-only feature). Prod endpoint by default, ORCA_CLOUD_API_BASE overrides to staging. Wired the shared httpx client (fixes a per-request socket leak). Frontend: device-code connect UI + api client methods; all 11 locales updated. |
||
|
|
09b739b95d |
fix(cloud): stop reporting an expired Bambu Cloud sign-in as connected (issue #2562)
An expired token was indistinguishable from a working one. set_token()
stamped token_expiry = now + 30 days every time a stored token was loaded,
so the expiry reset on every request and is_authenticated could never
return False. /cloud/status answered "connected" for as long as any token
existed, while every cloud call 401'd — and the user was shown Bambu's own
{"error": "Please login."} verbatim.
Bambu is now the authority: /cloud/status validates the token upstream
(cached 5m), and any 401 from any authenticated call durably records the
credential as dead via users.cloud_token_invalid_at, so MakerWorld, cloud
profiles, slicer presets and firmware checks all agree at once. An
unreachable Bambu is treated as unknown, never as expired, so an outage
cannot sign a working session out.
The user-facing message now names the Profiles page, where the Bambu Cloud
sign-in actually lives; the old text pointed at a Settings page that does
not exist. Same stale path corrected in the wiki.
|
||
|
|
ce807fb1cc |
fix(queue): upload to printers in parallel, cap wedge retries, make debug logs survive a farm
The reporter's 19-printer farm started prints "one by one", up to an hour apart. check_queue awaited each dispatch inline, and a dispatch includes the FTP upload, so every printer queued behind every other printer's transfer despite being an independent machine. His logs give the arithmetic: 40978500 bytes in 254.1s, 157 KB/s - a Bambu printer's SD write, not the network, is the bottleneck. Nineteen of those in series is ~80 minutes, and the next upload started 131 ms after the previous one finished. The delay is linear in fleet size, which is why it got worse the more printers he selected. Dispatch is now collected during the (still sequential) selection loop and run concurrently afterwards, capped by queue_max_concurrent_uploads - Settings -> Workflow -> Queue & Dispatch, default 4, 1 restores the old behaviour. Every gate is untouched; only the transfers overlap. The pass still awaits its uploads before returning: _start_print flips the row pending -> printing only after the upload, so an early return would let the next tick re-dispatch the same rows. FTP work moves to its own thread pool. It was on asyncio's default executor - min(32, cpu+4), six threads on a 2-core NAS, shared with everything else - which was survivable only while uploads were serial. Two problems the same bundle exposed: A printer that accepts project_file but never starts (#1678) was retried forever: 270s watchdog, revert to pending, re-upload the whole file, repeat. Hence his "printer who, since the morning, still not launch" - and on a farm each lap also eats an upload slot the other printers are waiting on. Attempts are now counted on the queue item; after three it fails with a message pointing at the printer instead of queueing a fourth re-upload. The debug bundle we asked him for held 4m49s of history. The push_status dumps fired on every frame rather than on change - several while their own comment claimed otherwise - which is 27,727 of the bundle's 29,830 lines and rolls 5 MB in under five minutes on 19 printers. They now log transitions only. The bundle also read just the live log while three rotated backups sat next to it, under a byte budget four times larger than the file it was reading. Migration verified on SQLite and Postgres: idempotent, backfills legacy NULLs (dispatch_attempts + 1 is NULL for a NULL row, which would silently disable the cap). Tests: 6 on concurrent dispatch (overlap, cap honoured, 1 == serial, default applies with no settings row, a failed printer does not cancel its siblings, no early return), 4 on the retry budget, 6 on the bundle's rotated-log span, 7 on the debug gating. Each verified to fail against the unfixed code - the first end-to-end log assertion I wrote passed without the fix and had to be tightened. |
||
|
|
c640ddc1f7 |
fix(projects): carry tags, due date and priority in the list payload (#2536)
The edit dialog is shared between the projects list and the project detail page and seeds itself from whichever project object it is handed. The list payload never carried tags, due_date or priority, so editing from the list showed a blank tags field -- and, unreported, submitted the dialog's default priority over a stored high/urgent one. The component read those fields through a cast, so the compiler never flagged that they were always absent. Put them on ProjectListResponse and ProjectListItem, drop the casts, and let an explicit null clear tags and due date the way it already clears budget and url -- an emptied field was previously sent as undefined and silently reverted. The template list was missing target_parts_count, which the same dialog edits. |
||
|
|
aba00598bb |
fix(smart-plugs): read a REST plug's lifetime counter, and derive Today/Yesterday from it (issue #2539)
A Shelly reports one energy figure — aenergy.total, a lifetime counter in Wh that never resets. Bambuddy had a single REST energy field and filed whatever it found under "today", so the value never reset at midnight, and Yesterday and Total stayed at zero: get_energy() simply never set those keys. With `total` unpopulated, the hourly snapshot recorder skipped the plug, so the Statistics page's energy figure was zero as well, not just the Settings card. Split the REST energy config in two: rest_energy_path still means "used today", rest_energy_total_path means "lifetime counter". A Shelly has only the latter; a Tasmota behind a REST bridge has both; sharing a URL costs one fetch, not two. Then derive Today and Yesterday from that counter using the snapshots we were already taking: today = counter now - counter at the last local midnight; yesterday = the gap between the two previous midnights. Local midnight, not UTC — a UTC boundary rolls Today over at 02:00 in Berlin. The snapshot loop now ticks on the local hour so a reading lands on the boundary instead of up to an hour early. A counter that goes backwards (factory reset) reports nothing rather than a negative. Collateral, found while verifying on both engines: the smart-plug DateTime columns are naive UTC but the code wrote aware datetimes into them. SQLite drops the offset; asyncpg raises DataError. So on Postgres every snapshot capture raised inside the loop's except, and every status poll raised on last_checked — the whole subsystem was dead on the database we recommend for multi-printer installs. All plug timestamps are naive UTC now. Existing REST users with a cumulative path in the today field must move it to the new lifetime field; the form and wiki now name which counter each wants. |
||
|
|
ca3f6e5ee0 |
fix(drying): P1 AMS drying is screen-only — stop offering it (#2533)
The reporter found what his P1S was doing, and it is in Bambu's P1 manual: "P1S connected AMS drying functions may only be controlled from the P1S screen." The firmware acks ams_filament_drying with result: success and then discards it, which is why three commands on an idle printer left the AMS 2 Pro at dry_status 0. No command can start a cycle on a P1, on any firmware, so don't offer one. supports_drying() now excludes the P1 series outright, replacing the 01.08+ gate carried since #292 — that version is when P1 firmware gained AMS 2 Pro support, not remote drying, and it was never checked against a live P1. Both drying routes refuse with a specific 400 instead of publishing a message the printer will drop; queue and ambient auto-drying skip P1s via the same helper. A new drying_screen_only flag keeps the control on the card, disabled, saying why — a P1 owner needs to learn where to dry, not watch the button disappear. A cycle started at the printer still shows with its countdown; only Stop goes away, since a P1 ignores stop exactly as it ignores start. Also corrects the wiki firmware matrix, which listed P1P/P1S as supported and (separately) P2S/H2S/H2C as unsupported. 8 tests. |
||
|
|
f6c6cfbad3 |
fix(ams): show "?" not "Empty" for non-RFID spools using tray_exist_bits (#2527)
A spool with no readable RFID was reported by the standard AMS with an empty tray_type and state=9 — structurally identical to a truly-empty slot at the tray level — so the AMS card rendered it "Empty" while Bambu Studio correctly showed "?". The authoritative "a spool is physically here" signal is firmware's AMS-level tray_exist_bits bitmask (what Studio uses), but Bambuddy inferred emptiness from the per-tray state/tray_type. Confirmed from the reporter's bundle: tray_exist_bits=f (all four slots present) with tray_is_bbl_bits=5 (only slots 0,2 Bambu) — the present-but-non-Bambu slots were the ones shown Empty. Supersedes closed #1838. apply_tray_exist_bits() already parses the bitmask to clear stale fields on absent slots; it now also annotates each slot with an authoritative `exists` bool, gated behind a new annotate_exists flag so only the printer-card path sets it. The VP bridge leaves it off, so the `exists` key never reaches the slicer wire format. `exists` flows through the AMSTray schema/serialization to the frontend, where getEmptySlotKind() uses it: exists===true + no tray_type -> "?" (present, unconfigured), exists===false -> "Empty", exists absent -> the previous state=9/10 heuristic (AMS-HT and missing-bitmask paths unchanged). H2D/X1C already reported present-unknown slots with a non-9 state and took the "?" path; with the fix they reach it via `exists` and are unaffected. |
||
|
|
168d9d8f8e |
fix(auth): let API keys manage projects via new can_manage_projects scope (#1893)
PROJECTS_CREATE/UPDATE/DELETE were in _APIKEY_DENIED_PERMISSIONS with no entry in _APIKEY_SCOPE_BY_PERMISSION, so every project mutation returned a generic 403 for any API key regardless of granted permissions -- the same regression class as archives (#1888) and library (#1832). Add a per-key can_manage_projects scope. Project routes gate on plain PROJECTS_* (no OWN/ALL split), so all three CRUD permissions map to the one scope; membership edits (add-archives) gate on PROJECTS_UPDATE and are covered. PROJECTS_READ is unchanged (already under can_read_status). Column defaults TRUE for new keys; existing rows backfill to FALSE so the upgrade never silently widens scope. Migration is BOOLEAN (SQLite + Postgres safe), verified on fresh SQLite and Postgres 17. Bundled SpoolBuddy kiosk key set to False. Settings API-key UI gets a Manage Projects toggle + Projects badge; 11-locale i18n. RBAC scope matrix + drift guards extended. |
||
|
|
6358e9544e |
fix(auth): allow API keys to delete/edit archives via new can_manage_archives scope (#1888)
DELETE /api/v1/archives/{id} rejected every API key with 403
"API keys cannot be used for administrative operations", regardless of
the print's owner or the key's scopes. ARCHIVES_DELETE_ALL/_OWN (and the
create/update variants) were on the denylist and absent from the scope
allowlist, so require_ownership_permission fell through to the generic
admin-denied 403 — the whole archive-management surface was unreachable
for API keys. Same regression class as the #1832 library/maintenance
carve-outs.
Add a can_manage_archives per-key scope: ARCHIVES_CREATE, ARCHIVES_
UPDATE_OWN/_ALL and ARCHIVES_DELETE_OWN/_ALL move from the denylist to
the allowlist under it (OWN and ALL fold into the same scope, matching
can_manage_library). ARCHIVES_PURGE stays admin-only — it drops the
print's Quick Stats contribution, mirroring LIBRARY_PURGE. Column
defaults TRUE for UI-created keys; existing rows backfill to FALSE so the
upgrade never silently widens scope. Bundled SpoolBuddy kiosk key stays
minimally scoped (False). Migration is dialect-agnostic and verified on
fresh SQLite and Postgres 17.
Adds the Settings API-key toggle + badge (11-locale i18n) and extends the
RBAC scope matrix to cover all five archive-management permissions.
|
||
|
|
006c3113a0 |
feat(api-keys): can_manage_maintenance scope for HA-style automations (#1832 follow-up)
Carve MAINTENANCE_CREATE/UPDATE/DELETE out of the admin denylist so HA automations can log "cleaned nozzle" / reset a counter via API key without granting broader printer control. Follows the same shape as can_manage_library and can_manage_inventory: new column, allowlist entry, UI checkbox, wiki row, RBAC test coverage. Distinct backfill: these perms were EXPLICITLY denied for every API key before this change (no existing integration relies on them), so existing rows migrate to FALSE — no silent scope widening on upgrade. New keys default to TRUE, matching the safe-on-by-default pattern. Bundled SpoolBuddy kiosk key gets False explicitly (kiosk doesn't need it). |
||
|
|
61a7f2e4ac |
feat(scheduler): preheat & heat-soak before queued prints with per-filament chamber targets + airduct flap control (#1468)
New scheduler stage that heats the bed (and the chamber, on supported
printers) and holds at temperature before each queued print starts —
the heat-soak engineering filaments need for adhesion and warp
control. Bambuddy waits between FTP upload and start_print, so the
soak runs while the printer is otherwise idle. M191 is silently
ignored by Bambu firmware, so doing this at the orchestration layer
is the only place it works.
Resolution order at dispatch:
1. PrintQueueItem.preheat_override ∈ {inherit, on, off}.
'off' skips entirely; 'inherit' falls back to the global
preheat_enabled toggle; 'on' forces the stage even when the
global is off.
2. chamber_target = item.preheat_chamber_target_override
?? max(filament_map[normalize(t.tray_type)] for loaded slots)
?? 0.
Mixed PA+PLA picks PA's 50 (max-across-slots — PA's chamber
requirement is binding, PLA doesn't suffer being warm). PLA-only
derives 0 and skips the chamber phase automatically.
3. Three hardware tiers for chamber heat:
- Active chamber heater (H2C/H2D/H2D Pro/H2S/X2D/X1E) → M141 +
chamber-sensor wait
- Chamber sensor only (X1C/P2S) → no M141, passive bed-radiation
wait with hard max-wait cap
- No chamber sensor (P1S/P1P/A1/A1 Mini) → bed + soak timer only
4. Airduct flap (H2C/H2D/H2D Pro/H2S/X2D/P2S) auto-switches to
match the chamber target — heating mode for engineering
filaments, cooling mode for PLA. Bambu firmware does NOT
auto-switch the flap with M141, so without this an ABS print
on a previously-cooling flap fights the open exhaust, and a
PLA print on a previously-hot flap recirculates ABS heat.
Idempotent: only fires set_airduct_mode when current ≠ desired.
Settings → Workflow → Queue & Dispatch → Preheat & Heat Soak card:
master enable toggle (default off — disabled installs see no change),
per-filament chamber-target editor (replaces a single global int that
shipped in the first cut and couldn't serve PA + PLA in the same
config), preheat_max_wait_seconds, preheat_soak_seconds. The Print
Options panel in PrintModal gets a Preheat sub-section with the
tri-state Inherit/On/Off control and an optional chamber-target
override input.
DB migration: PrintQueueItem gains preheat_override VARCHAR(10)
DEFAULT 'inherit' and preheat_chamber_target_override INTEGER NULL.
Idempotent via _safe_execute. Existing rows behave exactly as before
the migration.
Best-effort throughout: printer drops, refused M141 or set_airduct,
missing bed temp, lost MQTT state mid-wait all log and return cleanly.
Normal upload + start path runs after this returns regardless.
|
||
|
|
3ef197e4e0 |
feat(slicer): Pipelines — multi-copy + class targeting + fanout + runs dashboard + retry-failed + WS updates (#1425 PR C — completes the v3 design)
PR A/B turned the slice modal's preset bundle into a one-click dispatch
with a pinned target printer. PR C closes the original issue: operators
type in a number of copies, Bambuddy slices once and distributes prints
across a fleet per the pipeline's chosen fanout strategy. A new dashboard
surfaces every run with filters, expandable per-copy status, cancel,
and retry-failed-copies. WS pushes keep everything live.
Backend
- copies field on POST /run, capped by new pipeline_max_copies setting
(default 50, hard cap 1000). PipelineRun.parent_run_id chains retries.
- SlicerPipelineUpdate accepts target_kind (specific_printer /
printer_class), target_model_class, fanout_strategy.
- Eligibility matcher branches: class-targeting enumerates matching
Printer rows, runs per-printer checks via a status_lookup closure,
returns printer_reports[]. New issue kinds: no_class_matches,
class_not_set.
- _pick_assignments distributes copies per strategy:
- max_parallel: target_model set, printer_id None — scheduler picks
- round_robin: copy i → eligible[i % N], fixed printer_id
- fill_one_first: all copies pinned to eligible[0]
All three reuse the slice-once path through slice_dispatch.enqueue.
- New routes:
- GET /pipeline-runs (paginated, filterable by pipeline + status)
- POST /pipeline-runs/{id}/retry-failed (creates child run with
copies = failed+cancelled count, parent_run_id set)
- Cancel cascades to all N queue entries (only pending/queued)
- _roll_up_run_status computes run-level status from per-job statuses;
introduces partial_failure for "some completed, some failed".
- ws_manager.broadcast_to_user emits pipeline_run_updated on every
state transition with the full materialised response.
Frontend
- Pipeline editor: target_kind radio + class picker (filtered to
installed models) + fanout-strategy radio. Read-only row shows
"X1C · Round robin" for class pipelines.
- RunWithPipelineModal: copies number input bounded by
settings.pipeline_max_copies. Accepts class-targeted pipelines.
- Settings → Workflow → Queue & Dispatch: new "Slicer Pipeline limits"
card with the max-copies input.
- New /pipelines/runs dashboard page (sidebar entry, gated on
pipelines:read). Two-filter dropdown, 25-per-page pagination, per-row
expandable to job list, Cancel + Retry-failed buttons.
- useWebSocket case for pipeline_run_updated invalidates both
pipeline-runs-all and pipeline-runs/{id} query keys.
|
||
|
|
4bbf0f031e |
feat(slicer): Pipelines — archive entry point + slicer progress toast (#1425 PR B follow-up)
Two real gaps from the PR B drop:
1. Run-with-pipeline only existed in the file manager. Operators who keep
working files in archives had to copy them to the library to use a
pipeline.
2. Triggering a slice via a pipeline produced a silent multi-second-to-
minute wait. The manual SliceModal flow shows the sticky
"Slicing X - Generating G-code 75%" persistent toast; the pipeline
path went through asyncio.create_task directly and never registered
with SliceJobTracker.
Archive entry point
- POST /slicer-pipelines/{id}/check-eligibility and /run accept
source_archive_id as an alternative to source_library_file_id (XOR,
enforced by Pydantic validator).
- PipelineRun.source_archive_id is a new nullable FK column with the
ALTER TABLE migration in run_migrations (idempotent via _safe_execute,
works on SQLite + Postgres).
- _resolve_source branches: archive path reads source_3mf_path with
fallback to file_path, mirroring routes/archives.py.
- ArchiveCard's context menu picks up a "Run with pipeline" item next to
Slice (only on source archives), gated on useSlicerApi + pipelines:run.
Slice (only on source archives), gated on useSlicerApi + pipelines:run.
- Path-safety: SEC-PATH-OK markers added at both LibraryFile.file_path
and archive.source_3mf_path join sites, citing the upload-time
validators.
Progress toast
- Pipeline orchestration is now the `run` callable of a
slice_dispatch.enqueue call — the same dispatcher SliceModal uses —
instead of a bare asyncio.create_task. The SliceJob lifecycle drives
the existing progress toast end to end with no separate notification
surface for pipeline runs.
- PipelineRun.slice_job_id is set before the 202 returns.
- RunWithPipelineModal calls useSliceJobTracker().trackJob() from
runMutation.onSuccess.
- RunWithPipelineModal source prop is now {kind, id, filename}
mirroring SliceModal.SliceSource; api.checkPipelineEligibility +
api.runPipeline take a discriminated-union source argument.
|
||
|
|
d6bdb7e200 |
feat(slicer): Slicer Pipelines — save & reuse a preset bundle in one click (#1425 PR A)
The SliceModal forces the user to pick four slots every time (printer / process / filament(s) / bed type). For fleet production that's tedious and error-prone. Pipelines let an operator save a named bundle and apply it with one click on the next file. PR A is bundle-and-management only. PR B adds single-target dispatch, PR C adds multi-copy batch with capability-matched fanout. Future-PR columns (target_kind / target_printer_id / target_model_class / fanout_strategy) ship in this migration so PR B+ is code-only, not a schema bump. Backend - New model SlicerPipeline + slicer_pipelines table; soft-delete via is_deleted so PR B+ run history can still resolve metadata. - Pydantic schemas reuse the existing PresetRef shape from schemas/slicer.py. - CRUD routes at /api/v1/slicer-pipelines/ — list (newest first by id DESC), create (201), get-by-id, partial PUT, soft-delete (204). - Three new permissions: PIPELINES_READ / PIPELINES_WRITE / PIPELINES_RUN. Administrators + Operators get all three; Viewers get READ. Backfill in seed_default_groups() so existing installs upgrade cleanly. All three denied to API keys for now. Frontend - Settings → Workflow splits into two horizontal sub-tabs mirroring the Authentication tab pattern: "Queue & Dispatch" (existing Workflow content) and "Pipelines" (new). URL deep-link via ?tab=queue&sub=pipelines. - SlicerPipelinesPanel — list, inline rename, delete, stale-preset warning when a referenced preset no longer resolves. - SliceModal gets "Apply pipeline ▾" + "Save as pipeline". Apply fills all four slot states; the filament list right-pads from current state so a pipeline with fewer entries than the current source's slot count keeps the existing tail. |
||
|
|
d4ad41d850 |
fix(hms): action buttons actually reach the printer (#1830)
Three distinct bugs combined into one user-facing failure: clicking
Stop / Problem-solved-and-resume / Ignore-and-resume returned 200 OK
but the printer didn't act, modal stayed up, print stayed paused.
Verified by injecting candidate command shapes on device/<sn>/request
against a live H2D paused on a wrong-plate HMS (print_error=0x05008051).
(1) hms_resume / hms_stop dispatched the "err"-bearing shape that
BambuStudio doesn't actually send; Bambu firmware silently rejects it.
Both now send the plain shape ({"print":{"command":"<x>","param":"",
"sequence_id":"0"}}). PAUSE -> FAILED in 1.7s for stop, PAUSE -> RUNNING
in <2s for resume.
(2) IGNORE_RESUME mapped to idle_ignore, which is BambuStudio's
"dismiss a warning" command and only works for non-pause warnings.
hms_ignore now branches on state.state == "PAUSE": paused -> plain
resume; not-paused -> idle_ignore with the full-length err.
(3) 64-bit hms[]-array faults were truncated to a non-matching err.
short_code in _parse_status discarded 32 of the 64 identifier bits, so
the firmware didn't match it to the active fault. HMSError.full_code
now carries the canonical hex identifier (16 chars for hms[] faults,
8 chars for print_error faults). Catalog lookup tries 16-char first,
falls back to 8-char. HmsActionBody.print_error pattern relaxed to
^[0-9A-Fa-f]{8}([0-9A-Fa-f]{8})?$.
(4) execute_hms_action returned publish-success as success, masking
every silent-rejection bug above as 200 OK. Route now snapshots
(state.state, len(state.hms_errors)) before dispatch, awaits
HMS_ACTION_ACK_WAIT_SECONDS (default 2.5s, module-level so tests
override), and returns 502 with "Printer did not acknowledge HMS
action within 2.5s" if state didn't move.
|
||
|
|
3ddf8d847e | [Feature]: HMS Actions (#1743) | ||
|
|
1c683f063c |
fix(queue): ownership gates + TOCTOU lock + /reorder validator (#1625-followup)
Three issues from the post-merge audit of the unified-dispatch PR, all
pre-existed on dev but became more impactful once every print routes
through the queue:
1. Start/Stop ownership gates. /queue/{id}/stop required QUEUE_UPDATE_ALL
(admin-only) -- operators saw the Stop button in the queue UI but got
403 on click. /queue/{id}/start required QUEUE_UPDATE_OWN with no
ownership check -- _OWN holders could start anyone's queue items via
direct API. Both routes now use require_ownership_permission, mirroring
/cancel. Stop is strict (rejects unowned items for _OWN); start preserves
#1670's VP-import flow where _OWN can start NULL-owner items and claim
ownership at click-time. Frontend QueuePage Start/Stop buttons flip
from printers:control to canModify('queue', 'update', created_by_id).
2. TOCTOU race on insert_position. Concurrent ASAP inserts to the same
scope both computed MAX(position) from before the other committed; in
an empty scope, both inserted at position=1 (duplicate). Wraps the
read+update in a transaction-scoped Postgres pg_advisory_xact_lock
keyed on the printer_id. Different printers don't contend. SQLite
serializes writes implicitly so the path is no-op there. Dialect is
checked against the live session binding, not the is_sqlite() helper,
because the test fixture overrides get_db to SQLite while
settings.database_url still points at Postgres.
3. /reorder duplicate-position validator. POST /queue/reorder set position
from the payload in a loop with no uniqueness validation -- a buggy
drag-drop client could leave the queue with ambiguous ordering (the
scheduler's ORDER BY (printer_id, position) ties break by row order).
New model_validator on PrintQueueReorder rejects duplicates at the
schema layer with 422 + "Duplicate positions in reorder request: [N, ...]".
|
||
|
|
4c67d8a4e1 | feat: Unify print dispatch through the scheduler (#1625) | ||
|
|
70857af393 |
feat(auth): SSO autologin + disable local username/password login (#1589)
Adds a global local_login_enabled setting plus a per-provider is_autologin flag on OIDCProvider so operators who run their own SSO enabled, or if the calling admin has no UserOIDCLink — either would lock everyone out. App-layer invariant: at most one provider can carry is_autologin; setting it on one clears it on every other. /auth/advanced-auth/status surfaces both new fields so the LoginPage decides UI in one query. The env-var bypass flips the reported local_login_enabled back to true so the SPA matches what the route will accept. |
||
|
|
fd61812d01 |
feat(drying): show active-cycle filament + target temperature on the AMS drying badge
Bambu's per-tick AMS push carries only the dry_time countdown — the
filament name and target temperature the user chose are never echoed on
the wire. The AMS card had no source of truth for them and rendered the
bare "Drying · 11h 35m left". The badge now shows
"Drying · PETG @ 65°C · 11h 35m left", matching the cycle the user
actually started.
BambuMQTTClient caches {ams_id: {filament, temp}} on send_drying_command
(mode=1), clears on mode=0 and on the dry_time falling edge to 0 — the
same per-AMS edge detector that drives the smart-plug-after-drying
callback. PrinterManager.get_drying_targets exposes it, the four
printer_state_to_dict call sites thread it through, AMS schema gains
dry_target_temp + dry_filament, and routes/printers.py builds the same
fields into the manually-constructed AMSUnit response.
When no cached target exists (drying started in a previous backend
lifetime, or initiated outside Bambuddy), the badge falls back to the
first loaded tray's tray_type + RFID-recommended drying_temp — the
heuristic the popover already uses to seed defaults.
i18n: printers.drying.targetSummary = "{{filament}} @ {{temp}}°C" in
all 11 locales. Parity check 5356 leaves per locale.
Note: a user reported the H2D's own physical display still labels the
cycle by the loaded tray's filament (e.g. "PLA" instead of the
Bambuddy-requested "PETG"). The wire payload is correct end-to-end —
journalctl shows filament: "PETG" sent and result: success ACKed — and
the badge in Bambuddy's own UI now reflects what we actually sent,
independent of the firmware's display choice.
|
||
|
|
8d6f701f1d |
feat(drying): continue drying while printing + gate rotate-spool when tray loaded (issue #1816)
Continue Auto-Drying while a print is running on capable hardware. New Settings > Print Queue > "Continue drying while printing" toggle (default OFF). Extends _check_auto_drying in print_scheduler.py to evaluate running printers when supports_drying_while_printing(model, firmware) returns true. Strict allowlist verified per Bambu wiki release notes for "Print While Drying" / "printing while filament is drying": H2D 01.03.00.00+, H2C/H2S/P2S/H2D Pro 01.02.00.00+, X2D/A2L 01.01.00.00+, X1C 01.11.02.00+. P1*, A1, A1 Mini, X1 (non-C), X1E intentionally excluded. Mid-print drying temperature is capped at max(40, preset_temp - 5) to protect spools from heat damage inside the hot enclosure during a print, matching Bambu's own "lower drying temperature during printing" guidance. Rotate-spool toggle in the drying popover is now disabled when any tray in the targeted AMS has filament threaded into the feed tube (tray.state === 11). The whole AMS rotates as one mechanism, so a single loaded slot locks the entire unit. Previously the toggle was always clickable and the firmware rejected with dry_sf_reason=[3] (ConsumableAtAmsOutlet) after the click. The first cut keyed on the printer-level tray_now but missed the H2D's typical post-print state where tray_now resets to 255 while filament stays in the tube — the per-tray state field reports it correctly. Submission also clamps rotateTray off so a stale-true state from a previous AMS can't leak through. Backend: supports_drying_while_printing in printer_manager.py covers display names and internal SSDP/MQTT codes (O1D, O1E/O2D, O1C/O1C2, O1S, N6, BL-P001, N7, N9). New print_drying_enabled boolean in settings schema. Frontend: toggle on SettingsPage, gate + clamp on PrintersPage drying popover using existing amsData cache. i18n: 3 new keys x 11 locales, no English fallback. Tests: 7 cases on the gate matrix (TestSupportsDryingWhilePrinting), 4 cases on the scheduler mid-print path (TestMidPrintDrying), 9 cases on the rotate gate state transitions. Full backend pytest -n 30 green (4251/4251), ruff clean, frontend npm run build clean, i18n parity 5355 leaves per locale. |
||
|
|
7cb905ad0c |
feat(inventory): toggle to disable auto-add of unknown RFID spools + global confirmation modal (issue #1764)
New setting "Auto-add unknown RFID spools" under Settings -> Filament -> Filament Tracking,
default ON for back-compat. When turned off, the backend stops auto-creating an inventory
record for an unknown RFID tag and instead broadcasts an unknown_tag WS event that pops
a global confirmation modal in the Bambuddy UI showing the printer / AMS-X label / slot /
material / colour. Add or Cancel; no nag on every MQTT push.
Backend
- Module-level _unknown_tag_last_broadcast dict dedupes per (printer, slot, tag). Set is
committed AFTER ws_manager.broadcast() returns so a crashed broadcast doesn't poison
the dedup and permanently silence the slot.
- Empty-slot MQTT push clears that slot's entry, so remove+reinsert reliably re-prompts.
- Successful matches via get_spool_by_tag / find_matching_untagged_spool / create_spool
also clear the entry so a future tag swap re-prompts.
- Tray data (tray_type, tray_color, tray_sub_brands, tray_count) shipped in the WS payload
directly so the modal renders the real material / colour instead of relying on the
React Query cache that lags the WS event by several seconds.
- Two new endpoints back the modal's confirm action:
POST /api/v1/inventory/spools/from-slot (INVENTORY_UPDATE)
POST /api/v1/spoolman/spools/from-slot (FILAMENTS_UPDATE)
Both look up the slot's tray data server-side and create + auto-assign atomically.
- Spoolman /from-slot now raises HTTP 500 when the slot-assignment INSERT fails instead
of returning success while the DB rolled back the binding.
- sync_ams_tray gained an optional auto_add_unknown_rfid kwarg (default True so existing
callers are unaffected); auto-sync and both manual sync routes thread the setting.
Frontend
- useUnknownTagPrompt hook listens for the unknown-tag CustomEvent, reads the tray fields
out of the event detail, and feeds a single-modal queue. No long-lived dismissed set;
the backend dedup handles spam suppression.
- UnknownSpoolModal wraps the existing ConfirmModal with a material + colour-swatch
preview block.
- Mounted in Layout.tsx alongside useSponsorPrompt so SpoolBuddy kiosk / login / setup
routes are excluded.
- getAmsLabel moved to utils/amsHelpers.ts; ConfigureAmsSlotModal.tsx and PrintersPage.tsx
both import the shared version (canonical AMS-A / HT-A / External labels).
- AppSettings TS interface gained spoolman_enabled, auto_add_unknown_rfid, spoolman_url
so the runtime cast in the hook is no longer needed.
- SpoolmanSettings.tsx gets a new toggle row in the Filament Tracking card, visible in
both built-in and Spoolman branches; auto-save + toast already wired.
|
||
|
|
bb42b423af |
feat(file-manager): user-authored tags for cross-cutting filtering (#1268)
Third and final piece of #1268, alongside the recursive-search + README-panel commit that landed earlier in 0.2.5b1. Folders express hierarchy (one home per file); tags are orthogonal labels — "toy", "kid-safe", "petg-only" — and a single file can carry as many as the user wants. Reporter wanted to find "every toy regardless of which folder it lives in"; folders alone can't do that without forcing the file into one bucket. Design decisions locked with maziggy before code: - file-only (folders already express hierarchy) - multi-tag filter = AND - tag filter IGNORES the selected folder (cross-cutting by design) - bulk-tagging from multi-select toolbar in v1 - no auto-tags from 3MF metadata (user-authored only) - label-only chips, no color/icon Backend - LibraryTag (id, name, name_key UNIQUE = LOWER(TRIM(name))) in backend/app/models/library.py. Case-insensitive UNIQUE collapses "Toys"/"toys"/"TOYS " into one row, so the route returns 409 instead of silently fragmenting the catalog. - LibraryFileTag(file_id, tag_id) association, composite PK, ON DELETE CASCADE both directions. Deleting a tag drops every chip; files survive. Deleting a file drops its tag links; the catalog row survives. - Both tables auto-create via Base.metadata.create_all — no explicit run_migrations step needed for new tables. - New router at backend/app/api/routes/library_tags.py with: GET /library/tags (list + per-tag file_count) POST /library/tags (create, 409 on case-insensitive dup) PATCH /library/tags/{id} (rename, 409 on collision, self-rename OK) DELETE /library/tags/{id} (cascade) POST /library/tags/bulk-assign (add | remove | replace) - Bulk-assign add is idempotent; replace with empty tag_ids clears the file's tag set. Per-file ownership enforced — *_OWN callers can only modify their own files; unknown file_ids quietly skipped (matches library_trash bulk shape). - list_files gains tag_ids: list[int] query param. AND semantics via JOIN + GROUP BY + HAVING COUNT(DISTINCT) — portable across SQLite and Postgres. When tag_ids is non-empty, folder_id / project_id / include_root / recursive are all bypassed so the result is cross-cutting. - FileListResponse gains tags: list[{id, name}] via selectinload(LibraryFile.tags) — N+1-free chip render. - Permissions reuse existing constants: LIBRARY_UPDATE_ALL for catalog mutations (global catalog, ownership-aware update isn't meaningful), LIBRARY_UPDATE_ALL/OWN pair for bulk-assign, LIBRARY_READ_ALL/OWN for list — file_count projection narrows for *_OWN callers so chip counts match what they actually see. Frontend - LibraryTagsModal (catalog CRUD) opens from the toolbar's new Tags button. max-w-4xl so multi-language subtitles don't wrap. Delete-with-warning when file_count > 0 ("removes the chip from all of them; files themselves are untouched"). - BulkTagsPickerModal opens from the multi-select toolbar (new Tag button between Move and Delete). Add/Remove radio, checkbox list, inline "create new tag" disabled on dup. Apply disabled until at least one tag is selected. The replace action is exposed in the API but deliberately NOT in this UI — arbitrary multi-file replace is destructive and confusing. - FileManagerPage integration: * selectedTagIds state, sorted into the useQuery key so the cache hits are stable regardless of toggle order * filter rail above the file list lists EVERY catalog tag as a togglable chip — inactive outlined, active filled green with an X. Clear all when 1+ active. Bar hidden entirely when catalog is empty. * useEffect prunes selectedTagIds when a tag is deleted from the catalog so the filter never strands on a phantom id * dedicated Tags column in list view at minmax(0,200px) between Prints and Actions * grid view chips render below the metadata block * chip clicks stop propagation so they don't toggle file selection - libraryTagsQueryKey extracted to frontend/src/utils/ libraryTagsQuery.ts so component files export only components (Vite react-refresh rule). - LibraryFileListItem.tags is OPTIONAL even though the backend always emits an empty array — legacy msw mocks in pre-existing tests construct partial file shapes without the field. Without the ? the FileCard renderer crashed on .length and broke 49 unrelated tests across FileManagerPage + FileManagerExternalFolder. Read sites use file.tags ?? []. |
||
|
|
5cbefca6a0 |
feat(file-manager): recursive subfolder search + per-folder markdown description panel (#1268)
Reporter (@zumik3-del, seconded by @unLieb) asked for three File Manager
improvements: recursive search, tags, and a markdown preview side panel.
This commit ships the two scoped ones; tags is held back gated on the
"give the issue a thumbs up" interest check Martin posted on the issue
because it's a much larger surface (M2M schema, CRUD endpoints, tag UI +
filter + autocomplete + i18n for the management surface) and isn't the
right call without a real demand signal.
1) Recursive search inside the selected folder.
Until now, selecting "Toys" and typing "robot" only found files
directly in Toys/ — anything under Toys/Cars/Race/ stayed invisible.
The page's client-side filter ran over a server-narrowed listing
(/library/files?folder_id=X is strict equality on folder_id), so the
client filter couldn't see what the listing never loaded.
list_files (backend/app/api/routes/library.py:1729+) gains a
recursive=true query param. When combined with folder_id, the route
walks library_folders.parent_id via a recursive CTE rooted at the
requested folder and returns every descendant folder's files in one
query. Recursive CTEs work on both SQLite >=3.8.3 (2014, well below
Bambuddy's runtime floor) and Postgres without dialect branching.
Default off so the existing folder-browsing call sites (Project /
Archive detail, the FE's no-search case) keep their narrow scope.
FE opts in only when both a folder is selected AND searchQuery is
non-empty (FileManagerPage.tsx — derived as searchExpandsSubfolders,
threaded through the useQuery key so the cache invalidates on
toggle). Small "Including subfolders" caption renders under the
search input when active so the user understands why a file from two
levels deep showed up.
2) Per-folder markdown description panel.
New endpoint GET /library/folders/{folder_id}/readme returns the
first .md file in the folder as {filename, content, truncated}.
Selection prefers README.md / readme.md / description.md
(case-insensitive via func.lower(filename) LIKE '%.md' + an
in-Python stem-preference sort), falls back to the
alphabetically-first *.md otherwise. 404 when no markdown is present
so the FE can hide the side panel — non-users pay no UI cost.
Bytes are clipped at 512 KiB (_README_BYTES_CAP) with a truncated
flag so the panel can warn the reader. UTF-8 decode uses
errors="replace" so one bad byte never blanks the panel.
New FolderReadmePanel.tsx fetches on folder-select and renders via
react-markdown@9 + remark-gfm@4 (tables, strikethrough, task lists).
Collapsible (default expanded), max-height 24rem with internal
scroll. react-markdown 9 doesn't render raw HTML by default — no
dompurify needed. Links open in a new tab with rel=noopener
noreferrer. Tailwind has no typography plugin in this project so
per-element components map h1/h2/h3/p/ul/ol/code/blockquote/table
to explicit utility classes that match the rest of the app.
Scope and permissions.
Both endpoints reuse the existing LIBRARY_READ_ALL / LIBRARY_READ_OWN
ownership-aware pair, so a viewer-tier user with read_own only sees
their own files in recursive listings and can only fetch the README of
folders containing their own files. No new permission, no DB migration.
The recursive CTE is a single SQL query — no N+1, no per-folder
round-trip, scales to deeply-nested model libraries.
|
||
|
|
4206d675eb |
feat(notifications): dedicate AI Failure Detection notification event (#1794)
Split Obico failure-detection dispatch out of the multiplexed
on_printer_error event onto its own on_ai_failure_detection event so
users can subscribe to AI alerts without also enabling HMS hardware-
error pages, and so the discoverable label "AI Failure Detection" is
what subscribes them rather than the unrelated "Printer Error" toggle.
New column on notification_providers (default False, branched
SQLite/Postgres migration), new notification_service.on_ai_failure_detection
method, new ai_failure_detection template, obico_actions._notify swap.
Frontend gets a summary badge, a toggle row with description, and ntfy
priority surfacing. 14 new tests pin the routing + the regression guard
("Printer Error" alone must NOT receive AI notifications now). 11 locales
covered.
Existing providers keep working: HMS hardware errors continue to ride
on_printer_error unchanged; users who want spaghetti alerts opt in via
the new toggle.
|
||
|
|
166e9f9ef2 |
fix(vp): correct #1780 root cause — VP intake key mismatch dropped every slicer field
First-attempt fix ( |
||
|
|
4d16faed76 |
feat(file-manager): sort folder tree by recent activity (#1770)
Reporter has a lot of nested cad / slicer directories and wanted "folders that just got a new 3MF" surfaced without scrolling the alphabet. Tree was always alphabetical; LibraryFolder.updated_at only bumps on rename / move, not on file-add inside the folder. Backend exposes latest_activity_at = max(folder.updated_at, max(immediate-child file.updated_at)) on FolderResponse + FolderTreeItem. The /folders tree route picks up a sibling func.max(updated_at) group-by alongside the existing file-count subquery; the by-project / by-archive / single-folder routes collapse count + max into one trip. Recursion across subfolders is intentionally not computed - bubbles immediate parent only, keeps the query a single GROUP BY rather than a recursive CTE. Frontend adds a folder-sidebar sort dropdown (By name / By recent activity) plus an asc / desc arrow, persisted in localStorage. sortedFolders memo applies the comparator recursively so order is consistent at every depth. Empty folders fall back to name within the activity bucket so they never elbow a recently-used folder to a random position. Both the desktop sidebar and the mobile selector consume the sorted list so order is identical across breakpoints. External folders: LibraryFile rows are created for scanned external files too, so the aggregate works on them - but the timestamp reflects last scan, not filesystem mtime. Documented in the wiki. Same change also fixes File Manager list-view column alignment: header and body were sibling grids with min-content as the trailing column, computed independently. Header empty trailing div resolved to 0; body action strip to ~220px. Different trailing widths gave the 1fr Name column different remaining space, shifting every fixed column to its right. Replaced min-content with fixed 220px in both auth-on / auth-off grid templates. |
||
|
|
68b9d741d9 |
feat(humidity): per-filament humidity threshold for auto-drying + alarms (#1605)
Reporter @thenewguy runs an engineering farm with one AMS per material
(PLA, ASA, Nylon, PVB, HIPS) — Bambuddy's single global ams_humidity_fair
threshold (default 60%) was driving both the queue / ambient auto-drying
trigger AND the hourly humidity alarm uniformly, which is wrong for
multi-material setups where Nylon wants <10% and PLA is fine at 60%.
Drying RUN parameters were already per-filament via drying_presets;
this commit adds the missing per-filament TRIGGER.
New setting ams_humidity_thresholds — JSON map of filament-type to
threshold percent with a "default" key for unknown / unmapped types.
Empty / unset → both consumers fall back to ams_humidity_fair so the
upgrade is silent.
Resolver lives in PrintScheduler.resolve_humidity_threshold(trays,
thresholds, fallback) — picks the lowest (most-restrictive) threshold
across all loaded tray types, matching the conservative-params strategy
_get_conservative_drying_params already uses for temp / hours. Empty
tray slots contribute no constraint; all-empty AMS falls through to the
"default" key. Filament names normalized to uppercase base (so
"PLA Basic" / "pla basic" both map to PLA).
Two consumer sites rewired through the same resolver so the scheduler
and the alarm path can never disagree about whether an AMS is "too
humid":
- print_scheduler.py::_check_auto_drying — per-AMS humidity comparison
for start / stop / skip decisions.
- main.py AMS sensor / alarm worker — hourly humidity alarm notifier.
UI: new table in Settings → Workflow → Auto-Drying, below the existing
Drying Presets table. Default row + 8 default filament types
(PLA / PETG / TPU / ABS / ASA / PA / PC / PVA) pre-filled from the
current ams_humidity_fair value so the editor starts sensibly.
Input pattern: draft-on-edit / commit-on-blur (transient humidityDrafts
state per row). onChange only updates the draft; onBlur (and Enter)
parses + clamps to [5, 95] + commits. Empty value on blur clears the
override and falls back to default. Caught mid-PR via a typing test:
the naive per-keystroke clamp snapped "3" → 5 before the user could
type the second digit of "30".
Setting is in the public _UI_PREFERENCE_FIELDS allowlist (same rationale
as drying_presets and ams_humidity_fair — non-sensitive integer map,
no SETTINGS_READ permission required for badge-color rendering).
|
||
|
|
a5fe5cb3d4 |
feat(sponsor-prompt): in-app toast at earned milestones
ghcr.io pull baseline (~10k/day rising → ~8-12k active installs) puts
sponsor conversion at 0.08% — roughly an order of magnitude under
industry-benchmark for OSS with visible CTA. The Settings banner from
|
||
|
|
090c180ebf |
feat(heater-history): track nozzle / bed / chamber readings + per-tile chart-icon overlay opens history modal
New PrinterSensorHistory table + 60s recorder + GET/DELETE /printer-sensor-history route gated behind a new PRINTER_SENSOR_HISTORY_READ scope (separate from AMS). UI adds a 10x10 LineChart icon on each heater tile - click body opens the existing target-temp popover unchanged, click icon opens a HeaterHistoryModal mirroring the AMSHistoryModal shape (kind toggle + 6h/24h/48h/7d range + current/avg/min/max + recharts line for value + dashed target). Read-only X1C/P2S chamber tile finally gets an interaction. Retention configurable via printer_sensor_history_retention_days (default 30, sibling of ams_history_retention_days). 8 new i18n keys translated in all 11 locales, parity green. 4 backend + 6 frontend tests added; full pytest -n 30 6226/6226, vitest 2176/2176, ruff/eslint/build all clean. |
||
|
|
b1cb26f6ee |
feat(ams-backup): add status badge + toggle, fix prefer-lowest (#1766)
Two tightly-coupled deliverables in one drop -- a new AMS Filament Backup status/control surface, and the #1766 fix that depends on it. Added -- AMS Filament Backup status + control - Parse bit 18 of top-level print.cfg into PrinterState.ams_filament_backup on every push_status. Verified against OrcaSlicer source (DeviceManager.cpp:4961) and a live H2D ON/OFF capture. Tri-state (None = A1 family / pre-cfg push) preserves today's behaviour. - Hold-timer guard (3 s) prevents stale frames from flickering the badge back to the printer's old cfg after a user-initiated toggle. - POST /printers/{id}/ams-backup toggle, set_ams_filament_backup() client method calling _set_print_option("auto_switch_filament", enabled). - GET /printers/{id}/inventory-remain endpoint exposes the same map the dispatcher uses (internal and Spoolman modes both work uniformly). - Small icon badge in the printer card's "Filaments" section header (placement reads as printer-wide because the cfg bit is printer-wide, not per-AMS). Click to toggle, success toast. - 5 i18n keys x 11 locales for the badge UI. Fixed -- #1766: prefer_lowest didn't pick lowest, ignored backup state - Backend gate in _compute_ams_mapping_for_printer: coerce prefer_lowest to False when status.ams_filament_backup is False; log the skip. - New effectivePreferLowest(setting, backup) helper applied at every frontend sort entry point: single-printer PrintModal, multi-printer hook per-printer, PrinterSelector InlineMappingEditor, FilamentMapping standalone editor (the last had NO preferLowest awareness at all before this change). - New preferLowestSortKey(f, inventoryByTrayId) mirrors backend's two-tier key exactly, including the banding tie-break (regular AMS < AMS-HT < external) so the client-side pre-compute matches the dispatch-time pick. An earlier draft used a flat `amsId * 4 + trayId` priority which gave external slots (ams_id = -1) a NEGATIVE priority -- caught in code review before commit. - Settings -> Filament -> "Prefer lowest remaining filament" gets an explanatory note about the printer-side AMS Backup dependency, with i18n key in all 11 locales. |
||
|
|
d196cfc500 |
fix(virtual-printer): forward H2C rack-swap nozzle pick from slicer to dispatch (#1780)
BambuStudio's project_file MQTT command for O1C2 (the H2C dual- nozzle-rack variant) carries nozzle_mapping (per-filament physical nozzle position IDs) and nozzles_info (per-extruder rack metadata). The VP intake was dropping both, so the H2C firmware fell back to "last matching nozzle type" auto-pick and ignored the user's slicer choice — every HF print landed on R2, every standard print landed on R4. Carry both fields through the VP intake → queue item → MQTT dispatch path. New nullable TEXT columns on print_queue, non- branched ALTER (matches ams_mapping / filament_overrides precedent). Dual-nozzle gate at start_print() keeps the fields off single-nozzle dispatches. Fail-open on malformed JSON — firmware auto-picks, never worse than pre-fix. Stamps both fields on every plate in the multi-plate Send All loop (#1697 / #1188 precedent). ams_mapping2 still handles H2D/X2D dual-extruder routing unchanged; this fix is scoped to the O1C2 rack-swap mechanism. |
||
|
|
0f99b54d7e | feat: Update printer card UI for structure and readability (#1661) | ||
|
|
9a432f0050 |
Restrict printer secrets to update-authority callers
GET /api/v1/printers/ and /api/v1/printers/{id} return access_code
only when the caller holds PRINTERS_UPDATE. Adds PrinterResponseWithSecret
as the elevated response shape; PrinterResponse no longer carries the
field. Auth-disabled single-trust mode preserved.
|
||
|
|
48d3f68ae6 | feat: Centralise sidebar ordering and add page visibility options (#1673) | ||
|
|
af5d24e289 | feat(inventory): structured storage locations catalog (#1505) | ||
|
|
2940fbdcf7 |
feat(auth): admin-configurable session lifetime ceiling (#1706)
The 24h session cap from the M-2 audit finding was hard-coded, so the
"Remember Me" checkbox could only control storage location, never
duration. Add session_max_hours setting (default 24, max 720) honoured
at all four token-issuance sites: plain login, 2FA TOTP/email, 2FA
backup, OIDC.
- backend/app/core/auth.py: SESSION_MAX_HOURS_HARD_CEILING + resolver
that clamps to [1h, 720h] and falls back to 24h on missing/blank/
unparseable. DB errors propagate — the login transaction must abort
on a broken DB rather than silently extend or shrink the lifetime.
- backend/app/api/routes/auth.py, mfa.py: all four sites read the
resolved value instead of ACCESS_TOKEN_EXPIRE_MINUTES directly.
- backend/app/schemas/settings.py, routes/settings.py: schema field
with ge=1 le=720 + int coercion in _build_settings_response.
- frontend/src/pages/SettingsPage.tsx: half-width card at top of
Settings -> Users left column with 24h/7d/30d presets, custom input,
and a yellow warning when value > 24h.
- frontend/src/i18n/locales/*.ts: 8 new keys per locale, real
translations in all 11 (en/de/es/fr/it/ja/ko/pt-BR/tr/zh-CN/zh-TW).
- backend/tests/integration/test_session_policy.py: 15 tests across
resolver clamping, login JWT exp end-to-end, settings API round-trip.
Already-issued tokens keep their original expiry; the new setting only
affects future logins.
|
||
|
|
eb5154f61a |
feat(queue): tabbed page, batch grouping, multi-drag, Gantt timeline
Restructures the queue page around three tabs (Queue / History / Timeline)
and adds first-class batch grouping plus a real time-based timeline.
Queue tab
- Layout toggle: Sort by Position (flat list) or Group by Printer (per-
printer section cards with aggregate count / time / weight headers).
- Batch grouping: pending items sharing a batch_id render as a single
collapsible row with aggregate stats; children draggable within the
batch only. Per-batch collapse state in localStorage.
- Multi-drag: dragging any selected row moves all selected items as a
contiguous block via DragOverlay (+N ghost).
- Selection bar gains a Group as batch action when 2+ ungrouped items
are selected. Ungroup lives on the batch parent row.
History tab
- Two-line rich rows: filament color swatch + weight + type, user
attribution, inline error message on failed / skipped rows.
- Responsive 1 / 2 / 3 column grid so a long history uses available
width instead of stretching one row per line.
- Batch siblings group into a collapsible parent with status-rollup
chips (3 OK / 1 failed / etc).
- Thumbnail hover preview shows the full image at 192x192 next to the
small thumb.
Timeline tab
- Replaces the hourly-list view with a Gantt swimlane: one row per
printer (plus per target_model and unassigned), horizontal hour
axis, jobs as bars positioned by start time and sized by duration.
- Live NOW marker.
- Only committed schedules are rendered: currently printing items,
pending items with scheduled_time, and pending ASAP behind an active
print. Staged (manual_start), waiting (waiting_reason), and ASAP
jobs on idle printers are filtered out.
- 24h rolling window with 12h step controls.
- Per-bar tooltip with start, end, progress, batch name.
Backend
- POST /queue/batches creates a batch, optionally assigning existing
pending item_ids (manual grouping) or returning an empty batch the
client can attach to subsequent /queue/ POSTs.
- POST /queue/batches/{id}/ungroup clears batch_id from all members
(skipping items the caller does not own) and deletes the batch row
when no members remain.
- POST /queue/ accepts an optional batch_id and validates that the
batch exists, is active, and the caller may modify it. The existing
quantity > 1 auto-batch path still fires when no batch_id is sent.
PrintModal
- When N plates from one source are queued in a single submission
(model assignment or single printer), the modal pre-creates a batch
and passes its id to each addToQueue call so multi-plate jobs land
grouped automatically. Falls back to ungrouped items if the batch
pre-create fails.
|
||
|
|
b171980688 |
fix(archives): backfill NULL created_at + tolerate NULL in response (#1732)
Older print_archives rows (and rows that landed via the SQLite ↔ Postgres
cross-DB restore path) can have created_at = NULL because the column was
originally created without a DEFAULT clause — server_default=func.now()
only fires at table creation, not for existing rows or raw cross-DB
inserts. The list_archives response model required a datetime, so a
single NULL row 500'd the whole endpoint via Pydantic ResponseValidationError.
- Boot-time backfill: COALESCE(completed_at, started_at, now()) for
any row where created_at IS NULL. Dialect-branched (SQLite datetime('now')
vs Postgres NOW()).
- Schema: created_at is now Optional on ArchiveDuplicate, ArchiveResponse,
and ArchiveSlim so a future NULL-leaking path doesn't break the list
endpoint again.
|
||
|
|
1c42a9f1fd |
remove(slicer): drop bundle import; fix cloud preset type/from for CLI (#1712)
Bundle import never delivered what it implied: BambuStudio's .bbscfg export
strips system processes/filaments, so importing a bundle left users without
process presets and slicing fell back to embedded settings on STL. Bundle
mode also hid the standard tier behind a constrained dropdown, the actual
trap reported here.
Removed end-to-end:
- backend: POST/GET/DELETE /slicer/bundles*, SliceRequest.bundle,
SliceBundleSpec, dispatch fork in library.py, bundle-context params on
the filament-requirements endpoints, bundle-fingerprint cache key in
slice_preview.py, SlicerApiService.{import,list,get,delete}_bundle and
slice_with_bundle, BundleSummary / BundleNotFoundError.
- frontend: BundlePicker + BundleStringDropdown, isBundleMode + every
branch, bundle state/queries/dispatch in SliceModal.tsx, SlicerBundle /
SliceBundleSpec types, three bundle API methods. buildCompatibilityIndex
loses its bundle path; presetCompatibility keeps compatible_printers
plus the @BBL fallback.
- SlicerBundlesPanel turns into a permanent static notice explaining the
removal, alternative import paths, and the new slice-time lookup order
(Imported > Orca Cloud > Bambu Cloud > Standard sidecar fallback).
- i18n: slicerBundlesRemoved.{title,description,alternatives,lookupOrder}
translated across all 11 locales; slice.bundle*, slicerBundles.* keys
removed.
Fixed (surfaced by removing bundle mode):
- _resolve_cloud and _resolve_orca_cloud now force type per slot and pin
from: "system" on the payload before json.dumps. Bambu Cloud ships
type as "printer"/"print" and routinely empty `from`; the BS CLI's
--load-settings parser rejects both with return -5 / "input preset
file invalid". Standard tier already did this; cloud paths now match.
|
||
|
|
d459b6eabb |
fix(slicer): preset visibility + lookup precedence + signed-out banner + AMS slot badges (#1712)
Four #1712 issues from the 2026-06-04 Orca Cloud integration: (1) Tier order put Orca Cloud above everything across SliceModal, auto-pick scoring, dropdown groups, the AMS slot picker, and the backend precedence. Bambu-Cloud-only users saw their profiles deprioritised behind an empty Orca tier. (2) Cross-tier dedup hid a same-named preset in all but the highest- priority tier. A user with both a local-imported and an Orca-synced "Bambu PLA Basic" couldn't see the Orca copy as a picker option. (3) CloudStatusBanner nagged signed-out users with a permanent "Sign in to Orca Cloud" line at the top of every slice -- even after explicit logout. Bambu Cloud had the symmetric problem. (4) ConfigureAmsSlotModal source badges were inconsistent: Orca rows showed only "Custom" (no source identity), Bambu Cloud built-in rows had no badge at all, and the orthogonal isUser-driven "Custom" badge collided with the source badge for cloud user presets. Order is local > orca_cloud > cloud > standard everywhere it lives (SliceModal SLICE_MODAL_TIER_ORDER + TIER_BONUS + dropdown tier list, ConfigureAmsSlotModal sourceOrder, and backend precedence). The order drives auto-pick + visual group rendering; it does NOT hide profiles. _dedupe_by_name replaced with _enrich_cloud_metadata: every tier returns its full list across all three slots (printer / process / filament). The function still backfills Bambu Cloud filament metadata from same-named local / orca_cloud / standard entries so cloud filaments score in pickFilamentForSlot. CloudStatusBanner silently no-ops on not_authenticated for both clouds; expired / unreachable still surface. The not_authenticated i18n keys stay in the locale files dormant. ConfigureAmsSlotModal: one source badge per row, one colour per source (green Local / purple Orca Cloud / bambu-blue Bambu Cloud / amber Built-in). The legacy isUser-driven "Custom" badge is gone; every row identifies its tier consistently. |
||
|
|
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.
|
||
|
|
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.
|