From cc7bbd6fbee1f70810be623fb9e143061f66f9c6 Mon Sep 17 00:00:00 2001 From: maziggy Date: Tue, 10 Feb 2026 11:53:42 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + README.md | 2 +- .../src/__tests__/pages/PrintersPage.test.tsx | 92 ++++++++++++++ frontend/src/i18n/locales/de.ts | 3 + frontend/src/i18n/locales/en.ts | 3 + frontend/src/i18n/locales/it.ts | 3 + frontend/src/i18n/locales/ja.ts | 5 +- frontend/src/pages/PrintersPage.tsx | 63 ++++++---- static/assets/index-Az-fQOvm.css | 1 + .../{index-BJkEGpKa.js => index-BAz5WA4r.js} | 116 +++++++++--------- static/assets/index-Csb7GscS.css | 1 - static/index.html | 4 +- 12 files changed, 209 insertions(+), 85 deletions(-) create mode 100644 static/assets/index-Az-fQOvm.css rename static/assets/{index-BJkEGpKa.js => index-BAz5WA4r.js} (84%) delete mode 100644 static/assets/index-Csb7GscS.css diff --git a/CHANGELOG.md b/CHANGELOG.md index bacd4f8e6..86cdf98f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 6f5256805..64e601283 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/frontend/src/__tests__/pages/PrintersPage.test.tsx b/frontend/src/__tests__/pages/PrintersPage.test.tsx index a2ccaa07d..966ca9834 100644 --- a/frontend/src/__tests__/pages/PrintersPage.test.tsx +++ b/frontend/src/__tests__/pages/PrintersPage.test.tsx @@ -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(); + + 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(); + + 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(); + + 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(); + }); + }); }); diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index 3a34bf2ff..c32296f15 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -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', diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 106fdb2a0..905b99194 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -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', diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index e0d935333..d68316f00 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -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', diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index 7cb7524e2..6c3f61eaf 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -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: 'プリンター設定から取得', diff --git a/frontend/src/pages/PrintersPage.tsx b/frontend/src/pages/PrintersPage.tsx index 06c7f97fe..ebc5c603d 100644 --- a/frontend/src/pages/PrintersPage.tsx +++ b/frontend/src/pages/PrintersPage.tsx @@ -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} )} - {/* Firmware Update Badge */} - {firmwareInfo?.update_available && ( + {/* Firmware Version Badge */} + {firmwareInfo?.current_version && ( )} @@ -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(null); const [isUploading, setIsUploading] = useState(false); const [pollInterval, setPollInterval] = useState(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({
-
- +
+ {firmwareInfo.update_available + ? + : }
-

{t('printers.firmwareModal.title')}

+

+ {firmwareInfo.update_available ? t('printers.firmwareModal.title') : t('printers.firmwareModal.titleUpToDate')} +

{printer.name}

@@ -4119,15 +4134,19 @@ function FirmwareUpdateModal({
{t('printers.firmwareModal.currentVersion')} - {firmwareInfo.current_version || t('common.unknown')} -
-
- {t('printers.firmwareModal.latestVersion')} - {firmwareInfo.latest_version} + + {firmwareInfo.current_version || t('common.unknown')} +
+ {firmwareInfo.update_available && ( +
+ {t('printers.firmwareModal.latestVersion')} + {firmwareInfo.latest_version} +
+ )} {firmwareInfo.release_notes && ( -
- +
+ {t('printers.firmwareModal.releaseNotes')}
@@ -4137,8 +4156,8 @@ function FirmwareUpdateModal({ )}
- {/* Status / Progress */} - {isPreparing ? ( + {/* Status / Progress (only when update available) */} + {!firmwareInfo.update_available ? null : isPreparing ? (
{t('printers.firmwareModal.checkingPrereqs')} @@ -4209,7 +4228,7 @@ function FirmwareUpdateModal({ - {prepareInfo?.can_proceed && !isUploading && uploadStatus?.status !== 'complete' && ( + {prepareInfo?.can_proceed && !isUploading && uploadStatus?.status !== 'complete' && canUpdate && (