Round-3 review of the "Save AMS mapping" PR.
The queue item's ams_mapping was set unconditionally, on the reasoning that
honouring the slicer's own pick is a correctness fix rather than a feature.
It is both. Storing a resolved mapping makes _ensure_ams_mapping return
early, so _compute_ams_mapping_for_printer never runs — and that function is
where prefer_lowest_filament lives, along with the AMS-filament-backup gate
that qualifies it (#1766), the inventory-remain overrides, and the per-slot
force-colour overrides. Every existing queue-mode VP pointed at a printer
would have quietly lost all of it on upgrade, without a setting to turn it
back on.
So save_ams_mapping now gates the queue item too, not just the archive
persistence. Off is exactly the old behaviour. The correctness case the PR
was written for — two spools of the same red PLA, and the slot the user
picked in the slicer thrown away — is still fixed, for anyone who asks for
it.
Force color match wins over it when both are on. Its only effect on a
fixed-printer item is the filament_overrides written onto the queue item,
and those are read inside the function a stored mapping skips, so the two
toggles sitting next to each other on the same card silently cancelled. The
dispatch now matches strictly, as asked, while the slicer's pick is still
saved onto the archive — that is what the toggle's name promises, and a
later reprint is a separate decision from this print. The queue-add fallback
applies the same rule to a request that carries force-colour overrides.
A mapping shorter than a plate's highest slot id cannot address that plate's
own slots, and _ensure_ams_mapping would have kept it anyway, since it only
rejects an all-unresolved one. Each plate now checks the length it needs and
falls back to a computed mapping if the array does not reach. Bambu Studio
sends a file-global array, so this normally never fires; it also means a
multi-plate Send All degrades safely if that ever stops being true.
The badges claimed more than they delivered. Both rendered whenever a saved
mapping existed, ignoring which printer it belonged to, while the tooltips
promised the reprint would reuse those exact spools — true only on the
printer the trays were resolved against. The queue row's flag is now
computed against that row's own printer, which is precisely when dispatch
reuses the mapping, and the archive card names the printer instead of
implying any of them will do. It hides itself when that printer no longer
exists. Retranslated in all 13 locales.
Frontend tests, which the PR had none of. The printer-scoping rule is now a
pure function rather than an inline expression, covered for the mismatched
printer, the no-printer-selected case that would otherwise compare undefined
against undefined, and malformed extra_data. The toggle's undo bookkeeping
is covered for unresolved slots, short mappings, and hand-made picks —
preserved when the toggle never wrote that slot, replaced when it did, which
is behaviour worth pinning either way.
Also reverts all three queue-mode switches when a save fails, not just the
new one; without it the card shows a setting the server rejected.
Round-2 review fixes for #2700.
Blocking: the toggle didn't actually gate the archive write. archive.py's
promotion fired for any print_data carrying ams_mapping, but bambu_mqtt's
request-topic interception captures ams_mapping unconditionally for every
print source (slicer-direct LAN prints included). Since main.py's
real-printer auto-archive path forwards the full MQTT payload as
print_data, every archive on any install — VP or not — grew
extra_data.slicer_ams_mapping. Fixed by replacing the print_data-sniffing
with an explicit `slicer_ams_mapping` param on archive_print() that only
the VP-queue path (already gated on save_ams_mapping) ever passes.
Blocking: a saved mapping could get reused on a printer it was never
resolved against — tray IDs only mean something relative to one printer's
AMS layout. extra_data.slicer_ams_mapping is now stored as
{mapping, printer_id} instead of a bare array:
- add_to_queue's fallback only fires when the reprint's target printer_id
matches the mapping's origin printer.
- The frontend's archiveAmsMapping only surfaces (and the Mapping button
only appears) when the print modal's selected printer matches too.
- A model-based VP (target_printer_id=None, no MQTT bridge to any real
printer) never stamps a mapping in the first place — there's no live AMS
layout for the slicer to have resolved tray IDs against.
Also from review:
- Multi-plate archives now get the Mapping button too (the per-plate
FilamentMapping loop was missing archiveAmsMapping entirely).
- Added coverage for the previously-untested late-MQTT archive patch path
(_restamp_recent_queue_item), including the model-based-VP skip case.
- usingArchiveMapping now also resets on printer change, not just
plate/archive (it already worked via the printer-scoping above, but is
now an explicit dependency too).
- The Mapping button's revert (OFF) now undoes only the slots it itself
set, not every manual pick in scope — matches the comment above it.
- Added a comment on why negative-value slots (external spool) are
skipped rather than cleared when applying a saved mapping.
Lets a reprint reuse the AMS slot the slicer itself picked, instead of
re-deriving one from the file's static type/color.
When a Print Queue VP has "Save AMS mapping" on, the slicer's own
live-resolved ams_mapping (from the project_file MQTT command) is
persisted onto the archive as extra_data.slicer_ams_mapping. A later
reprint can reuse it via a new "Mapping" button in the filament-mapping
panel — one click snaps every slot to the saved pick, click again
reverts to auto-match. Archive cards and queue rows get an "AMS mapping
saved" badge so it's visible beforehand. add_to_queue also falls back
to the saved mapping automatically when the caller sends no explicit
ams_mapping (e.g. a plain reprint with no per-slot edits).
The queue item's own ams_mapping (used for that dispatch) is still
captured unconditionally whenever the slicer provides it — that part is
a correctness fix, not gated behind the toggle. Only the archive
persistence for future reprints is opt-in.
Split out from the original combined PR per review: this half is
genuinely opt-in and low-risk (#2684). The dispatch-time validation
gate that keeps a stored mapping honest (#1308) changes behaviour for
every existing user and will land as its own PR.
Review fixes applied:
- _extract_slicer_ams_mapping_json: dropped the unreachable `v is None`
arm and rejected bool explicitly (isinstance(v, int) accepts bool).
- Translated the Russian docstring text to English.
- save_ams_mapping's model comment moved to a trailing comment on the
column line, matching the file's convention.
- usingArchiveMapping now resets when the plate or archive changes, so
the Mapping button can't read ON against a mapping it never applied.
- Translated "Click to change slot assignment" and "Re-read".
- add_to_queue's fallback is now called out explicitly in code comments
and covered by three new integration tests (fallback fires, explicit
mapping wins, unrelated extra_data doesn't false-trigger).
Closes#2684
In the Print dialog's Filament Mapping, each required filament shows its
name and the grams the job needs, e.g. "Bambu PLA Basic (281.2g)". Name and
grams shared one fixed-width column with truncate on the whole string, so a
long name pushed the "(...g)" off the end and clipped it -- partially on a
wide screen, entirely in mobile portrait. The gram usage is the number that
matters (does the spool have enough left?), so it shouldn't be the part that
gets dropped.
Pin the gram usage (shrink-0, whitespace-nowrap) and let only the name
truncate, with the full name on hover. Applied to both the Specific-Printer
(FilamentMapping) and Any-model (PrinterSelector) panels. Layout only.
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
A P1S queue row with use_ams=true but ams_mapping=[-1] was silently
printed with no AMS, starting against the empty external feed and pausing
with a runout. Two faults combined:
- start_print treated -1 (unresolved) the same as >=254 (explicit
external) when deciding to force use_ams=False. Only genuine external
now downgrades; -1 never does.
- The scheduler trusted a stored [-1] as "already resolved" and passed it
through. It now recomputes from live AMS trays whenever the stored
mapping is entirely unresolved, and clears it if nothing matches rather
than sending a doomed command.
Frontend: the Print dialog no longer serializes an all-[-1] mapping while
the printer status is still loading (the hook returns no mapping), and
submit waits for AMS status with a "Waiting for AMS status" notice.
Tests: new backend + frontend regression coverage; corrected one existing
test that pinned the old [-1] -> use_ams=False behavior.
A queue item's "Any <model>" button labeled itself from the file's slice
metadata while the scheduler used the row's target_model, so an X1C-sliced
item targeting H2D showed "Any X1C" above "assign to first idle H2D". The
mismatch itself was created silently: sliced-for metadata loads async, and
switching to model mode before it arrived pre-selected the alphabetically
first model (H2D on a mixed farm), after which the model dropdown hid
itself. Nothing validated compatibility, so the scheduler would hand X1C
G-code to an H2D.
Frontend: never default the target silently, keep the dropdown visible in
model mode (incompatible models disabled), label from the actual target,
warn on mismatch, block submit when incompatible.
Backend: new GCODE_COMPAT_FAMILIES table (X1/X1C/X1E/P1P/P1S interchange;
everything else exact-match; missing metadata never blocks). Queue create
and update reject incompatible targets with 400; the scheduler holds back
pre-existing mismatched rows with an actionable waiting_reason instead of
dispatching them.
queueing plates we cannot map (#2552)
The override panel disappeared for a multi-plate selection in Any [model]
mode, but only once the dialog had been opened before -- which the reporter
saw as "after the file was queued or printed". The filament requirements are
keyed on the selected plate, which is null as soon as two plates are ticked.
On a cold cache the modal cannot yet tell the file is multi-plate and fetches
the whole file's requirements for one render; the panel rendered from that
union. On a warm cache it knows from the first render, the whole-file fetch
never runs, and the panel had nothing to render. Visibility was decided by a
cache race, and the "working" case listed filaments from plates the user had
not selected.
Model mode now renders one panel per selected plate from that plate's own
requirements, and each queued plate carries only the overrides for the slots
it prints, so a colour forced on one plate no longer blocks another.
Reviewing the per-plate machinery turned up four more holes, all closed here:
a manual tray pick survived a change of printer, and a global tray id names a
different spool on a different machine; a plate whose filaments could not be
read was indistinguishable from one needing none and was queued with neither
mapping nor forced colours, so Print now waits for every selected plate to
answer and names the one it cannot read; the insufficient-filament check still
weighed the whole file against a mapping the plates no longer use, and now
follows what each plate dispatches, summing demand per tray; and the
per-printer tray editor no longer appears for a multi-plate fan-out, where its
choices were collected and then discarded.
Selecting several plates hid the filament mapping panel but did not stop the
modal sending a mapping. With no single plate selected it fell back to the
whole file's filament list -- the union of every plate -- and matched against
that. Tray assignment is stateful, so where plate 1 prints red on slot 1 and
plate 2 prints red on slot 2, slot 1 claimed the only red spool and slot 2 fell
through to a type-only match on black. That one mapping went out with every
plate, and the scheduler uses a stored mapping verbatim, so plate 2 printed in
the wrong colour -- decided by a panel the user never saw.
Fetch each selected plate's requirements and map them separately: one panel per
plate, named after it, with its own tray overrides, and each queue item carries
its own plate's mapping. A fan-out across several printers would be a panel per
plate per printer, so those items carry no mapping and the scheduler maps each
plate against the printer it picks. Model mode is unchanged -- no printer means
no trays to map onto.
The tray matcher existed twice and this needed a third caller, so extract it
once and have both existing paths delegate; its 62 tests pass unchanged.
The bug only reproduces with a realistic query cache -- the shared test harness
sets gcTime: 0, which evicts the union and makes the modal look innocent -- so
the new modal tests bring their own client.
The app was built dark-first, so hundreds of hardcoded Tailwind semantic
text/icon utilities at light shades (text-amber-400, text-blue-300, ...) had
no dark: variant. With darkMode:'class' they applied in light theme too,
producing washed-out text on pale tints and white cards — including the three
reported spots (AMS Drying banner, Archives no-3MF warning, debug-logging
banner). Give each a theme-aware pair: a darker readable shade in light theme
with the original pinned to dark:, so dark theme is unchanged. ~100 files.
The bambu-* CSS-variable palette (self-correcting) and the dark-only SpoolBuddy
kiosk are left untouched. Plain text-white is already theme-aware via the
existing index.css .text-white override, so it needed no changes.
Editing a queue item that was created with "Any of model X" left the
printer selection area completely blank — the assignmentMode was
initialised to 'model' from queueItem.target_model, but the three
model-mode props (onAssignmentModeChange, onTargetModelChange,
onTargetLocationChange) were gated behind !isEditing. That flipped
modelAssignmentAvailable to false in PrinterSelector and hid the mode
toggle, the model dropdown, AND the location filter; combined with the
assignmentMode === 'printer' gate on the printer list, the whole
selector rendered empty.
Users hit this whenever they queued something to "Any of model X" and
then wanted to change the target model / location — the only workaround
was delete + re-queue.
Fix: drop the !isEditing gate on all three PrinterSelector props. The
submit path already handles both flavours (target_model+target_location
with printer_id=null vs. printer_id with the target fields nulled), so
un-gating the UI just surfaces the machinery that was already there.
Edit is still only offered on pending items, so the mode-flip can't race
an in-flight dispatch.
PrintModal carried a useEffect that reset scheduleOptions.gcodeInjection
to false whenever mode === 'create' AND effectiveQuantity <= 1. The
comment claimed the checkbox only renders for quantity > 1, but the
actual render gate in ScheduleOptions is just hasGcodeSnippets — no
quantity check. So with snippets configured + quantity = 1 (the OP
scenario): user clicks the checkbox, React updates state to true,
the parent's useEffect immediately sees effectiveQuantity <= 1 and
resets to false, and the checkbox appears un-clickable. Edit-queue-
item mode worked because mode !== 'create' short-circuited the reset.
Drop the effectiveQuantity <= 1 clause from the reset. Keep the
!settings?.gcode_snippets half as the legitimate cleanup for the
"admin removes all snippets while modal is open" case. The scheduler
reads item.gcode_injection per queue item regardless of batch size,
so single prints can inject too.
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.
Reported off-list by a corporate supporter running a multi-operator
farm shift. Backend already rejects double-sends with HTTP 409 (see
background_dispatch._dispatch at lines 283-290), so no double-print
was possible, but PrinterSelector consulted only PrinterStatus.state
({IDLE, FINISH, FAILED}) to decide whether a printer was selectable.
PRINT_START is the only signal that flips the printer out of IDLE, so
during the upload + print-command + firmware-ack window the card
stayed clickable and operators only found out after submitting.
New useDispatchedPrinterIds hook subscribes to the existing
background-dispatch WebSocket event (already consumed for the toast
overlay), collects printer_ids from dispatched_jobs + active_jobs,
and exposes them via useSyncExternalStore so every PrinterSelector
instance shares one snapshot. OR'd into isPrinterBusy; badge label
flips to "Dispatching..." instead of a misleading "Idle".
No backend change — the reservation already exists, the UI just
didn't reflect it. No behaviour change for add-to-queue / edit-queue
modes (disableBusy stays false for those).
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.
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.
Before this change, the Re-print and Schedule modals' per-filament
slot dropdown hid every slot whose extruder didn't match the filament's
slicer-assigned nozzle. On H2D with AMS A+C on the left and B on the
right, an L-assigned filament could only pick A or C and an R-assigned
one could only pick B — locking the user out of cross-extruder picks
even when they'd intentionally loaded the required filament into the
other AMS.
The slicer wasn't the source of the asymmetry: BambuStudio Desktop,
OrcaSlicer Desktop, and the Bambuddy sidecar all produced identical
filament_map values for the same source 3MF. Bambuddy's UI filter was.
Drop the f.extruderId === item.nozzle_id clause in FilamentMapping's
loadedFilaments filter. Single-nozzle and FTS short-circuits stay; the
L/R row badge stays as a hint to the slicer's intent. Printer firmware
decides at start-print whether the cross-extruder ams_mapping is valid.
Round 2 fixed the model-mode FilamentOverride: tray_info_idx →
sub-brand, plus a material-disambiguated colour name from a new
/inventory/colors/by-material endpoint. The printer-mode panel that
renders the same 3MF (FilamentMapping) was reading the same raw
fields — item.type for the required label, getColorName(item.color)
for the swatch tooltip — and was not touched, so picking "Specific
Printer" still showed "Required: PLA - Black" for a slice the
"Any H2D" branch already labelled "Bambu PLA Matte - Charcoal".
Extract the three-query resolution machinery from FilamentOverride
into a shared hook useFilamentLabels (returns positional
{resolvedName, colorLabel} per slot). Both panels call it; both
read the same labels. The hook also owns extractMaterialHint so the
"strip leading brand token" rule has one source of truth.
FilamentMapping required-side now reads {resolvedName} instead of
{item.type}; swatch tooltip reads `Required: {resolvedName} -
{colorLabel}` instead of `Required: {item.type} -
getColorName(item.color)`.
The Print Queue's schedule dialog hid the per-slot "Force color match"
checkbox when a specific printer was picked, even though the scheduler
in print_scheduler.py:535 honours force_color_match regardless of how
the queue item was created. Model-mode ("Any A1") rendered
FilamentOverride which carries the checkbox; printer-mode rendered
FilamentMapping which had no force-match UI at all. Pure UI gap.
Extend FilamentMapping to accept optional forceColorMatch +
onForceColorMatchChange props, mirroring FilamentOverride's signature,
and render the same <Palette>-iconed checkbox under each filament row
when a handler is wired. PrintModal/index.tsx passes the existing
forceColorMatch state through — same object both modes write into so
toggling between modes preserves the user's selection. No new i18n
keys (printModal.forceColorMatch already ships in all 11 locales).
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.
Reporter on a multi-printer farm with 40+-plate runs needed to walk to
the printer with the right physical plate, but the queue and the
scheduling modal didn't surface curr_bed_type the way the archive card
already did.
New utils/threemf_tools.extract_bed_type_from_3mf helper (per-plate) so
both the queue API and the /plates endpoint can return per-plate values.
PrintQueueItemResponse.bed_type populated from archive.bed_type /
library_file.file_metadata['bed_type'] as the default, then overridden
per-plate via the helper when item.plate_id is set. /archives/{id}/plates
(and library equivalent) include bed_type in each plate object.
Per-plate accuracy matters because archive.bed_type is captured at
ingest as only the first plate's value (services/archive.py:235) -
a 40-plate 3MF mixing PEI + Engineering returns PEI at the archive
level for every plate. The helper re-reads the 3MF and returns the
truth.
Frontend: queue card meta row + PlateSelector per-plate row + PrintModal
header all render bed icon + canonical label via the existing
getBedTypeInfo() helper, same as the archive card.
Bambuddy's project_file MQTT payload hardcoded "nozzle_offset_cali": 2 (skip),
giving users on H2D / H2D Pro / H2C / X2D no way to control the same toggle
BambuStudio exposes. Critical for diamond-nozzle setups that must keep the
calibration off.
start_print() now takes a nozzle_offset_cali kwarg; the value is encoded as
1 (run) or 2 (skip) and gated on is_dual_nozzle so single-nozzle machines
always send 2 even if a stale flag arrives. The kwarg threads through
printer_manager, both background_dispatch sites, and print_scheduler so
every dispatch path respects the per-item setting.
print_queue gains a nozzle_offset_cali column (DEFAULT TRUE, is_sqlite()
branch for Postgres BOOLEAN). Settings default key default_nozzle_offset_cali
defaults to TRUE to match BambuStudio. Schemas updated across print_queue,
library FilePrintRequest, archive ReprintRequest, settings.
PrintModal renders the new toggle only when the selected printer is dual-
nozzle (printer-mode: nozzle_count===2; model-mode: DUAL_NOZZLE_MODELS).
SettingsPage default-print-options row + QueuePage bulk-edit tri-state both
hide unless any registered printer is dual-nozzle. Labels reuse the existing
settings.default* keys so the only new i18n strings are
settings.defaultNozzleOffsetCali / Desc and queue.bulkEdit.nozzleOffsetCali
- real translations in all 11 locales.
The FTS routes any AMS slot to either extruder, so AMS info reports
bits 8-11 = 0xE (uninitialized) and ams_extruder_map ends up empty.
The print modal's per-nozzle dropdown filter then hides every loaded
slot, leaving the user with an empty filament dropdown.
Detection: parse print.device.fila_switch from MQTT push_status into a
new FilaSwitchState dataclass on PrinterState; surface it through the
GET /printers/{id}/status response as a nullable FilaSwitchResponse.
Frontend: useFilamentMapping and FilamentMapping skip the per-extruder
filter when fila_switch.installed is true. Slots currently fed into a
track display an [L]/[R] routing badge in the dropdown so the user
can see where the FTS is currently routing them.
Tests: 4 backend unit (TestFilamentTrackSwitchDetection), 2 backend
integration (status route), 2 hook regression, 2 component regression.
The "Print" button on a printer card (and drag-drop-onto-card) used
FileUploadModal to persist the file as a LibraryFile, then dispatched
through POST /library/files/{id}/print. The LibraryFile row + disk file
were left behind after every one-off print, polluting File Manager with
entries the user never asked to save.
FilePrintRequest.cleanup_library_after_dispatch (default False) opts
into post-dispatch cleanup. When set, _run_print_library_file stages
db.delete(lib_file) in the same transaction as archive_print so a
mid-flight FTP / start_print failure rolls both back cleanly, commits
together, then unlinks the library disk file + thumbnail after commit
succeeds. External library files (is_external=True) are never touched.
Only the Printers-page Direct-Print PrintModal sets the flag. Every
other api.printLibraryFile caller (File Manager Print, Project Detail
Print) leaves it unset — their entries are there by user intent.
Also moves formatPrintName out of PrintersPage.tsx into a new
utils/printName.ts module — fa1c46d9 (#881) exported it inline so its
test could import it, tripping react-refresh/only-export-components.
* feat: print files directly from project view (closes#930)
Show printable files from linked library folders directly in the project
detail page, with Print Now and Add to Queue buttons per file. Removes
the detour through the File Manager for common reprint workflows.
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.
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.
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.
Multi-plate 3MF files now support selecting a subset of plates to queue
via checkboxes, instead of the binary "one plate" or "all plates" toggle.
In add-to-queue mode, each plate gets a checkbox for multi-select with a
Select All / Deselect All toggle. Reprint and edit modes remain single-select.
Bambu Lab firmware treats PA-CF, PA12-CF, and PAHT-CF as interchangeable,
but the print scheduler and filament override UI used exact string matching.
A 3MF requiring PA-CF wouldn't match PA12-CF in the AMS, leaving the
override dropdown disabled and preventing scheduler assignment.
Add a filament type equivalence system (both backend and frontend) so
these PA variants are treated as compatible in scheduler assignment,
AMS slot matching, force color match validation, and the override dropdown.
Also fix the "Force color match" checkbox click target being too wide
(entire row instead of just the checkbox/label) by changing flex to
inline-flex on the label element.
* Added "Force Color Match" feature to filament override
* Update settings.py
* Update settings.py
* Label updates for warnings and checkbox case
* Initial plan
* Add noMatchingMaterial i18n translations to all 7 locale files
Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>
* Use {{material}} interpolation in noMatchingMaterial; populate from filament_overrides with getColorName
Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>
* Updates for PR#625 comments
* Added "Force Color Match" feature to filament override
* Update settings.py
* Update settings.py
* Label updates for warnings and checkbox case
* Initial plan
* Add noMatchingMaterial i18n translations to all 7 locale files
Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>
* Use {{material}} interpolation in noMatchingMaterial; populate from filament_overrides with getColorName
Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>
* Updates to PrintersPage and QueuePage
* Update QueuePage.tsx
* Rebase to 0.2.2b3
* Fix compile errors with FilamentOverride
* Updates for PR#625 comments
https://github.com/maziggy/bambuddy/pull/625#pullrequestreview-3908738261
* Fix force color match default, loadedFilamentTypes bug, remove dead translations
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>
Co-authored-by: MartinNYHC <mz@v8w.de>
Multi-plate 3MF files can now be queued in one action via a "Queue All
Plates" toggle in the plate selector (add-to-queue mode only). Each
plate becomes a separate queue entry, individually editable.
Fix: "Clear Plate & Start Next" no longer appears when all pending
queue items are staged (manual_start). The prompt only shows when
there are auto-dispatchable items the scheduler will actually start.
When printing from the file manager, the print modal showed all printers
including busy ones. Selecting a busy printer resulted in a failed send.
The printer selector now fetches live status for each printer and shows
state badges (Idle, Printing, Paused, etc.). In reprint mode, non-available
printers (anything other than IDLE, FINISH, FAILED) are grayed out and
not selectable. "Select all" skips them. Queue/edit modes still allow
selecting busy printers since the job will wait.
All filament mapping dropdowns (single-printer, multi-printer, and
"Print to Any" model-based) showed only the base material type (e.g.
"PLA") without distinguishing subtypes like "PLA Basic" vs "PLA Matte",
making entries with different subtypes but same color look identical.
Backend: Add tray_sub_brands to available-filaments response and include
it in the dedup key so different subtypes of the same color appear as
separate entries.
Frontend: Show tray_sub_brands in FilamentMapping, FilamentOverride, and
PrinterSelector dropdowns, falling back to base type when unset. Add
traySubBrands field to LoadedFilament interface.
The per-printer InlineMappingEditor showed filaments from both nozzles
on H2D printers. The single-printer FilamentMapping dropdown was fixed
in 29e9593 but the multi-printer code path was missed. Added nozzle_id
filtering to both auto-match logic and dropdown options via shared
autoMatchFilament() and filterFilamentsByNozzle() functions in
amsHelpers.ts, with 10 regression tests.
FTP upload: reduce chunk size from 1MB to 64KB for smooth progress bar
updates (~1s intervals instead of 20+ second gaps). Skip voidresp() for
all printer models — H2D delays the 226 response by 30+ seconds after
data transfer, causing a hang at 100%. Add transfer speed and TLS
handshake timing to logs for diagnosing slow connections.
Print/Schedule modal: widen from max-w-lg (512px) to max-w-2xl (672px)
to accommodate long filament profile names like "PLA Support for PETG
PETG Basic @Bambu Lab H2D 0.4 nozzle".
* Add printer card print flow with drag-drop and reusable LibraryUploadModal
Extract inline upload modal from FileManagerPage into a reusable
LibraryUploadModal component. Add a Print button and drag-drop zone
to printer cards that upload files to the library and open PrintModal
with the printer pre-selected. Only .gcode/.gcode.3mf files are
accepted for printing. PrintModal hides printer selector when a
printer is provided via initialSelectedPrinterIds prop.
* Add printer compatibility check for drag-drop and upload-to-print flows
Validates sliced_for_model from file metadata against the target printer model
before opening the print modal, preventing users from printing with incompatible files.
* Improve FileUploadModal: auto-close on success, inline errors, file validation, and i18n
- Close modal automatically after successful upload instead of showing summary
- Show printer compatibility errors inline in the modal instead of closing
- Add validateFile and accept props to restrict file types for print flow
- Add onFileUploaded error return to prevent modal close on incompatible files
- Internationalize all hardcoded error/warning strings across 6 locales
* Clean up incompatible files from library and fix print button i18n
- Delete uploaded file from library when printer compatibility check fails
in both drag-drop and modal upload flows
- Fix print button tooltip to use existing common.print i18n key
- Fix accept attribute to use .gcode,.3mf for proper browser support
* Add printer information modal accessible from card 3-dot menu
Replace inline IP/serial display at bottom of printer card with a
dedicated "Printer Information" modal opened via the card menu. The
modal shows model, status, state, IP address (copyable), serial number
(copyable), WiFi signal, firmware, developer mode, nozzle count, SD
card, auto-archive, total print hours, location, and added date, along
with the printer image. Includes full i18n support across all 6 locales
and accessibility attributes (role="dialog", aria-modal).
* Extract getPrinterImage and getWifiStrength into shared printer utils
---------
Co-authored-by: MartinNYHC <mz@v8w.de>
When scheduling a print to "Any {model}", a redundant "Target Model"
dropdown appeared even though the G-code is already sliced for a
specific printer model. Changing it would lead to print failures.
Hide the dropdown when slicedForModel is known — the tab label already
communicates the target. The dropdown still appears as a fallback for
legacy files without model metadata.
All backend timestamps used datetime.now() (server local time) or the
deprecated datetime.utcnow(). The frontend's parseUTCDate() assumes
timestamps without timezone indicators are UTC and appends 'Z', so
stored timestamps were off by the timezone offset when the container's
timezone wasn't UTC.
Backend: replaced datetime.now() and datetime.utcnow() with
datetime.now(timezone.utc) across 16 files (~80 call sites) for all
database fields and DB comparisons. Cosmetic timestamps (filenames,
user-facing local time formatting) intentionally left as local time.
Frontend: replaced 13 new Date(backendTimestamp) calls with
parseUTCDate() across 8 files to correctly interpret UTC timestamps.
The "Power off printer when done" checkbox in the print modal and the
auto power off tri-state toggle in the bulk edit modal were accessible
to all users regardless of permissions. Users without printers:control
can now no longer enable auto power off — controls are disabled and
visually dimmed.