fix(queue): enforce sliced-model compatibility on cross-model dispatch (#2578)

A queue item's "Any <model>" button labeled itself from the file's slice
metadata while the scheduler used the row's target_model, so an X1C-sliced
item targeting H2D showed "Any X1C" above "assign to first idle H2D". The
mismatch itself was created silently: sliced-for metadata loads async, and
switching to model mode before it arrived pre-selected the alphabetically
first model (H2D on a mixed farm), after which the model dropdown hid
itself. Nothing validated compatibility, so the scheduler would hand X1C
G-code to an H2D.

Frontend: never default the target silently, keep the dropdown visible in
model mode (incompatible models disabled), label from the actual target,
warn on mismatch, block submit when incompatible.

Backend: new GCODE_COMPAT_FAMILIES table (X1/X1C/X1E/P1P/P1S interchange;
everything else exact-match; missing metadata never blocks). Queue create
and update reject incompatible targets with 400; the scheduler holds back
pre-existing mismatched rows with an actionable waiting_reason instead of
dispatching them.
This commit is contained in:
maziggy
2026-07-17 06:51:51 +02:00
parent a6e7d671f2
commit e97413edc7
12 changed files with 729 additions and 174 deletions
+22
View File
@@ -18,6 +18,28 @@ export function getPrinterImage(model: string | null | undefined): string {
return '/img/printers/default.png';
}
// G-code interchange families (#2578). Mirrors backend GCODE_COMPAT_FAMILIES
// in backend/app/utils/printer_models.py — keep the two in sync. A sliced 3MF
// may target a different model ONLY within its family; everything else is
// exact-match only.
const GCODE_COMPAT_FAMILIES: ReadonlyArray<ReadonlySet<string>> = [
new Set(['X1', 'X1C', 'X1E', 'P1P', 'P1S']),
];
/** True when G-code sliced for one model may be dispatched to the other.
* Unknown/missing metadata on either side returns true (can't validate). */
export function isGcodeCompatible(
slicedForModel: string | null | undefined,
targetModel: string | null | undefined,
): boolean {
if (!slicedForModel || !targetModel) return true;
const norm = (m: string) => m.trim().toUpperCase().replace(/[\s-]/g, '');
const a = norm(slicedForModel);
const b = norm(targetModel);
if (a === b) return true;
return GCODE_COMPAT_FAMILIES.some((family) => family.has(a) && family.has(b));
}
export function getWifiStrength(rssi: number): { labelKey: string; color: string; bars: number } {
if (rssi >= -50) return { labelKey: 'printers.wifiSignal.excellent', color: 'text-bambu-green', bars: 4 };
if (rssi >= -60) return { labelKey: 'printers.wifiSignal.good', color: 'text-bambu-green', bars: 3 };