فهرست منبع

VP: tell the two H2C models apart by serial

- The H2C is an O1C (older units, serials 094...) or an O1C2 (from
  2026, 31B8B...). Bambu Studio only syncs printer info with a VP that
  reports the printer's own one ("Printer not connected" otherwise),
  but the model list showed both as "H2C"
- The list names H2C entries with their serial family and says to
  pick the one matching the printer's serial (16 locales)
- A proxy VP takes the code from its target's serial
- A virtual O1C2 gets a 31B8B serial like a real one, not the H2D's
  094; an existing one has to be added in the slicer again once
maziggy 2 روز پیش
والد
کامیت
08bf5c716e
58فایلهای تغییر یافته به همراه209 افزوده شده و 22 حذف شده
  1. 1 0
      CHANGELOG.md
  2. 21 14
      backend/app/api/routes/virtual_printers.py
  3. 30 2
      backend/app/services/virtual_printer/manager.py
  4. 25 0
      backend/tests/integration/test_virtual_printer_api.py
  5. 35 0
      backend/tests/unit/services/test_virtual_printer.py
  6. 38 0
      frontend/src/__tests__/components/VirtualPrinterCard.test.tsx
  7. 2 0
      frontend/src/api/client.ts
  8. 20 2
      frontend/src/components/VirtualPrinterCard.tsx
  9. 2 1
      frontend/src/components/VirtualPrinterList.tsx
  10. 2 0
      frontend/src/i18n/locales/de.ts
  11. 2 0
      frontend/src/i18n/locales/en.ts
  12. 2 0
      frontend/src/i18n/locales/es.ts
  13. 2 0
      frontend/src/i18n/locales/fr.ts
  14. 2 0
      frontend/src/i18n/locales/it.ts
  15. 2 0
      frontend/src/i18n/locales/ja.ts
  16. 2 0
      frontend/src/i18n/locales/ko.ts
  17. 2 0
      frontend/src/i18n/locales/nl.ts
  18. 2 0
      frontend/src/i18n/locales/pl.ts
  19. 2 0
      frontend/src/i18n/locales/pt-BR.ts
  20. 2 0
      frontend/src/i18n/locales/ru.ts
  21. 2 0
      frontend/src/i18n/locales/sv.ts
  22. 2 0
      frontend/src/i18n/locales/tr.ts
  23. 2 0
      frontend/src/i18n/locales/uk.ts
  24. 2 0
      frontend/src/i18n/locales/zh-CN.ts
  25. 2 0
      frontend/src/i18n/locales/zh-TW.ts
  26. 0 0
      static/assets/ArchivesPage-BWa3tFir.js
  27. 0 0
      static/assets/CamWallPage-BWNCDLxo.js
  28. 0 0
      static/assets/CameraPage-BJMlPAhW.js
  29. 0 0
      static/assets/FileManagerPage-Dn1ku26I.js
  30. 0 0
      static/assets/InventoryPage-vY9rTMhX.js
  31. 0 0
      static/assets/Layout-Djs1064l.js
  32. 0 0
      static/assets/ModelSourcesPage-DtSYK84a.js
  33. 0 0
      static/assets/PrintersPage-CyRFlKDW.js
  34. 0 0
      static/assets/SettingsPage-CTCm32W7.js
  35. 0 0
      static/assets/SettingsPage-dIBYvfL1.js
  36. 0 0
      static/assets/SliceModal-CAPTUZZX.js
  37. 0 0
      static/assets/SpoolBuddyInventoryPage-LXkx-ydO.js
  38. 1 1
      static/assets/SpoolBuddySettingsPage-D2DF2JEm.js
  39. 0 0
      static/assets/de-C8CbhPuk.js
  40. 0 0
      static/assets/es-3RllJWj1.js
  41. 0 0
      static/assets/fr-BWjSX08r.js
  42. 0 0
      static/assets/index-13kgnbdY.js
  43. 0 0
      static/assets/it-DOIRYKop.js
  44. 0 0
      static/assets/ja-BdNtrLOk.js
  45. 0 0
      static/assets/ko-BwjCNGwE.js
  46. 0 0
      static/assets/locationSensorDefaults-CydzHXtM.js
  47. 0 0
      static/assets/nl-DAmtG6hZ.js
  48. 0 0
      static/assets/pl-D0sniCY6.js
  49. 0 0
      static/assets/pt-BR-CknMFuJP.js
  50. 0 0
      static/assets/ru-CZVF-zZ_.js
  51. 0 0
      static/assets/spoolCost-CgmynAMw.js
  52. 0 0
      static/assets/sv-BJoeBN4W.js
  53. 0 0
      static/assets/tr-C7UE9ljS.js
  54. 0 0
      static/assets/uk-DtapMovC.js
  55. 0 0
      static/assets/usePageFileDrop-Bb9Ou8TY.js
  56. 0 0
      static/assets/zh-CN-D8o4oMpS.js
  57. 0 0
      static/assets/zh-TW-c9kTtV3S.js
  58. 2 2
      static/index.html

