Fix archived files counted as printed in project statistics (#630)

Files added to a project from the archive (status="archived") were
  incorrectly counted in completed_prints and parts_progress stats.
  Only status="completed" (actually printed) now counts toward completion.
This commit is contained in:
maziggy
2026-03-06 13:32:19 +01:00
parent 8c308487c7
commit 46097a9ed4
3 changed files with 120 additions and 4 deletions
+1
View File
@@ -13,6 +13,7 @@ All notable changes to Bambuddy will be documented in this file.
- **Print Queue Scheduler Diagnostics** ([#616](https://github.com/maziggy/bambuddy/issues/616)) — Added diagnostic logging to the print queue scheduler to help diagnose why queued prints aren't starting. After each queue check, the scheduler now logs a skip summary (how many items were skipped due to manual_start, scheduled_time, etc.) and for each busy printer, logs the exact state preventing it from being considered idle (connected status, printer state, plate_cleared flag). Previously the scheduler only logged "found N pending items" with no visibility into why items were skipped.
### Fixed
- **Project Statistics Count Archived Files as Printed** ([#630](https://github.com/maziggy/bambuddy/issues/630)) — Files added to a project from the archive were counted in project statistics (completed prints, parts progress) as if they had already been printed. Only files with `status="completed"` (actually printed via a printer) now count toward completion stats. Files with `status="archived"` (stored but not yet printed) are no longer included. Reported by @SebSeifert.
- **Python 3.10 Compatibility** — Bambuddy failed to start on Python 3.10 with `ImportError: cannot import name 'StrEnum' from 'enum'` because `enum.StrEnum` was added in Python 3.11. Added a compatibility shim that falls back to `(str, Enum)` on Python < 3.11, matching the documented requirement of Python 3.10+.
- **Bug Report Bubble Overlapping Toasts** — Moved toast notifications and upload progress up so they stack above the bug report bubble instead of overlapping on top of each other.
- **Virtual Printer: Bind-TLS Proxy Handshake Failure on OpenSSL 3.x** — The TLS proxy connecting to the printer's bind port (3002) failed with `SSLV3_ALERT_HANDSHAKE_FAILURE` on systems with OpenSSL 3.x (e.g. Python 3.12+) because the default cipher set excludes plain RSA key exchange, which is the only mode Bambu printers support. Added `AES256-GCM-SHA384` and `AES128-GCM-SHA256` to the client SSL context's cipher list.
+4 -4
View File
@@ -97,11 +97,11 @@ async def compute_project_stats(
)
in_progress_prints = in_progress_result.scalar() or 0
# Sum completed items (parts) - sum of quantities for successful prints
# Sum completed items (parts) - sum of quantities for actually printed jobs
completed_items_result = await db.execute(
select(func.coalesce(func.sum(PrintArchive.quantity), 0)).where(
PrintArchive.project_id == project_id,
PrintArchive.status.in_(["completed", "archived"]),
PrintArchive.status == "completed",
)
)
completed_items = int(completed_items_result.scalar() or 0)
@@ -192,11 +192,11 @@ async def list_projects(
)
queue_count = queue_count_result.scalar() or 0
# Sum completed parts (quantities) - includes "archived" as successful
# Sum completed parts (quantities) - only actually printed jobs
completed_result = await db.execute(
select(func.coalesce(func.sum(PrintArchive.quantity), 0)).where(
PrintArchive.project_id == project.id,
PrintArchive.status.in_(["completed", "archived"]),
PrintArchive.status == "completed",
)
)
completed_count = int(completed_result.scalar() or 0)
@@ -262,6 +262,121 @@ class TestProjectPartsTracking:
assert data["stats"]["parts_progress_percent"] == 40.0 # parts: 10/25
class TestProjectArchivedStatusNotCounted:
"""Tests for bug #630: archived files added to a project should not count as printed."""
@pytest.fixture
async def project_factory(self, db_session):
"""Factory to create test projects."""
async def _create_project(**kwargs):
from backend.app.models.project import Project
defaults = {
"name": "Archived Status Test",
"description": "Test project",
"color": "#FF0000",
}
defaults.update(kwargs)
project = Project(**defaults)
db_session.add(project)
await db_session.commit()
await db_session.refresh(project)
return project
return _create_project
@pytest.fixture
async def archive_factory(self, db_session):
"""Factory to create test archives."""
async def _create_archive(**kwargs):
from backend.app.models.archive import PrintArchive
defaults = {
"filename": "test.3mf",
"file_path": "test/test.3mf",
"file_size": 1000,
"print_name": "Test Print",
"status": "completed",
"quantity": 1,
}
defaults.update(kwargs)
archive = PrintArchive(**defaults)
db_session.add(archive)
await db_session.commit()
await db_session.refresh(archive)
return archive
return _create_archive
@pytest.mark.asyncio
@pytest.mark.integration
async def test_archived_files_not_counted_as_completed(
self, async_client: AsyncClient, project_factory, archive_factory, db_session
):
"""Archived files added to a project should not count in completed_prints stats."""
project = await project_factory(target_parts_count=20)
# 2 actually printed (completed), 3 just archived (not printed yet)
await archive_factory(project_id=project.id, quantity=2, status="completed")
await archive_factory(project_id=project.id, quantity=3, status="archived")
await archive_factory(project_id=project.id, quantity=5, status="archived")
response = await async_client.get(f"/api/v1/projects/{project.id}")
assert response.status_code == 200
data = response.json()
# Only the completed archive should count
assert data["stats"]["completed_prints"] == 2
assert data["stats"]["parts_progress_percent"] == 10.0 # 2/20 = 10%
assert data["stats"]["remaining_parts"] == 18
@pytest.mark.asyncio
@pytest.mark.integration
async def test_archived_files_not_counted_in_project_list(
self, async_client: AsyncClient, project_factory, archive_factory, db_session
):
"""Project list endpoint should not count archived files as completed."""
project = await project_factory(name="List Archived Test", target_parts_count=50)
await archive_factory(project_id=project.id, quantity=4, status="completed")
await archive_factory(project_id=project.id, quantity=6, status="archived")
response = await async_client.get("/api/v1/projects/")
assert response.status_code == 200
data = response.json()
our_project = next((p for p in data if p["name"] == "List Archived Test"), None)
assert our_project is not None
assert our_project["completed_count"] == 4 # Only completed, not archived
assert our_project["archive_count"] == 2 # Both archives exist as plates
@pytest.mark.asyncio
@pytest.mark.integration
async def test_only_completed_status_counts(
self, async_client: AsyncClient, project_factory, archive_factory, db_session
):
"""Only 'completed' status should count in stats, not archived/failed/etc."""
project = await project_factory(target_parts_count=100)
await archive_factory(project_id=project.id, quantity=10, status="completed")
await archive_factory(project_id=project.id, quantity=5, status="archived")
await archive_factory(project_id=project.id, quantity=3, status="failed")
await archive_factory(project_id=project.id, quantity=2, status="aborted")
response = await async_client.get(f"/api/v1/projects/{project.id}")
assert response.status_code == 200
data = response.json()
assert data["stats"]["completed_prints"] == 10 # Only "completed"
assert data["stats"]["failed_prints"] == 2 # failed + aborted (count of archives, not sum)
assert data["stats"]["total_archives"] == 4 # All archives
assert data["stats"]["total_items"] == 20 # Sum of all quantities
class TestProjectArchivesAPI:
"""Tests for project-archive relationships."""