From b63adb815121ae1a70c1d23544979a4948d62f89 Mon Sep 17 00:00:00 2001 From: maziggy Date: Tue, 11 Aug 2026 08:45:33 +0200 Subject: [PATCH] fix(virtual-printer): wrap the card header instead of overflowing it (#2808) Every item in the collapsed header was flex-shrink-0, so the row was as wide as its contents and the Card doesn't clip -- the remote-interface IP and the enable toggle painted outside the card border. The name's `truncate` couldn't save it: a flex item defaults to min-width:auto, so it never shrank below its text (`flex-shrink-0 truncate` on the target name was self-cancelling for the same reason). Move the metadata into a flex-1 min-w-0 flex-wrap group so it wraps to a second line, and keep the chevron, dot and toggle outside it. Wrapping rather than truncating: the bind and remote-interface addresses are what the page exists to show. Needs three things at once, hence the report -- both IPs set (Bambuddy and printer on different subnets), a target named "Printer at " from discovery, and the 3-column card grid. overflow-hidden is scoped to this card, not added to Card: half the cards in the app render menus that deliberately paint outside their bounds. --- CHANGELOG.md | 1 + .../components/VirtualPrinterCard.test.tsx | 87 +++++++++++++++++++ .../src/components/VirtualPrinterCard.tsx | 64 ++++++++++---- .../{index-Bmu-wyBZ.js => index-DhpDphGT.js} | 2 +- static/index.html | 2 +- 5 files changed, 136 insertions(+), 20 deletions(-) rename static/assets/{index-Bmu-wyBZ.js => index-DhpDphGT.js} (99%) diff --git a/CHANGELOG.md b/CHANGELOG.md index d240daa45..62148073b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ All notable changes to Bambuddy will be documented in this file. - **Error and warning toasts now stay up twice as long** — Every pop-up notification disappeared after three seconds regardless of what it said. That is about right for "Settings saved", which confirms something you just did and is skimmed rather than read, but errors and warnings are a different kind of message: they carry a reason, often one relayed from the printer or the backend, and they run to a couple of lines. Three seconds was not long enough to finish reading one, and a missed error message is gone for good — there is no notification history to go back to. Errors and warnings now hold for six seconds. Success and informational toasts keep the three-second default, so the common case of clicking something and seeing it confirmed is unchanged, and the close button and the manual dismiss work exactly as before on all of them. The background print-dispatch toast is unaffected: it stays up while it has work in progress and clears itself shortly after the last job settles. Covered by frontend tests. ### Fixed +- **A virtual printer's address spilled outside its card on the Virtual Printer page (#2808, reported by @underscan)** — The collapsed header of each virtual-printer card lays its details out in one row: name, mode, model, the printer it proxies to, and the IP addresses it is bound to. Every item in that row was marked as not shrinkable, so the row was always as wide as its contents, and the card does not clip what does not fit — the last IP address and the enable toggle were drawn outside the card's border, on the page background. The name was supposed to shorten with an ellipsis to prevent this, but could not: an item in a row like that will not shrink below its own text unless it is explicitly allowed to, so the ellipsis never appeared. It takes three things at once to run out of room, which is why it took a report to surface: both address fields are filled in only when Bambuddy and the printer are on different subnets (the reporter runs two VLANs), a printer adopted without a name is called "Printer at
", and the cards sit three to a row on a wide screen. The details now wrap onto a second line inside the card when they do not fit. Nothing is hidden or shortened away — these are the addresses an operator opens the page to check — and the card clips at its own border as a backstop. - **Restoring a SQLite backup into PostgreSQL failed part-way with a foreign-key violation on the file library** — The restore rebuilds the schema and then imports every table, and it is meant to create those tables carrying no foreign keys at all, so that the order rows arrive in cannot matter; the constraints go back on once everything has landed. It did that by taking the keys off the application's own schema description before creating the tables, which only suppresses the `REFERENCES` clause written inside each `CREATE TABLE`. SQLAlchemy keeps a second view of a table's keys, derived from its columns, and that one was untouched -- so whenever it met a group of tables whose dependencies form a loop and could not put them in order, it fell back to adding their keys afterwards with `ALTER TABLE`, reading them from exactly the view that still had them. `library_files`, `library_folders` and `print_archives` all point at one another, so twelve constraints came back across those three tables. The same loop also costs them their place in the import order, so they were imported alphabetically instead -- which puts `library_files` ahead of the `library_folders` rows its `folder_id` refers to, and PostgreSQL refused the very first batch. The restore now drops the foreign keys in the database itself, after the tables exist, instead of trying to stop them being written: it no longer matters how a constraint came to be there, and a future loop between other tables cannot bring the problem back. One more fault went with it -- the keys were stripped from a schema description shared by the whole running process and only restored after the rebuild had finished, so a failure in between left the process without them until it was restarted. Verified end to end against a real PostgreSQL: a backup whose child rows import before their parents now restores cleanly, with all ninety constraints in place afterwards. Separately, the warning about keys that genuinely cannot be put back -- because the backup itself holds rows pointing at something that no longer exists -- now names them by the columns they link, such as `print_archives(library_file_id) -> library_files.id`. These constraints have no name of their own, so the warning used to print `print_archives.None` once for each of that table's five keys, which said nothing about where to look; the offending value PostgreSQL reports is logged alongside it. The restore itself is unaffected, and so is the data -- those columns are simply left unenforced. Covered by backend tests. - **The slice dialog showed a guessed filament list for projects saved by a newer Bambu Studio than the slicer sidecar** — Opening the slice dialog on an unsliced project runs a quick preview slice, purely to ask the slicer which AMS slots the chosen plate actually consumes. Bambu Studio 2.8 writes a machine G-code template containing `{if timelapse_inline_photo}` but does not export a definition for that variable, so the template is unresolvable the moment it leaves Studio: a sidecar running an older build stops with a placeholder parse error before producing any slice data. The preview then returned nothing and the dialog quietly fell back to guessing from the file's painted faces, with no indication the numbers were an estimate. On the H2D project this was found with, the guess dropped a whole slot -- the support material -- from a four-filament plate. Bambuddy now retries the preview once with just that one unparsable template emptied, leaving every other setting in the file untouched, which is what keeps the answer honest: the process settings, support configuration and per-slot filament assignments are all still the project's own, so the filament list and its gram figures match what the file would really print. Only templates that cannot extrude are ever emptied -- a start or filament-change template lays a prime line or purges, so silencing one would change the very grams the preview reports, and Bambuddy would rather return nothing than a confident wrong number. Verified against a real H2D slice: the retry reproduces the full four-slot list, gram for gram. Covered by backend tests. - **Cancelling or deleting a queued item did not stop the preheat already running for it (#2727, contributed by @ticfinack)** — The cancel and delete routes write the item's new status to the database, and nothing else. A dispatch that had already begun preheating was parked in a sleep waiting for the chamber to reach temperature, where it could not see that write — so the heaters kept running out the rest of the wait and soak for a print that was not going to happen, twenty minutes at the default settings and longer if those have been raised, and the printer stayed marked busy the whole time, holding up every other job queued behind it. Those routes now tell the scheduler directly and the waits are taken in slices, so a cancelled preheat is abandoned within seconds and the heaters are switched off on the way out. The same unwinding now covers every other way a dispatch can end without starting a print — a failed upload, an error mid-dispatch, a plate whose file has gone missing — each of which used to leave the bed and chamber heating with nothing to turn them off. What preheat set is recorded and reversed, and a bed the printer reports at some other target is left alone rather than switched off, on the grounds that it belongs to whoever set it. A cancellation that lands after the upload has begun still starts the print, as it always has. Covered by backend tests. diff --git a/frontend/src/__tests__/components/VirtualPrinterCard.test.tsx b/frontend/src/__tests__/components/VirtualPrinterCard.test.tsx index 3d0c06f06..65f2a18dc 100644 --- a/frontend/src/__tests__/components/VirtualPrinterCard.test.tsx +++ b/frontend/src/__tests__/components/VirtualPrinterCard.test.tsx @@ -486,3 +486,90 @@ describe('VirtualPrinterCard - access code inherits from target', () => { expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument(); }); }); + +// The collapsed header used to be a single flex row of flex-shrink-0 items, +// so it was as wide as its contents and spilled past the card's border -- +// the reporter of #2808 saw an IP address and the enable toggle floating on +// the page background. jsdom does no layout, so these pin the CSS contract +// that makes wrapping possible rather than the pixels: a flexible, wrappable +// metadata group, and no item that both refuses to shrink and claims it will +// truncate. +describe('VirtualPrinterCard - header does not overflow the card', () => { + const printers = [ + { + id: 9, + // The name a printer adopted without one gets (discovery.py) -- 25 + // unbreakable characters, and half of why this overflowed. + name: 'Printer at 192.168.30.210', + ip_address: '192.168.30.210', + access_code: 'TGTCODE1', + serial_number: '01P00A391800001', + model: 'H2C', + is_active: true, + }, + ]; + + const twoVlanPrinter = () => + createMockPrinter({ + name: 'H2C', + mode: 'proxy', + model_name: 'H2C', + target_printer_id: 9, + // Both are populated only when Bambuddy and the printer sit on + // different subnets -- the reporter's case. + bind_ip: '192.168.20.175', + remote_interface_ip: '192.168.30.210', + }); + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(multiVirtualPrinterApi.update).mockResolvedValue(createMockPrinter()); + vi.mocked(api.getPrinters).mockResolvedValue(printers as unknown as Awaited>); + }); + + it('keeps both IPs visible instead of truncating them away', async () => { + render(); + + await waitFor(() => { + expect(screen.getByText('192.168.20.175')).toBeInTheDocument(); + }); + // An operator opens this page to check exactly these two values, so the + // fix has to wrap them, not hide them. + expect(screen.getByText('192.168.30.210')).toBeInTheDocument(); + }); + + it('puts the metadata in a shrinkable, wrapping group', async () => { + render(); + + const bindIp = await screen.findByText('192.168.20.175'); + const group = bindIp.parentElement as HTMLElement; + + expect(group.className).toContain('flex-wrap'); + // Without min-w-0 a flex item cannot shrink below its content, which is + // what defeated the name's `truncate` before. + expect(group.className).toContain('min-w-0'); + expect(group.className).toContain('flex-1'); + }); + + it('has no header item that is both unshrinkable and truncating', async () => { + render(); + + const bindIp = await screen.findByText('192.168.20.175'); + const group = bindIp.parentElement as HTMLElement; + + const selfCancelling = Array.from(group.children).filter( + (el) => el.className.includes('flex-shrink-0') && el.className.includes('truncate') + ); + expect(selfCancelling).toHaveLength(0); + }); + + it('clips at the card border as a backstop', async () => { + const { container } = render(); + + await screen.findByText('192.168.20.175'); + // Scoped to this card on purpose: cards elsewhere render menus that paint + // outside their own bounds, so this must not migrate into `Card`. + const card = container.querySelector('.rounded-xl') as HTMLElement; + expect(card.className).toContain('overflow-hidden'); + }); +}); diff --git a/frontend/src/components/VirtualPrinterCard.tsx b/frontend/src/components/VirtualPrinterCard.tsx index 0d0ff5f5d..ff888dec9 100644 --- a/frontend/src/components/VirtualPrinterCard.tsx +++ b/frontend/src/components/VirtualPrinterCard.tsx @@ -255,7 +255,14 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps) return ( <> - + {/* + Clip at the border as a backstop, so a value longer than anything + anticipated above lands inside the card instead of on the page + background (#2808). Scoped here rather than added to `Card` itself: + half the cards in the app render dropdowns and menus that deliberately + paint outside their bounds, and this one has none. + */} + {/* Collapsed header - always visible, clickable to expand */}
- {printer.name} - {modeLabel} - {printer.model_name && ( - {printer.model_name} - )} - {targetPrinterName && ( - - {localMode === 'proxy' && } - {targetPrinterName} - - )} - {localBindIp && ( - {localBindIp} - )} - {localRemoteInterfaceIp && ( - {localRemoteInterfaceIp} - )} + {/* + Metadata wraps rather than overflowing (#2808). Every item in this + row used to be flex-shrink-0, so nothing could give and the row was + as wide as its contents -- which the Card doesn't clip, so the last + IP and the toggle were painted outside the card's border. The name's + `truncate` didn't save it either: a flex item defaults to + min-width:auto, so it couldn't shrink below its text and the + ellipsis never engaged (`flex-shrink-0 truncate` on the target name + was self-cancelling for the same reason). + + It needs three things at once to overflow, which is why it took a + two-VLAN setup to surface: bind_ip and remote_interface_ip are both + set only when Bambuddy and the printer are on different subnets, a + printer adopted without a name is called "Printer at " (25 + characters of unbreakable text), and the cards sit in a 3-column + grid. Wrapping keeps every value readable -- these are the addresses + an operator came to this page to check, so truncating them away + would trade one bug for a quieter one. + */} +
+ {printer.name} + {modeLabel} + {printer.model_name && ( + {printer.model_name} + )} + {targetPrinterName && ( + + {localMode === 'proxy' && } + {targetPrinterName} + + )} + {localBindIp && ( + {localBindIp} + )} + {localRemoteInterfaceIp && ( + {localRemoteInterfaceIp} + )} +
e.stopPropagation()}>