mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-19 07:33:18 +02:00
Firmware version badge on printer cards (#311)
- Show firmware version badge on printer card (green=up to date, orange=update available) with click to view release notes or start update workflow - Badge only shown when firmware checking is enabled in settings - Modal adapts: info-only view when up to date (release notes auto-expanded), update workflow when update available - Gate badge query with firmware:read permission, upload button with firmware:update permission
This commit is contained in:
@@ -21,6 +21,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- **H2C Nozzle Rack — Translate Type Codes & Add Flow Info** ([#300](https://github.com/maziggy/bambuddy/issues/300)) — Raw nozzle type codes (e.g. "HS", "HH01") are now translated to human-readable names: material (Hardened Steel, Stainless Steel, Tungsten Carbide) and flow type (High Flow, Standard). New "Flow" row in the hover card. Translations added in all 4 locales (en, de, ja, it).
|
||||
- **H2C Nozzle Rack — Show Filament Material in Hover Card** ([#300](https://github.com/maziggy/bambuddy/issues/300)) — Nozzle hover card now shows the loaded filament material type (e.g. "PLA", "PETG") alongside the color swatch, captured from MQTT nozzle info data.
|
||||
- **H2C Nozzle Rack Compact Layout** ([#300](https://github.com/maziggy/bambuddy/issues/300)) — Redesigned nozzle rack from a 2×3 grid to a compact single-row layout with bottom accent bars (green = mounted, gray = docked). Temperature cards are thinner, rack card is wider (flex-[2]), and all cards vertically centered.
|
||||
- **Firmware Version Badge on Printer Card** ([#311](https://github.com/maziggy/bambuddy/issues/311)) — Printer cards now show a firmware version badge (when firmware checking is enabled). Green with checkmark when up to date, orange with download icon when an update is available. Clicking the badge opens a firmware info modal showing release notes (auto-expanded when up to date) or the existing update workflow. Badge and modal respect `firmware:read` and `firmware:update` permissions. Translations added in all 4 locales.
|
||||
- **Auto-Detect Subnet for Printer Discovery** — Docker users no longer need to manually enter a subnet in the Add Printer dialog. Bambuddy auto-detects available network subnets and pre-selects the first one. When multiple subnets are available (e.g., eth0 + wlan0), a dropdown lets users choose. Falls back to manual text input if no subnets are detected.
|
||||
- **Japanese Locale Complete Overhaul** — Restructured `ja.ts` from a divergent format (different key structure, 12 structural conflicts, 1,366 missing translations) to match the English/German locale structure exactly. Translated all 2,083 keys into Japanese, achieving full parity with EN/DE. Zero structural divergences, zero missing keys.
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ Perfect for remote print farms, traveling makers, or accessing your home printer
|
||||
- Interval reminders (hours/days)
|
||||
- Print time accuracy stats
|
||||
- File manager for printer storage
|
||||
- Firmware update helper (LAN-only printers)
|
||||
- Firmware update helper with version badge (LAN-only printers)
|
||||
- Debug logging toggle with live indicator
|
||||
- Live application log viewer with filtering
|
||||
- Support bundle generator with comprehensive diagnostics (privacy-filtered)
|
||||
|
||||
@@ -183,4 +183,96 @@ describe('PrintersPage', () => {
|
||||
expect(disabledPrinter).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('firmware version badge', () => {
|
||||
const firmwareUpToDate = {
|
||||
printer_id: 1,
|
||||
current_version: '01.09.00.00',
|
||||
latest_version: '01.09.00.00',
|
||||
update_available: false,
|
||||
download_url: null,
|
||||
release_notes: 'Bug fixes and improvements.',
|
||||
};
|
||||
|
||||
const firmwareUpdateAvailable = {
|
||||
printer_id: 1,
|
||||
current_version: '01.08.00.00',
|
||||
latest_version: '01.09.00.00',
|
||||
update_available: true,
|
||||
download_url: 'https://example.com/firmware.bin',
|
||||
release_notes: 'New features added.',
|
||||
};
|
||||
|
||||
it('shows green badge when firmware is up to date', async () => {
|
||||
server.use(
|
||||
http.get('/api/v1/firmware/updates/:id', () => {
|
||||
return HttpResponse.json(firmwareUpToDate);
|
||||
}),
|
||||
http.get('/api/v1/settings/', () => {
|
||||
return HttpResponse.json({
|
||||
check_printer_firmware: true,
|
||||
auto_archive: true,
|
||||
save_thumbnails: true,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
render(<PrintersPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('01.09.00.00').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const badge = screen.getAllByText('01.09.00.00')[0].closest('button');
|
||||
expect(badge).toBeInTheDocument();
|
||||
expect(badge?.className).toContain('text-status-ok');
|
||||
});
|
||||
|
||||
it('shows orange badge when firmware update is available', async () => {
|
||||
server.use(
|
||||
http.get('/api/v1/firmware/updates/:id', () => {
|
||||
return HttpResponse.json(firmwareUpdateAvailable);
|
||||
}),
|
||||
http.get('/api/v1/settings/', () => {
|
||||
return HttpResponse.json({
|
||||
check_printer_firmware: true,
|
||||
auto_archive: true,
|
||||
save_thumbnails: true,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
render(<PrintersPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('01.08.00.00').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const badge = screen.getAllByText('01.08.00.00')[0].closest('button');
|
||||
expect(badge).toBeInTheDocument();
|
||||
expect(badge?.className).toContain('text-orange-400');
|
||||
});
|
||||
|
||||
it('hides badge when firmware check is disabled', async () => {
|
||||
server.use(
|
||||
http.get('/api/v1/settings/', () => {
|
||||
return HttpResponse.json({
|
||||
check_printer_firmware: false,
|
||||
auto_archive: true,
|
||||
save_thumbnails: true,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
render(<PrintersPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('X1 Carbon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Version should not appear when firmware check is disabled
|
||||
expect(screen.queryByText('01.09.00.00')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('01.08.00.00')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -378,6 +378,7 @@ export default {
|
||||
openCameraWindow: 'Kamera in neuem Fenster öffnen',
|
||||
// Firmware
|
||||
firmwareUpdateAvailable: 'Firmware-Update verfügbar: {{current}} → {{latest}}',
|
||||
firmwareUpToDate: 'Firmware {{version}} — Aktuell',
|
||||
firmwareUpdateButton: 'Update',
|
||||
// Plate detection
|
||||
plateDetection: {
|
||||
@@ -414,6 +415,7 @@ export default {
|
||||
// Firmware modal
|
||||
firmwareModal: {
|
||||
title: 'Firmware-Update',
|
||||
titleUpToDate: 'Firmware-Info',
|
||||
currentVersion: 'Aktuell:',
|
||||
latestVersion: 'Neueste:',
|
||||
releaseNotes: 'Versionshinweise',
|
||||
@@ -428,6 +430,7 @@ export default {
|
||||
done: 'Fertig',
|
||||
starting: 'Starte...',
|
||||
uploadFirmware: 'Firmware hochladen',
|
||||
uploadFailed: 'Upload fehlgeschlagen: {{error}}',
|
||||
uploadedToast: 'Firmware hochgeladen! Starten Sie das Update vom Druckerbildschirm.',
|
||||
},
|
||||
accessCodePlaceholder: 'Leer lassen, um den aktuellen zu behalten',
|
||||
|
||||
@@ -378,6 +378,7 @@ export default {
|
||||
openCameraWindow: 'Open camera in new window',
|
||||
// Firmware
|
||||
firmwareUpdateAvailable: 'Firmware update available: {{current}} → {{latest}}',
|
||||
firmwareUpToDate: 'Firmware {{version}} — Up to date',
|
||||
firmwareUpdateButton: 'Update',
|
||||
// Plate detection
|
||||
plateDetection: {
|
||||
@@ -414,6 +415,7 @@ export default {
|
||||
// Firmware modal
|
||||
firmwareModal: {
|
||||
title: 'Firmware Update',
|
||||
titleUpToDate: 'Firmware Info',
|
||||
currentVersion: 'Current:',
|
||||
latestVersion: 'Latest:',
|
||||
releaseNotes: 'Release Notes',
|
||||
@@ -428,6 +430,7 @@ export default {
|
||||
done: 'Done',
|
||||
starting: 'Starting...',
|
||||
uploadFirmware: 'Upload Firmware',
|
||||
uploadFailed: 'Failed to start upload: {{error}}',
|
||||
uploadedToast: 'Firmware uploaded! Trigger update from printer screen.',
|
||||
},
|
||||
accessCodePlaceholder: 'Leave empty to keep current',
|
||||
|
||||
@@ -378,6 +378,7 @@ export default {
|
||||
openCameraWindow: 'Apri camera in nuova finestra',
|
||||
// Firmware
|
||||
firmwareUpdateAvailable: 'Aggiornamento firmware disponibile: {{current}} → {{latest}}',
|
||||
firmwareUpToDate: 'Firmware {{version}} — Aggiornato',
|
||||
firmwareUpdateButton: 'Aggiorna',
|
||||
// Plate detection
|
||||
plateDetection: {
|
||||
@@ -414,6 +415,7 @@ export default {
|
||||
// Firmware modal
|
||||
firmwareModal: {
|
||||
title: 'Aggiornamento Firmware',
|
||||
titleUpToDate: 'Info Firmware',
|
||||
currentVersion: 'Corrente:',
|
||||
latestVersion: 'Ultima:',
|
||||
releaseNotes: 'Note di rilascio',
|
||||
@@ -428,6 +430,7 @@ export default {
|
||||
done: 'Fatto',
|
||||
starting: 'Avvio...',
|
||||
uploadFirmware: 'Carica Firmware',
|
||||
uploadFailed: 'Avvio caricamento fallito: {{error}}',
|
||||
uploadedToast: 'Firmware caricato! Avvia aggiornamento dal display.',
|
||||
},
|
||||
accessCodePlaceholder: 'Lascia vuoto per mantenere quello attuale',
|
||||
|
||||
@@ -335,6 +335,7 @@ export default {
|
||||
},
|
||||
filaments: 'フィラメント',
|
||||
firmwareUpdateAvailable: 'ファームウェアアップデートあり: {{current}} → {{latest}}',
|
||||
firmwareUpToDate: 'ファームウェア {{version}} — 最新',
|
||||
plateDetection: {
|
||||
noPermission: 'このページにアクセスする権限がありません。',
|
||||
title: 'プレート検出',
|
||||
@@ -377,7 +378,8 @@ export default {
|
||||
estimatedCompletion: '完了予定時刻',
|
||||
slotOptions: 'スロットオプション',
|
||||
firmwareModal: {
|
||||
title: 'プリンター',
|
||||
title: 'ファームウェアアップデート',
|
||||
titleUpToDate: 'ファームウェア情報',
|
||||
currentVersion: '現在のバージョン',
|
||||
latestVersion: '最新バージョン',
|
||||
releaseNotes: 'リリースノート',
|
||||
@@ -392,6 +394,7 @@ export default {
|
||||
uploadFirmware: 'ファームウェアをアップロード',
|
||||
checkingPrereqs: '前提条件を確認中...',
|
||||
uploadedSuccess: 'ファームウェアをSDカードにアップロードしました!',
|
||||
uploadFailed: 'アップロード開始に失敗しました: {{error}}',
|
||||
uploadedToast: 'ファームウェアをアップロードしました!プリンター画面からアップデートを実行してください。',
|
||||
},
|
||||
accessCodePlaceholder: 'プリンター設定から取得',
|
||||
|
||||
@@ -1260,7 +1260,7 @@ function PrinterCard({
|
||||
queryFn: () => firmwareApi.checkPrinterUpdate(printer.id),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
refetchInterval: 5 * 60 * 1000,
|
||||
enabled: checkPrinterFirmware,
|
||||
enabled: checkPrinterFirmware && hasPermission('firmware:read'),
|
||||
});
|
||||
|
||||
// Collect unique tray_info_idx values for cloud filament info lookup
|
||||
@@ -1991,15 +1991,23 @@ function PrinterCard({
|
||||
{queueCount}
|
||||
</button>
|
||||
)}
|
||||
{/* Firmware Update Badge */}
|
||||
{firmwareInfo?.update_available && (
|
||||
{/* Firmware Version Badge */}
|
||||
{firmwareInfo?.current_version && (
|
||||
<button
|
||||
onClick={() => setShowFirmwareModal(true)}
|
||||
className="flex items-center gap-1 px-2 py-1 rounded-full text-xs bg-orange-500/20 text-orange-400 hover:opacity-80 transition-opacity"
|
||||
title={t('printers.firmwareUpdateAvailable', { current: firmwareInfo.current_version, latest: firmwareInfo.latest_version })}
|
||||
className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs hover:opacity-80 transition-opacity ${
|
||||
firmwareInfo.update_available
|
||||
? 'bg-orange-500/20 text-orange-400'
|
||||
: 'bg-status-ok/20 text-status-ok'
|
||||
}`}
|
||||
title={
|
||||
firmwareInfo.update_available
|
||||
? t('printers.firmwareUpdateAvailable', { current: firmwareInfo.current_version, latest: firmwareInfo.latest_version })
|
||||
: t('printers.firmwareUpToDate', { version: firmwareInfo.current_version })
|
||||
}
|
||||
>
|
||||
<Download className="w-3 h-3" />
|
||||
{t('printers.firmwareUpdateButton')}
|
||||
{firmwareInfo.update_available ? <Download className="w-3 h-3" /> : <CheckCircle className="w-3 h-3" />}
|
||||
{firmwareInfo.current_version}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -4045,15 +4053,18 @@ function FirmwareUpdateModal({
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const { showToast } = useToast();
|
||||
const { hasPermission } = useAuth();
|
||||
const canUpdate = hasPermission('firmware:update');
|
||||
const [uploadStatus, setUploadStatus] = useState<FirmwareUploadStatus | null>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [pollInterval, setPollInterval] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Prepare check query
|
||||
// Prepare check query (only when update available and user can update)
|
||||
const { data: prepareInfo, isLoading: isPreparing } = useQuery({
|
||||
queryKey: ['firmwarePrepare', printer.id],
|
||||
queryFn: () => firmwareApi.prepareUpload(printer.id),
|
||||
staleTime: 30000,
|
||||
enabled: firmwareInfo.update_available && canUpdate,
|
||||
});
|
||||
|
||||
// Start upload mutation
|
||||
@@ -4082,7 +4093,7 @@ function FirmwareUpdateModal({
|
||||
setPollInterval(interval);
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
showToast(`Failed to start upload: ${error.message}`, 'error');
|
||||
showToast(t('printers.firmwareModal.uploadFailed', { error: error.message }), 'error');
|
||||
setIsUploading(false);
|
||||
},
|
||||
});
|
||||
@@ -4104,11 +4115,15 @@ function FirmwareUpdateModal({
|
||||
<Card className="w-full max-w-md mx-4">
|
||||
<CardContent>
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<div className="p-2 rounded-full bg-orange-500/20">
|
||||
<Download className="w-5 h-5 text-orange-400" />
|
||||
<div className={`p-2 rounded-full ${firmwareInfo.update_available ? 'bg-orange-500/20' : 'bg-status-ok/20'}`}>
|
||||
{firmwareInfo.update_available
|
||||
? <Download className="w-5 h-5 text-orange-400" />
|
||||
: <CheckCircle className="w-5 h-5 text-status-ok" />}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-white">{t('printers.firmwareModal.title')}</h3>
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
{firmwareInfo.update_available ? t('printers.firmwareModal.title') : t('printers.firmwareModal.titleUpToDate')}
|
||||
</h3>
|
||||
<p className="text-sm text-bambu-gray mt-1">
|
||||
{printer.name}
|
||||
</p>
|
||||
@@ -4119,15 +4134,19 @@ function FirmwareUpdateModal({
|
||||
<div className="bg-bambu-dark rounded-lg p-3 mb-4">
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-bambu-gray">{t('printers.firmwareModal.currentVersion')}</span>
|
||||
<span className="text-white font-mono">{firmwareInfo.current_version || t('common.unknown')}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-sm mt-1">
|
||||
<span className="text-bambu-gray">{t('printers.firmwareModal.latestVersion')}</span>
|
||||
<span className="text-orange-400 font-mono">{firmwareInfo.latest_version}</span>
|
||||
<span className={`font-mono ${firmwareInfo.update_available ? 'text-white' : 'text-status-ok'}`}>
|
||||
{firmwareInfo.current_version || t('common.unknown')}
|
||||
</span>
|
||||
</div>
|
||||
{firmwareInfo.update_available && (
|
||||
<div className="flex justify-between items-center text-sm mt-1">
|
||||
<span className="text-bambu-gray">{t('printers.firmwareModal.latestVersion')}</span>
|
||||
<span className="text-orange-400 font-mono">{firmwareInfo.latest_version}</span>
|
||||
</div>
|
||||
)}
|
||||
{firmwareInfo.release_notes && (
|
||||
<details className="mt-3 text-sm">
|
||||
<summary className="text-orange-400 cursor-pointer hover:underline">
|
||||
<details className="mt-3 text-sm" open={!firmwareInfo.update_available}>
|
||||
<summary className={`cursor-pointer hover:underline ${firmwareInfo.update_available ? 'text-orange-400' : 'text-status-ok'}`}>
|
||||
{t('printers.firmwareModal.releaseNotes')}
|
||||
</summary>
|
||||
<div className="mt-2 text-bambu-gray text-xs max-h-40 overflow-y-auto whitespace-pre-wrap">
|
||||
@@ -4137,8 +4156,8 @@ function FirmwareUpdateModal({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status / Progress */}
|
||||
{isPreparing ? (
|
||||
{/* Status / Progress (only when update available) */}
|
||||
{!firmwareInfo.update_available ? null : isPreparing ? (
|
||||
<div className="flex items-center gap-2 text-bambu-gray text-sm mb-4">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
{t('printers.firmwareModal.checkingPrereqs')}
|
||||
@@ -4209,7 +4228,7 @@ function FirmwareUpdateModal({
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
{uploadStatus?.status === 'complete' ? t('printers.firmwareModal.done') : t('common.cancel')}
|
||||
</Button>
|
||||
{prepareInfo?.can_proceed && !isUploading && uploadStatus?.status !== 'complete' && (
|
||||
{prepareInfo?.can_proceed && !isUploading && uploadStatus?.status !== 'complete' && canUpdate && (
|
||||
<Button
|
||||
onClick={handleStartUpload}
|
||||
disabled={uploadMutation.isPending}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -23,8 +23,8 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-BJkEGpKa.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Csb7GscS.css">
|
||||
<script type="module" crossorigin src="/assets/index-BAz5WA4r.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Az-fQOvm.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user