- Per-render-position focal_length_mm/sensor_width_mm (DB → pipeline → Blender)
- FOV-based camera distance with min clamp fix for wide-angle lenses
- Unmapped materials blocking dialog on "Dispatch Renders" with batch alias creation
- Material check endpoint (GET /orders/{id}/check-materials)
- Batch alias endpoint (POST /materials/batch-aliases)
- Quick-map "No alias" badges on Materials page
- Full product hard-delete with storage cleanup (MinIO + disk files + orphaned CadFile)
- Delete button on ProductDetail page with confirmation
- Clickable product names in Media Browser (links to product page)
- Single-line render dispatch/retry (POST /orders/{id}/lines/{id}/dispatch-render)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9.5 KiB
Plan: Material Alias Completeness with Blocking Dialog
Context
The material system currently has three resolution tiers in resolve_material_map():
- Alias lookup (case-insensitive) — maps raw names to SCHAEFFLER library materials
- Exact Material.name match — the raw name IS a library material
- Pass-through — unresolved names fall through, causing magenta "FailedMaterial" in Blender renders
Materials are stored on Product.cad_part_materials as [{part_name, material}] JSONB. They are auto-populated from Excel components during STEP processing (_auto_populate_materials_for_cad). The Materials page shows "Custom" materials (no schaeffler_code) separately from categorized SCHAEFFLER library materials.
Problem: When a product has a material name like "Steel--Stahl" that has no alias pointing to a SCHAEFFLER library material, the render silently produces magenta parts. There is no pre-flight check before dispatching renders.
Solution: Add a blocking dialog at "Dispatch Renders" that checks for unmapped materials, lets the user map them inline to library materials, and only proceeds when all materials are resolved.
Architecture
Frontend (OrderDetail.tsx)
|
|-- Click "Dispatch Renders"
|-- Call GET /api/orders/{id}/check-materials (new endpoint)
|-- If unmapped materials exist:
| |-- Show UnmappedMaterialsDialog (new component)
| |-- User maps each material via dropdown
| |-- Call POST /api/materials/batch-aliases (new endpoint)
| |-- On success, proceed with dispatchRenders()
|-- If all mapped: proceed directly
Affected Files
| File | Change |
|---|---|
backend/app/services/material_service.py |
Add find_unmapped_materials() function |
backend/app/api/routers/orders.py |
Add GET /orders/{id}/check-materials endpoint |
backend/app/api/routers/materials.py |
Add POST /materials/batch-aliases endpoint |
frontend/src/api/materials.ts |
Add checkOrderMaterials(), batchCreateAliases() API functions + types |
frontend/src/components/orders/UnmappedMaterialsDialog.tsx |
New blocking dialog component |
frontend/src/pages/OrderDetail.tsx |
Intercept "Dispatch Renders" with material check |
frontend/src/pages/Materials.tsx |
Add warning badges for unmapped custom materials |
Tasks (in order)
[x] Task 1: Backend — find_unmapped_materials() service function
- File:
backend/app/services/material_service.py - What: Add an async function
find_unmapped_materials(material_names: list[str], db: AsyncSession) -> list[dict]that:- Takes a list of raw material name strings
- Loads all
MaterialAliasrecords and allMaterialrecords withschaeffler_code - For each raw name, checks: (a) alias match (case-insensitive), (b) exact
Material.namematch where it has aschaeffler_code(i.e. it IS a library material) - Returns a list of
{"raw_name": str, "suggestions": [{"id": str, "name": str, "schaeffler_code": str}]}for each unmapped name suggestions: top 5 SCHAEFFLER materials bydifflib.SequenceMatchersimilarity (ratio > 0.3)
- Acceptance gate: Returns empty list when all materials are mapped; returns unmapped entries with suggestions when some are not
- Dependencies: None
- Risk: None
[x] Task 2: Backend — GET /orders/{id}/check-materials endpoint
- File:
backend/app/api/routers/orders.py - What: Add endpoint that:
- Loads all
OrderLinerecords for the order, joiningProduct - Collects all unique material names from
product.cad_part_materials[*].materialacross all products - Calls
find_unmapped_materials()with those names - Returns
{"unmapped": [...], "total_materials": int, "mapped_count": int}
- Loads all
- Response schema:
class MaterialSuggestion(BaseModel): id: uuid.UUID name: str schaeffler_code: str class UnmappedMaterial(BaseModel): raw_name: str suggestions: list[MaterialSuggestion] class UnmappedMaterialCheck(BaseModel): unmapped: list[UnmappedMaterial] total_materials: int mapped_count: int - Auth:
get_current_user(any authenticated user can check) - Acceptance gate: Returns correct unmapped materials for an order with mixed mapped/unmapped materials
- Dependencies: Task 1
- Risk: None
[x] Task 3: Backend — POST /materials/batch-aliases endpoint
- File:
backend/app/api/routers/materials.py - What: Add batch alias creation endpoint:
- Accepts
{"mappings": [{"alias": str, "material_id": uuid}]} - For each mapping, creates a
MaterialAliasrecord (skips if alias already exists, case-insensitive) - Returns
{"created": int, "skipped": int}
- Accepts
- Auth:
require_admin_or_pm— only admins/PMs should be able to create aliases - Validation: Verify each
material_idexists; reject if alias is empty; skip duplicate aliases - Acceptance gate: Batch creates multiple aliases in one request; subsequent
resolve_material_map()calls resolve through new aliases - Dependencies: None
- Risk: None
[x] Task 4: Frontend — API functions for material checking and batch alias creation
- File:
frontend/src/api/materials.ts - What: Add TypeScript interfaces and API functions:
export interface MaterialSuggestion { id: string name: string schaeffler_code: string } export interface UnmappedMaterial { raw_name: string suggestions: MaterialSuggestion[] } export interface UnmappedMaterialCheck { unmapped: UnmappedMaterial[] total_materials: number mapped_count: number } export async function checkOrderMaterials(orderId: string): Promise<UnmappedMaterialCheck> export async function batchCreateAliases(mappings: Array<{ alias: string; material_id: string }>): Promise<{ created: number; skipped: number }> - Acceptance gate: Types compile; functions callable from components
- Dependencies: Tasks 2, 3
- Risk: None
[x] Task 5: Frontend — UnmappedMaterialsDialog component
- File:
frontend/src/components/orders/UnmappedMaterialsDialog.tsx - What: Create a modal dialog component:
- Props:
open: boolean,unmapped: UnmappedMaterial[],onResolved: () => void,onCancel: () => void - UI: Fixed overlay (same pattern as existing modals), wider (
max-w-xl):- Warning icon (
AlertTrianglefrom lucide-react) + title "Unmapped Materials" - Subtitle: "The following materials have no alias to a library material. Map them before rendering."
- For each unmapped material: row with raw name on left,
<select>dropdown on right - Dropdown pre-populated with
suggestionsfrom API, plus option to browse all library materials
- Warning icon (
- State:
mappings: Record<string, string>— raw_name → selected material_id - "Map All & Proceed" button: disabled until all unmapped materials have a selection; on click calls
batchCreateAliases(), thenonResolved()(triggers dispatch) - Cancel button: calls
onCancel(), does not dispatch
- Props:
- Styling: Follow existing modal patterns, Tailwind classes, lucide-react icons
- Acceptance gate: Dialog renders, dropdowns work, batch alias creation succeeds, dialog closes and render dispatches
- Dependencies: Task 4
- Risk: Low — need to match existing modal patterns
[x] Task 6: Frontend — Intercept "Dispatch Renders" in OrderDetail.tsx
- File:
frontend/src/pages/OrderDetail.tsx - What:
- Add state for material check data and dialog visibility
- Replace direct
dispatchMut.mutate()on "Dispatch Renders" button withhandleDispatch():- Calls
checkOrderMaterials(id) - If
unmapped.length > 0: showUnmappedMaterialsDialog - If
unmapped.length === 0: proceed withdispatchMut.mutate()
- Calls
onResolvedcallback: close dialog, calldispatchMut.mutate()- Import and render
<UnmappedMaterialsDialog>conditionally
- Acceptance gate: Dispatch with unmapped materials shows dialog; mapping all and clicking proceed dispatches; dispatch with all mapped skips dialog
- Dependencies: Task 5
- Risk: Low
[x] Task 7: Frontend — Warning badges on Materials page
- File:
frontend/src/pages/Materials.tsx - What:
- In the "Custom" materials section, show a warning badge (
AlertTriangleicon + "No alias") for materials that have noschaeffler_codeAND no aliases - Add a "Map to Library" action button that opens a dropdown to quickly assign a SCHAEFFLER material as alias
- Optional: "Show unmapped only" filter toggle
- In the "Custom" materials section, show a warning badge (
- Acceptance gate: Custom materials without aliases show a warning; mapping removes it
- Dependencies: None (independent)
- Risk: Low
Migration Check
No — no new database columns needed. MaterialAlias model already exists. All changes use existing tables.
Order Recommendation
- Backend service function (Task 1)
- Backend endpoints (Tasks 2-3, parallel)
- Frontend API types (Task 4)
- Frontend dialog + integration (Tasks 5-6)
- Materials page badges (Task 7, independent)
Risks / Open Questions
-
Performance:
find_unmapped_materials()loads all aliases and library materials per check. For the current scale (~50 materials, ~100 aliases) this is fine. If scale grows, add caching. -
Material name normalization: Should the check be case-insensitive? Yes — alias lookup is already case-insensitive, so the check should match.
-
Products without cad_part_materials: Some products may not have materials assigned yet (no STEP file processed). These are skipped — the check only validates materials that exist.