fix(inventory): drop profile-only mismatch popup and clarify reconfigure intent (#1552)

Reporter assigned a spool to a slot whose stored slicer profile differed
  from the new spool's, got the Cancel / Assign Anyway popup, and was under
  the impression that confirming the popup just linked the spool in
  Bambuddy's DB without pushing the new profile to the AMS — i.e. that he
  then had to manually open Configure AMS Slot to fix it. The auto-push has
  actually been in place since the assign route existed:
  backend/app/api/routes/inventory.py::assign_spool calls
  apply_spool_to_slot_via_mqtt after upserting the SpoolAssignment row,
  which publishes ams_filament_setting + extrusion_cali_sel over MQTT, and
  backend/app/api/routes/spoolman_inventory.py::assign_spoolman_slot does
  the same for the Spoolman backend. The only short-circuit is when
  firmware reports the slot explicitly empty (tray_state in {9, 10}), in
  which case main.py::on_ams_change deferred-replays the configure once a
  spool appears. So the popup was friction without revealing what it did.

  Two changes:

  - frontend/src/components/AssignSpoolModal.tsx and
    spoolbuddy/AssignToAmsModal.tsx: profile-only mismatch no longer
    fires the popup. The condition becomes
    `if (materialMatchResult !== 'exact')` instead of
    `materialMatchResult !== 'exact' || !profileMatches`. The 'profile'
    member is dropped from the mismatchType union and its standalone
    branch in each popup render body is removed as dead code. Material
    mismatch still warns — Bambu firmware can refuse the print when the
    type is wrong.

  - Every firing warning (material, partial, material+profile,
    partial+profile) now appends one line via a new
    inventory.assignReconfigureNote i18n key:
    "The AMS slot will be reconfigured to use the spool's profile."
    Real translations across all 9 locales per
    feedback_translate_dont_fallback; parity script clean at 4999
    leaves per locale.
This commit is contained in:
maziggy
2026-05-29 10:38:11 +02:00
parent 632334953c
commit 9347921378
14 changed files with 65 additions and 38 deletions
+21 -12
View File
@@ -40,8 +40,16 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr
const [searchFilter, setSearchFilter] = useState('');
const [pendingAssignId, setPendingAssignId] = useState<number | null>(null);
const [showMismatchConfirm, setShowMismatchConfirm] = useState(false);
// Profile-only mismatch no longer triggers the popup — the backend's
// `apply_spool_to_slot_via_mqtt` pushes the spool's slicer profile to the
// AMS slot on every assign anyway, so warning the user about a profile
// delta then "fixing" it during the same action was friction without
// benefit (#1552). Material mismatch still warns because the firmware can
// refuse a print when type doesn't match; combined material+profile
// mismatches keep the profile detail in the same popup as the material
// warning.
const [mismatchDetails, setMismatchDetails] = useState<{
type: 'material' | 'partial' | 'profile' | 'material_profile' | 'partial_profile';
type: 'material' | 'partial' | 'material_profile' | 'partial_profile';
spoolMaterial: string;
trayMaterial: string;
spoolProfile?: string;
@@ -291,17 +299,18 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr
const trayProfile = trayInfo.profile || trayInfo.type;
const profileMatches = checkProfileMatch(spoolProfile, trayProfile);
// Always evaluate both checks; if both fail, show a combined warning.
if (materialMatchResult !== 'exact' || !profileMatches) {
let mismatchType: 'material' | 'partial' | 'profile' | 'material_profile' | 'partial_profile' = 'profile';
// Only material-bearing mismatches warn — profile-only deltas are
// silently resolved by the backend's AMS reconfigure on every assign
// (#1552).
if (materialMatchResult !== 'exact') {
let mismatchType: 'material' | 'partial' | 'material_profile' | 'partial_profile';
if (materialMatchResult === 'none' && !profileMatches) {
mismatchType = 'material_profile';
} else if (materialMatchResult === 'partial' && !profileMatches) {
mismatchType = 'partial_profile';
} else if (materialMatchResult === 'none') {
mismatchType = 'material';
} else if (materialMatchResult === 'partial') {
} else {
mismatchType = 'partial';
}
@@ -584,14 +593,14 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr
trayProfile: mismatchDetails.trayProfile || t('common.unknown'),
location: trayInfo.location,
})}`;
} else if (mismatchDetails.type === 'profile') {
message = t('inventory.assignProfileMismatchMessage', {
spoolProfile: mismatchDetails.spoolProfile || t('common.unknown'),
trayProfile: mismatchDetails.trayProfile || t('common.unknown'),
location: trayInfo.location,
});
}
// Always tell the user the AMS slot is going to be reconfigured —
// the existing wording made "Assign Anyway" sound like the popup was
// a no-op confirmation, when the backend in fact pushes the spool's
// profile to the slot on every assign (#1552).
message = `${message}\n\n${t('inventory.assignReconfigureNote')}`;
return (
<ConfirmModal
title={t('inventory.assignMismatchTitle')}
@@ -66,8 +66,13 @@ export function AssignToAmsModal({ isOpen, onClose, spool, printerId, spoolmanMo
const [statusMessage, setStatusMessage] = useState<string | null>(null);
const [statusType, setStatusType] = useState<'info' | 'success' | 'error' | null>(null);
const [showMismatchConfirm, setShowMismatchConfirm] = useState(false);
// Profile-only mismatches no longer trigger the popup — the backend
// pushes the spool's slicer profile to the AMS slot on every assign
// anyway, so the warning was friction without benefit (#1552). Material
// mismatch still warns because firmware can refuse a print when type
// doesn't match.
const [mismatchDetails, setMismatchDetails] = useState<{
type: 'material' | 'partial' | 'profile' | 'material_profile' | 'partial_profile';
type: 'material' | 'partial' | 'material_profile' | 'partial_profile';
spoolMaterial: string;
trayMaterial: string;
spoolProfile?: string;
@@ -266,15 +271,15 @@ export function AssignToAmsModal({ isOpen, onClose, spool, printerId, spoolmanMo
const trayProfile = tray.tray_type || '';
const profileMatches = checkProfileMatch(spoolProfile, trayProfile);
if (materialMatchResult !== 'exact' || !profileMatches) {
let mismatchType: 'material' | 'partial' | 'profile' | 'material_profile' | 'partial_profile' = 'profile';
if (materialMatchResult !== 'exact') {
let mismatchType: 'material' | 'partial' | 'material_profile' | 'partial_profile';
if (materialMatchResult === 'none' && !profileMatches) {
mismatchType = 'material_profile';
} else if (materialMatchResult === 'partial' && !profileMatches) {
mismatchType = 'partial_profile';
} else if (materialMatchResult === 'none') {
mismatchType = 'material';
} else if (materialMatchResult === 'partial') {
} else {
mismatchType = 'partial';
}
@@ -542,14 +547,13 @@ export function AssignToAmsModal({ isOpen, onClose, spool, printerId, spoolmanMo
trayProfile: mismatchDetails.trayProfile || t('common.unknown'),
location: mismatchDetails.location,
})}`;
} else if (mismatchDetails.type === 'profile') {
message = t('inventory.assignProfileMismatchMessage', {
spoolProfile: mismatchDetails.spoolProfile || t('common.unknown'),
trayProfile: mismatchDetails.trayProfile || t('common.unknown'),
location: mismatchDetails.location,
});
}
// Always tell the user the AMS slot will be reconfigured — without
// this, "Assign Anyway" reads as a no-op confirmation when the
// backend in fact pushes the spool profile on every assign (#1552).
message = `${message}\n\n${t('inventory.assignReconfigureNote')}`;
return (
<ConfirmModal
title={t('inventory.assignMismatchTitle')}
+1
View File
@@ -3831,6 +3831,7 @@ export default {
assignMismatchConfirm: 'Trotzdem zuweisen',
assignPartialMismatchMessage: 'Das Spulenmaterial "{{spoolMaterial}}" ist ähnlich, stimmt aber nicht genau mit "{{trayMaterial}}" in {{location}} überein. Möchten Sie fortfahren?',
assignProfileMismatchMessage: 'Das Spulenprofil "{{spoolProfile}}" stimmt nicht mit dem Fachprofil "{{trayProfile}}" in {{location}} überein. Möchten Sie fortfahren?',
assignReconfigureNote: 'Der AMS-Slot wird mit dem Profil der Spule neu konfiguriert.',
// Spoolman filament catalog picker
spoolmanFilamentCatalog: 'Spoolman-Filamentkatalog',
pickFromSpoolmanCatalog: 'Aus Spoolman-Katalog wählen…',
+1
View File
@@ -3843,6 +3843,7 @@ export default {
assignMismatchConfirm: 'Assign Anyway',
assignPartialMismatchMessage: 'The spool material "{{spoolMaterial}}" is similar to but not exactly matching "{{trayMaterial}}" in {{location}}. Do you want to proceed?',
assignProfileMismatchMessage: 'The spool profile "{{spoolProfile}}" does not match the tray profile "{{trayProfile}}" in {{location}}. Do you want to proceed?',
assignReconfigureNote: 'The AMS slot will be reconfigured to use the spool\'s profile.',
// Spoolman filament catalog picker
spoolmanFilamentCatalog: 'Spoolman Filament Catalog',
pickFromSpoolmanCatalog: 'Pick from Spoolman catalog…',
+1
View File
@@ -3839,6 +3839,7 @@ export default {
assignMismatchConfirm: 'Asignar de todos modos',
assignPartialMismatchMessage: 'El material de la bobina "{{spoolMaterial}}" es similar pero no coincide exactamente con "{{trayMaterial}}" en {{location}}. ¿Desea continuar?',
assignProfileMismatchMessage: 'El perfil de la bobina "{{spoolProfile}}" no coincide con el perfil de la bandeja "{{trayProfile}}" en {{location}}. ¿Desea continuar?',
assignReconfigureNote: 'La ranura del AMS se reconfigurará para usar el perfil de la bobina.',
// Spoolman filament catalog picker
spoolmanFilamentCatalog: 'Catálogo de filamentos de Spoolman',
pickFromSpoolmanCatalog: 'Elegir del catálogo de Spoolman…',
+1
View File
@@ -3820,6 +3820,7 @@ export default {
assignMismatchConfirm: 'Assigner quand même',
assignPartialMismatchMessage: 'Le matériau de la bobine "{{spoolMaterial}}" est similaire, mais ne correspond pas exactement à "{{trayMaterial}}" dans {{location}}. Voulez-vous continuer ?',
assignProfileMismatchMessage: 'Le profil de la bobine "{{spoolProfile}}" ne correspond pas au profil du plateau "{{trayProfile}}" dans {{location}}. Voulez-vous continuer ?',
assignReconfigureNote: 'Le slot AMS sera reconfiguré pour utiliser le profil de la bobine.',
// Spoolman filament catalog picker
spoolmanFilamentCatalog: 'Catalogue de filaments Spoolman',
pickFromSpoolmanCatalog: 'Choisir dans le catalogue Spoolman…',
+1
View File
@@ -3819,6 +3819,7 @@ export default {
assignMismatchConfirm: 'Assegna comunque',
assignPartialMismatchMessage: 'Il materiale della bobina "{{spoolMaterial}}" è simile ma non corrisponde esattamente a "{{trayMaterial}}" in {{location}}. Vuoi procedere?',
assignProfileMismatchMessage: 'Il profilo della bobina "{{spoolProfile}}" non corrisponde al profilo del vassoio "{{trayProfile}}" in {{location}}. Vuoi procedere?',
assignReconfigureNote: 'Lo slot AMS verrà riconfigurato per utilizzare il profilo della bobina.',
// Spoolman filament catalog picker
spoolmanFilamentCatalog: 'Catalogo filamenti Spoolman',
pickFromSpoolmanCatalog: 'Scegli dal catalogo Spoolman…',
+1
View File
@@ -3831,6 +3831,7 @@ export default {
assignMismatchConfirm: '強制的に割り当て',
assignPartialMismatchMessage: 'スプールの材料「{{spoolMaterial}}」は「{{trayMaterial}}」に似ていますが、{{location}} と完全には一致しません。続行しますか?',
assignProfileMismatchMessage: 'スプールのプロファイル「{{spoolProfile}}」は {{location}} のトレイプロファイル「{{trayProfile}}」と一致しません。続行しますか?',
assignReconfigureNote: 'AMSスロットはスプールのプロファイルで再構成されます。',
// Spoolman filament catalog picker
spoolmanFilamentCatalog: 'Spoolmanフィラメントカタログ',
pickFromSpoolmanCatalog: 'Spoolmanカタログから選択…',
+1
View File
@@ -3819,6 +3819,7 @@ export default {
assignMismatchConfirm: 'Atribuir mesmo assim',
assignPartialMismatchMessage: 'O material do carretel "{{spoolMaterial}}" é semelhante, mas não corresponde exatamente a "{{trayMaterial}}" em {{location}}. Deseja prosseguir?',
assignProfileMismatchMessage: 'O perfil do carretel "{{spoolProfile}}" não corresponde ao perfil da bandeja "{{trayProfile}}" em {{location}}. Deseja prosseguir?',
assignReconfigureNote: 'O slot do AMS será reconfigurado para usar o perfil do carretel.',
// Spoolman filament catalog picker
spoolmanFilamentCatalog: 'Catálogo de filamentos Spoolman',
pickFromSpoolmanCatalog: 'Escolher do catálogo Spoolman…',
+1
View File
@@ -3651,6 +3651,7 @@ export default {
assignMismatchConfirm: '仍然分配',
assignPartialMismatchMessage: '线轴材料 "{{spoolMaterial}}" 与 {{location}} 的 "{{trayMaterial}}" 相近但不完全一致。是否继续?',
assignProfileMismatchMessage: '线轴配置 "{{spoolProfile}}" 与 {{location}} 的料槽配置 "{{trayProfile}}" 不一致。是否继续?',
assignReconfigureNote: 'AMS 槽位将重新配置为使用线轴的配置。',
selectSpool: '选择要分配到此槽位的耗材',
assigned: '已分配',
assigning: '分配中...',
+1
View File
@@ -3651,6 +3651,7 @@ export default {
assignMismatchConfirm: '仍然分配',
assignPartialMismatchMessage: '料盤材料 "{{spoolMaterial}}" 與 {{location}} 的 "{{trayMaterial}}" 相近但不完全一致。是否繼續?',
assignProfileMismatchMessage: '料盤設定 "{{spoolProfile}}" 與 {{location}} 的料槽設定 "{{trayProfile}}" 不一致。是否繼續?',
assignReconfigureNote: 'AMS 槽位將重新設定為使用料盤的設定。',
selectSpool: '選擇要分配到此槽位的耗材',
assigned: '已分配',
assigning: '分配中...',