+ 1 - 0
CHANGELOG.md

@@ -19,6 +19,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **"Any model" jobs go to the printer with the fewest print hours (#3480, sponsor vote Q4 2026)** — When several printers of a model can take a job, the queue used to pick the first one in its list, so on a farm the same printer got most of the work. It now picks the one with the fewest print hours (the Maintenance counter, including baseline hours entered there), so jobs and wear spread across the farm. Preferred colours still come first, and the queue powers on the least used printer too when it has to switch one on with an Auto Power On plug.
 
 ### Fixed
+- **A virtual H2C reports the right model to the slicer** — The H2C comes as two models: older units (O1C, serials starting 094, like the H2D) and those from 2026 (O1C2, serials starting 31B8B). Bambu Studio only syncs printer information with a virtual printer that reports the printer's own one, and says "Printer not connected" otherwise, but the virtual printer list showed both as "H2C". The list now names each H2C entry with its serial family, and says to pick the one whose serial starts like the printer's. A proxy virtual printer takes the right one from its target printer's serial, and a virtual O1C2 now has a serial starting 31B8B like a real one, instead of the H2D's 094, so the slicer also shows it the H2C's HMS texts. An existing virtual O1C2 gets a new serial with this: add it in the slicer again once.
 - **A slicer whose clock is a little behind can connect to a virtual printer again** — A virtual printer makes a new certificate every time Bambuddy starts, and it was valid only from that second on. Bambu Studio on a computer whose clock ran even a minute behind saw a certificate that was not valid yet, refused it, and reported only "-1" when connecting with the access code, which looks exactly like a missing certificate. New certificates now start a day back (a printer certificate never before its CA), so a restart no longer locks out such a computer. Nothing needs to be imported again.
 - **A printer prompt answered on the printer leaves the HMS list (#3487)** — Some faults, such as the A1 mini's "is the filament extruded?" prompt while loading filament, come in the printer's print_error field rather than its HMS list. Bambuddy showed them with the HMS errors but did not notice when the printer set that field back to 0, so the entry stayed on the printer card until "Clear errors" was clicked. It now goes as soon as the printer clears it, and a newer code replaces the old one. An HMS list update that arrives while such a fault is still active no longer drops it either.
 - **Failed and cancelled prints get their timelapse too (#3481)** — The printer saves the timelapse of a print that fails or is cancelled, up to where it stopped, and that video is the best way to see what went wrong. Bambuddy fetched timelapses only for prints that completed, so a stopped print's video stayed on the printer until it was attached by hand with **Scan for Timelapse**. It is now fetched and attached to that print's run like any other. Wiki updated.

+ 21 - 14
backend/app/api/routes/virtual_printers.py

@@ -76,21 +76,16 @@ class VirtualPrinterUpdate(IgnoresNullForRequiredColumns):
     tailscale_disabled: bool | None = None
 
 
-def _resolve_printer_model(printer_model: str | None) -> str | None:
+def _resolve_printer_model(printer_model: str | None, serial: str | None = None) -> str | None:
     """Map a printer's model (display name or SSDP code) to a valid VP SSDP model code.
 
-    Printers store display names like 'X1C' while VPs need SSDP codes like 'BL-P001'.
+    Printers store display names like 'X1C' while VPs need SSDP codes like
+    'BL-P001'. ``serial`` tells apart the codes one name covers (H2C: O1C or
+    O1C2); see ``manager.model_code_for_printer``.
     """
-    if not printer_model:
-        return None
-    from backend.app.services.virtual_printer import VIRTUAL_PRINTER_MODELS
-    from backend.app.services.virtual_printer.manager import DISPLAY_NAME_TO_MODEL_CODE
+    from backend.app.services.virtual_printer.manager import model_code_for_printer
 
-    # Already a valid SSDP model code
-    if printer_model in VIRTUAL_PRINTER_MODELS:
-        return printer_model
-    # Map display name to SSDP code
-    return DISPLAY_NAME_TO_MODEL_CODE.get(printer_model)
+    return model_code_for_printer(printer_model, serial)
 
 
 async def _check_upload_folder(db: AsyncSession, folder_id: int, user) -> JSONResponse | None:
@@ -187,9 +182,14 @@ async def list_virtual_printers(
         status = instance.get_status() if instance else {"running": False, "pending_files": 0}
         printers.append(await _vp_to_dict(vp, db, status))
 
+    from backend.app.services.virtual_printer.manager import MODEL_SERIAL_PREFIXES
+
     return {
         "printers": printers,
         "models": VIRTUAL_PRINTER_MODELS,
+        # The serial family each model's VP gets (Bambu's first 3 characters),
+        # which tells apart models of one name (H2C: O1C 094, O1C2 31B).
+        "model_serial_prefixes": {code: prefix[:3] for code, prefix in MODEL_SERIAL_PREFIXES.items()},
     }
 
 
@@ -296,7 +296,10 @@ async def create_virtual_printer(
         enabled=body.enabled,
         mode=body.mode,
         model=body.model
-        or _resolve_printer_model(target_printer.model if target_printer and body.mode == "proxy" else None)
+        or _resolve_printer_model(
+            target_printer.model if target_printer and body.mode == "proxy" else None,
+            target_printer.serial_number if target_printer else None,
+        )
         or DEFAULT_VIRTUAL_PRINTER_MODEL,
         access_code=effective_access_code,
         target_printer_id=body.target_printer_id,
@@ -550,7 +553,9 @@ async def update_virtual_printer(
         vp.target_printer_id = body.target_printer_id
         # Auto-inherit model from target printer in proxy mode (unless user explicitly set model)
         if body.model is None and vp.mode == "proxy" and target_printer.model:
-            vp.model = _resolve_printer_model(target_printer.model) or target_printer.model
+            vp.model = (
+                _resolve_printer_model(target_printer.model, target_printer.serial_number) or target_printer.model
+            )
     if "library_folder_id" in body.model_fields_set and body.library_folder_id is None:
         vp.library_folder_id = None  # back to the shared "Virtual Printer" folder
     elif body.library_folder_id is not None:
@@ -581,7 +586,9 @@ async def update_virtual_printer(
         result = await db.execute(select(PrinterModel).where(PrinterModel.id == vp.target_printer_id))
         existing_target = result.scalar_one_or_none()
         if existing_target and existing_target.model:
-            vp.model = _resolve_printer_model(existing_target.model) or existing_target.model
+            vp.model = (
+                _resolve_printer_model(existing_target.model, existing_target.serial_number) or existing_target.model
+            )
 
     # Force-inherit the access code from the target printer for non-proxy VPs.
     # See create_virtual_printer for the rationale: the bridge forwards slicer

+ 30 - 2
backend/app/services/virtual_printer/manager.py

@@ -99,14 +99,42 @@ MODEL_SERIAL_PREFIXES = {
     "O1D": "09400A",  # H2D
     "O1E": "09400A",  # H2D Pro (same prefix family as H2D)
     "O2D": "09400A",  # H2D Pro
-    "O1C": "09400A",  # H2C
-    "O1C2": "09400A",  # H2C (dual nozzle variant)
+    "O1C": "09400A",  # H2C, older units: they share the H2D's 094 prefix
+    # H2C from 2026: real serials start 31B8B (31B model, 8B revision, then the
+    # revision letter), e.g. 31B8BP... A virtual one in the H2D's 094 family
+    # also got the H2D's HMS texts in the slicer.
+    "O1C2": "31B8BP",
     "O1S": "09400A",  # H2S
 }
 
 # Reverse mapping: display name → SSDP model code (for auto-inheriting from printer model)
 DISPLAY_NAME_TO_MODEL_CODE = {v: k for k, v in VIRTUAL_PRINTER_MODELS.items()}
 
+# Models whose display name covers more than one code, told apart by the
+# printer's serial: (first characters, code). The first match wins; no match
+# keeps the code the name maps to.
+_CODES_BY_SERIAL: dict[str, tuple[tuple[str, str], ...]] = {
+    "H2C": (("31B", "O1C2"), ("094", "O1C")),
+}
+
+
+def model_code_for_printer(printer_model: str | None, serial: str | None = None) -> str | None:
+    """The VP model code for a printer's model (display name or SSDP code) and serial.
+
+    An H2C is an O1C or an O1C2, which only its serial tells: a slicer syncs
+    with the virtual printer only when it reports the printer's own code.
+    """
+    if not printer_model:
+        return None
+    code = printer_model if printer_model in VIRTUAL_PRINTER_MODELS else DISPLAY_NAME_TO_MODEL_CODE.get(printer_model)
+    name = VIRTUAL_PRINTER_MODELS.get(code or "")
+    if name and serial:
+        for prefix, by_serial in _CODES_BY_SERIAL.get(name, ()):
+            if serial.upper().startswith(prefix):
+                return by_serial
+    return code
+
+
 # Default model
 DEFAULT_VIRTUAL_PRINTER_MODEL = "BL-P001"  # X1C
 

+ 25 - 0
backend/tests/integration/test_virtual_printer_api.py

@@ -707,6 +707,31 @@ class TestVirtualPrinterSerialSurface:
         listed = next(p for p in list_resp.json()["printers"] if p["id"] == vp_id)
         assert listed["serial"] == "00M09A123456789"
 
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    @pytest.mark.parametrize(("serial", "code"), [("31B8BP610600650", "O1C2"), ("0948BB540200427", "O1C")])
+    async def test_a_proxy_for_an_h2c_reports_its_code_by_serial(
+        self, async_client: AsyncClient, printer_factory, serial, code
+    ):
+        """An H2C is an O1C or an O1C2; the slicer only syncs when the VP reports the right one."""
+        target = await printer_factory(name="Real H2C", model="H2C", serial_number=serial)
+
+        create_resp = await async_client.post(
+            "/api/v1/virtual-printers",
+            json={"name": "ProxyH2C", "mode": "proxy", "target_printer_id": target.id},
+        )
+
+        assert create_resp.status_code == 200
+        assert create_resp.json()["model"] == code
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_the_list_names_each_models_serial_family(self, async_client: AsyncClient):
+        families = (await async_client.get("/api/v1/virtual-printers")).json()["model_serial_prefixes"]
+
+        assert families["O1C2"] == "31B"
+        assert families["O1C"] == "094"
+
     @pytest.mark.asyncio
     @pytest.mark.integration
     async def test_non_proxy_vp_keeps_self_generated_serial(self, async_client: AsyncClient, printer_factory):

+ 35 - 0
backend/tests/unit/services/test_virtual_printer.py

@@ -4840,6 +4840,41 @@ class TestResolveModelCodes:
         assert _resolve_printer_model("UnknownModel") is None
 
 
+class TestH2cCodeBySerial:
+    """An H2C is an O1C (serial 094..., older units) or an O1C2 (31B8B..., from 2026).
+
+    A slicer syncs with a virtual H2C only when it reports the printer's own
+    code, and the serial is the only thing that tells them apart.
+    """
+
+    @pytest.mark.parametrize(
+        ("model", "serial", "code"),
+        [
+            ("H2C", "31B8BP610600650", "O1C2"),
+            ("H2C", "0948BB540200427", "O1C"),
+            ("O1C", "31b8bp610600650", "O1C2"),  # a stored code follows the serial too
+            ("O1C2", "0948BB540200427", "O1C"),
+            ("H2D", "31B8BP610600650", "O1D"),  # other models ignore the serial
+        ],
+    )
+    def test_the_serial_picks_the_code(self, model, serial, code):
+        from backend.app.api.routes.virtual_printers import _resolve_printer_model
+
+        assert _resolve_printer_model(model, serial) == code
+
+    def test_without_a_serial_the_name_decides_as_before(self):
+        from backend.app.api.routes.virtual_printers import _resolve_printer_model
+
+        assert _resolve_printer_model("H2C") == "O1C2"
+        assert _resolve_printer_model("O1C") == "O1C"
+
+    def test_a_virtual_o1c2_has_a_real_o1c2_serial(self):
+        from backend.app.services.virtual_printer.manager import _get_serial_for_model
+
+        assert _get_serial_for_model("O1C2", "391800001").startswith("31B8B")
+        assert _get_serial_for_model("O1C", "391800001").startswith("094")
+
+
 class TestMqttIpRewrite:
     """Tests for TLSProxy._rewrite_mqtt_ip() MQTT packet IP rewriting."""
 

+ 38 - 0
frontend/src/__tests__/components/VirtualPrinterCard.test.tsx

@@ -721,3 +721,41 @@ describe('VirtualPrinterCard - collapsed by default', () => {
     expect(screen.getByRole('button', { name: 'Collapse' }).getAttribute('aria-expanded')).toBe('true');
   });
 });
+
+describe('VirtualPrinterCard - model codes of one name', () => {
+  // An H2C is an O1C (serial 094...) or an O1C2 (31B...); the H2D Pro's two codes share 094.
+  const h2Models: Record<string, string> = { O1C: 'H2C', O1C2: 'H2C', O1E: 'H2D Pro', O2D: 'H2D Pro', 'BL-P001': 'X1C' };
+  const families: Record<string, string> = { O1C: '094', O1C2: '31B', O1E: '094', O2D: '094', 'BL-P001': '00M' };
+
+  it('labels the H2C codes with their serial family and says how to pick', async () => {
+    render(
+      <VirtualPrinterCard
+        defaultExpanded
+        printer={createMockPrinter({ model: 'O1C', model_name: 'H2C' })}
+        models={h2Models}
+        modelSerialPrefixes={families}
+      />,
+    );
+
+    expect(await screen.findByRole('option', { name: /O1C2.*31B/ })).toBeInTheDocument();
+    expect(screen.getByRole('option', { name: /\(O1C,.*094/ })).toBeInTheDocument();
+    // Codes the serial doesn't tell apart, and unique names, keep the plain label.
+    expect(screen.getByRole('option', { name: 'H2D Pro (O1E)' })).toBeInTheDocument();
+    expect(screen.getByRole('option', { name: 'X1C (BL-P001)' })).toBeInTheDocument();
+    expect(screen.getByTestId('vp-model-same-name-hint')).toBeInTheDocument();
+  });
+
+  it('gives no hint for a model whose codes the serial does not tell apart', async () => {
+    render(
+      <VirtualPrinterCard
+        defaultExpanded
+        printer={createMockPrinter({ model: 'O1E', model_name: 'H2D Pro' })}
+        models={h2Models}
+        modelSerialPrefixes={families}
+      />,
+    );
+
+    expect(await screen.findByRole('option', { name: 'H2D Pro (O1E)' })).toBeInTheDocument();
+    expect(screen.queryByTestId('vp-model-same-name-hint')).not.toBeInTheDocument();
+  });
+});

+ 2 - 0
frontend/src/api/client.ts

@@ -9299,6 +9299,8 @@ export interface VirtualPrinterConfig {
 export interface VirtualPrinterListResponse {
   printers: VirtualPrinterConfig[];
   models: Record<string, string>;
+  // Serial family (first 3 characters) per model code: tells apart codes of one name (H2C: O1C 094, O1C2 31B).
+  model_serial_prefixes?: Record<string, string>;
 }
 
 export const multiVirtualPrinterApi = {

+ 20 - 2
frontend/src/components/VirtualPrinterCard.tsx

@@ -48,11 +48,20 @@ function uploadFolderOptions(tree: LibraryFolderTree[] | undefined, depth = 0):
 interface VirtualPrinterCardProps {
   printer: VirtualPrinterConfig;
   models: Record<string, string>;
+  // Serial family per model code, shown for codes that share a display name (H2C: O1C / O1C2).
+  modelSerialPrefixes?: Record<string, string>;
   // Collapsed by default: with several virtual printers the page stays a list.
   defaultExpanded?: boolean;
 }
 
-export function VirtualPrinterCard({ printer, models, defaultExpanded = false }: VirtualPrinterCardProps) {
+/** Whether the codes called ``name`` are told apart by their serial (H2C: O1C 094, O1C2 31B). */
+function serialTellsApart(models: Record<string, string>, prefixes: Record<string, string>, name: string | undefined): boolean {
+  if (!name) return false;
+  const families = new Set<string | undefined>(Object.entries(models).filter(([, n]) => n === name).map(([code]) => prefixes[code]));
+  return families.size > 1 && !families.has(undefined);
+}
+
+export function VirtualPrinterCard({ printer, models, modelSerialPrefixes = {}, defaultExpanded = false }: VirtualPrinterCardProps) {
   const { t } = useTranslation();
   const queryClient = useQueryClient();
   const { showToast } = useToast();
@@ -678,11 +687,20 @@ export function VirtualPrinterCard({ printer, models, defaultExpanded = false }:
                     className="w-full bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-md px-3 py-1.5 text-white text-sm appearance-none cursor-pointer disabled:opacity-50 pr-10"
                   >
                     {Object.entries(models).map(([code, name]) => (
-                      <option key={code} value={code}>{name} ({code})</option>
+                      <option key={code} value={code}>
+                        {serialTellsApart(models, modelSerialPrefixes, name)
+                          ? t('virtualPrinter.model.optionSerial', { name, code, prefix: modelSerialPrefixes[code] })
+                          : `${name} (${code})`}
+                      </option>
                     ))}
                   </select>
                   <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
                 </div>
+                {serialTellsApart(models, modelSerialPrefixes, models[localModel]) && (
+                  <p className="text-xs text-bambu-gray mt-1" data-testid="vp-model-same-name-hint">
+                    {t('virtualPrinter.model.sameNameHint', { menu: t('printers.printerInformation') })}
+                  </p>
+                )}
               </div>
             )}
 

+ 2 - 1
frontend/src/components/VirtualPrinterList.tsx

@@ -100,6 +100,7 @@ export function VirtualPrinterList() {
 
   const printers = data?.printers || [];
   const models = data?.models || {};
+  const modelSerialPrefixes = data?.model_serial_prefixes || {};
 
   return (
     <div className="space-y-4">
@@ -276,7 +277,7 @@ export function VirtualPrinterList() {
       ) : (
         <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 items-start">
           {printers.map((printer) => (
-            <VirtualPrinterCard key={printer.id} printer={printer} models={models} />
+            <VirtualPrinterCard key={printer.id} printer={printer} models={models} modelSerialPrefixes={modelSerialPrefixes} />
           ))}
         </div>
       )}

+ 2 - 0
frontend/src/i18n/locales/de.ts

@@ -6346,6 +6346,8 @@ export default {
     model: {
       title: 'Druckermodell',
       description: 'Wähle welches Druckermodell emuliert werden soll.',
+      optionSerial: '{{name}} ({{code}}, Seriennummer {{prefix}}…)',
+      sameNameHint: 'Mehrere Codes tragen diesen Namen: Wähle den, dessen Seriennummer so beginnt wie die deines Druckers (zu sehen unter {{menu}} im Menü der Druckerkarte).',
     },
     accessCode: {
       title: 'Zugangscode',

+ 2 - 0
frontend/src/i18n/locales/en.ts

@@ -6399,6 +6399,8 @@ export default {
     model: {
       title: 'Printer Model',
       description: 'Select which printer model to emulate.',
+      optionSerial: '{{name}} ({{code}}, serial {{prefix}}…)',
+      sameNameHint: 'Several codes share this name: pick the one whose serial starts like your printer\'s (shown under {{menu}} in the printer card\'s menu).',
     },
     accessCode: {
       title: 'Access Code',

+ 2 - 0
frontend/src/i18n/locales/es.ts

@@ -6354,6 +6354,8 @@ export default {
     model: {
       title: 'Modelo de impresora',
       description: 'Seleccione qué modelo de impresora emular.',
+      optionSerial: '{{name}} ({{code}}, número de serie {{prefix}}…)',
+      sameNameHint: 'Varios códigos comparten este nombre: elige aquel cuyo número de serie empieza como el de tu impresora (aparece en {{menu}}, en el menú de la tarjeta de la impresora).',
     },
     accessCode: {
       title: 'Código de acceso',

+ 2 - 0
frontend/src/i18n/locales/fr.ts

@@ -6334,6 +6334,8 @@ export default {
     model: {
       title: 'Modèle d\'imprimante',
       description: 'Choisissez le modèle à émuler.',
+      optionSerial: '{{name}} ({{code}}, numéro de série {{prefix}}…)',
+      sameNameHint: 'Plusieurs codes portent ce nom : choisissez celui dont le numéro de série commence comme celui de votre imprimante (visible sous {{menu}} dans le menu de la carte de l\'imprimante).',
     },
     accessCode: {
       title: 'Code d\'accès',

+ 2 - 0
frontend/src/i18n/locales/it.ts

@@ -6333,6 +6333,8 @@ export default {
     model: {
       title: 'Modello stampante',
       description: 'Seleziona il modello stampante da emulare.',
+      optionSerial: '{{name}} ({{code}}, seriale {{prefix}}…)',
+      sameNameHint: 'Più codici hanno questo nome: scegli quello il cui seriale inizia come quello della tua stampante (si trova in {{menu}} nel menu della scheda della stampante).',
     },
     accessCode: {
       title: 'Codice accesso',

+ 2 - 0
frontend/src/i18n/locales/ja.ts

@@ -6347,6 +6347,8 @@ export default {
     model: {
       title: 'プリンターモデル',
       description: 'エミュレートするプリンターモデルを選択。',
+      optionSerial: '{{name}}({{code}}、シリアル {{prefix}}…)',
+      sameNameHint: 'この名前のコードが複数あります。シリアル番号の先頭がプリンターと同じものを選んでください(プリンターカードのメニューの「{{menu}}」に表示されます)。',
     },
     accessCode: {
       title: 'アクセスコード',

+ 2 - 0
frontend/src/i18n/locales/ko.ts

@@ -6083,6 +6083,8 @@ export default {
     model: {
       title: '프린터 모델',
       description: '에뮬레이션할 프린터 모델을 선택하세요.',
+      optionSerial: '{{name}} ({{code}}, 시리얼 {{prefix}}…)',
+      sameNameHint: '이 이름의 코드가 여러 개 있습니다. 시리얼 번호가 프린터와 같이 시작하는 것을 선택하세요(프린터 카드 메뉴의 {{menu}}에 표시됩니다).',
     },
     accessCode: {
       title: '액세스 코드',

+ 2 - 0
frontend/src/i18n/locales/nl.ts

@@ -6397,6 +6397,8 @@ export default {
     model: {
       title: 'Printermodel',
       description: 'Selecteer welk printermodel moet worden geëmuleerd.',
+      optionSerial: '{{name}} ({{code}}, serienummer {{prefix}}…)',
+      sameNameHint: 'Meerdere codes hebben deze naam: kies de code waarvan het serienummer begint zoals dat van je printer (te zien onder {{menu}} in het menu van de printerkaart).',
     },
     accessCode: {
       title: 'Toegangscode',

+ 2 - 0
frontend/src/i18n/locales/pl.ts

@@ -6399,6 +6399,8 @@ export default {
     model: {
       title: 'Model drukarki',
       description: 'Wybierz model drukarki do emulacji.',
+      optionSerial: '{{name}} ({{code}}, numer seryjny {{prefix}}…)',
+      sameNameHint: 'Kilka kodów ma tę nazwę: wybierz ten, którego numer seryjny zaczyna się tak jak numer twojej drukarki (widać go w {{menu}} w menu karty drukarki).',
     },
     accessCode: {
       title: 'Kod dostępu',

+ 2 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -6333,6 +6333,8 @@ export default {
     model: {
       title: 'Modelo da Impressora',
       description: 'Selecione qual modelo de impressora emular.',
+      optionSerial: '{{name}} ({{code}}, número de série {{prefix}}…)',
+      sameNameHint: 'Vários códigos têm este nome: escolha aquele cujo número de série começa como o da sua impressora (aparece em {{menu}}, no menu do cartão da impressora).',
     },
     accessCode: {
       title: 'Código de acesso',

+ 2 - 0
frontend/src/i18n/locales/ru.ts

@@ -6071,6 +6071,8 @@ export default {
     model: {
       title: "Модель принтера",
       description: "Выберите модель принтера, которую нужно эмулировать.",
+      optionSerial: '{{name}} ({{code}}, серийный номер {{prefix}}…)',
+      sameNameHint: 'Это имя носят несколько кодов: выберите тот, чей серийный номер начинается так же, как у вашего принтера (он показан в пункте «{{menu}}» меню карточки принтера).',
     },
     accessCode: {
       title: "Код доступа",

+ 2 - 0
frontend/src/i18n/locales/sv.ts

@@ -6428,6 +6428,8 @@ errors: {
     model: {
       title: 'Skrivarmodell',
       description: 'Välj vilken skrivarmodell som ska emuleras.',
+      optionSerial: '{{name}} ({{code}}, serienummer {{prefix}}…)',
+      sameNameHint: 'Flera koder har det här namnet: välj den vars serienummer börjar som din skrivares (visas under {{menu}} i skrivarkortets meny).',
     },
     accessCode: {
       title: 'Åtkomstkod',

+ 2 - 0
frontend/src/i18n/locales/tr.ts

@@ -6310,6 +6310,8 @@ export default {
     model: {
       title: 'Yazıcı Modeli',
       description: 'Öykünülecek yazıcı modelini seçin.',
+      optionSerial: '{{name}} ({{code}}, seri no {{prefix}}…)',
+      sameNameHint: 'Bu adı birden fazla kod taşıyor: seri numarası yazıcınızınki gibi başlayanı seçin (yazıcı kartının menüsündeki {{menu}} bölümünde görünür).',
     },
     accessCode: {
       title: 'Erişim Kodu',

+ 2 - 0
frontend/src/i18n/locales/uk.ts

@@ -6387,6 +6387,8 @@ export default {
     model: {
       title: "Модель принтера",
       description: "Виберіть модель принтера для емуляції.",
+      optionSerial: '{{name}} ({{code}}, серійний номер {{prefix}}…)',
+      sameNameHint: 'Цю назву мають кілька кодів: виберіть той, чий серійний номер починається так само, як у вашого принтера (він показаний у пункті «{{menu}}» меню картки принтера).',
     },
     accessCode: {
       title: "Код доступу",

+ 2 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -6333,6 +6333,8 @@ export default {
     model: {
       title: '打印机型号',
       description: '选择要模拟的打印机型号。',
+      optionSerial: '{{name}}({{code}},序列号 {{prefix}}…)',
+      sameNameHint: '多个代码使用此名称:请选择序列号开头与你的打印机相同的那个(在打印机卡片菜单的“{{menu}}”中可以看到)。',
     },
     accessCode: {
       title: '访问码',

+ 2 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -6333,6 +6333,8 @@ export default {
     model: {
       title: '印表機型號',
       description: '選擇要模擬的印表機型號。',
+      optionSerial: '{{name}}({{code}},序號 {{prefix}}…)',
+      sameNameHint: '多個代碼使用此名稱:請選擇序號開頭與你的印表機相同的那個(在印表機卡片選單的「{{menu}}」中可以看到)。',
     },
     accessCode: {
       title: '存取碼',

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ArchivesPage-BWa3tFir.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/CamWallPage-BWNCDLxo.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/CameraPage-BJMlPAhW.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/FileManagerPage-Dn1ku26I.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/InventoryPage-vY9rTMhX.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/Layout-Djs1064l.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ModelSourcesPage-DtSYK84a.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/PrintersPage-CyRFlKDW.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SettingsPage-CTCm32W7.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SettingsPage-dIBYvfL1.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SliceModal-CAPTUZZX.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SpoolBuddyInventoryPage-LXkx-ydO.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 1
static/assets/SpoolBuddySettingsPage-D2DF2JEm.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/de-C8CbhPuk.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/es-3RllJWj1.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/fr-BWjSX08r.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/index-13kgnbdY.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/it-DOIRYKop.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ja-BdNtrLOk.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ko-BwjCNGwE.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/locationSensorDefaults-CydzHXtM.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/nl-DAmtG6hZ.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/pl-D0sniCY6.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/pt-BR-CknMFuJP.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ru-CZVF-zZ_.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/spoolCost-CgmynAMw.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/sv-BJoeBN4W.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/tr-C7UE9ljS.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/uk-DtapMovC.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/usePageFileDrop-Bb9Ou8TY.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/zh-CN-D8o4oMpS.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/zh-TW-c9kTtV3S.js


+ 2 - 2
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-BCp8ipS2.js"></script>
+    <script type="module" crossorigin src="/assets/index-13kgnbdY.js"></script>
     <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-aKtaBQYM.js">
     <link rel="modulepreload" crossorigin href="/assets/client-Bol9xAus.js">
     <link rel="modulepreload" crossorigin href="/assets/react-dom-DOwGWmSv.js">
@@ -122,7 +122,7 @@
     <link rel="modulepreload" crossorigin href="/assets/useColorCatalogVersion-5VMJBKdj.js">
     <link rel="modulepreload" crossorigin href="/assets/circle-minus-PUlx2UXc.js">
     <link rel="modulepreload" crossorigin href="/assets/ConnectionDiagnostic-CKKaaGUZ.js">
-    <link rel="modulepreload" crossorigin href="/assets/Layout-BgbPF2oW.js">
+    <link rel="modulepreload" crossorigin href="/assets/Layout-Djs1064l.js">
     <link rel="stylesheet" crossorigin href="/assets/index-bwCl253z.css">
   </head>
   <body>

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است