From 2de2d3be8d7a4f40e7334c776dc9ed211c2247ff Mon Sep 17 00:00:00 2001 From: maziggy Date: Sun, 15 Feb 2026 14:05:11 +0100 Subject: [PATCH] fix: detect archive duplicates by print name in list view (#315) The duplicate badge on archive cards only matched by content hash, so re-sliced prints of the same model (different GCODE, same name) were not flagged. Now also matches by print name (case-insensitive), consistent with the detail view's find_duplicates() logic. --- CHANGELOG.md | 1 + backend/app/api/routes/archives.py | 10 ++++++---- backend/app/services/archive.py | 18 ++++++++++++++---- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f29b5f31..3d64672a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ All notable changes to Bambuddy will be documented in this file. - **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. - **Loose Archive Name Matching Could Cause Wrong Archive Reuse** ([#374](https://github.com/maziggy/bambuddy/issues/374)) — The `on_print_start` callback used `ilike('%{name}%')` to find existing "printing" archives, which meant a print named "Clip" could incorrectly match "Cable Clip" or "Clip Stand". This could cause a new print to reuse the wrong archive or skip creating one. Tightened to exact `print_name` match or exact filename variants (`.3mf`, `.gcode.3mf`). +- **Archive Duplicate Badge Misses Name-Based Duplicates** ([#315](https://github.com/maziggy/bambuddy/issues/315)) — The duplicate badge on archive cards only matched by file content hash, so re-sliced prints of the same model (different GCODE, same print name) were not flagged as duplicates. Now also matches by print name (case-insensitive), consistent with the detail view's duplicate detection. ### Improved - **Phantom Print Investigation — Logging & Hardening** ([#374](https://github.com/maziggy/bambuddy/issues/374)) — Added targeted logging and hardening to help diagnose reports of prints starting automatically without user input. Debug log volume reduced ~90% by suppressing `sqlalchemy.engine` (changed from INFO to WARNING) and `aiosqlite` (new WARNING suppression) noise that previously filled 2.5MB in 16 minutes. Every `start_print()` call now logs a `PRINT COMMAND` trace with the caller's file, line, and function name. The print scheduler logs pending queue items when found. `on_print_complete` warns when multiple queue items are in "printing" status for the same printer, which signals a state inconsistency. diff --git a/backend/app/api/routes/archives.py b/backend/app/api/routes/archives.py index c5f6e409d..abe2aa221 100644 --- a/backend/app/api/routes/archives.py +++ b/backend/app/api/routes/archives.py @@ -134,13 +134,15 @@ async def list_archives( offset=offset, ) - # Get set of hashes that have duplicates (efficient single query) - duplicate_hashes = await service.get_duplicate_hashes() + # Get sets of hashes and names that have duplicates (efficient single queries) + duplicate_hashes, duplicate_names = await service.get_duplicate_hashes_and_names() - # Mark archives that have duplicates + # Mark archives that have duplicates (by hash or by print name) result = [] for a in archives: - has_duplicate = a.content_hash in duplicate_hashes if a.content_hash else False + has_hash_dup = a.content_hash in duplicate_hashes if a.content_hash else False + has_name_dup = a.print_name and a.print_name.lower() in duplicate_names + has_duplicate = has_hash_dup or has_name_dup result.append(archive_to_response(a, duplicate_count=1 if has_duplicate else 0)) return result diff --git a/backend/app/services/archive.py b/backend/app/services/archive.py index 60317cfaa..a1dccf9be 100644 --- a/backend/app/services/archive.py +++ b/backend/app/services/archive.py @@ -727,10 +727,10 @@ class ArchiveService: sha256.update(chunk) return sha256.hexdigest() - async def get_duplicate_hashes(self) -> set[str]: - """Get all content hashes that appear more than once. + async def get_duplicate_hashes_and_names(self) -> tuple[set[str], set[str]]: + """Get all content hashes and print names that appear more than once. - Returns a set of hashes that have duplicates. + Returns a tuple of (duplicate_hashes, duplicate_names). """ from sqlalchemy import func @@ -740,7 +740,17 @@ class ArchiveService: .group_by(PrintArchive.content_hash) .having(func.count(PrintArchive.id) > 1) ) - return {row[0] for row in result.all()} + duplicate_hashes = {row[0] for row in result.all()} + + result = await self.db.execute( + select(func.lower(PrintArchive.print_name)) + .where(PrintArchive.print_name.isnot(None)) + .group_by(func.lower(PrintArchive.print_name)) + .having(func.count(PrintArchive.id) > 1) + ) + duplicate_names = {row[0] for row in result.all()} + + return duplicate_hashes, duplicate_names async def find_duplicates( self,