Browse Source

Merge branch '0.2.1b' into feature/brazilianPortugueseTranslation

MartinNYHC 3 months ago
parent
commit
8a0fbbd7b4

+ 3 - 0
CHANGELOG.md

@@ -20,6 +20,9 @@ All notable changes to Bambuddy will be documented in this file.
 - **Usage Tracking Wrong Spool on Dual-Nozzle / Multi-AMS Printers** ([#364](https://github.com/maziggy/bambuddy/issues/364)) — On H2C, H2D Pro, and other dual-nozzle printers with multiple AMS units, the usage tracker attributed filament consumption to the wrong spools. The MQTT `mapping` field — a per-print array that maps slicer filament slots to physical AMS trays — was preserved in state but never parsed or used. The tracker fell back to `slot_id - 1` as the global tray ID, which is incorrect when AMS hardware IDs differ from sequential indices (e.g., AMS-HT units with ID 128). Now decodes the MQTT mapping field from its snow encoding (`ams_hw_id * 256 + local_slot`) into bambuddy global tray IDs and uses it as a universal mapping source — working for all printer models and all print sources (slicer, queue, reprint) without relying on `tray_now` disambiguation.
 - **Usage Tracking Wrong Spool on Dual-Nozzle / Multi-AMS Printers** ([#364](https://github.com/maziggy/bambuddy/issues/364)) — On H2C, H2D Pro, and other dual-nozzle printers with multiple AMS units, the usage tracker attributed filament consumption to the wrong spools. The MQTT `mapping` field — a per-print array that maps slicer filament slots to physical AMS trays — was preserved in state but never parsed or used. The tracker fell back to `slot_id - 1` as the global tray ID, which is incorrect when AMS hardware IDs differ from sequential indices (e.g., AMS-HT units with ID 128). Now decodes the MQTT mapping field from its snow encoding (`ams_hw_id * 256 + local_slot`) into bambuddy global tray IDs and uses it as a universal mapping source — working for all printer models and all print sources (slicer, queue, reprint) without relying on `tray_now` disambiguation.
 - **npm audit: suppress moderate ajv ReDoS finding** — Added `audit-level=high` to `frontend/.npmrc` so `npm audit` exits cleanly. The ajv@6 ReDoS (GHSA-2g4f-4pwh-qvx6) is a transitive dependency of eslint@9 with no patched v6 release; ajv@8 override breaks eslint. The vulnerability requires crafted `$data` schema input — not an attack vector in a linting config.
 - **npm audit: suppress moderate ajv ReDoS finding** — Added `audit-level=high` to `frontend/.npmrc` so `npm audit` exits cleanly. The ajv@6 ReDoS (GHSA-2g4f-4pwh-qvx6) is a transitive dependency of eslint@9 with no patched v6 release; ajv@8 override breaks eslint. The vulnerability requires crafted `$data` schema input — not an attack vector in a linting config.
 
 
+### Changed
+- **Filament Catalog API Renamed** ([#427](https://github.com/maziggy/bambuddy/issues/427)) — Renamed `/api/v1/filaments/` to `/api/v1/filament-catalog/` to avoid confusion with the inventory spools page (labeled "Filament" in the UI). The old endpoint managed material type definitions (cost, temperature, density), not physical spools — the shared name caused users to expect the API to return their spool inventory.
+
 ### Improved
 ### Improved
 - **AMS Mapping Test Coverage** — Added 63 backend tests for scheduler AMS mapping (nozzle filtering, external spool extruder assignment, fallback behavior) and 43 frontend tests for `useFilamentMapping` hook (nozzle-aware matching, AMS-HT handling, external spool extruder logic).
 - **AMS Mapping Test Coverage** — Added 63 backend tests for scheduler AMS mapping (nozzle filtering, external spool extruder assignment, fallback behavior) and 43 frontend tests for `useFilamentMapping` hook (nozzle-aware matching, AMS-HT handling, external spool extruder logic).
 - **Tray Now Disambiguation Test Coverage** — Added 28 MQTT message replay tests covering all `tray_now` disambiguation paths: single-nozzle passthrough (X1E/P2S), H2D dual-nozzle snow field, pending target, `ams_extruder_map` fallback, active extruder switching, and full multi-color print lifecycles.
 - **Tray Now Disambiguation Test Coverage** — Added 28 MQTT message replay tests covering all `tray_now` disambiguation paths: single-nozzle passthrough (X1E/P2S), H2D dual-nozzle snow field, pending target, `ams_extruder_map` fallback, active extruder switching, and full multi-color print lifecycles.

+ 1 - 1
backend/app/api/routes/filaments.py

@@ -14,7 +14,7 @@ from backend.app.schemas.filament import (
     FilamentUpdate,
     FilamentUpdate,
 )
 )
 
 
-router = APIRouter(prefix="/filaments", tags=["filaments"])
+router = APIRouter(prefix="/filament-catalog", tags=["filament-catalog"])
 
 
 
 
 @router.get("/", response_model=list[FilamentResponse])
 @router.get("/", response_model=list[FilamentResponse])

+ 3 - 3
backend/tests/integration/test_endpoint_auth.py

@@ -70,7 +70,7 @@ class TestEndpointAuthenticationEnforcement:
     async def test_filaments_list_accessible_without_auth_when_disabled(self, async_client: AsyncClient):
     async def test_filaments_list_accessible_without_auth_when_disabled(self, async_client: AsyncClient):
         """Verify filaments list is accessible when auth is disabled."""
         """Verify filaments list is accessible when auth is disabled."""
         with patch("backend.app.core.auth.is_auth_enabled", return_value=False):
         with patch("backend.app.core.auth.is_auth_enabled", return_value=False):
-            response = await async_client.get("/api/v1/filaments/")
+            response = await async_client.get("/api/v1/filament-catalog/")
             assert response.status_code == 200
             assert response.status_code == 200
 
 
     @pytest.mark.asyncio
     @pytest.mark.asyncio
@@ -153,7 +153,7 @@ class TestAuthenticationPatterns:
         """Verify require_permission_if_auth_enabled allows access when auth disabled."""
         """Verify require_permission_if_auth_enabled allows access when auth disabled."""
         with patch("backend.app.core.auth.is_auth_enabled", return_value=False):
         with patch("backend.app.core.auth.is_auth_enabled", return_value=False):
             # Test a protected endpoint
             # Test a protected endpoint
-            response = await async_client.get("/api/v1/filaments/")
+            response = await async_client.get("/api/v1/filament-catalog/")
             assert response.status_code == 200
             assert response.status_code == 200
 
 
     @pytest.mark.asyncio
     @pytest.mark.asyncio
@@ -162,7 +162,7 @@ class TestAuthenticationPatterns:
         """Verify multiple protected endpoints are accessible when auth is disabled."""
         """Verify multiple protected endpoints are accessible when auth is disabled."""
         with patch("backend.app.core.auth.is_auth_enabled", return_value=False):
         with patch("backend.app.core.auth.is_auth_enabled", return_value=False):
             endpoints = [
             endpoints = [
-                "/api/v1/filaments/",
+                "/api/v1/filament-catalog/",
                 "/api/v1/external-links/",
                 "/api/v1/external-links/",
                 "/api/v1/notifications/",
                 "/api/v1/notifications/",
                 "/api/v1/maintenance/types",
                 "/api/v1/maintenance/types",

+ 9 - 9
backend/tests/integration/test_filaments_api.py

@@ -5,7 +5,7 @@ from httpx import AsyncClient
 
 
 
 
 class TestFilamentsAPI:
 class TestFilamentsAPI:
-    """Integration tests for /api/v1/filaments/ endpoints."""
+    """Integration tests for /api/v1/filament-catalog/ (material types) endpoints."""
 
 
     @pytest.fixture
     @pytest.fixture
     async def filament_factory(self, db_session):
     async def filament_factory(self, db_session):
@@ -36,7 +36,7 @@ class TestFilamentsAPI:
     @pytest.mark.integration
     @pytest.mark.integration
     async def test_list_filaments_empty(self, async_client: AsyncClient):
     async def test_list_filaments_empty(self, async_client: AsyncClient):
         """Verify empty list when no filaments exist."""
         """Verify empty list when no filaments exist."""
-        response = await async_client.get("/api/v1/filaments/")
+        response = await async_client.get("/api/v1/filament-catalog/")
         assert response.status_code == 200
         assert response.status_code == 200
         assert isinstance(response.json(), list)
         assert isinstance(response.json(), list)
 
 
@@ -45,7 +45,7 @@ class TestFilamentsAPI:
     async def test_list_filaments_with_data(self, async_client: AsyncClient, filament_factory, db_session):
     async def test_list_filaments_with_data(self, async_client: AsyncClient, filament_factory, db_session):
         """Verify list returns existing filaments."""
         """Verify list returns existing filaments."""
         await filament_factory(name="Test Filament")
         await filament_factory(name="Test Filament")
-        response = await async_client.get("/api/v1/filaments/")
+        response = await async_client.get("/api/v1/filament-catalog/")
         assert response.status_code == 200
         assert response.status_code == 200
         data = response.json()
         data = response.json()
         assert any(f["name"] == "Test Filament" for f in data)
         assert any(f["name"] == "Test Filament" for f in data)
@@ -62,7 +62,7 @@ class TestFilamentsAPI:
             "brand": "Bambu",
             "brand": "Bambu",
             "cost_per_kg": 30.0,
             "cost_per_kg": 30.0,
         }
         }
-        response = await async_client.post("/api/v1/filaments/", json=data)
+        response = await async_client.post("/api/v1/filament-catalog/", json=data)
         assert response.status_code == 200
         assert response.status_code == 200
         result = response.json()
         result = response.json()
         assert result["name"] == "New PETG"
         assert result["name"] == "New PETG"
@@ -73,7 +73,7 @@ class TestFilamentsAPI:
     async def test_get_filament(self, async_client: AsyncClient, filament_factory, db_session):
     async def test_get_filament(self, async_client: AsyncClient, filament_factory, db_session):
         """Verify single filament can be retrieved."""
         """Verify single filament can be retrieved."""
         filament = await filament_factory(name="Get Test")
         filament = await filament_factory(name="Get Test")
-        response = await async_client.get(f"/api/v1/filaments/{filament.id}")
+        response = await async_client.get(f"/api/v1/filament-catalog/{filament.id}")
         assert response.status_code == 200
         assert response.status_code == 200
         assert response.json()["name"] == "Get Test"
         assert response.json()["name"] == "Get Test"
 
 
@@ -81,7 +81,7 @@ class TestFilamentsAPI:
     @pytest.mark.integration
     @pytest.mark.integration
     async def test_get_filament_not_found(self, async_client: AsyncClient):
     async def test_get_filament_not_found(self, async_client: AsyncClient):
         """Verify 404 for non-existent filament."""
         """Verify 404 for non-existent filament."""
-        response = await async_client.get("/api/v1/filaments/9999")
+        response = await async_client.get("/api/v1/filament-catalog/9999")
         assert response.status_code == 404
         assert response.status_code == 404
 
 
     @pytest.mark.asyncio
     @pytest.mark.asyncio
@@ -90,7 +90,7 @@ class TestFilamentsAPI:
         """Verify filament can be updated."""
         """Verify filament can be updated."""
         filament = await filament_factory(name="Original")
         filament = await filament_factory(name="Original")
         response = await async_client.patch(
         response = await async_client.patch(
-            f"/api/v1/filaments/{filament.id}", json={"name": "Updated", "cost_per_kg": 35.0}
+            f"/api/v1/filament-catalog/{filament.id}", json={"name": "Updated", "cost_per_kg": 35.0}
         )
         )
         assert response.status_code == 200
         assert response.status_code == 200
         result = response.json()
         result = response.json()
@@ -102,8 +102,8 @@ class TestFilamentsAPI:
     async def test_delete_filament(self, async_client: AsyncClient, filament_factory, db_session):
     async def test_delete_filament(self, async_client: AsyncClient, filament_factory, db_session):
         """Verify filament can be deleted."""
         """Verify filament can be deleted."""
         filament = await filament_factory()
         filament = await filament_factory()
-        response = await async_client.delete(f"/api/v1/filaments/{filament.id}")
+        response = await async_client.delete(f"/api/v1/filament-catalog/{filament.id}")
         assert response.status_code == 200
         assert response.status_code == 200
         # Verify deleted
         # Verify deleted
-        response = await async_client.get(f"/api/v1/filaments/{filament.id}")
+        response = await async_client.get(f"/api/v1/filament-catalog/{filament.id}")
         assert response.status_code == 404
         assert response.status_code == 404

+ 4 - 4
frontend/src/api/client.ts

@@ -3312,10 +3312,10 @@ export const api = {
       { method: 'POST' }
       { method: 'POST' }
     ),
     ),
 
 
-  // Filaments
-  listFilaments: () => request<Filament[]>('/filaments/'),
-  getFilament: (id: number) => request<Filament>(`/filaments/${id}`),
-  getFilamentsByType: (type: string) => request<Filament[]>(`/filaments/by-type/${type}`),
+  // Filament Catalog (material types with cost/temp data)
+  listFilaments: () => request<Filament[]>('/filament-catalog/'),
+  getFilament: (id: number) => request<Filament>(`/filament-catalog/${id}`),
+  getFilamentsByType: (type: string) => request<Filament[]>(`/filament-catalog/by-type/${type}`),
 
 
   // Notification Providers
   // Notification Providers
   getNotificationProviders: () => request<NotificationProvider[]>('/notifications/'),
   getNotificationProviders: () => request<NotificationProvider[]>('/notifications/'),

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


+ 1 - 1
static/index.html

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

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