Преглед на файлове

fix(presets): match Bambu cloud @BBL A1M as A1 Mini (#1649)

  Reporter on an A1 Mini saw the AMS slot Configure dropdown render no
  Bambu / Generic filament profiles, and saw the Profiles tab strip
  A1 Mini results when filtering by that model. Bambu rolled out a
  profile rename mid-2026: the @BBL <code> suffix on 106 cloud profiles
  shifted from the long display form to a terse model code -- e.g.
  "Bambu PLA Basic @BBL A1 Mini ..." is now
  "Bambu PLA Basic @BBL A1M ...". User-authored profiles still use the
  long form. Bambuddy's filters did a verbatim uppercase compare
  ("A1M" vs "A1 MINI"), so every renamed cloud profile silently
  disappeared from the picker.

  Centralize the alias check in slicerPrinterMatch.ts. New
  PRINTER_MODEL_SUFFIX_ALIASES table maps "A1 Mini" <-> "A1M"
  bidirectionally; exported matchesPrinterModelSuffix() does the
  case-insensitive compare with the alias fallback. Two consumer
  sites swap to the helper:

    * ConfigureAmsSlotModal.tsx (Orca cloud and Bambu cloud filter
      branches) -- the AMS slot picker, hit directly and reached from
      SpoolBuddy's AMS page via mapModelCode(printer?.model)
    * slicerPrinterMatch.ts:classifyByBambuName -- the SliceModal
      Process / Filament compatibility check

  Backend printer_models.py also gets a "Bambu Lab A1M" -> "A1 Mini"
  entry so server-side 3MF model normalization stays consistent if a
  3MF ever embeds the short form.

  Kept the alias table narrow on purpose. Wide-net aliasing (e.g.
  "X1" <-> "X1C") would silently collapse physically distinct
  printers. When Bambu introduces the next rename, it is one new row
  in the table -- /api/v1/cloud/settings is the place to grep, called
  out in the source comment.
maziggy преди 3 месеца
родител
ревизия
b8916ac3de

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
CHANGELOG.md


+ 3 - 0
backend/app/utils/printer_models.py

@@ -15,6 +15,9 @@ PRINTER_MODEL_MAP = {
     "Bambu Lab A1": "A1",
     "Bambu Lab A1 Mini": "A1 Mini",
     "Bambu Lab A1 mini": "A1 Mini",
+    # Bambu cloud rolled out a terse model-code rename mid-2026 (#1649);
+    # 3MFs prepared with newer cloud presets may carry this short form.
+    "Bambu Lab A1M": "A1 Mini",
     "Bambu Lab H2D": "H2D",
     "Bambu Lab H2D Pro": "H2D Pro",
     "Bambu Lab H2C": "H2C",

+ 31 - 0
frontend/src/__tests__/components/ConfigureAmsSlotModal.test.tsx

@@ -323,6 +323,37 @@ describe('ConfigureAmsSlotModal', () => {
     expect(screen.queryByText(/Bambu PLA Basic @BBL X1C/)).not.toBeInTheDocument();
   });
 
+  it('treats Bambu cloud rename @BBL A1M as a match for A1 Mini (#1649)', async () => {
+    // Bambu cloud shifted A1 Mini filament profiles from
+    // "Bambu PLA Basic @BBL A1 Mini ..." to the terse "@BBL A1M" mid-2026.
+    // Without an alias-aware compare, the model filter strips every cloud
+    // profile from the picker when the user selects an A1 Mini printer.
+    (api.getCloudSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
+      filament: [
+        { setting_id: 'GFA00_A1M', name: 'Bambu PLA Basic @BBL A1M', filament_id: 'GFA00' },
+        { setting_id: 'GFA00_A1', name: 'Bambu PLA Basic @BBL A1', filament_id: 'GFA00' },
+      ],
+    });
+    render(<ConfigureAmsSlotModal {...defaultProps} printerModel="A1 Mini" />);
+    await waitFor(() => {
+      expect(screen.getByText('Bambu PLA Basic @BBL A1M')).toBeInTheDocument();
+    });
+    // The A1 (non-mini) preset must still be filtered out — the alias
+    // table must not collapse two physically distinct printers.
+    expect(screen.queryByText('Bambu PLA Basic @BBL A1')).not.toBeInTheDocument();
+  });
+
+  it('still filters cross-model cloud profiles when the printer is A1 Mini', async () => {
+    // Sanity check that the alias addition didn't accidentally widen the
+    // matcher: an X1C cloud preset stays hidden when the picker is for an
+    // A1 Mini printer.
+    render(<ConfigureAmsSlotModal {...defaultProps} printerModel="A1 Mini" />);
+    await waitFor(() => {
+      expect(screen.getByText(/Configure AMS/)).toBeInTheDocument();
+    });
+    expect(screen.queryByText('Bambu PLA Basic @BBL X1C')).not.toBeInTheDocument();
+  });
+
   it('shows current preset even when it does not match model filter', async () => {
     // Render with printerModel="H2D" but savedPresetId pointing to the X1C preset
     const slotInfo = {

+ 61 - 0
frontend/src/__tests__/utils/slicerPrinterMatch.test.ts

@@ -1,6 +1,7 @@
 import { describe, it, expect } from 'vitest';
 import {
   buildCompatibilityIndex,
+  matchesPrinterModelSuffix,
   presetCompatibility,
   EMPTY_COMPATIBILITY_INDEX,
   type CompatibilityBundle,
@@ -425,3 +426,63 @@ describe('presetCompatibility — nozzle filtering on @BBL name fallback', () =>
     ).toBe('mismatch');
   });
 });
+
+describe('matchesPrinterModelSuffix (#1649)', () => {
+  it('matches the canonical short code against itself', () => {
+    expect(matchesPrinterModelSuffix('X1C', 'X1C')).toBe(true);
+  });
+
+  it('is case-insensitive on both sides', () => {
+    expect(matchesPrinterModelSuffix('x1c', 'X1C')).toBe(true);
+    expect(matchesPrinterModelSuffix('a1 mini', 'A1 Mini')).toBe(true);
+  });
+
+  it('matches Bambu cloud rename A1M against the long form A1 Mini', () => {
+    expect(matchesPrinterModelSuffix('A1M', 'A1 Mini')).toBe(true);
+  });
+
+  it('matches the long form A1 Mini against the short Bambu cloud code A1M', () => {
+    expect(matchesPrinterModelSuffix('A1 Mini', 'A1M')).toBe(true);
+  });
+
+  it('does NOT match A1M against A1 (different printer, must not collapse)', () => {
+    expect(matchesPrinterModelSuffix('A1M', 'A1')).toBe(false);
+  });
+
+  it('does NOT match A1 against A1 Mini', () => {
+    expect(matchesPrinterModelSuffix('A1', 'A1 Mini')).toBe(false);
+  });
+
+  it('does NOT match unrelated models', () => {
+    expect(matchesPrinterModelSuffix('X1C', 'P1S')).toBe(false);
+  });
+});
+
+describe('presetCompatibility with Bambu cloud A1M rename (#1649)', () => {
+  const A1_MINI = 'Bambu Lab A1 mini 0.4 nozzle';
+  const A1 = 'Bambu Lab A1 0.4 nozzle';
+  const idx = buildCompatibilityIndex([], PRINTER_MODELS);
+
+  it('matches a cloud preset using the new @BBL A1M suffix against an A1 Mini printer', () => {
+    // The slicer-mirrored case: technopaw's report — A1 Mini cloud presets
+    // newly ship as "Bambu PLA Basic @BBL A1M ..." and used to be filtered
+    // out by the model check that compared "A1M" vs "A1 mini" verbatim.
+    expect(
+      presetCompatibility({ name: 'Bambu PLA Basic @BBL A1M' }, 'filament', A1_MINI, idx),
+    ).toBe('match');
+  });
+
+  it('matches a 0.4-nozzle process with @BBL A1M against an A1 Mini printer', () => {
+    expect(
+      presetCompatibility({ name: '0.20mm Standard @BBL A1M' }, 'process', A1_MINI, idx),
+    ).toBe('match');
+  });
+
+  it('does NOT match @BBL A1M against an A1 (non-mini) printer', () => {
+    // The alias must not collapse two physically different printers — A1
+    // and A1 Mini ship distinct profile sets.
+    expect(
+      presetCompatibility({ name: '0.20mm Standard @BBL A1M' }, 'process', A1, idx),
+    ).toBe('mismatch');
+  });
+});

+ 6 - 3
frontend/src/components/ConfigureAmsSlotModal.tsx

@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
 import { X, Loader2, Settings2, ChevronDown, CheckCircle2, RotateCcw } from 'lucide-react';
 import { api } from '../api/client';
 import type { KProfile } from '../api/client';
+import { matchesPrinterModelSuffix } from '../utils/slicerPrinterMatch';
 import { Button } from './Button';
 
 interface SlotInfo {
@@ -583,7 +584,7 @@ export function ConfigureAmsSlotModal({
         if (query && !op.name.toLowerCase().includes(query)) continue;
         if (printerModel) {
           const presetModel = extractPresetModel(op.name);
-          if (presetModel && presetModel.toUpperCase() !== printerModel.toUpperCase()) continue;
+          if (presetModel && !matchesPrinterModelSuffix(presetModel, printerModel)) continue;
         }
         // All Orca Cloud profiles are user-authored, so isUser is always true.
         items.push({ id: orcaId, name: op.name, source: 'orca_cloud', isUser: true });
@@ -601,10 +602,12 @@ export function ConfigureAmsSlotModal({
           || (trayIdx && (cp.setting_id === trayIdx || convertToTrayInfoIdx(cp.setting_id) === trayIdx));
         // Search filter applies to ALL presets (including saved) — no bypass
         if (query && !cp.name.toLowerCase().includes(query)) continue;
-        // Filter by printer model if set (skip for current preset)
+        // Filter by printer model if set (skip for current preset). Uses the
+        // alias-aware match so Bambu's "A1 Mini" → "A1M" cloud rename (#1649)
+        // doesn't hide A1 Mini cloud profiles.
         if (!isCurrentPreset && printerModel) {
           const presetModel = extractPresetModel(cp.name);
-          if (presetModel && presetModel.toUpperCase() !== printerModel.toUpperCase()) continue;
+          if (presetModel && !matchesPrinterModelSuffix(presetModel, printerModel)) continue;
         }
         items.push({ id: cp.setting_id, name: cp.name, source: 'cloud', isUser: isUserPreset(cp.setting_id) });
       }

+ 37 - 1
frontend/src/utils/slicerPrinterMatch.ts

@@ -62,6 +62,35 @@ function normalizePresetName(name: string): string {
   return name.replace(/^#\s*/, '').trim();
 }
 
+// Bambu cloud started shipping terse model codes in `@BBL <code>` suffixes
+// mid-2026 — the most visible one is "A1 Mini" → "A1M" (#1649, reported by
+// @technopaw). User-authored profiles still use the long display name, so
+// both shapes have to match the same printer. The table is uppercase-normalised
+// for case-insensitive lookups; add a row when a future rename is spotted via
+// `/api/v1/cloud/settings`. Keep narrow on purpose — wide-net aliasing
+// (e.g. "X1" ⇄ "X1C") would silently group truly distinct printers.
+const PRINTER_MODEL_SUFFIX_ALIASES: Record<string, readonly string[]> = {
+  'A1 MINI': ['A1M'],
+};
+
+/**
+ * True when ``presetSuffix`` (the token extracted from a "@BBL <code>" or
+ * preset-name suffix) refers to the same printer as ``printerModel``
+ * (the display name selected in the picker). Case-insensitive; consults
+ * the alias table for short codes Bambu introduced after the long forms
+ * shipped (#1649).
+ */
+export function matchesPrinterModelSuffix(presetSuffix: string, printerModel: string): boolean {
+  const p = presetSuffix.toUpperCase();
+  const m = printerModel.toUpperCase();
+  if (p === m) return true;
+  const aliasesOfM = PRINTER_MODEL_SUFFIX_ALIASES[m];
+  if (aliasesOfM && aliasesOfM.includes(p)) return true;
+  const aliasesOfP = PRINTER_MODEL_SUFFIX_ALIASES[p];
+  if (aliasesOfP && aliasesOfP.includes(m)) return true;
+  return false;
+}
+
 /**
  * Invert the backend's PRINTER_MODEL_MAP into the shape the @BBL fallback
  * needs: short code → printer-preset fragment (the part of "Bambu Lab X1
@@ -187,7 +216,14 @@ function classifyByBambuName(
   const inferredModel = bambuModelByShortCode[parsed.token] ?? parsed.token;
   const selectedParts = extractPrinterPresetModel(selectedPrinterName);
   if (!selectedParts) return 'unknown';
-  if (normalizeModelFragment(selectedParts.model) !== normalizeModelFragment(inferredModel)) {
+  // The raw inferred model and the printer-preset fragment may differ only by
+  // the Bambu short-code rename (e.g. preset token "A1M" vs printer "A1 Mini").
+  // ``matchesPrinterModelSuffix`` consults the alias table before declaring a
+  // mismatch — see #1649.
+  if (
+    normalizeModelFragment(selectedParts.model) !== normalizeModelFragment(inferredModel)
+    && !matchesPrinterModelSuffix(parsed.token, selectedParts.model)
+  ) {
     return 'mismatch';
   }
   // Nozzle compare — only when we have a usable size from the printer

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
static/assets/index-BCHcur4g.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-DMeff9V8.js"></script>
+    <script type="module" crossorigin src="/assets/index-BCHcur4g.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-DgecYhis.css">
   </head>
   <body>

Някои файлове не бяха показани, защото твърде много файлове са промени