Ver Fonte

refactor(api): rename /filaments/ to /filament-catalog/ for clarity (#427)

The /filaments/ endpoint manages material type definitions (cost, temps,
density) but shares its name with the UI's "Filament" page which shows
the spool inventory (/inventory/spools/). This caused users to expect
the API to return their spool inventory.

Rename to /filament-catalog/ to make the distinction clear. No frontend
behavior change — these API methods are defined but unused in the UI.
maziggy há 3 meses atrás
pai
commit
2fd90e9dce

+ 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.
 - **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
 - **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.

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

@@ -14,7 +14,7 @@ from backend.app.schemas.filament import (
     FilamentUpdate,
 )
 
-router = APIRouter(prefix="/filaments", tags=["filaments"])
+router = APIRouter(prefix="/filament-catalog", tags=["filament-catalog"])
 
 
 @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):
         """Verify filaments list is accessible when auth is disabled."""
         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
 
     @pytest.mark.asyncio
@@ -153,7 +153,7 @@ class TestAuthenticationPatterns:
         """Verify require_permission_if_auth_enabled allows access when auth disabled."""
         with patch("backend.app.core.auth.is_auth_enabled", return_value=False):
             # 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
 
     @pytest.mark.asyncio
@@ -162,7 +162,7 @@ class TestAuthenticationPatterns:
         """Verify multiple protected endpoints are accessible when auth is disabled."""
         with patch("backend.app.core.auth.is_auth_enabled", return_value=False):
             endpoints = [
-                "/api/v1/filaments/",
+                "/api/v1/filament-catalog/",
                 "/api/v1/external-links/",
                 "/api/v1/notifications/",
                 "/api/v1/maintenance/types",

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

@@ -5,7 +5,7 @@ from httpx import AsyncClient
 
 
 class TestFilamentsAPI:
-    """Integration tests for /api/v1/filaments/ endpoints."""
+    """Integration tests for /api/v1/filament-catalog/ (material types) endpoints."""
 
     @pytest.fixture
     async def filament_factory(self, db_session):
@@ -36,7 +36,7 @@ class TestFilamentsAPI:
     @pytest.mark.integration
     async def test_list_filaments_empty(self, async_client: AsyncClient):
         """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 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):
         """Verify list returns existing filaments."""
         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
         data = response.json()
         assert any(f["name"] == "Test Filament" for f in data)
@@ -62,7 +62,7 @@ class TestFilamentsAPI:
             "brand": "Bambu",
             "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
         result = response.json()
         assert result["name"] == "New PETG"
@@ -73,7 +73,7 @@ class TestFilamentsAPI:
     async def test_get_filament(self, async_client: AsyncClient, filament_factory, db_session):
         """Verify single filament can be retrieved."""
         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.json()["name"] == "Get Test"
 
@@ -81,7 +81,7 @@ class TestFilamentsAPI:
     @pytest.mark.integration
     async def test_get_filament_not_found(self, async_client: AsyncClient):
         """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
 
     @pytest.mark.asyncio
@@ -90,7 +90,7 @@ class TestFilamentsAPI:
         """Verify filament can be updated."""
         filament = await filament_factory(name="Original")
         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
         result = response.json()
@@ -102,8 +102,8 @@ class TestFilamentsAPI:
     async def test_delete_filament(self, async_client: AsyncClient, filament_factory, db_session):
         """Verify filament can be deleted."""
         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
         # 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

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

@@ -3312,10 +3312,10 @@ export const api = {
       { 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
   getNotificationProviders: () => request<NotificationProvider[]>('/notifications/'),

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
static/assets/index-ph3tf3Mr.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-xpjLLAhl.js"></script>
+    <script type="module" crossorigin src="/assets/index-ph3tf3Mr.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-tulFiIvt.css">
   </head>
   <body>

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff