mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-08 23:21:58 +02:00
Name a spool by its subtype on the slot it is assigned to
A spool's subtype is half of what it is called: "PLA" and "PLA Wood" are
different filaments. The AMS slot's hover card built the assigned-spool
line out of brand, material and colour name and left the subtype out, so
a roll of Bambu PLA Wood Classic Birch in an H2C's A4 was announced as
"Bambu Lab PLA - Classic Birch".
Everything else named it correctly at the same moment -- the RFID read,
the inventory row, the slot's own profile line, which is built from the
spool's slicer preset rather than reassembled, and Bambu Studio -- so the
one wrong line read like a bad tag read rather than a display fault.
It was not only the render. The card's assignedSpool prop had no subtype
field at all, and the six places the printer card fills it in -- regular
AMS, AMS-HT and external spool, each in both Spoolman and internal-
inventory mode -- never passed one, so the value could not reach the
component. The field is required rather than optional, which is what
stops the next call site from quietly omitting it; that omission is the
whole of this bug.
Three more surfaces rebuilt the name the same way and are fixed with it:
the SpoolBuddy AMS slot panel in both inventory modes, and the write-tag
confirmation. Every other place a spool is named -- the assign dialogs,
the inventory cards, the forecast rows, the label picker -- already
included the subtype, so these four were the outliers.
This is the display-side half of #2902, which stopped the backend
reducing a filled or foamed filament onto its base material. The card was
doing the same thing to the same spools, one layer further out.
This commit is contained in:
@@ -14,6 +14,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- **The Windows installer build is split in two so a signing request can wait for a human (SignPath Foundation)** — Release tags are Authenticode-signed through the SignPath Foundation OSS programme, and the production certificate does not sign on demand the way the self-signed test certificate does: every request has to be approved by hand in the SignPath UI, because the Foundation verifies what is being signed and which build it came from. The submitting action waits for that approval with a default timeout of 600 seconds, which is ample when the test policy approves automatically in seconds and far too short once the wait is a person noticing a tag went out. A tag pushed at night would have failed the run ten minutes later with the installer already compiled and thrown away. The compile now ends in its own job that uploads the unsigned artifact and stops; a second job downloads it, signs it, and does the release-facing work, with the wait raised to an hour. Because the artifact is uploaded before the wait begins and is addressed by id, a missed approval window is recovered by re-running the second job alone rather than rebuilding the installer — which is the reason to separate them rather than simply raise the timeout in place. The second job runs for unsigned builds too, so the daily prereleases that are deliberately left unsigned to preserve the signing quota keep going out through exactly one set of alias, artifact and release steps. The property that matters is unchanged and now recorded next to the steps that depend on it: none of the alias, upload or release-attach steps carry `always()`, so GitHub skips all three when signing fails or times out, and an unsigned `.exe` cannot reach a release. Nothing about the signed output changes, and the restructure behaves identically under the test policy — the request simply completes immediately instead of waiting — so it can be proven green before the production certificate arrives.
|
||||
|
||||
### Fixed
|
||||
- **A wood-filled spool was named as plain PLA on the slot it was assigned to** — A spool's subtype is half of what it is called: "PLA" and "PLA Wood" are different filaments, and the AMS slot's hover card built the assigned-spool line out of brand, material and colour name with the subtype left out. A roll of Bambu PLA Wood Classic Birch in an H2C's A4 was therefore announced as "Bambu Lab PLA - Classic Birch". Everything else named it correctly at the same moment — the RFID read, the inventory row, the slot's own profile line, which is built from the spool's slicer preset rather than reassembled, and Bambu Studio — so the one wrong line read like a bad tag read rather than a display fault. It was not only the render: the card's `assignedSpool` prop had no subtype field at all, and the six places the printer card fills it in (regular AMS, AMS-HT and external spool, each in both Spoolman and internal-inventory mode) never passed one, so the value could not reach the component. The field is required now rather than optional, which is what stops the next call site from quietly omitting it — that omission is the whole of this bug. Three more surfaces were rebuilding the name the same way and are fixed with it: the SpoolBuddy AMS slot panel in both inventory modes, and the write-tag confirmation. Every other place a spool is named — the assign dialogs, the inventory cards, the forecast rows, the label picker — already included the subtype, so these four were the outliers. This is the display-side half of #2902, which stopped the backend reducing a filled or foamed filament onto its base material; the card was doing the same thing to the same spools, one layer further out.
|
||||
- **The bundled chamber-preheat table was unreadable to the code that reads it** — every lookup of the per-filament chamber map happens after the keys are upper-cased, but an install that had never opened the setting got the bundled table back exactly as declared, with its lowercase `default` row. The scheduler then looked for `DEFAULT`, found nothing, and used a hardcoded 0 for any filament without a row of its own. It reported the right number only because that bundled default is 0 — raising it would have silently changed nothing for everyone who had not customised the map. Both paths out of the parser now honour the one contract it documents.
|
||||
- **The archives API never reported which plate was printed (#2796, contributed by @sgiffhorn)** — `archive_to_response()` builds the archives response field by field and had no line for `plate_id`, so `GET /archives/`, the detail endpoint, search, PATCH and the project archive list all answered `plate_id: null` — for archives whose column was populated as well. `ArchiveResponse.plate_id` defaults to `None`, so Pydantic filled the null in without complaint and nothing ever raised. The column has been written since #2603 and is backfilled from the queue on startup, so the plate was recorded all along and simply could not be read back out; on the reporting instance 181 of 257 rows carry one. Archive cards now name the plate again — but only when it is not the first one. The queue records a plate for single-plate files too, because the print dialog auto-selects the only plate there is, so labelling every archive that has a `plate_id` would have put "Plate 1" on most cards in the grid and taken room from the truncated print name. A multi-plate archive printed from its first plate still identifies itself through the plate carousel, which has always gated on whether the source 3MF holds more than one plate.
|
||||
- **The first AMS sync after enabling Spoolman from Settings failed on every slot (#2903, diagnosed by @ojimpo)** — Spoolman rejects a spool whose `extra` dict carries a key it has not been told about, answering HTTP 400 `Unknown extra field tag.`. Bambuddy stores the tray UUID in `extra.tag`, so that key has to be registered before the first spool is created — and registration only ever ran from three hand-maintained lists that fire when the integration is *set up*: the Connect button, application startup, and two inline blocks in the inventory routes. Enabling Spoolman from the Settings page reaches none of them, so "Sync AMS Data" reported `Synced 0 spools with 3 errors` with vendor and filament creation succeeding and only spool creation rejected. Restarting Bambuddy cleared it, which made the failure look like a connectivity problem. The Connect button would also have fixed it, but it is not on screen by then: saving the settings initialises the Spoolman client as a side effect of syncing locations, the status endpoint reads any live client as "connected", and the UI shows the Connect button only while disconnected — so the one registration path reachable from the interface hides itself exactly when it is needed. Registration now travels with the write instead of the feature: every spool write that carries an `extra` dict registers the keys it is about to send, once per client, before it sends them. That covers all five places Bambuddy writes a tag — AMS sync, linking and unlinking a tag from a spool, and both inventory edit paths — and it closes the class rather than the instance, since a write that carries a key is now a write that registers it. `bambu_color_name` is the cautionary case: it never made it into the Connect or startup lists at all, and worked only because two call sites remembered to register it by hand. Registration stays best-effort — if it fails, the write is still attempted and reports exactly what it reported before, and the failure is not cached, so a Spoolman that was merely restarting is retried on the next write.
|
||||
|
||||
@@ -205,6 +205,7 @@ describe('FilamentHoverCard', () => {
|
||||
assignedSpool: {
|
||||
id: 1,
|
||||
material: 'PLA',
|
||||
subtype: null,
|
||||
brand: 'Devil Design',
|
||||
color_name: 'Black',
|
||||
},
|
||||
@@ -280,6 +281,7 @@ describe('FilamentHoverCard', () => {
|
||||
const inventorySpool = {
|
||||
id: 42,
|
||||
material: 'PLA',
|
||||
subtype: null,
|
||||
brand: 'eSun',
|
||||
color_name: 'Black',
|
||||
};
|
||||
@@ -420,7 +422,7 @@ describe('FilamentHoverCard', () => {
|
||||
<FilamentHoverCard
|
||||
data={baseFilamentData}
|
||||
inventory={{
|
||||
assignedSpool: { id: 7, material: 'PLA', brand: 'eSun', color_name: 'Black' },
|
||||
assignedSpool: { id: 7, material: 'PLA', subtype: null, brand: 'eSun', color_name: 'Black' },
|
||||
onUnassignSpool,
|
||||
}}
|
||||
>
|
||||
@@ -607,7 +609,7 @@ describe('FilamentHoverCard colour name (#2875)', () => {
|
||||
data={{ ...whiteMatte, colorName: 'Jade White' }}
|
||||
inventory={{
|
||||
isAssigned: true,
|
||||
assignedSpool: { id: 17, material: 'PLA', brand: 'Bambu Lab', color_name: 'Matte Ivory White' },
|
||||
assignedSpool: { id: 17, material: 'PLA', subtype: null, brand: 'Bambu Lab', color_name: 'Matte Ivory White' },
|
||||
}}
|
||||
>
|
||||
<div>trigger</div>
|
||||
@@ -626,7 +628,7 @@ describe('FilamentHoverCard colour name (#2875)', () => {
|
||||
data={whiteMatte}
|
||||
inventory={{
|
||||
isAssigned: true,
|
||||
assignedSpool: { id: 17, material: 'PLA', brand: 'Bambu Lab', color_name: 'A06-D0' },
|
||||
assignedSpool: { id: 17, material: 'PLA', subtype: null, brand: 'Bambu Lab', color_name: 'A06-D0' },
|
||||
}}
|
||||
>
|
||||
<div>trigger</div>
|
||||
@@ -646,7 +648,7 @@ describe('FilamentHoverCard colour name (#2875)', () => {
|
||||
data={whiteMatte}
|
||||
inventory={{
|
||||
isAssigned: true,
|
||||
assignedSpool: { id: 17, material: 'PLA', brand: 'Bambu Lab', color_name: colorName },
|
||||
assignedSpool: { id: 17, material: 'PLA', subtype: null, brand: 'Bambu Lab', color_name: colorName },
|
||||
}}
|
||||
>
|
||||
<div>trigger</div>
|
||||
@@ -664,7 +666,7 @@ describe('FilamentHoverCard colour name (#2875)', () => {
|
||||
data={whiteMatte}
|
||||
inventory={{
|
||||
isAssigned: true,
|
||||
assignedSpool: { id: 17, material: 'PLA', brand: 'Bambu Lab', color_name: null },
|
||||
assignedSpool: { id: 17, material: 'PLA', subtype: null, brand: 'Bambu Lab', color_name: null },
|
||||
}}
|
||||
>
|
||||
<div>trigger</div>
|
||||
@@ -674,3 +676,58 @@ describe('FilamentHoverCard colour name (#2875)', () => {
|
||||
await waitFor(() => expect(screen.getByText('Ivory White')).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
|
||||
// A spool's subtype is part of its name. Dropping it made a wood-filled roll
|
||||
// read as plain PLA on the slot card, which is the display-side version of
|
||||
// the mistake #2902 fixed on the backend -- and the printer, the inventory
|
||||
// page and Studio all named it correctly at the same time, so the card was
|
||||
// the only thing saying otherwise.
|
||||
describe('FilamentHoverCard assigned spool name', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
__resetColorCatalogForTests();
|
||||
});
|
||||
|
||||
function showAssigned(assignedSpool: {
|
||||
id: number;
|
||||
material: string;
|
||||
subtype: string | null;
|
||||
brand: string | null;
|
||||
color_name: string | null;
|
||||
}) {
|
||||
renderWithHover(
|
||||
<FilamentHoverCard data={baseFilamentData} inventory={{ isAssigned: true, assignedSpool }}>
|
||||
<div>trigger</div>
|
||||
</FilamentHoverCard>
|
||||
);
|
||||
vi.advanceTimersByTime(100);
|
||||
}
|
||||
|
||||
it('names a filled filament by its subtype, not by its base material', async () => {
|
||||
showAssigned({
|
||||
id: 77,
|
||||
material: 'PLA',
|
||||
subtype: 'Wood',
|
||||
brand: 'Bambu Lab',
|
||||
color_name: 'Classic Birch',
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Bambu Lab PLA Wood - Classic Birch')).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('omits the subtype entirely for a spool that has none', async () => {
|
||||
showAssigned({
|
||||
id: 78,
|
||||
material: 'PLA',
|
||||
subtype: null,
|
||||
brand: 'Bambu Lab',
|
||||
color_name: 'Jade White',
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Bambu Lab PLA - Jade White')).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,7 +33,10 @@ interface SpoolmanConfig {
|
||||
interface InventoryConfig {
|
||||
onAssignSpool?: () => void;
|
||||
onUnassignSpool?: () => void;
|
||||
assignedSpool?: { id: number; material: string; brand: string | null; color_name: string | null; remainingWeightGrams?: number | null } | null;
|
||||
// `subtype` is part of the spool's name, not decoration: "PLA" and "PLA Wood"
|
||||
// are different filaments, and a card that prints only the material tells the
|
||||
// user their wood-filled roll is plain PLA (the display-side half of #2902).
|
||||
assignedSpool?: { id: number; material: string; subtype: string | null; brand: string | null; color_name: string | null; remainingWeightGrams?: number | null } | null;
|
||||
isAssigned?: boolean;
|
||||
}
|
||||
|
||||
@@ -401,6 +404,7 @@ export function FilamentHoverCard({ data, children, disabled, className = '', sp
|
||||
<p className="text-xs text-white truncate">
|
||||
{inventory.assignedSpool.brand ? `${inventory.assignedSpool.brand} ` : ''}
|
||||
{inventory.assignedSpool.material}
|
||||
{inventory.assignedSpool.subtype ? ` ${inventory.assignedSpool.subtype}` : ''}
|
||||
{inventory.assignedSpool.color_name ? ` - ${inventory.assignedSpool.color_name}` : ''}
|
||||
</p>
|
||||
<span className="text-[10px] font-mono text-bambu-gray shrink-0">#{inventory.assignedSpool.id}</span>
|
||||
|
||||
@@ -5573,6 +5573,7 @@ function PrinterCard({
|
||||
assignedSpool: spoolmanSpool ? {
|
||||
id: spoolmanSpool.id,
|
||||
material: spoolmanSpool.material,
|
||||
subtype: spoolmanSpool.subtype,
|
||||
brand: spoolmanSpool.brand ?? null,
|
||||
color_name: spoolmanSpool.color_name ?? null,
|
||||
remainingWeightGrams: spoolmanSpool.label_weight
|
||||
@@ -5600,6 +5601,7 @@ function PrinterCard({
|
||||
assignedSpool: assignment?.spool ? {
|
||||
id: assignment.spool.id,
|
||||
material: assignment.spool.material,
|
||||
subtype: assignment.spool.subtype,
|
||||
brand: assignment.spool.brand,
|
||||
color_name: assignment.spool.color_name,
|
||||
remainingWeightGrams: Math.max(0, Math.round(assignment.spool.label_weight - assignment.spool.weight_used)),
|
||||
@@ -5961,6 +5963,7 @@ function PrinterCard({
|
||||
assignedSpool: spoolmanSpool ? {
|
||||
id: spoolmanSpool.id,
|
||||
material: spoolmanSpool.material,
|
||||
subtype: spoolmanSpool.subtype,
|
||||
brand: spoolmanSpool.brand ?? null,
|
||||
color_name: spoolmanSpool.color_name ?? null,
|
||||
remainingWeightGrams: spoolmanSpool.label_weight
|
||||
@@ -5988,6 +5991,7 @@ function PrinterCard({
|
||||
assignedSpool: assignment?.spool ? {
|
||||
id: assignment.spool.id,
|
||||
material: assignment.spool.material,
|
||||
subtype: assignment.spool.subtype,
|
||||
brand: assignment.spool.brand,
|
||||
color_name: assignment.spool.color_name,
|
||||
remainingWeightGrams: Math.max(0, Math.round(assignment.spool.label_weight - assignment.spool.weight_used)),
|
||||
@@ -6234,6 +6238,7 @@ function PrinterCard({
|
||||
assignedSpool: spoolmanSpool ? {
|
||||
id: spoolmanSpool.id,
|
||||
material: spoolmanSpool.material,
|
||||
subtype: spoolmanSpool.subtype,
|
||||
brand: spoolmanSpool.brand ?? null,
|
||||
color_name: spoolmanSpool.color_name ?? null,
|
||||
remainingWeightGrams: spoolmanSpool.label_weight
|
||||
@@ -6261,6 +6266,7 @@ function PrinterCard({
|
||||
assignedSpool: assignment?.spool ? {
|
||||
id: assignment.spool.id,
|
||||
material: assignment.spool.material,
|
||||
subtype: assignment.spool.subtype,
|
||||
brand: assignment.spool.brand,
|
||||
color_name: assignment.spool.color_name,
|
||||
remainingWeightGrams: Math.max(0, Math.round(assignment.spool.label_weight - assignment.spool.weight_used)),
|
||||
|
||||
@@ -756,6 +756,7 @@ export function SpoolBuddyAmsPage() {
|
||||
)}
|
||||
<span className="text-sm text-white">
|
||||
{assignment.spool.brand ? `${assignment.spool.brand} ` : ''}{assignment.spool.material}
|
||||
{assignment.spool.subtype ? ` ${assignment.spool.subtype}` : ''}
|
||||
{assignment.spool.color_name ? ` - ${assignment.spool.color_name}` : ''}
|
||||
</span>
|
||||
<span className="text-[10px] font-mono text-zinc-500 shrink-0 ml-auto">#{assignment.spool.id}</span>
|
||||
@@ -789,6 +790,7 @@ export function SpoolBuddyAmsPage() {
|
||||
)}
|
||||
<span className="text-sm text-white">
|
||||
{spoolmanAssignedSpool.brand ? `${spoolmanAssignedSpool.brand} ` : ''}{spoolmanAssignedSpool.material}
|
||||
{spoolmanAssignedSpool.subtype ? ` ${spoolmanAssignedSpool.subtype}` : ''}
|
||||
{spoolmanAssignedSpool.color_name ? ` - ${spoolmanAssignedSpool.color_name}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1030,6 +1030,7 @@ function NfcStatusPanel({ writeStatus, writeMessage, selectedSpool, tagOnReader,
|
||||
{selectedSpool && (
|
||||
<p className="text-zinc-400 text-sm">
|
||||
{selectedSpool.brand ? `${selectedSpool.brand} ` : ''}{selectedSpool.material}
|
||||
{selectedSpool.subtype ? ` ${selectedSpool.subtype}` : ''}
|
||||
{selectedSpool.color_name ? ` - ${selectedSpool.color_name}` : ''}
|
||||
</p>
|
||||
)}
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -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-e8_8xCtK.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-B6R8pq98.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DjndScv6.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user