Browse Source

Fix archive card showing "Source" badge for sliced .3mf files

  The isSlicedFile() check only matched .gcode or .gcode.3mf extensions,
  so archives from .3mf prints (the standard Bambu slicer output) showed
  a "SOURCE" badge instead of "GCODE". Since .3mf can be either sliced or
  a raw CAD export, now checks the archive's total_layers and
  print_time_seconds metadata to distinguish them. Also passes the
  original filename when creating archives from file manager prints.
maziggy 2 months ago
parent
commit
19a1a7584b

+ 1 - 0
CHANGELOG.md

@@ -21,6 +21,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Inventory Scale Weight Check Column** — Added a "Weight Check" column (hidden by default) to the inventory table that compares each spool's last scale measurement against its calculated gross weight (net remaining + core weight). Spools within a ±50g tolerance show a green checkmark; mismatched spools show a yellow warning with the difference and a sync button that trusts the scale reading and resets weight tracking. The backend stores `last_scale_weight` and `last_weighed_at` on each spool whenever weight is synced via SpoolBuddy, and the column tooltip shows scale weight, calculated weight, and difference. Edge case: when scale weight is below core weight (empty spool or not on scale), the comparison treats it as a match since sync can't correct this.
 
 ### Fixed
+- **Archive Card Shows "Source" Badge for Sliced .3mf Files** — Archive cards created from prints showed a "SOURCE" badge instead of "GCODE" when the filename was a plain `.3mf` (without `.gcode` in the name). The `isSlicedFile()` check only matched `.gcode` or `.gcode.3mf` extensions, but `.3mf` files can be either sliced (contains gcode) or raw source models. Now checks the archive's `total_layers` and `print_time_seconds` metadata — if either is present, the file is sliced. Also passes the original human-readable filename when creating archives from the file manager print flow (previously stored the UUID library filename).
 - **AMS Slot Shows Wrong Material for "Support for" Profiles** — Configuring an AMS slot with a filament profile like "PLA Support for PETG PETG Basic @Bambu Lab H2D 0.4 nozzle" set the slot material to PLA instead of PETG. The name parser iterated material types in order and returned the first match ("PLA"), ignoring that "PLA Support for PETG" means the filament type is PETG. Both the frontend `parsePresetName()` and backend `_parse_material_from_name()` now detect the "X Support for Y" naming pattern and extract the material after "Support for". The frontend also prefers the corrected parsed material over the stored `filament_type` (which may have been saved with the old parser during import).
 - **Spurious Error Notifications During Normal Printing (0300_0002)** — Some firmware versions send non-zero `print_error` values in MQTT during normal printing (e.g., `0x03000002` → short code `0300_0002`). The `print_error` parser treated any non-zero value as a real error, appending it to `hms_errors` and triggering notifications — even though the printer was printing fine. All known real HMS error codes have their low 16 bits >= `0x4000` (`0x4xxx` = fatal, `0x8xxx` = warning/pause, `0xCxxx` = prompt). Values below `0x4000` are status/phase indicators, not faults. Now skips `print_error` values where the error portion is below `0x4000`.
 - **K-Profile Apply Fails With Greenlet Error on Auto-Created Spools** — When a Bambu Lab spool was detected via RFID for the first time (auto-creating a new inventory entry), the K-profile application step logged `WARNING greenlet_spawn has not been called; can't call await_only() here`. The `create_spool_from_tray()` function flushed the new spool to the database but didn't eagerly load the `k_profiles` relationship. When `auto_assign_spool()` then iterated `spool.k_profiles` to find a matching K-profile, SQLAlchemy attempted a lazy load — which requires a synchronous DB call that's illegal inside an async context. The K-profile step was silently skipped (caught by `except Exception`), so spool assignment still worked but without K-profile selection. Now eagerly sets `k_profiles = []` on newly created spools since they can never have K-profiles yet.

+ 1 - 0
backend/app/services/background_dispatch.py

@@ -715,6 +715,7 @@ class BackgroundDispatchService:
             archive = await archive_service.archive_print(
                 printer_id=job.printer_id,
                 source_file=file_path,
+                original_filename=lib_file.filename,
             )
             if not archive:
                 raise RuntimeError("Failed to create archive")

+ 21 - 15
frontend/src/pages/ArchivesPage.tsx

