Merge branch '0.2.0b' into main

This commit is contained in:
ZwF
2026-02-13 10:14:28 -04:00
committed by GitHub
36 changed files with 2299 additions and 615 deletions
@@ -347,3 +347,182 @@ describe('computeAmsMapping', () => {
expect(result).toEqual([254]); // External spool global ID
});
});
describe('buildLoadedFilaments - nozzle awareness', () => {
it('sets extruderId from ams_extruder_map', () => {
const status = createPrinterStatus([
{
id: 0,
tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }],
},
{
id: 1,
tray: [{ id: 0, tray_type: 'PETG', tray_color: '00FF00' }],
},
]);
(status as any).ams_extruder_map = { '0': 1, '1': 0 };
const result = buildLoadedFilaments(status);
expect(result[0].extruderId).toBe(1); // AMS 0 → left nozzle
expect(result[1].extruderId).toBe(0); // AMS 1 → right nozzle
});
it('leaves extruderId undefined when no ams_extruder_map', () => {
const status = createPrinterStatus([
{
id: 0,
tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }],
},
]);
const result = buildLoadedFilaments(status);
expect(result[0].extruderId).toBeUndefined();
});
});
describe('computeAmsMapping - nozzle filtering', () => {
it('filters candidates by nozzle_id when set', () => {
// Filament requires left nozzle (extruder 1), only AMS 0 is on left
const reqs = {
filaments: [
{ slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 10, nozzle_id: 1 },
],
};
const status = createPrinterStatus([
{
id: 0, // Left nozzle
tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }],
},
{
id: 1, // Right nozzle
tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }],
},
]);
(status as any).ams_extruder_map = { '0': 1, '1': 0 };
const result = computeAmsMapping(reqs, status);
expect(result).toEqual([0]); // AMS 0, tray 0 (on left nozzle)
});
it('filters to right nozzle when nozzle_id=0', () => {
const reqs = {
filaments: [
{ slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 10, nozzle_id: 0 },
],
};
const status = createPrinterStatus([
{
id: 0, // Left nozzle
tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }],
},
{
id: 1, // Right nozzle
tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }],
},
]);
(status as any).ams_extruder_map = { '0': 1, '1': 0 };
const result = computeAmsMapping(reqs, status);
expect(result).toEqual([4]); // AMS 1, tray 0 (global ID = 1*4+0 = 4, on right nozzle)
});
it('falls back to all trays when target nozzle has no trays at all', () => {
// Requires nozzle_id=1 (left), but no AMS units are on left nozzle
const reqs = {
filaments: [
{ slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 10, nozzle_id: 1 },
],
};
const status = createPrinterStatus([
{
id: 0, // Right nozzle only
tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }],
},
]);
(status as any).ams_extruder_map = { '0': 0 }; // AMS 0 → right nozzle, none on left
const result = computeAmsMapping(reqs, status);
expect(result).toEqual([0]); // Falls back to unfiltered (right nozzle PLA)
});
it('stays restricted when target nozzle has trays but wrong type', () => {
// Left nozzle has PETG, right has PLA — but requires PLA on left
const reqs = {
filaments: [
{ slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 10, nozzle_id: 1 },
],
};
const status = createPrinterStatus([
{
id: 0, // Left nozzle - only PETG
tray: [{ id: 0, tray_type: 'PETG', tray_color: '00FF00' }],
},
{
id: 1, // Right nozzle - has PLA
tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }],
},
]);
(status as any).ams_extruder_map = { '0': 1, '1': 0 };
const result = computeAmsMapping(reqs, status);
expect(result).toEqual([-1]); // No PLA on left nozzle, stays restricted
});
it('skips nozzle filtering when nozzle_id is undefined', () => {
const reqs = {
filaments: [
{ slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 10 }, // No nozzle_id
],
};
const status = createPrinterStatus([
{
id: 0,
tray: [{ id: 0, tray_type: 'PETG', tray_color: '00FF00' }],
},
{
id: 1,
tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }],
},
]);
(status as any).ams_extruder_map = { '0': 1, '1': 0 };
const result = computeAmsMapping(reqs, status);
expect(result).toEqual([4]); // Picks best match regardless of nozzle
});
it('handles dual-nozzle multi-slot mapping', () => {
// Two filaments: one for left, one for right
const reqs = {
filaments: [
{ slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 10, nozzle_id: 1 }, // Left
{ slot_id: 2, type: 'PETG', color: '#00FF00', used_grams: 10, nozzle_id: 0 }, // Right
],
};
const status = createPrinterStatus([
{
id: 0, // Left nozzle
tray: [
{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' },
],
},
{
id: 1, // Right nozzle
tray: [
{ id: 0, tray_type: 'PETG', tray_color: '00FF00' },
],
},
]);
(status as any).ams_extruder_map = { '0': 1, '1': 0 };
const result = computeAmsMapping(reqs, status);
expect(result).toEqual([0, 4]); // Left gets AMS0-T0, Right gets AMS1-T0
});
});
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Circle, Check, AlertTriangle, RefreshCw, ChevronDown, ChevronUp } from 'lucide-react';
import { api } from '../../api/client';
@@ -17,6 +18,7 @@ export function FilamentMapping({
onManualMappingChange,
defaultExpanded = false,
}: FilamentMappingProps & { defaultExpanded?: boolean }) {
const { t } = useTranslation();
const queryClient = useQueryClient();
const [isRefreshing, setIsRefreshing] = useState(false);
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
@@ -32,6 +34,7 @@ export function FilamentMapping({
useFilamentMapping(filamentReqs, printerStatus, manualMappings);
const hasFilamentReqs = filamentReqs?.filaments && filamentReqs.filaments.length > 0;
const isDualNozzle = filamentReqs?.filaments?.some((f) => f.nozzle_id != null) ?? false;
// Don't render if no filament requirements
if (!hasFilamentReqs) {
@@ -126,8 +129,16 @@ export function FilamentMapping({
<span title={`Required: ${item.type} - ${getColorName(item.color)}`}>
<Circle className="w-3 h-3" fill={item.color} stroke={item.color} />
</span>
{/* Required type + grams */}
<span className="text-white truncate">
{/* Required type + grams + nozzle badge */}
<span className="text-white truncate flex items-center gap-1">
{isDualNozzle && item.nozzle_id != null && (
<span
className="inline-flex items-center justify-center w-3.5 h-3.5 rounded text-[9px] font-bold leading-none bg-bambu-gray/20 text-bambu-gray shrink-0"
title={item.nozzle_id === 1 ? t('printModal.leftNozzleTooltip') : t('printModal.rightNozzleTooltip')}
>
{item.nozzle_id === 1 ? t('printModal.leftNozzle') : t('printModal.rightNozzle')}
</span>
)}
{item.type} <span className="text-bambu-gray">({item.used_grams}g)</span>
</span>
{/* Arrow */}
@@ -158,6 +158,7 @@ export interface FilamentReqsData {
color: string;
used_grams: number;
used_meters: number;
nozzle_id?: number;
}>;
}
+27 -44
View File
@@ -15,6 +15,8 @@ import type { PrinterStatus } from '../api/client';
*/
export function buildLoadedFilaments(printerStatus: PrinterStatus | undefined): LoadedFilament[] {
const filaments: LoadedFilament[] = [];
const amsExtruderMap = printerStatus?.ams_extruder_map;
const hasDualNozzle = amsExtruderMap && Object.keys(amsExtruderMap).length > 0;
// Add filaments from all AMS units (regular and HT)
printerStatus?.ams?.forEach((amsUnit) => {
@@ -33,6 +35,7 @@ export function buildLoadedFilaments(printerStatus: PrinterStatus | undefined):
label: formatSlotLabel(amsUnit.id, tray.id, isHt, false),
globalTrayId: getGlobalTrayId(amsUnit.id, tray.id, false),
trayInfoIdx: tray.tray_info_idx || '',
extruderId: amsExtruderMap?.[String(amsUnit.id)],
});
}
});
@@ -52,6 +55,7 @@ export function buildLoadedFilaments(printerStatus: PrinterStatus | undefined):
label: 'External',
globalTrayId: 254,
trayInfoIdx: printerStatus.vt_tray.tray_info_idx || '',
extruderId: hasDualNozzle ? 0 : undefined,
});
}
@@ -90,7 +94,15 @@ export function computeAmsMapping(
const reqTrayInfoIdx = req.tray_info_idx || '';
// Get available trays (not already used)
const available = loadedFilaments.filter((f) => !usedTrayIds.has(f.globalTrayId));
let available = loadedFilaments.filter((f) => !usedTrayIds.has(f.globalTrayId));
// Nozzle-aware filtering: restrict to trays on the correct nozzle
if (req.nozzle_id != null) {
const nozzleFiltered = available.filter((f) => f.extruderId === req.nozzle_id);
if (nozzleFiltered.length > 0) {
available = nozzleFiltered;
}
}
let idxMatch: LoadedFilament | undefined;
let exactMatch: LoadedFilament | undefined;
@@ -191,6 +203,8 @@ export interface LoadedFilament {
globalTrayId: number;
/** Unique spool identifier (e.g., "GFA00", "P4d64437") */
trayInfoIdx?: string;
/** Extruder ID for dual-nozzle printers (0=right, 1=left) */
extruderId?: number;
}
/**
@@ -203,6 +217,8 @@ export interface FilamentRequirement {
used_grams: number;
/** Unique spool identifier from slicing (e.g., "GFA00", "P4d64437") */
tray_info_idx?: string;
/** Target nozzle for dual-nozzle printers (0=right, 1=left) */
nozzle_id?: number;
}
/**
@@ -247,48 +263,7 @@ export function useLoadedFilaments(
printerStatus: PrinterStatus | undefined
): LoadedFilament[] {
return useMemo(() => {
const filaments: LoadedFilament[] = [];
// Add filaments from all AMS units (regular and HT)
printerStatus?.ams?.forEach((amsUnit) => {
const isHt = amsUnit.tray.length === 1; // AMS-HT has single tray
amsUnit.tray.forEach((tray) => {
if (tray.tray_type) {
const color = normalizeColor(tray.tray_color);
filaments.push({
type: tray.tray_type,
color,
colorName: getColorName(color),
amsId: amsUnit.id,
trayId: tray.id,
isHt,
isExternal: false,
label: formatSlotLabel(amsUnit.id, tray.id, isHt, false),
globalTrayId: getGlobalTrayId(amsUnit.id, tray.id, false),
trayInfoIdx: tray.tray_info_idx || '',
});
}
});
});
// Add external spool if loaded
if (printerStatus?.vt_tray?.tray_type) {
const color = normalizeColor(printerStatus.vt_tray.tray_color);
filaments.push({
type: printerStatus.vt_tray.tray_type,
color,
colorName: getColorName(color),
amsId: -1,
trayId: 0,
isHt: false,
isExternal: true,
label: 'External',
globalTrayId: 254,
trayInfoIdx: printerStatus.vt_tray.tray_info_idx || '',
});
}
return filaments;
return buildLoadedFilaments(printerStatus);
}, [printerStatus]);
}
@@ -355,7 +330,15 @@ export function useFilamentMapping(
const reqTrayInfoIdx = req.tray_info_idx || '';
// Get available trays (not already used)
const available = loadedFilaments.filter((f) => !usedTrayIds.has(f.globalTrayId));
let available = loadedFilaments.filter((f) => !usedTrayIds.has(f.globalTrayId));
// Nozzle-aware filtering: restrict to trays on the correct nozzle
if (req.nozzle_id != null) {
const nozzleFiltered = available.filter((f) => f.extruderId === req.nozzle_id);
if (nozzleFiltered.length > 0) {
available = nozzleFiltered;
}
}
let idxMatch: LoadedFilament | undefined;
let exactMatch: LoadedFilament | undefined;
@@ -124,26 +124,32 @@ function computeMatchDetails(
}
}
// Auto-match
const exactMatch = loadedFilaments.find(
// Auto-match with nozzle-aware filtering
let candidates = loadedFilaments.filter((f) => !usedTrayIds.has(f.globalTrayId));
if (req.nozzle_id != null) {
const nozzleFiltered = candidates.filter((f) => f.extruderId === req.nozzle_id);
if (nozzleFiltered.length > 0) {
candidates = nozzleFiltered;
}
}
const exactMatch = candidates.find(
(f) =>
!usedTrayIds.has(f.globalTrayId) &&
f.type?.toUpperCase() === req.type?.toUpperCase() &&
normalizeColorForCompare(f.color) === normalizeColorForCompare(req.color)
);
const similarMatch = exactMatch
? undefined
: loadedFilaments.find(
: candidates.find(
(f) =>
!usedTrayIds.has(f.globalTrayId) &&
f.type?.toUpperCase() === req.type?.toUpperCase() &&
colorsAreSimilar(f.color, req.color)
);
const typeOnlyMatch =
exactMatch || similarMatch
? undefined
: loadedFilaments.find(
(f) => !usedTrayIds.has(f.globalTrayId) && f.type?.toUpperCase() === req.type?.toUpperCase()
: candidates.find(
(f) => f.type?.toUpperCase() === req.type?.toUpperCase()
);
const loaded = exactMatch ?? similarMatch ?? typeOnlyMatch;
@@ -196,26 +202,32 @@ function computeMappingWithOverrides(
continue;
}
// Auto-match
const exactMatch = loadedFilaments.find(
// Auto-match with nozzle-aware filtering
let candidates = loadedFilaments.filter((f) => !usedTrayIds.has(f.globalTrayId));
if (req.nozzle_id != null) {
const nozzleFiltered = candidates.filter((f) => f.extruderId === req.nozzle_id);
if (nozzleFiltered.length > 0) {
candidates = nozzleFiltered;
}
}
const exactMatch = candidates.find(
(f) =>
!usedTrayIds.has(f.globalTrayId) &&
f.type?.toUpperCase() === req.type?.toUpperCase() &&
normalizeColorForCompare(f.color) === normalizeColorForCompare(req.color)
);
const similarMatch = exactMatch
? undefined
: loadedFilaments.find(
: candidates.find(
(f) =>
!usedTrayIds.has(f.globalTrayId) &&
f.type?.toUpperCase() === req.type?.toUpperCase() &&
colorsAreSimilar(f.color, req.color)
);
const typeOnlyMatch =
exactMatch || similarMatch
? undefined
: loadedFilaments.find(
(f) => !usedTrayIds.has(f.globalTrayId) && f.type?.toUpperCase() === req.type?.toUpperCase()
: candidates.find(
(f) => f.type?.toUpperCase() === req.type?.toUpperCase()
);
const loaded = exactMatch ?? similarMatch ?? typeOnlyMatch;
+10
View File
@@ -124,6 +124,7 @@ export default {
nozzleCount: 'Düsenanzahl',
autoArchive: 'Automatische Archivierung',
status: {
available: 'Verfügbar',
idle: 'Bereit',
printing: 'Druckt',
paused: 'Pausiert',
@@ -163,6 +164,7 @@ export default {
},
// Controls
hideOffline: 'Offline ausblenden',
nextAvailable: 'Nächster verfügbar',
powerOn: 'Einschalten',
offlinePrintersWithPlugs: 'Offline-Drucker mit Smart-Plugs',
noPrintersConfigured: 'Noch keine Drucker konfiguriert',
@@ -980,6 +982,7 @@ export default {
removeFromPrinter: 'Von diesem Drucker entfernen',
// Types
types: {
lubricateCarbonRods: 'Karbonstäbe schmieren',
lubricateRails: 'Linearschienen schmieren',
cleanNozzle: 'Düse/Hotend reinigen',
checkBelts: 'Riemenspannung prüfen',
@@ -988,6 +991,7 @@ export default {
checkCooling: 'Kühlungslüfter prüfen',
generalInspection: 'Allgemeine Inspektion',
cleanCarbonRods: 'Kohlenstoffstangen reinigen',
cleanLinearRails: 'Linearschienen reinigen',
checkPtfeTube: 'PTFE-Schlauch prüfen',
replaceHepaFilter: 'HEPA-Filter ersetzen',
replaceCarbonFilter: 'Aktivkohlefilter ersetzen',
@@ -2632,6 +2636,10 @@ export default {
sameTypeDifferentColor: 'Gleicher Typ, andere Farbe',
filamentTypeNotLoaded: 'Filamenttyp nicht geladen',
openCalendar: 'Kalender öffnen',
leftNozzle: 'L',
rightNozzle: 'R',
leftNozzleTooltip: 'Linke Düse',
rightNozzleTooltip: 'Rechte Düse',
},
// Backup
@@ -2973,6 +2981,7 @@ export default {
// Maintenance type descriptions (built-in)
maintenanceDescriptions: {
lubricateCarbonRods: 'Schmiermittel auf Karbonstäbe für sanfte Bewegung auftragen',
lubricateRails: 'Schmiermittel auf Linearschienen für sanfte Bewegung auftragen',
cleanNozzle: 'Hotend und Düse reinigen, um Verstopfungen zu verhindern',
checkBelts: 'Riemenspannung für präzise Drucke überprüfen',
@@ -2981,6 +2990,7 @@ export default {
checkCooling: 'Sicherstellen, dass Lüfter ordnungsgemäß funktionieren',
generalInspection: 'Allgemeine Druckerinspektion',
cleanCarbonRods: 'Karbonstäbe reinigen, um Reibung zu reduzieren',
cleanLinearRails: 'Linearschienen abwischen, um Staub und Schmutz zu entfernen',
checkPtfeTube: 'PTFE-Schlauch auf Verschleiß oder Beschädigung prüfen',
replaceHepaFilter: 'HEPA-Filter für Luftqualität ersetzen',
replaceCarbonFilter: 'Aktivkohlefilter ersetzen',
+10
View File
@@ -124,6 +124,7 @@ export default {
nozzleCount: 'Nozzle Count',
autoArchive: 'Auto Archive',
status: {
available: 'Available',
idle: 'Idle',
printing: 'Printing',
paused: 'Paused',
@@ -163,6 +164,7 @@ export default {
},
// Controls
hideOffline: 'Hide offline',
nextAvailable: 'Next available',
powerOn: 'Power On',
offlinePrintersWithPlugs: 'Offline printers with smart plugs',
noPrintersConfigured: 'No printers configured yet',
@@ -980,6 +982,7 @@ export default {
removeFromPrinter: 'Remove from this printer',
// Types
types: {
lubricateCarbonRods: 'Lubricate Carbon Rods',
lubricateRails: 'Lubricate Linear Rails',
cleanNozzle: 'Clean Nozzle/Hotend',
checkBelts: 'Check Belt Tension',
@@ -988,6 +991,7 @@ export default {
checkCooling: 'Check Cooling Fans',
generalInspection: 'General Inspection',
cleanCarbonRods: 'Clean Carbon Rods',
cleanLinearRails: 'Clean Linear Rails',
checkPtfeTube: 'Check PTFE Tube',
replaceHepaFilter: 'Replace HEPA Filter',
replaceCarbonFilter: 'Replace Carbon Filter',
@@ -2636,6 +2640,10 @@ export default {
sameTypeDifferentColor: 'Same type, different color',
filamentTypeNotLoaded: 'Filament type not loaded',
openCalendar: 'Open calendar',
leftNozzle: 'L',
rightNozzle: 'R',
leftNozzleTooltip: 'Left nozzle',
rightNozzleTooltip: 'Right nozzle',
},
// Backup
@@ -2978,6 +2986,7 @@ export default {
// Maintenance type descriptions (built-in)
maintenanceDescriptions: {
lubricateCarbonRods: 'Apply lubricant to carbon rods for smooth motion',
lubricateRails: 'Apply lubricant to linear rails for smooth motion',
cleanNozzle: 'Clean hotend and nozzle to prevent clogs',
checkBelts: 'Verify belt tension for accurate prints',
@@ -2986,6 +2995,7 @@ export default {
checkCooling: 'Ensure cooling fans are working properly',
generalInspection: 'General printer inspection',
cleanCarbonRods: 'Clean carbon rods to reduce friction',
cleanLinearRails: 'Wipe linear rails to remove dust and debris',
checkPtfeTube: 'Inspect PTFE tube for wear or damage',
replaceHepaFilter: 'Replace HEPA filter for air quality',
replaceCarbonFilter: 'Replace activated carbon filter',
+10
View File
@@ -121,6 +121,7 @@ export default {
nozzleCount: 'Numero Ugelli',
autoArchive: 'Auto Archiviazione',
status: {
available: 'Disponibile',
idle: 'Inattiva',
printing: 'In stampa',
paused: 'In pausa',
@@ -160,6 +161,7 @@ export default {
},
// Controls
hideOffline: 'Nascondi offline',
nextAvailable: 'Prossima disponibile',
powerOn: 'Accendi',
offlinePrintersWithPlugs: 'Stampanti offline con smart plug',
noPrintersConfigured: 'Nessuna stampante configurata',
@@ -967,6 +969,7 @@ export default {
removeFromPrinter: 'Rimuovi da questa stampante',
// Types
types: {
lubricateCarbonRods: 'Lubrifica aste in carbonio',
lubricateRails: 'Lubrifica guide lineari',
cleanNozzle: 'Pulisci ugello/Hotend',
checkBelts: 'Controlla tensione cinghie',
@@ -975,6 +978,7 @@ export default {
checkCooling: 'Controlla ventole raffreddamento',
generalInspection: 'Ispezione generale',
cleanCarbonRods: 'Pulisci aste in carbonio',
cleanLinearRails: 'Pulisci guide lineari',
checkPtfeTube: 'Controlla tubo PTFE',
replaceHepaFilter: 'Sostituisci filtro HEPA',
replaceCarbonFilter: 'Sostituisci filtro carbone',
@@ -2306,6 +2310,10 @@ export default {
noPrintersAvailable: 'Nessuna stampante disponibile',
printerBusy: 'Stampante occupata',
printerOffline: 'Stampante offline',
leftNozzle: 'L',
rightNozzle: 'R',
leftNozzleTooltip: 'Ugello sinistro',
rightNozzleTooltip: 'Ugello destro',
},
// Backup
@@ -2647,6 +2655,7 @@ export default {
// Maintenance type descriptions (built-in)
maintenanceDescriptions: {
lubricateCarbonRods: 'Applica lubrificante alle aste in carbonio per un movimento fluido',
lubricateRails: 'Applica lubrificante alle guide lineari per un movimento fluido',
cleanNozzle: 'Pulisci hotend e ugello per prevenire intasamenti',
checkBelts: 'Verifica tensione cinghie per stampe accurate',
@@ -2655,6 +2664,7 @@ export default {
checkCooling: 'Assicurati che le ventole di raffreddamento funzionino',
generalInspection: 'Ispezione generale stampante',
cleanCarbonRods: 'Pulisci le aste in carbonio per ridurre attrito',
cleanLinearRails: 'Pulisci le guide lineari per rimuovere polvere e detriti',
checkPtfeTube: 'Ispeziona il tubo PTFE per usura o danni',
replaceHepaFilter: 'Sostituisci filtro HEPA per qualità aria',
replaceCarbonFilter: 'Sostituisci filtro a carbone attivo',
+10
View File
@@ -133,6 +133,7 @@ export default {
nozzleCount: 'ノズル数',
autoArchive: '自動アーカイブ',
status: {
available: '利用可能',
idle: '待機中',
printing: '印刷中',
paused: '一時停止',
@@ -167,6 +168,7 @@ export default {
extraLarge: '特大',
},
hideOffline: 'オフラインを非表示',
nextAvailable: '次に完了',
powerOn: '電源オン',
noPrintersConfigured: 'プリンターが設定されていません',
readyToPrint: '印刷可能',
@@ -1015,6 +1017,7 @@ export default {
noPrintersAssigned: 'プリンター未割り当て',
removeFromPrinter: 'このプリンターから削除',
types: {
lubricateCarbonRods: 'カーボンロッドの潤滑',
lubricateRails: 'リニアレールの潤滑',
cleanNozzle: 'ノズル/ホットエンドの清掃',
checkBelts: 'ベルト張力の確認',
@@ -1023,6 +1026,7 @@ export default {
checkCooling: '冷却ファンの確認',
generalInspection: '総合点検',
cleanCarbonRods: 'カーボンロッドの清掃',
cleanLinearRails: 'リニアレールの清掃',
checkPtfeTube: 'PTFEチューブの確認',
replaceHepaFilter: 'HEPAフィルター交換',
replaceCarbonFilter: 'カーボンフィルター交換',
@@ -2552,6 +2556,10 @@ export default {
printerBusy: 'プリンターは使用中です',
printerOffline: 'プリンターはオフラインです',
cancel: 'キャンセル',
leftNozzle: 'L',
rightNozzle: 'R',
leftNozzleTooltip: '左ノズル',
rightNozzleTooltip: '右ノズル',
},
backup: {
restoreBackup: 'バックアップの復元',
@@ -2856,6 +2864,7 @@ export default {
},
},
maintenanceDescriptions: {
lubricateCarbonRods: 'カーボンロッドに潤滑剤を塗布してスムーズな動きを確保',
lubricateRails: 'リニアレールの潤滑',
cleanNozzle: 'ノズル/ホットエンドの清掃',
checkBelts: 'ベルト張力の確認',
@@ -2864,6 +2873,7 @@ export default {
checkCooling: '冷却ファンの確認',
generalInspection: '総合点検',
cleanCarbonRods: 'カーボンロッドの清掃',
cleanLinearRails: 'リニアレールを拭いてほこりや汚れを除去',
checkPtfeTube: 'PTFEチューブの確認',
replaceHepaFilter: 'HEPAフィルター交換',
replaceCarbonFilter: 'カーボンフィルター交換',
+19 -9
View File
@@ -140,14 +140,19 @@ function getMaintenanceWikiUrl(typeName: string, printerModel: string | null): s
const isP2S = model.includes('P2S');
switch (typeName) {
case 'Lubricate Linear Rails':
case 'Lubricate Carbon Rods':
// X1, P1, P2S series have carbon rods
if (isX1) return 'https://wiki.bambulab.com/en/x1/maintenance/basic-maintenance';
if (isP1) return 'https://wiki.bambulab.com/en/p1/maintenance/p1p-maintenance';
if (isP2S) return 'https://wiki.bambulab.com/en/p2s/maintenance/belt-tension';
return null;
case 'Lubricate Linear Rails':
// A1 and H2 series have linear rails
if (isA1Mini) return 'https://wiki.bambulab.com/en/a1-mini/maintenance/lubricate-y-axis';
if (isA1) return 'https://wiki.bambulab.com/en/a1/maintenance/lubricate-y-axis';
if (isH2) return 'https://wiki.bambulab.com/en/h2/maintenance/x-axis-lubrication';
if (isP2S) return 'https://wiki.bambulab.com/en/p2s/maintenance/belt-tension'; // P2S maintenance page
return 'https://wiki.bambulab.com/en/general/lead-screws-lubrication';
return null;
case 'Clean Nozzle/Hotend':
if (isX1 || isP1) return 'https://wiki.bambulab.com/en/x1/troubleshooting/nozzle-clog';
@@ -168,11 +173,16 @@ function getMaintenanceWikiUrl(typeName: string, printerModel: string | null): s
return 'https://wiki.bambulab.com/en/x1/maintenance/belt-tension';
case 'Clean Carbon Rods':
// Only X1 and P1 series have carbon rods
if (isX1 || isP1) return 'https://wiki.bambulab.com/en/general/carbon-rods-clearance';
// A1, H2, P2S don't have carbon rods - return null
if (isA1Mini || isA1 || isH2 || isP2S) return null;
return 'https://wiki.bambulab.com/en/general/carbon-rods-clearance';
// X1, P1, P2S series have carbon rods
if (isX1 || isP1 || isP2S) return 'https://wiki.bambulab.com/en/general/carbon-rods-clearance';
return null;
case 'Clean Linear Rails':
// A1 and H2 series have linear rails
if (isA1Mini) return 'https://wiki.bambulab.com/en/a1-mini/maintenance/lubricate-y-axis';
if (isA1) return 'https://wiki.bambulab.com/en/a1/maintenance/lubricate-y-axis';
if (isH2) return 'https://wiki.bambulab.com/en/h2/maintenance/x-axis-lubrication';
return null;
case 'Clean Build Plate':
// Same for all printers
@@ -381,7 +391,7 @@ function PrinterSection({
hasPermission: (permission: Permission) => boolean;
t: TFunction;
}) {
const [expanded, setExpanded] = useState(true);
const [expanded, setExpanded] = useState(false);
const [editingHours, setEditingHours] = useState(false);
const [hoursInput, setHoursInput] = useState(overview.total_print_hours.toFixed(1));
+66 -41
View File
@@ -1137,21 +1137,17 @@ function getWifiStrength(rssi: number): { labelKey: string; color: string; bars:
}
/**
* Check if a tray contains a Bambu Lab spool.
* Uses same logic as backend: tray_info_idx (GF*), tray_uuid, or tag_uid.
* Check if a tray contains a Bambu Lab spool (RFID-tagged).
* Only checks hardware identifiers (tray_uuid, tag_uid) — NOT tray_info_idx,
* which is a filament profile/preset ID that third-party spools also get when
* the user selects a generic Bambu preset (e.g. "GFA00" for Generic PLA).
*/
function isBambuLabSpool(tray: {
tray_uuid?: string | null;
tag_uid?: string | null;
tray_info_idx?: string | null;
} | null | undefined): boolean {
if (!tray) return false;
// Check tray_info_idx first (most reliable - Bambu preset IDs start with "GF")
if (tray.tray_info_idx && tray.tray_info_idx.startsWith('GF')) {
return true;
}
// Check tray_uuid (32 hex chars, non-zero)
if (tray.tray_uuid && tray.tray_uuid !== '00000000000000000000000000000000') {
return true;
@@ -1240,39 +1236,16 @@ function StatusSummaryBar({ printers }: { printers: Printer[] | undefined }) {
const { t } = useTranslation();
const queryClient = useQueryClient();
const counts = useMemo(() => {
let printing = 0;
let idle = 0;
let offline = 0;
let loading = 0;
printers?.forEach((printer) => {
const status = queryClient.getQueryData<{ connected: boolean; state: string | null }>(['printerStatus', printer.id]);
if (status === undefined) {
// Status not yet loaded - don't count as offline yet
loading++;
} else if (!status.connected) {
offline++;
} else if (status.state === 'RUNNING') {
printing++;
} else {
idle++;
}
});
return { printing, idle, offline, loading, total: (printers?.length || 0) };
}, [printers, queryClient]);
// Subscribe to query cache changes to re-render when status updates
// Throttled to prevent rapid re-renders from causing tab crashes
const [, setTick] = useState(0);
const [cacheTick, setCacheTick] = useState(0);
useEffect(() => {
let pending = false;
const unsubscribe = queryClient.getQueryCache().subscribe(() => {
if (!pending) {
pending = true;
requestAnimationFrame(() => {
setTick(t => t + 1);
setCacheTick(t => t + 1);
pending = false;
});
}
@@ -1280,10 +1253,53 @@ function StatusSummaryBar({ printers }: { printers: Printer[] | undefined }) {
return () => unsubscribe();
}, [queryClient]);
const { counts, nextFinish } = useMemo(() => {
let printing = 0;
let idle = 0;
let offline = 0;
let loading = 0;
let nextPrinterName: string | null = null;
let nextRemainingMin: number | null = null;
let nextProgress: number = 0;
printers?.forEach((printer) => {
const status = queryClient.getQueryData<{ connected: boolean; state: string | null; remaining_time: number | null; progress: number | null }>(['printerStatus', printer.id]);
if (status === undefined) {
// Status not yet loaded - don't count as offline yet
loading++;
} else if (!status.connected) {
offline++;
} else if (status.state === 'RUNNING') {
printing++;
if (status.remaining_time != null && status.remaining_time > 0) {
if (nextRemainingMin === null || status.remaining_time < nextRemainingMin) {
nextRemainingMin = status.remaining_time;
nextPrinterName = printer.name;
nextProgress = status.progress || 0;
}
}
} else {
idle++;
}
});
return {
counts: { printing, idle, offline, loading, total: (printers?.length || 0) },
nextFinish: nextPrinterName && nextRemainingMin ? { name: nextPrinterName, remainingMin: nextRemainingMin, progress: nextProgress } : null,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [printers, queryClient, cacheTick]);
if (!printers?.length) return null;
return (
<div className="flex items-center gap-4 text-sm">
<div className="flex items-center gap-1.5">
<div className={`w-2 h-2 rounded-full ${counts.idle > 0 ? 'bg-bambu-green' : 'bg-gray-500'}`} />
<span className="text-bambu-gray">
<span className="text-white font-medium">{counts.idle}</span> {t('printers.status.available').toLowerCase()}
</span>
</div>
{counts.printing > 0 && (
<div className="flex items-center gap-1.5">
<div className="w-2 h-2 rounded-full bg-bambu-green animate-pulse" />
@@ -1292,14 +1308,6 @@ function StatusSummaryBar({ printers }: { printers: Printer[] | undefined }) {
</span>
</div>
)}
{counts.idle > 0 && (
<div className="flex items-center gap-1.5">
<div className="w-2 h-2 rounded-full bg-blue-400" />
<span className="text-bambu-gray">
<span className="text-white font-medium">{counts.idle}</span> {t('printers.status.idle').toLowerCase()}
</span>
</div>
)}
{counts.offline > 0 && (
<div className="flex items-center gap-1.5">
<div className="w-2 h-2 rounded-full bg-gray-400" />
@@ -1308,6 +1316,23 @@ function StatusSummaryBar({ printers }: { printers: Printer[] | undefined }) {
</span>
</div>
)}
{nextFinish && (
<>
<div className="w-px h-4 bg-bambu-dark-tertiary" />
<div className="flex items-center gap-2">
<span className="text-bambu-green font-medium">{t('printers.nextAvailable')}:</span>
<span className="text-white font-medium">{nextFinish.name}</span>
<div className="w-16 bg-bambu-dark-tertiary rounded-full h-1.5">
<div
className="bg-bambu-green h-1.5 rounded-full transition-all"
style={{ width: `${nextFinish.progress}%` }}
/>
</div>
<span className="text-white font-medium">{Math.round(nextFinish.progress)}%</span>
<span className="text-bambu-gray">({formatTime(nextFinish.remainingMin * 60)})</span>
</div>
</>
)}
</div>
);
}