Browse Source

fix: disable reprint for archives without 3MF file (#376)

When a print is sent from an external slicer and Bambuddy can't
download the 3MF during auto-archiving, the fallback archive has no
file. Reprinting such archives tried to upload the data directory,
causing a confusing SD card error. Backend now returns a clear 404
for empty file_path and checks is_file() instead of exists(). Frontend
disables Print/Schedule/Open in Slicer for file-less archives with an
explanatory tooltip. Added i18n key in all 5 locales.
maziggy 3 months ago
parent
commit
045326d8ca

+ 1 - 0
CHANGELOG.md

@@ -28,6 +28,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **K-Profile Selection Corrupts Existing Profiles on X1C/P1S** — The `extrusion_cali_sel` command included a `setting_id` field that BambuStudio never sends, causing firmware to mislink calibration data. The `extrusion_cali_set` command was sent unconditionally, overwriting existing profile metadata. Now `setting_id` is removed from selection commands, and `extrusion_cali_set` is only sent when no existing profile is selected (`cali_idx < 0`).
 - **AMS Slot Configure — Black Filament Color Not Pre-Populated** — When re-opening the Configure AMS Slot modal for a slot with black filament, the color field was empty despite the preset and K-profile being correctly pre-selected. The color pre-population logic excluded hex `000000` (black) as a guard against empty slots, but empty slots already skip color data entirely. Removed the unnecessary check so black is now pre-populated like any other color.
 - **Archive List View Not Labeling Failed Prints** ([#365](https://github.com/maziggy/bambuddy/issues/365)) — The archive grid view displayed a red "Failed" / "Cancelled" badge on failed and aborted prints, but the list view had no equivalent indicator. Now shows an inline status badge next to the print name in list view.
+- **Reprint Fails with SD Card Error for Archives Without 3MF File** ([#376](https://github.com/maziggy/bambuddy/issues/376)) — When a print was sent from an external slicer and Bambuddy couldn't download the 3MF from the printer during auto-archiving, the fallback archive had no file. Attempting to reprint such an archive tried to upload the data directory as a file, causing a confusing "SD card error." The backend now returns a clear error for file-less archives, and the frontend disables Print/Schedule/Open in Slicer buttons with a tooltip explaining that the 3MF file is unavailable.
 - **Inventory Spool Weight Resets After Print Completes** — After a print, the usage tracker correctly updated `weight_used` (e.g., +1.6g), but periodic AMS status updates recalculated `weight_used` from the AMS remain% sensor and overwrote the precise value. For small prints on large spools (e.g., 1.6g on 1000g), the AMS remain% stays at 100% (integer resolution = 10g steps), resetting `weight_used` back to 0. The AMS weight sync now only increases `weight_used`, never decreases it, preserving precise values from the usage tracker.
 
 ### Improved

+ 7 - 1
backend/app/api/routes/archives.py

@@ -2738,8 +2738,14 @@ async def reprint_archive(
         raise HTTPException(400, "Printer is not connected")
 
     # Get the sliced 3MF file path
+    if not archive.file_path:
+        raise HTTPException(
+            404,
+            "No 3MF file available for this archive. "
+            "The file could not be downloaded from the printer when the print was recorded.",
+        )
     file_path = settings.base_dir / archive.file_path
-    if not file_path.exists():
+    if not file_path.is_file():
         raise HTTPException(404, "Archive file not found")
 
     # Upload file to printer via FTP

+ 1 - 0
frontend/src/i18n/locales/de.ts

@@ -610,6 +610,7 @@ export default {
       slicedFor: 'Geslict für {{model}}',
       uploadedBy: 'Hochgeladen von',
       noPermissionReprint: 'Sie haben keine Berechtigung, erneut zu drucken',
+      noFileForReprint: 'Keine 3MF-Datei verfügbar — die Datei konnte beim Aufzeichnen des Drucks nicht vom Drucker heruntergeladen werden',
       noPermissionEdit: 'Sie haben keine Berechtigung, Archive zu bearbeiten',
       noPermissionDelete: 'Sie haben keine Berechtigung, Archive zu löschen',
       reprint: 'Drucken',

+ 1 - 0
frontend/src/i18n/locales/en.ts

@@ -610,6 +610,7 @@ export default {
       slicedFor: 'Sliced for {{model}}',
       uploadedBy: 'Uploaded By',
       noPermissionReprint: 'You do not have permission to reprint',
+      noFileForReprint: 'No 3MF file available — the file could not be downloaded from the printer when the print was recorded',
       noPermissionEdit: 'You do not have permission to edit archives',
       noPermissionDelete: 'You do not have permission to delete archives',
       reprint: 'Reprint',

+ 1 - 0
frontend/src/i18n/locales/fr.ts

@@ -610,6 +610,7 @@ export default {
       slicedFor: 'Découpé pour {{model}}',
       uploadedBy: 'Téléversé par',
       noPermissionReprint: 'Pas d\'autorisation de réimpression',
+      noFileForReprint: 'Aucun fichier 3MF disponible — le fichier n\'a pas pu être téléchargé depuis l\'imprimante lors de l\'enregistrement',
       noPermissionEdit: 'Pas d\'autorisation de modification',
       noPermissionDelete: 'Pas d\'autorisation de suppression',
       reprint: 'Réimprimer',

+ 1 - 0
frontend/src/i18n/locales/it.ts

@@ -601,6 +601,7 @@ export default {
       slicedFor: 'Sliced per {{model}}',
       uploadedBy: 'Caricato da',
       noPermissionReprint: 'Non hai il permesso di ristampare',
+      noFileForReprint: 'Nessun file 3MF disponibile — il file non è stato scaricato dalla stampante durante la registrazione',
       noPermissionEdit: 'Non hai il permesso di modificare archivi',
       noPermissionDelete: 'Non hai il permesso di eliminare archivi',
       reprint: 'Ristampa',

+ 1 - 0
frontend/src/i18n/locales/ja.ts

@@ -623,6 +623,7 @@ export default {
       slicedFor: '{{model}}用にスライス',
       uploadedBy: 'アップロード者',
       noPermissionReprint: '再印刷する権限がありません',
+      noFileForReprint: '3MFファイルがありません — 印刷記録時にプリンターからファイルをダウンロードできませんでした',
       noPermissionDelete: 'アーカイブを削除する権限がありません',
       openInBambuStudio: 'スライサーで開く',
       openInBambuStudioToSlice: 'スライサーでスライス',

+ 16 - 12
frontend/src/pages/ArchivesPage.tsx

@@ -303,15 +303,15 @@ function ArchiveCard({
         label: t('archives.menu.print'),
         icon: <Printer className="w-4 h-4" />,
         onClick: () => setShowReprint(true),
-        disabled: !canModify('archives', 'reprint', archive.created_by_id),
-        title: !canModify('archives', 'reprint', archive.created_by_id) ? t('archives.permission.noReprint') : undefined,
+        disabled: !archive.file_path || !canModify('archives', 'reprint', archive.created_by_id),
+        title: !archive.file_path ? t('archives.card.noFileForReprint') : !canModify('archives', 'reprint', archive.created_by_id) ? t('archives.permission.noReprint') : undefined,
       },
       {
         label: t('archives.menu.schedule'),
         icon: <Calendar className="w-4 h-4" />,
         onClick: () => setShowSchedule(true),
-        disabled: !hasPermission('queue:create'),
-        title: !hasPermission('queue:create') ? t('archives.permission.noAddToQueue') : undefined,
+        disabled: !archive.file_path || !hasPermission('queue:create'),
+        title: !archive.file_path ? t('archives.card.noFileForReprint') : !hasPermission('queue:create') ? t('archives.permission.noAddToQueue') : undefined,
       },
       {
         label: t('archives.menu.openInBambuStudio'),
@@ -321,6 +321,8 @@ function ArchiveCard({
           const downloadUrl = `${window.location.origin}${api.getArchiveForSlicer(archive.id, filename)}`;
           openInSlicer(downloadUrl, preferredSlicer);
         },
+        disabled: !archive.file_path,
+        title: !archive.file_path ? t('archives.card.noFileForReprint') : undefined,
       },
     ] : [
       {
@@ -926,8 +928,8 @@ function ArchiveCard({
                 size="sm"
                 className="flex-1 min-w-0"
                 onClick={() => setShowReprint(true)}
-                disabled={!canModify('archives', 'reprint', archive.created_by_id)}
-                title={!canModify('archives', 'reprint', archive.created_by_id) ? t('archives.card.noPermissionReprint') : undefined}
+                disabled={!archive.file_path || !canModify('archives', 'reprint', archive.created_by_id)}
+                title={!archive.file_path ? t('archives.card.noFileForReprint') : !canModify('archives', 'reprint', archive.created_by_id) ? t('archives.card.noPermissionReprint') : undefined}
               >
                 <Printer className="w-3 h-3 flex-shrink-0" />
                 <span className="hidden sm:inline">{t('archives.card.reprint')}</span>
@@ -937,8 +939,8 @@ function ArchiveCard({
                 size="sm"
                 className="flex-1 min-w-0"
                 onClick={() => setShowSchedule(true)}
-                disabled={!hasPermission('queue:create')}
-                title={!hasPermission('queue:create') ? t('archives.permission.noAddToQueue') : t('archives.card.schedulePrint')}
+                disabled={!archive.file_path || !hasPermission('queue:create')}
+                title={!archive.file_path ? t('archives.card.noFileForReprint') : !hasPermission('queue:create') ? t('archives.permission.noAddToQueue') : t('archives.card.schedulePrint')}
               >
                 <Calendar className="w-3 h-3 flex-shrink-0" />
                 <span className="hidden sm:inline">{t('archives.card.schedule')}</span>
@@ -1438,15 +1440,15 @@ function ArchiveListRow({
         label: t('archives.menu.print'),
         icon: <Printer className="w-4 h-4" />,
         onClick: () => setShowReprint(true),
-        disabled: !canModify('archives', 'reprint', archive.created_by_id),
-        title: !canModify('archives', 'reprint', archive.created_by_id) ? t('archives.permission.noReprint') : undefined,
+        disabled: !archive.file_path || !canModify('archives', 'reprint', archive.created_by_id),
+        title: !archive.file_path ? t('archives.card.noFileForReprint') : !canModify('archives', 'reprint', archive.created_by_id) ? t('archives.permission.noReprint') : undefined,
       },
       {
         label: t('archives.menu.schedule'),
         icon: <Calendar className="w-4 h-4" />,
         onClick: () => setShowSchedule(true),
-        disabled: !hasPermission('queue:create'),
-        title: !hasPermission('queue:create') ? t('archives.permission.noAddToQueue') : undefined,
+        disabled: !archive.file_path || !hasPermission('queue:create'),
+        title: !archive.file_path ? t('archives.card.noFileForReprint') : !hasPermission('queue:create') ? t('archives.permission.noAddToQueue') : undefined,
       },
       {
         label: t('archives.menu.openInBambuStudio'),
@@ -1456,6 +1458,8 @@ function ArchiveListRow({
           const downloadUrl = `${window.location.origin}${api.getArchiveForSlicer(archive.id, filename)}`;
           openInSlicer(downloadUrl, preferredSlicer);
         },
+        disabled: !archive.file_path,
+        title: !archive.file_path ? t('archives.card.noFileForReprint') : undefined,
       },
     ] : [
       {

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-DgABqL-m.js


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-C9JxyacH.js"></script>
+    <script type="module" crossorigin src="/assets/index-DgABqL-m.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-OqmBOPoC.css">
   </head>
   <body>

Some files were not shown because too many files changed in this diff