@@ -82,14 +82,20 @@ import { formatFileSize } from '../utils/file';
 type TFunction = (key: string, options?: Record<string, unknown>) => string;
 
 /**
- * Check if an archive filename represents a sliced/printable file.
- * Matches: .gcode, .gcode.3mf, .gcode.anything
+ * Check if an archive represents a sliced/printable file.
+ * Uses filename (.gcode, .gcode.3mf) as primary check, then falls back to
+ * metadata — a .3mf with total_layers or print_time is sliced (contains gcode),
+ * while a raw source .3mf (CAD export) has neither.
  */
-function isSlicedFile(filename: string | null | undefined): boolean {
-  if (!filename) return false;
-  const lower = filename.toLowerCase();
-  // Match .gcode at end OR .gcode. followed by anything (like .gcode.3mf)
-  return lower.endsWith('.gcode') || lower.includes('.gcode.');
+function isSlicedFile(archive: { filename?: string | null; total_layers?: number | null; print_time_seconds?: number | null }): boolean {
+  const filename = archive.filename;
+  if (filename) {
+    const lower = filename.toLowerCase();
+    if (lower.endsWith('.gcode') || lower.includes('.gcode.')) return true;
+  }
+  // .3mf can be either sliced or source — check for gcode metadata
+  if (archive.total_layers || archive.print_time_seconds) return true;
+  return false;
 }
 
 function getArchiveFileType(filename: string | null | undefined): string | undefined {
@@ -342,7 +348,7 @@ function ArchiveCard({
     setContextMenu({ x: e.clientX, y: e.clientY });
   };
 
-  const isGcodeFile = isSlicedFile(archive.filename);
+  const isGcodeFile = isSlicedFile(archive);
 
   const contextMenuItems: ContextMenuItem[] = [
     // For gcode files: show Print option
@@ -867,17 +873,17 @@ function ArchiveCard({
           {/* File type badge */}
           <span
             className={`text-[10px] px-1.5 py-0.5 rounded font-medium ${
-              isSlicedFile(archive.filename)
+              isSlicedFile(archive)
                 ? 'bg-bambu-green/20 text-bambu-green'
                 : 'bg-orange-500/20 text-orange-400'
             }`}
             title={
-              isSlicedFile(archive.filename)
+              isSlicedFile(archive)
                 ? t('archives.card.slicedFile')
                 : t('archives.card.sourceFile')
             }
           >
-            {isSlicedFile(archive.filename) ? t('archives.card.gcode') : t('archives.card.source')}
+            {isSlicedFile(archive) ? t('archives.card.gcode') : t('archives.card.source')}
           </span>
           {archive.project_name && (
             <span
@@ -1020,7 +1026,7 @@ function ArchiveCard({
 
         {/* Actions */}
         <div className="flex gap-1 mt-3">
-          {isSlicedFile(archive.filename) ? (
+          {isSlicedFile(archive) ? (
             // Sliced file - can print directly
             <>
               <Button
@@ -1564,7 +1570,7 @@ function ArchiveListRow({
     setContextMenu({ x: e.clientX, y: e.clientY });
   };
 
-  const isGcodeFile = isSlicedFile(archive.filename);
+  const isGcodeFile = isSlicedFile(archive);
 
   const contextMenuItems: ContextMenuItem[] = [
     ...(isGcodeFile ? [
@@ -1922,7 +1928,7 @@ function ArchiveListRow({
           {formatFileSize(archive.file_size)}
         </div>
         <div className="col-span-2 flex justify-end gap-1">
-          {isSlicedFile(archive.filename) && (
+          {isSlicedFile(archive) && (
             <Button
               variant="ghost"
               size="sm"
@@ -2599,7 +2605,7 @@ export function ArchivesPage() {
       const matchesTag = !filterTag || archiveTags.includes(filterTag);
 
       // File type filter (gcode = sliced, source = project file only)
-      const isGcodeFile = isSlicedFile(a.filename);
+      const isGcodeFile = isSlicedFile(a);
       const matchesFileType = filterFileType === 'all' ||
         (filterFileType === 'gcode' && isGcodeFile) ||
         (filterFileType === 'source' && !isGcodeFile);

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-BNe8DBiX.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-CZ3HFZyM.js"></script>
+    <script type="module" crossorigin src="/assets/index-BNe8DBiX.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-D5I4wfky.css">
   </head>
   <body>

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