Files
HartOMat/plan.md
T
Hartmut b583b0d7a2 feat: per-position camera settings, material alias dialog, product delete, media browser links
- 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>
2026-03-14 12:16:37 +01:00

9.5 KiB

Plan: Material Alias Completeness with Blocking Dialog

Context

The material system currently has three resolution tiers in resolve_material_map():

  1. Alias lookup (case-insensitive) — maps raw names to SCHAEFFLER library materials
  2. Exact Material.name match — the raw name IS a library material
  3. 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:
    1. Takes a list of raw material name strings
    2. Loads all MaterialAlias records and all Material records with schaeffler_code
    3. For each raw name, checks: (a) alias match (case-insensitive), (b) exact Material.name match where it has a schaeffler_code (i.e. it IS a library material)
    4. Returns a list of {"raw_name": str, "suggestions": [{"id": str, "name": str, "schaeffler_code": str}]} for each unmapped name
    5. suggestions: top 5 SCHAEFFLER materials by difflib.SequenceMatcher similarity (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:
    1. Loads all OrderLine records for the order, joining Product
    2. Collects all unique material names from product.cad_part_materials[*].material across all products
    3. Calls find_unmapped_materials() with those names
    4. Returns {"unmapped": [...], "total_materials": int, "mapped_count": int}
  • 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:
    1. Accepts {"mappings": [{"alias": str, "material_id": uuid}]}
    2. For each mapping, creates a MaterialAlias record (skips if alias already exists, case-insensitive)
    3. Returns {"created": int, "skipped": int}
  • Auth: require_admin_or_pm — only admins/PMs should be able to create aliases
  • Validation: Verify each material_id exists; 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:
    1. Props: open: boolean, unmapped: UnmappedMaterial[], onResolved: () => void, onCancel: () => void
    2. UI: Fixed overlay (same pattern as existing modals), wider (max-w-xl):
      • Warning icon (AlertTriangle from 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 suggestions from API, plus option to browse all library materials
    3. State: mappings: Record<string, string> — raw_name → selected material_id
    4. "Map All & Proceed" button: disabled until all unmapped materials have a selection; on click calls batchCreateAliases(), then onResolved() (triggers dispatch)
    5. Cancel button: calls onCancel(), does not dispatch
  • 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:
    1. Add state for material check data and dialog visibility
    2. Replace direct dispatchMut.mutate() on "Dispatch Renders" button with handleDispatch():
      • Calls checkOrderMaterials(id)
      • If unmapped.length > 0: show UnmappedMaterialsDialog
      • If unmapped.length === 0: proceed with dispatchMut.mutate()
    3. onResolved callback: close dialog, call dispatchMut.mutate()
    4. 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:
    1. In the "Custom" materials section, show a warning badge (AlertTriangle icon + "No alias") for materials that have no schaeffler_code AND no aliases
    2. Add a "Map to Library" action button that opens a dropdown to quickly assign a SCHAEFFLER material as alias
    3. Optional: "Show unmapped only" filter toggle
  • 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

  1. Backend service function (Task 1)
  2. Backend endpoints (Tasks 2-3, parallel)
  3. Frontend API types (Task 4)
  4. Frontend dialog + integration (Tasks 5-6)
  5. Materials page badges (Task 7, independent)

Risks / Open Questions

  1. 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.

  2. Material name normalization: Should the check be case-insensitive? Yes — alias lookup is already case-insensitive, so the check should match.

  3. 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.