From 2b6dbe5ae520bd40802847db8ddb93f70e8a4bd6 Mon Sep 17 00:00:00 2001 From: Adam Spice Date: Wed, 30 Sep 2026 03:26:40 +1300 Subject: [PATCH] fix: display friendly legacy printer model names in stream overlay (#3134) --- .../pages/StreamOverlayPage.test.tsx | 15 +++++- frontend/src/pages/PrintersPage.tsx | 46 +------------------ frontend/src/pages/StreamOverlayPage.tsx | 3 +- .../pages/spoolbuddy/SpoolBuddyAmsPage.tsx | 16 +------ frontend/src/utils/printerModel.ts | 44 ++++++++++++++++++ 5 files changed, 61 insertions(+), 63 deletions(-) create mode 100644 frontend/src/utils/printerModel.ts diff --git a/frontend/src/__tests__/pages/StreamOverlayPage.test.tsx b/frontend/src/__tests__/pages/StreamOverlayPage.test.tsx index 88ff342f6..bb0e093a0 100644 --- a/frontend/src/__tests__/pages/StreamOverlayPage.test.tsx +++ b/frontend/src/__tests__/pages/StreamOverlayPage.test.tsx @@ -150,6 +150,17 @@ describe('StreamOverlayPage', () => { if (query === '?show=model') expect(screen.queryByText(/X1 Carbon/)).not.toBeInTheDocument(); }); + it.each([ + ['BL-P001', 'X1C'], + ['O1D', 'H2D'], + ['H2D', 'H2D'], + ['Future printer', 'Future printer'], + ])('displays the saved model %s as %s', async (model, displayName) => { + server.use(http.get('/api/v1/printers/:id', () => HttpResponse.json({ ...mockPrinter, model }))); + renderOverlayPage(1, '?show=model'); + expect(await screen.findByText(displayName)).toBeInTheDocument(); + }); + it.each([null, ''])('omits a missing model without adding a separator (%s)', async (model) => { server.use(http.get('/api/v1/printers/:id', () => HttpResponse.json({ ...mockPrinter, model }))); renderOverlayPage(1, '?show=printer,model'); @@ -157,7 +168,7 @@ describe('StreamOverlayPage', () => { expect(screen.queryByText(/·/)).not.toBeInTheDocument(); }); - it.each([['H2D', 'Workshop · H2D'], [null, 'Workshop'], ['', 'Workshop']] as const)('reads the model (%s) from the OBS token feed without requesting printer details', async (model, identity) => { + it.each([['H2D', 'H2D'], ['BL-P001', 'X1C'], ['O1D', 'H2D'], [null, ''], ['', '']] as const)('reads the model %s from the OBS token feed without requesting printer details', async (model, displayName) => { let printerHit = false; server.use( http.get('/api/v1/printers/:id/overlay-status', () => HttpResponse.json({ @@ -170,7 +181,7 @@ describe('StreamOverlayPage', () => { }), ); renderOverlayPage(1, '?token=obs-tok&show=printer,model'); - expect(await screen.findByText(identity)).toBeInTheDocument(); + expect(await screen.findByText(displayName ? `Workshop · ${displayName}` : 'Workshop')).toBeInTheDocument(); expect(printerHit).toBe(false); if (!model) expect(screen.queryByText(/·/)).not.toBeInTheDocument(); }); diff --git a/frontend/src/pages/PrintersPage.tsx b/frontend/src/pages/PrintersPage.tsx index 4279c46dc..31bad1b42 100644 --- a/frontend/src/pages/PrintersPage.tsx +++ b/frontend/src/pages/PrintersPage.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useLayoutEffect, useMemo, useRef, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { compareFwVersions } from '../utils/firmwareVersion'; +import { mapModelCode } from '../utils/printerModel'; import { formatPrintName } from '../utils/printName'; import { isBedSlinger } from '../utils/bedSlinger'; import { computePopoverPosition, type PopoverPosition } from '../utils/popoverPosition'; @@ -1747,51 +1748,6 @@ const MODELS_WITH_EXHAUST_LABEL: ReadonlySet = new Set([ 'X2D', ]); -// Map SSDP model codes to display names -function mapModelCode(ssdpModel: string | null): string { - if (!ssdpModel) return ''; - const modelMap: Record = { - // H2 Series - 'O1D': 'H2D', - 'O1E': 'H2D Pro', - 'O2D': 'H2D Pro', - 'O1C': 'H2C', - 'O1C2': 'H2C', - 'O1S': 'H2S', - // X1 Series - 'BL-P001': 'X1C', - 'BL-P002': 'X1', - 'BL-P003': 'X1E', - // X2 Series - 'N6': 'X2D', - // A2 Series - 'N9': 'A2L', - // P Series - 'C11': 'P1S', - 'C12': 'P1P', - 'C13': 'P2S', - // A1 Series - 'N2S': 'A1', - 'N1': 'A1 Mini', - // Direct matches - 'X1C': 'X1C', - 'X1': 'X1', - 'X1E': 'X1E', - 'X2D': 'X2D', - 'P1S': 'P1S', - 'P1P': 'P1P', - 'P2S': 'P2S', - 'A1': 'A1', - 'A1 Mini': 'A1 Mini', - 'A2L': 'A2L', - 'H2D': 'H2D', - 'H2D Pro': 'H2D Pro', - 'H2C': 'H2C', - 'H2S': 'H2S', - }; - return modelMap[ssdpModel] || ssdpModel; -} - // ─── AMS Name Hover Card ────────────────────────────────────────────────────── // Wraps the AMS label (e.g. "AMS-A") and shows a popup with: // • User-defined friendly name (editable, protected by printers:update) diff --git a/frontend/src/pages/StreamOverlayPage.tsx b/frontend/src/pages/StreamOverlayPage.tsx index 2ffd3a4f8..fab12dd37 100644 --- a/frontend/src/pages/StreamOverlayPage.tsx +++ b/frontend/src/pages/StreamOverlayPage.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'; import { Layers, Clock, Timer, Printer, Flame, Square, Box } from 'lucide-react'; import { api, ApiError, withStreamToken } from '../api/client'; import { formatDuration, formatETA, type TimeFormat } from '../utils/date'; +import { mapModelCode } from '../utils/printerModel'; type TFunction = (key: string, options?: Record) => string; @@ -217,7 +218,7 @@ export function StreamOverlayPage() { ); const printerIdentity = [ config.showPrinter ? printer?.name : null, - config.showModel ? printer?.model : null, + config.showModel ? mapModelCode(printer?.model ?? null) : null, ].filter(Boolean).join(' · '); const status = kiosk ? overlay : statusData; const timeFormat: TimeFormat = (kiosk ? overlay?.time_format : settings?.time_format) || 'system'; diff --git a/frontend/src/pages/spoolbuddy/SpoolBuddyAmsPage.tsx b/frontend/src/pages/spoolbuddy/SpoolBuddyAmsPage.tsx index 7b01c5c4e..f246097a9 100644 --- a/frontend/src/pages/spoolbuddy/SpoolBuddyAmsPage.tsx +++ b/frontend/src/pages/spoolbuddy/SpoolBuddyAmsPage.tsx @@ -8,6 +8,7 @@ import { api } from '../../api/client'; import type { PrinterStatus, AMSTray, SpoolAssignment } from '../../api/client'; import { getGlobalTrayId, getFillBarColor, getSpoolmanFillLevel, getFallbackSpoolTag, formatSlotLabel, isBambuLabSpool, resolveSlotNozzleDiameter } from '../../utils/amsHelpers'; import { getSwatchStyle, resolveSpoolColorName } from '../../utils/colors'; +import { mapModelCode } from '../../utils/printerModel'; import { spoolSwatchStyle } from '../../components/spoolbuddy/spoolPaint'; /** @@ -39,21 +40,6 @@ function getAmsName(amsId: number): string { return `AMS ${amsId}`; } -function mapModelCode(ssdpModel: string | null): string { - if (!ssdpModel) return ''; - const modelMap: Record = { - 'O1D': 'H2D', 'O1E': 'H2D Pro', 'O2D': 'H2D Pro', 'O1C': 'H2C', 'O1C2': 'H2C', 'O1S': 'H2S', - 'BL-P001': 'X1C', 'BL-P002': 'X1', 'BL-P003': 'X1E', - 'N6': 'X2D', - 'N9': 'A2L', - 'C11': 'P1S', 'C12': 'P1P', 'C13': 'P2S', - 'N2S': 'A1', 'N1': 'A1 Mini', - 'X1C': 'X1C', 'X1': 'X1', 'X1E': 'X1E', 'X2D': 'X2D', 'P1S': 'P1S', 'P1P': 'P1P', 'P2S': 'P2S', - 'A1': 'A1', 'A1 Mini': 'A1 Mini', 'A2L': 'A2L', 'H2D': 'H2D', 'H2D Pro': 'H2D Pro', 'H2C': 'H2C', 'H2S': 'H2S', - }; - return modelMap[ssdpModel] || ssdpModel; -} - function isTrayEmpty(tray: AMSTray): boolean { return !tray.tray_type || tray.tray_type === ''; } diff --git a/frontend/src/utils/printerModel.ts b/frontend/src/utils/printerModel.ts new file mode 100644 index 000000000..1659d79b0 --- /dev/null +++ b/frontend/src/utils/printerModel.ts @@ -0,0 +1,44 @@ +// Map SSDP model codes to display names +export function mapModelCode(ssdpModel: string | null): string { + if (!ssdpModel) return ''; + const modelMap: Record = { + // H2 Series + 'O1D': 'H2D', + 'O1E': 'H2D Pro', + 'O2D': 'H2D Pro', + 'O1C': 'H2C', + 'O1C2': 'H2C', + 'O1S': 'H2S', + // X1 Series + 'BL-P001': 'X1C', + 'BL-P002': 'X1', + 'BL-P003': 'X1E', + // X2 Series + 'N6': 'X2D', + // A2 Series + 'N9': 'A2L', + // P Series + 'C11': 'P1S', + 'C12': 'P1P', + 'C13': 'P2S', + // A1 Series + 'N2S': 'A1', + 'N1': 'A1 Mini', + // Direct matches + 'X1C': 'X1C', + 'X1': 'X1', + 'X1E': 'X1E', + 'X2D': 'X2D', + 'P1S': 'P1S', + 'P1P': 'P1P', + 'P2S': 'P2S', + 'A1': 'A1', + 'A1 Mini': 'A1 Mini', + 'A2L': 'A2L', + 'H2D': 'H2D', + 'H2D Pro': 'H2D Pro', + 'H2C': 'H2C', + 'H2S': 'H2S', + }; + return modelMap[ssdpModel] || ssdpModel; +}