Browse Source

feat(printers): show plate name on card for multi-plate active prints (#881)

  When two printers were running different plates of the same multi-plate
  3MF, the Printers page cards displayed the same file name on both and
  there was no way to tell them apart. The Queue view already had this
  information by cross-referencing the archive's plate list; the card
  didn't have the linkage.

  Expose `current_archive_id` (resolved by matching the MQTT `subtask_id`
  against `PrintArchive.subtask_id` — the bridge introduced in #972 for
  restart-resume) and `current_plate_id` (parsed from `gcode_file` by a
  new shared `parse_plate_id` helper) on the status endpoint. The helper
  is also called from the WebSocket push path so plate transitions
  reflect within 100 ms instead of waiting 30 s for the next REST poll;
  the archive id itself stays REST-only since it's stable for the life
  of a print and shouldn't make the push path touch the DB.

  The card fetches plate metadata via the same `api.getArchivePlates()`
  call QueuePage uses — shared React Query cache keeps it cheap across
  polls — and renders the actual plate name (or a "Plate N" fallback)
  only when `is_multi_plate` is true. Single-plate prints stay clean.
  Falls back to the previous `plate_N.gcode` regex path when there's no
  archive linkage (e.g. prints started directly from the printer LCD).

  Tests cover the plate-id extraction across Bambu Studio path shapes
  (backend parse_plate_id, printer_state_to_dict wiring) and the label
  override precedence in formatPrintName (frontend).
maziggy 1 month ago
parent
commit
fa1c46d9a5

+ 3 - 0
CHANGELOG.md

@@ -4,6 +4,9 @@ All notable changes to Bambuddy will be documented in this file.
 
 
 ## [0.2.4b1] - Unreleased
 ## [0.2.4b1] - Unreleased
 
 
+### Improved
+- **Printer Card Shows Plate Name on Multi-Plate Prints** ([#881](https://github.com/maziggy/bambuddy/issues/881)) — When two printers were running different plates of the same multi-plate 3MF, the Printers page cards displayed the same file name on both and gave no visual way to tell them apart. The Queue view already showed the plate name by querying the archive's plate list; the Printers page didn't have that linkage. The `GET /printers/{id}/status` endpoint now returns `current_archive_id` (resolved by matching the MQTT `subtask_id` against `PrintArchive.subtask_id`, the same bridge introduced in #972 for restart-resume) and `current_plate_id` (parsed from the MQTT `gcode_file` path by a new shared `parse_plate_id` helper that's also used by the WebSocket push path, so plate transitions within a running print reflect immediately instead of waiting 30 s for the next REST poll). The card fetches plate metadata via the same `api.getArchivePlates()` call the Queue page uses — shared React Query cache keeps it cheap across polls — and renders the actual plate name (or a "Plate N" fallback) only when the source 3MF is multi-plate, so single-plate prints stay noise-free. Falls back to the previous `plate_(\d+).gcode` regex when there's no archive linkage (e.g. prints started directly from the printer LCD). Regression tests cover the plate-id extraction across Bambu Studio path shapes and the label-override precedence in `formatPrintName`. Thanks to @stringham for the follow-up and screenshot.
+
 ### Fixed
 ### Fixed
 - **Spoolman Iframe Blocked by CSP on HTTP Instances** ([#1054](https://github.com/maziggy/bambuddy/issues/1054)) — The Filament tab showed a blank page with a brief Spoolman flash on reload. Browser console reported `Content-Security-Policy: The page's settings blocked the loading of a resource (frame-src) at http://<host>:7912/spool because it violates the following directive: "frame-src 'self' https:"`. Root cause: commit `53a70e37` (#995) tightened the CSP to allow external sidebar iframes but only whitelisted `https:`, overlooking that self-hosted services on LANs — Spoolman, OctoPrint, etc. — almost always run over plain HTTP. The `frame-src` directive now allows `http:` as well (`frame-src 'self' http: https:`), matching the `connect-src 'self' ws: wss:` pattern already used for WebSockets. `frame-ancestors 'none'` still prevents Bambuddy itself from being framed cross-origin. Thanks to @saint-hh for reporting.
 - **Spoolman Iframe Blocked by CSP on HTTP Instances** ([#1054](https://github.com/maziggy/bambuddy/issues/1054)) — The Filament tab showed a blank page with a brief Spoolman flash on reload. Browser console reported `Content-Security-Policy: The page's settings blocked the loading of a resource (frame-src) at http://<host>:7912/spool because it violates the following directive: "frame-src 'self' https:"`. Root cause: commit `53a70e37` (#995) tightened the CSP to allow external sidebar iframes but only whitelisted `https:`, overlooking that self-hosted services on LANs — Spoolman, OctoPrint, etc. — almost always run over plain HTTP. The `frame-src` directive now allows `http:` as well (`frame-src 'self' http: https:`), matching the `connect-src 'self' ws: wss:` pattern already used for WebSockets. `frame-ancestors 'none'` still prevents Bambuddy itself from being framed cross-origin. Thanks to @saint-hh for reporting.
 - **AMS-HT: Custom Filament Preset Reverts to "Generic" in UI After Configure** ([#1053](https://github.com/maziggy/bambuddy/issues/1053)) — After configuring an AMS-HT slot (HT-A/HT-B) with a custom Bambu Cloud preset (e.g. "Devil Design PLA Basic"), the slot card and Configure modal kept showing "Generic PLA" even though the `ams_filament_setting` command succeeded and BambuStudio / the printer's LCD both rendered the correct custom preset. Root cause: the `GET /api/v1/printers/{id}/slot-presets` endpoint keyed its response dict by `ams_id * 4 + tray_id`, which collapses cleanly to the same integer the frontend uses for regular AMS slots (0 through 15) but produces `128 * 4 + 0 = 512` for HT-A — a key nothing looks up. The frontend's PrintersPage HT render path calls `getGlobalTrayId(ams.id, …, false)` which returns the ams_id itself (`128` for HT-A), and SpoolBuddy's AMS page used a third, unrelated formula (`(amsId - 128) * 4 + trayId + 64 = 64`). All three agreed for regular AMS so the mismatch only surfaced on HT, where the saved preset name never reached the UI and the render fell through to `tray.tray_type` → rendered as "Generic PLA". Backend now keys the response via a `_slot_preset_key` helper that mirrors frontend `getGlobalTrayId` (HT → `ams_id`, regular/external → `ams_id * 4 + tray_id`), and SpoolBuddyAmsPage uses the shared `getGlobalTrayId` helper instead of its home-grown formula. Regression test covers the key scheme for regular, HT, and external slots. Thanks to @mrnoisytiger for the detailed reproduction.
 - **AMS-HT: Custom Filament Preset Reverts to "Generic" in UI After Configure** ([#1053](https://github.com/maziggy/bambuddy/issues/1053)) — After configuring an AMS-HT slot (HT-A/HT-B) with a custom Bambu Cloud preset (e.g. "Devil Design PLA Basic"), the slot card and Configure modal kept showing "Generic PLA" even though the `ams_filament_setting` command succeeded and BambuStudio / the printer's LCD both rendered the correct custom preset. Root cause: the `GET /api/v1/printers/{id}/slot-presets` endpoint keyed its response dict by `ams_id * 4 + tray_id`, which collapses cleanly to the same integer the frontend uses for regular AMS slots (0 through 15) but produces `128 * 4 + 0 = 512` for HT-A — a key nothing looks up. The frontend's PrintersPage HT render path calls `getGlobalTrayId(ams.id, …, false)` which returns the ams_id itself (`128` for HT-A), and SpoolBuddy's AMS page used a third, unrelated formula (`(amsId - 128) * 4 + trayId + 64 = 64`). All three agreed for regular AMS so the mismatch only surfaced on HT, where the saved preset name never reached the UI and the render fell through to `tray.tray_type` → rendered as "Generic PLA". Backend now keys the response via a `_slot_preset_key` helper that mirrors frontend `getGlobalTrayId` (HT → `ams_id`, regular/external → `ams_id * 4 + tray_id`), and SpoolBuddyAmsPage uses the shared `getGlobalTrayId` helper instead of its home-grown formula. Regression test covers the key scheme for regular, HT, and external slots. Thanks to @mrnoisytiger for the detailed reproduction.

+ 23 - 0
backend/app/api/routes/printers.py

@@ -39,6 +39,7 @@ from backend.app.services.bambu_ftp import (
 )
 )
 from backend.app.services.printer_manager import (
 from backend.app.services.printer_manager import (
     get_derived_status_name,
     get_derived_status_name,
+    parse_plate_id,
     printer_manager,
     printer_manager,
     supports_chamber_temp,
     supports_chamber_temp,
     supports_drying,
     supports_drying,
@@ -561,6 +562,26 @@ async def get_printer_status(
             k: v for k, v in temperatures.items() if k not in ("chamber", "chamber_target", "chamber_heating")
             k: v for k, v in temperatures.items() if k not in ("chamber", "chamber_target", "chamber_heating")
         }
         }
 
 
+    # Resolve the active print's archive + plate (#881 follow-up): lets the
+    # printer card show the actual plate name for multi-plate 3MFs instead of
+    # just the 3MF filename. Only attempted for active prints, since subtask_id
+    # is only meaningful then.
+    current_archive_id: int | None = None
+    current_plate_id: int | None = None
+    if state.state in ("RUNNING", "PAUSE"):
+        current_plate_id = parse_plate_id(state.gcode_file)
+        if state.subtask_id:
+            from backend.app.models.archive import PrintArchive
+
+            archive_row = await db.execute(
+                select(PrintArchive.id)
+                .where(PrintArchive.subtask_id == state.subtask_id)
+                .where(PrintArchive.printer_id == printer_id)
+                .order_by(PrintArchive.created_at.desc())
+                .limit(1)
+            )
+            current_archive_id = archive_row.scalar_one_or_none()
+
     return PrinterStatus(
     return PrinterStatus(
         id=printer_id,
         id=printer_id,
         name=printer.name,
         name=printer.name,
@@ -612,6 +633,8 @@ async def get_printer_status(
         developer_mode=state.developer_mode if state else None,
         developer_mode=state.developer_mode if state else None,
         awaiting_plate_clear=printer_manager.is_awaiting_plate_clear(printer_id),
         awaiting_plate_clear=printer_manager.is_awaiting_plate_clear(printer_id),
         supports_drying=supports_drying(printer.model, state.firmware_version),
         supports_drying=supports_drying(printer.model, state.firmware_version),
+        current_archive_id=current_archive_id,
+        current_plate_id=current_plate_id,
     )
     )
 
 
 
 

+ 8 - 0
backend/app/schemas/printer.py

@@ -273,3 +273,11 @@ class PrinterStatus(BaseModel):
     awaiting_plate_clear: bool = False
     awaiting_plate_clear: bool = False
     # AMS drying support
     # AMS drying support
     supports_drying: bool = False
     supports_drying: bool = False
+    # Linked archive for the active print (resolved via subtask_id). Frontend uses
+    # this to fetch plate metadata and show the plate name when the source 3MF is
+    # multi-plate (#881 follow-up).
+    current_archive_id: int | None = None
+    # 1-indexed plate number parsed from gcode_file (e.g. /Metadata/plate_2.gcode).
+    # Set for every active print regardless of plate count; the frontend decides
+    # whether to render it based on current_archive_id's is_multi_plate flag.
+    current_plate_id: int | None = None

+ 23 - 0
backend/app/services/printer_manager.py

@@ -1,5 +1,6 @@
 import asyncio
 import asyncio
 import logging
 import logging
+import re
 import traceback
 import traceback
 from collections.abc import Callable
 from collections.abc import Callable
 
 
@@ -622,6 +623,22 @@ def get_derived_status_name(state: PrinterState, model: str | None = None) -> st
     return None
     return None
 
 
 
 
+_PLATE_ID_RE = re.compile(r"plate_(\d+)\.gcode")
+
+
+def parse_plate_id(gcode_file: str | None) -> int | None:
+    """Extract the 1-indexed plate number from a Bambu gcode_file path.
+
+    Returns None when the path is missing or has no `plate_N.gcode` segment.
+    Shared by the REST status route and the WebSocket push path so both agree
+    on the value sent to the frontend (#881 follow-up).
+    """
+    if not gcode_file:
+        return None
+    match = _PLATE_ID_RE.search(gcode_file)
+    return int(match.group(1)) if match else None
+
+
 def printer_state_to_dict(state: PrinterState, printer_id: int | None = None, model: str | None = None) -> dict:
 def printer_state_to_dict(state: PrinterState, printer_id: int | None = None, model: str | None = None) -> dict:
     """Convert PrinterState to a JSON-serializable dict.
     """Convert PrinterState to a JSON-serializable dict.
 
 
@@ -838,6 +855,12 @@ def printer_state_to_dict(state: PrinterState, printer_id: int | None = None, mo
         ],
         ],
         # AMS drying support
         # AMS drying support
         "supports_drying": supports_drying(model, state.firmware_version),
         "supports_drying": supports_drying(model, state.firmware_version),
+        # 1-indexed plate number parsed from gcode_file (e.g. /Metadata/plate_2.gcode).
+        # Pushed via WebSocket so the printer card picks up plate transitions within
+        # a multi-plate 3MF without waiting for the 30 s REST poll (#881 follow-up).
+        # current_archive_id is intentionally REST-only — it's stable for the life
+        # of a print and needs a DB lookup the WebSocket path shouldn't pay for.
+        "current_plate_id": parse_plate_id(state.gcode_file),
     }
     }
     # Add cover URL if there's an active print and printer_id is provided
     # Add cover URL if there's an active print and printer_id is provided
     # Include PAUSE state so skip objects modal can show cover
     # Include PAUSE state so skip objects modal can show cover

+ 58 - 0
backend/tests/unit/services/test_printer_manager.py

@@ -13,6 +13,7 @@ from backend.app.services.printer_manager import (
     get_derived_status_name,
     get_derived_status_name,
     has_stg_cur_idle_bug,
     has_stg_cur_idle_bug,
     init_printer_connections,
     init_printer_connections,
+    parse_plate_id,
     printer_state_to_dict,
     printer_state_to_dict,
     supports_chamber_temp,
     supports_chamber_temp,
     supports_drying,
     supports_drying,
@@ -834,6 +835,22 @@ class TestPrinterStateToDict:
 
 
         assert result["cover_url"] == "/api/v1/printers/1/cover"
         assert result["cover_url"] == "/api/v1/printers/1/cover"
 
 
+    def test_current_plate_id_extracted_from_gcode_file(self, mock_state):
+        """Verify current_plate_id is parsed from a Bambu plate path (#881)."""
+        mock_state.gcode_file = "/Metadata/plate_3.gcode"
+
+        result = printer_state_to_dict(mock_state)
+
+        assert result["current_plate_id"] == 3
+
+    def test_current_plate_id_none_when_no_plate_segment(self, mock_state):
+        """Verify current_plate_id stays None when gcode_file has no plate marker."""
+        mock_state.gcode_file = "/sdcard/test.gcode"
+
+        result = printer_state_to_dict(mock_state)
+
+        assert result["current_plate_id"] is None
+
     def test_cover_url_none_when_not_running(self, mock_state):
     def test_cover_url_none_when_not_running(self, mock_state):
         """Verify cover_url is None when not printing."""
         """Verify cover_url is None when not printing."""
         mock_state.state = "IDLE"
         mock_state.state = "IDLE"
@@ -1295,3 +1312,44 @@ class TestAmsChangeCallback:
         # This tests the callback signature
         # This tests the callback signature
         assert manager._on_ams_change is not None
         assert manager._on_ams_change is not None
         assert callable(manager._on_ams_change)
         assert callable(manager._on_ams_change)
+
+
+class TestParsePlateId:
+    """Tests for parse_plate_id() — active-print plate extraction from gcode paths.
+
+    Regression coverage for #881 follow-up: the REST /status endpoint and the
+    WebSocket push path both use this helper, so they must agree on the plate
+    number the frontend sees.
+    """
+
+    def test_bambu_metadata_path(self):
+        # Canonical path that Bambu Studio / OrcaSlicer stamp into the 3MF.
+        assert parse_plate_id("/Metadata/plate_2.gcode") == 2
+
+    def test_plate_one(self):
+        assert parse_plate_id("/Metadata/plate_1.gcode") == 1
+
+    def test_double_digit_plate(self):
+        assert parse_plate_id("/Metadata/plate_12.gcode") == 12
+
+    def test_none_input(self):
+        assert parse_plate_id(None) is None
+
+    def test_empty_string(self):
+        assert parse_plate_id("") is None
+
+    def test_path_without_plate_segment(self):
+        # Some firmware / slicers report a bare filename without the plate marker.
+        assert parse_plate_id("/upload/my-model.gcode") is None
+
+    def test_similar_but_non_matching_names(self):
+        # "plate.gcode" (no number) and "nameplate_2.gcode" (substring) must not
+        # be mistaken for real plate markers. The regex anchors on `plate_<num>`.
+        assert parse_plate_id("/Metadata/plate.gcode") is None
+        assert parse_plate_id("/plates/3.gcode") is None
+
+    def test_substring_match_still_extracts(self):
+        # The regex isn't anchored to the start of a segment — any occurrence
+        # wins. This matches real Bambu paths where the segment is preceded by
+        # arbitrary directory noise, and matches the equivalent frontend regex.
+        assert parse_plate_id("/uploads/project/plate_5.gcode.md5") == 5

+ 46 - 0
frontend/src/__tests__/pages/PrintersPageFormatPrintName.test.ts

@@ -0,0 +1,46 @@
+/**
+ * Unit tests for the formatPrintName helper on PrintersPage.
+ *
+ * Regression coverage for the #881 follow-up: when the printer card has an
+ * archive-linked plate label (resolved from the backend's current_archive_id
+ * + the archive's is_multi_plate plate list), the label must take precedence
+ * over the gcode_file regex fallback, including for plate 1.
+ */
+
+import { describe, it, expect } from 'vitest';
+import { formatPrintName } from '../../pages/PrintersPage';
+
+// Minimal translator stub: returns the fallback with the plate number interpolated
+// the same way i18next would. Keeps these tests independent of the i18n setup.
+const t = (_key: string, fallback: string, opts?: Record<string, unknown>) =>
+  fallback.replace('{{number}}', String(opts?.number ?? ''));
+
+describe('formatPrintName', () => {
+  it('returns the name unchanged when neither plate source is available', () => {
+    expect(formatPrintName('Benchy', null, t)).toBe('Benchy');
+  });
+
+  it('appends gcode-file plate number only when > 1 (single-plate noise guard)', () => {
+    // Plate 1 from gcode_file alone is ambiguous (could be a single-plate 3MF)
+    // so the legacy fallback path keeps it silent.
+    expect(formatPrintName('Benchy', '/Metadata/plate_1.gcode', t)).toBe('Benchy');
+    expect(formatPrintName('Benchy', '/Metadata/plate_2.gcode', t)).toBe('Benchy — Plate 2');
+  });
+
+  it('uses plateLabel verbatim when provided, overriding the gcode_file fallback', () => {
+    // plateLabel comes from the archive lookup and is already disambiguated
+    // (only set when is_multi_plate === true). It must show even for plate 1.
+    expect(formatPrintName('Benchy', '/Metadata/plate_1.gcode', t, 'Plate 1')).toBe('Benchy — Plate 1');
+    expect(formatPrintName('Benchy', '/Metadata/plate_2.gcode', t, 'Small Parts')).toBe('Benchy — Small Parts');
+  });
+
+  it('returns empty string when name is missing, regardless of plate info', () => {
+    expect(formatPrintName(null, '/Metadata/plate_2.gcode', t)).toBe('');
+    expect(formatPrintName(null, null, t, 'Plate 3')).toBe('');
+  });
+
+  it('treats null/empty plateLabel as absent and falls through to gcode_file parsing', () => {
+    expect(formatPrintName('Benchy', '/Metadata/plate_2.gcode', t, null)).toBe('Benchy — Plate 2');
+    expect(formatPrintName('Benchy', '/Metadata/plate_2.gcode', t, '')).toBe('Benchy — Plate 2');
+  });
+});

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

@@ -220,6 +220,8 @@ export interface PrinterStatus {
   state: string | null;
   state: string | null;
   current_print: string | null;
   current_print: string | null;
   subtask_name: string | null;
   subtask_name: string | null;
+  current_archive_id: number | null;
+  current_plate_id: number | null;
   gcode_file: string | null;
   gcode_file: string | null;
   progress: number | null;
   progress: number | null;
   remaining_time: number | null;
   remaining_time: number | null;

+ 32 - 4
frontend/src/pages/PrintersPage.tsx

@@ -90,9 +90,20 @@ import { getColorName, parseFilamentColor, isLightColor } from '../utils/colors'
 // Color names resolve via getColorName() which reads the backend color_catalog
 // Color names resolve via getColorName() which reads the backend color_catalog
 // (loaded once by ColorCatalogProvider). No hardcoded tables here — see #857.
 // (loaded once by ColorCatalogProvider). No hardcoded tables here — see #857.
 
 
-// Extract plate number from gcode_file path and append to print name
-function formatPrintName(name: string | null, gcodeFile: string | null | undefined, t: (key: string, fallback: string, opts?: Record<string, unknown>) => string): string {
+// Append a plate label to the print name. When `plateLabel` is provided (resolved
+// by the caller from the linked archive's plate list — see #881 follow-up), it
+// is used verbatim, including the explicit "Plate 1" case on multi-plate 3MFs.
+// Falls back to parsing `plate_N.gcode` from the MQTT gcode_file path, and in
+// that fallback we only show N > 1 because we can't tell from the path alone
+// whether the 3MF is multi-plate.
+export function formatPrintName(
+  name: string | null,
+  gcodeFile: string | null | undefined,
+  t: (key: string, fallback: string, opts?: Record<string, unknown>) => string,
+  plateLabel?: string | null,
+): string {
   if (!name) return '';
   if (!name) return '';
+  if (plateLabel) return `${name} — ${plateLabel}`;
   if (!gcodeFile) return name;
   if (!gcodeFile) return name;
   const match = gcodeFile.match(/plate_(\d+)\.gcode/);
   const match = gcodeFile.match(/plate_(\d+)\.gcode/);
   if (match && parseInt(match[1], 10) > 1) {
   if (match && parseInt(match[1], 10) > 1) {
@@ -1528,6 +1539,23 @@ function PrinterCard({
     staleTime: 2 * 60 * 1000, // 2 minutes
     staleTime: 2 * 60 * 1000, // 2 minutes
   });
   });
 
 
+  // Fetch plate list for the archive linked to the active print (#881 follow-up).
+  // Only queried when there's a running print backed by an archive; shared
+  // React Query cache with the Queue / Archives pages keeps it cheap.
+  const activeArchiveId =
+    (status?.state === 'RUNNING' || status?.state === 'PAUSE') ? status?.current_archive_id ?? null : null;
+  const { data: activeArchivePlates } = useQuery({
+    queryKey: ['archive-plates', activeArchiveId],
+    queryFn: () => api.getArchivePlates(activeArchiveId!),
+    enabled: activeArchiveId != null,
+    staleTime: 5 * 60 * 1000,
+  });
+  const activePlateLabel = (() => {
+    if (!activeArchivePlates?.is_multi_plate || status?.current_plate_id == null) return null;
+    const plate = activeArchivePlates.plates.find(p => p.index === status.current_plate_id);
+    return plate?.name || t('printers.plateNumber', 'Plate {{number}}', { number: status.current_plate_id });
+  })();
+
   // Fetch user-defined AMS friendly names from the database
   // Fetch user-defined AMS friendly names from the database
   const { data: amsLabels, refetch: refetchAmsLabels } = useQuery({
   const { data: amsLabels, refetch: refetchAmsLabels } = useQuery({
     queryKey: ['amsLabels', printer.id],
     queryKey: ['amsLabels', printer.id],
@@ -2718,7 +2746,7 @@ function PrinterCard({
                     {/* Cover Image */}
                     {/* Cover Image */}
                     <CoverImage
                     <CoverImage
                       url={(status.state === 'RUNNING' || status.state === 'PAUSE') ? status.cover_url : null}
                       url={(status.state === 'RUNNING' || status.state === 'PAUSE') ? status.cover_url : null}
-                      printName={(status.state === 'RUNNING' || status.state === 'PAUSE') ? (formatPrintName(status.subtask_name || status.current_print || null, status.gcode_file, t) || undefined) : undefined}
+                      printName={(status.state === 'RUNNING' || status.state === 'PAUSE') ? (formatPrintName(status.subtask_name || status.current_print || null, status.gcode_file, t, activePlateLabel) || undefined) : undefined}
                     />
                     />
                     {/* Print Info */}
                     {/* Print Info */}
                     <div className="flex-1 min-w-0">
                     <div className="flex-1 min-w-0">
@@ -2729,7 +2757,7 @@ function PrinterCard({
                             {plateStatusPill}
                             {plateStatusPill}
                           </div>
                           </div>
                           <p className="text-white text-sm mb-2 truncate">
                           <p className="text-white text-sm mb-2 truncate">
-                            {formatPrintName(status.subtask_name || status.current_print || null, status.gcode_file, t)}
+                            {formatPrintName(status.subtask_name || status.current_print || null, status.gcode_file, t, activePlateLabel)}
                           </p>
                           </p>
                           <div className="flex items-center justify-between text-sm">
                           <div className="flex items-center justify-between text-sm">
                             <div className="flex-1 bg-bambu-dark-tertiary rounded-full h-2 mr-3">
                             <div className="flex-1 bg-bambu-dark-tertiary rounded-full h-2 mr-3">

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-06WvZfP-.js


+ 1 - 1
static/index.html

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

Some files were not shown because too many files changed in this diff