# Schaeffler Automat — Master Roadmap

> **Consolidated:** 2026-03-11
> **Branch:** `refactor/v2`
> **Sources merged:** `PLAN.md` (Phases A–F), `PLAN_REFACTOR.md` (Phases 1–8), `plan.md` (GMSH), `docs/rfcs/0001`, `visual-audit-report.md`

---

## ✅ What Is Done

| Area | Detail |
|---|---|
| Phase A runtime | Flamenco removed from the active pipeline, `render-worker` replaced the old Blender service path, MinIO added; legacy `blender-renderer/` and `threejs-renderer/` directories still remain in the repo |
| Phase B | Domain-driven project structure, Tenant model + RLS migrations 035/036, Tenant management UI |
| Phase C | WorkflowDefinition model, standard workflows seeded, React Flow Workflow Editor |
| Phase D | OCC mesh attributes extraction, Blender integration |
| Phase E | MediaAsset catalog (model + API + frontend) |
| Sharp Edges V02 | GCPnts curve sampling → 17,129 segment pairs in GLB extras → Blender KD-tree marks sharp+seam |
| Tessellation presets | Draft/Standard/Fine preset buttons in Admin UI, default deflections updated |
| Media cache-bust | `?v={file_size_bytes}` in download URLs + `Cache-Control: no-cache` headers |
| GPU activation order | Fix: `_activate_gpu()` called before AND after `open_mainfile` to survive engine reset |
| Material system | Aliases-first lookup, `get_material_library_path()` via AssetLibrary |
| Pipeline task split | `backend/app/tasks/step_tasks.py` is now a 23-line compatibility shim; active task implementations live in `backend/app/domains/pipeline/tasks/` |
| Render job tracking | `RenderJobDocument`, `PipelineLogger`, and cancel-via-real-`celery_task_id` are already wired into the render pipeline |
| Tenant isolation baseline | `TenantContextMiddleware`, JWT `tenant_id`, and the `global_admin` / `tenant_admin` role hierarchy are in place for HTTP requests |
| Hash groundwork | `compute_step_hash()` exists and `CadFile.step_file_hash` is already persisted during thumbnail processing |
| Performance profiling | `@timed_step` decorator, Blender timing laps, MeshRegistry (17→8 traverse calls eliminated), `_pipeline_session` context manager, KD-tree spatial pre-filter for sharp edges, batch material library append, GMSH single-session batching |
| Part-materials validation | `PUT /cad/{id}/part-materials` and `PUT /cad/{id}/manual-material-overrides` now reject unknown part names against `parsed_objects` |
| Dead code cleanup | Verified: `blender-renderer/`, `threejs-renderer/`, `flamenco/`, `renderproblems_tmp/` all deleted; STL endpoints/settings removed; no Pillow imports; docker-compose clean |

---

## 🔎 Status Snapshot

Verified against the repository on `2026-03-13`.

| Priority | Status | Re-evaluated state |
|---|---|---|
| 1. Pipeline Cleanup Foundation | **Done** | All P1 gates pass: dead code absent, `blender_render.py` split into 8 submodules (68 lines), `step_tasks.py` is 24 lines, legacy dirs deleted |
| 2. USD Foundation Without Viewer Regression | **Done** | `export_step_to_usd.py`, `import_usd.py`, `usd_master` MediaAsset, `scene-manifest`, `partKey`, `part_key_service.py` all implemented; migrations 060-062 applied |
| 3. Tessellation and Topology Quality | **Done** | GMSH 4.15.1 installed, `_tessellate_with_gmsh()` implemented, `tessellation_engine` wired admin → pipeline → CLI |
| 4. Viewer Migration to Canonical Part Identity | **Done** | GLB node `extras.partKey` injected; `userData.partKey` stamped in viewer; hover tooltip shows slug; scene manifest verified |
| 5. Canonical USD Export and Render Migration | **Done** | All milestones complete: M1–M7; production GLB deprecated; digit-only prim name fix (`p_` prefix); `EXPORT_GLB_PRODUCTION` enum removed |
| 6. Admin and Product Surface Simplification | **Done** | Settings renamed `scene_*`/`render_*`, migration applied, Admin progressive disclosure, ProductDetail single canonical scene, MediaAssetType deprecated values commented |
| 7. Render Job Tracking and Structured Logging | **Done** | `RenderJobDocument`, migration `048`, `PipelineLogger`, and revoke-by-real-task-id are present |
| 8. Tenant Isolation Completion | **Done** | HTTP: `TenantContextMiddleware` + JWT `tenant_id`; Celery: `set_tenant_context_sync()` in all pipeline tasks; all routers migrated to `require_global_admin` |
| 9. Hash-Based Scene Conversion Caching | **Done** | Composite cache key (hash + deflection + engine) in both geometry and USD tasks; disk existence check; `render_config` stored; `step_hash` in API |
| 10. UI/UX Polish | **Done** | Empty states, Admin help text, notification batching, per-line reject in OrderDetail with portal modal, kanban drag-to-reject with native HTML5 DnD |

---

## 🗺 Open Work — Reconciled Priorities

This roadmap now treats the USD refactor as an implementation workstream, not as a blocked strategic idea.

The key architectural clarification from [docs/rfcs/0001-step-to-usd-workflow.md](/home/hartmut/Documents/Copilot/schaefflerautomat/docs/rfcs/0001-step-to-usd-workflow.md#L139) is:

- USD becomes the canonical persisted scene asset
- the browser does not need to render USD directly
- the current 3D viewer workflow is preserved through a derived preview asset plus canonical `partKey`

That removes the old assumption that USD work must wait for a Three.js USD loader.

### Priority 1 — Pipeline Cleanup Foundation

**Goal:** Reduce refactor risk by simplifying the current pipeline before introducing canonical scene concepts.

This priority combines dead-code deletion and task decomposition because both are prerequisites for a controlled cut-over to USD.

**Status:** In progress. M2 is complete; M1 and M3 remain open.

**Milestones:**
- M1: Dead code deleted — Pillow block, STL settings, orphaned directories
- M2: `step_tasks.py` decomposed into `backend/app/domains/pipeline/tasks/` submodules
- M3: `blender_render.py` decomposed into `render-worker/scripts/_blender_*.py` submodules

**File targets:**

| Action | Path |
|---|---|
| DELETE lines 798–851 | `render-worker/scripts/blender_render.py` (Pillow overlay, `else:` branch never runs) |
| DELETE | `blender-renderer/` directory |
| DELETE | `threejs-renderer/` directory |
| DELETE | `flamenco/` directory |
| DELETE | `renderproblems_tmp/` |
| DONE | `backend/app/tasks/step_tasks.py` — reduced to compatibility shim only |
| REMOVE settings | `admin.py`: `VALID_STL_QUALITIES`, `stl_quality`, `generate-missing-stls` endpoint |
| REMOVE endpoint | `cad.py`: `POST /cad/{id}/generate-stl/{quality}` |
| DONE | `backend/app/domains/pipeline/tasks/extract_metadata.py` — metadata extraction |
| DONE | `backend/app/domains/pipeline/tasks/render_thumbnail.py` — thumbnail render task |
| DONE (combined) | `backend/app/domains/pipeline/tasks/render_order_line.py` — still/dispatch pipeline entry |
| OPEN | turntable-specific pipeline task split still needs to be carved out explicitly if kept as a separate concern |
| THIN (< 80 lines) | `backend/app/tasks/step_tasks.py` — dispatch only |
| CREATE | `render-worker/scripts/_blender_gpu.py` |
| CREATE | `render-worker/scripts/_blender_import.py` |
| CREATE | `render-worker/scripts/_blender_materials.py` |
| CREATE | `render-worker/scripts/_blender_camera.py` |
| CREATE | `render-worker/scripts/_blender_scene.py` |
| THIN (< 80 lines) | `render-worker/scripts/blender_render.py` — entry point only |

**Acceptance gates:**
- `grep -r "VALID_STL_QUALITIES\|stl_quality\|from PIL\|from Pillow" backend/ render-worker/` → 0 matches
- `wc -l backend/app/tasks/step_tasks.py` → < 100 lines
- `wc -l render-worker/scripts/blender_render.py` → < 80 lines
- Upload `81113-l_cut.stp`, trigger thumbnail → still renders correctly (no regression)
- `ls blender-renderer/ threejs-renderer/ flamenco/` → all return "No such file"

### Priority 2 — USD Foundation Without Viewer Regression

**Goal:** Introduce canonical part identity and the three-layer material assignment model while keeping the current GLB-based browser UX working end-to-end.

**Status:** Not started in code. Architecture decisions are documented, but repo work has not begun.

**Milestones:**
- M1: `export_step_to_usd.py` produces valid USD with part hierarchy and `schaeffler:partKey` on every prim
- M2: `usd_master` MediaAsset type exists in DB and is stored after each export
- M3: `GET /api/cad/{id}/scene-manifest` returns partKey list with effective assignments
- M4: `PUT /api/cad/{id}/part-materials` accepts `{partKey → materialName}` map and persists it
- M5: Browser ThreeDViewer saves material overrides keyed by `partKey`, survives page reload

**File targets:**

| Action | Path |
|---|---|
| ADD pip install | `render-worker/Dockerfile` — `usd-core>=24.11` (provides `pxr` module) ✅ decided |
| CREATE | `render-worker/scripts/export_step_to_usd.py` — XCAF → USD, hierarchy + metadata + partKey |
| ADD enum value | `backend/app/domains/media/models.py` — `usd_master` to `MediaAssetType` |
| CREATE migration | `backend/alembic/versions/060_usd_master_asset_type.py` |
| ADD JSONB columns | `backend/app/domains/products/models.py` — `CadFile.source_material_assignments`, `resolved_material_assignments`, `manual_material_overrides` |
| CREATE migration | `backend/alembic/versions/061_material_assignment_layers.py` |
| CREATE | `backend/app/services/part_key_service.py` — `generate_part_key(xcaf_label)`, `build_scene_manifest()` |
| CREATE | `backend/app/domains/products/schemas.py` — `SceneManifest`, `PartEntry` Pydantic models |
| ADD endpoint | `backend/app/api/routers/cad.py` — `GET /cad/{id}/scene-manifest` |
| MODIFY endpoint | `backend/app/api/routers/cad.py` — `GET/PUT /cad/{id}/part-materials` → partKey-keyed |
| ADD task | `backend/app/domains/pipeline/tasks/export_glb.py` — `generate_usd_master_task` (dual-writes beside GLB) |
| CREATE | `frontend/src/api/sceneManifest.ts` — `SceneManifest` interface, `fetchSceneManifest()` |

**Open questions to decide before M1:**
- None blocking at the architecture level. The roadmap decisions for `usd-core` and index-space seam/sharp primvars are already captured in `docs/plans/0001-step-to-usd-implementation.md`.

**Acceptance gates:**
- `python3 export_step_to_usd.py --step_path 81113-l_cut.stp` → valid `.usd` file, 25 part prims, each has `schaeffler:partKey` attribute
- `GET /api/cad/{id}/scene-manifest` returns `parts[]` array with `part_key`, `source_name`, `effective_material`, `is_unassigned`
- Click part in ThreeDViewer → assign material → reload page → material still assigned (persisted via `partKey`, not mesh name)
- CAD file with mismatched Excel names: UI shows `unmatched_source_rows` count > 0 and unassigned parts highlighted
- No regression: existing click/select/isolate/ghost/hide still works in browser

**References:**
- RFC: `docs/rfcs/0001-step-to-usd-workflow.md`
- Execution checklist: `docs/plans/0001-step-to-usd-implementation.md`

### Priority 3 — Tessellation and Topology Quality

**Goal:** Eliminate fan triangles on cylindrical surfaces (rings, bearings) and produce clean seams for UV unwrap.

**Status:** Not started in code. This is still a pure planning workstream at the moment.

**Milestones:**
- M1: GMSH 4.15+ installed in render-worker container
- M2: `export_step_to_gltf.py --tessellation_engine gmsh` produces fan-free GLB
- M3: `tessellation_engine` system setting wired through CLI → Admin UI dropdown

**File targets:**

| Action | Path |
|---|---|
| ADD pip install | `render-worker/Dockerfile` — `gmsh>=4.15.0` |
| ADD arg + function | `render-worker/scripts/export_step_to_gltf.py` — `--tessellation_engine`, `_tessellate_with_gmsh()` |
| ADD setting | `backend/app/api/routers/admin.py` — `tessellation_engine` in `SETTINGS_DEFAULTS` + `SettingsOut` |
| MODIFY task | `backend/app/domains/pipeline/tasks/export_glb.py` — read setting, pass to CLI |
| ADD UI | `frontend/src/pages/Admin.tsx` — dropdown: OCC vs. GMSH |

*(Full task breakdown in `plan.md`)*

**Acceptance gates:**
- `docker compose exec render-worker python3 -c "import gmsh; print(gmsh.__version__)"` → `4.15.x`
- `python3 export_step_to_gltf.py --step_path 81113-l_cut.stp --tessellation_engine gmsh` → no vertex with valence > 10 at cylinder seam edges (inspect via Blender Mesh Analysis overlay)
- Standard preset output size with GMSH ≤ 2× size with OCC at same deflection
- Sharp edge pairs still extracted and injected into GLB extras after GMSH tessellation

**Note:** Better tessellation directly benefits Priority 2 (USD seam/sharp payload) and Priority UV-unwrap work.

### Priority 4 — Viewer Migration to Canonical Part Identity

**Goal:** Move browser interactions from raw GLB mesh-name matching to canonical `partKey` without any UX regression.

**Milestones:**
- M1: Preview GLB derivation embeds `partKey` as mesh `extras.partKey` on every selectable object
- M2: `ThreeDViewer` reads `partKey` from scene manifest + mesh extras on click, no longer uses raw `mesh.name`
- M3: `MaterialPanel` shows `partKey`, source name, assignment provenance; saves overrides by `partKey`
- M4: Unmatched source rows and unassigned parts surfaced in `MaterialPanel` reconciliation section

**File targets:**

| Action | Path |
|---|---|
| CREATE | `frontend/src/api/sceneManifest.ts` — `SceneManifest` + `PartEntry` interfaces |
| MODIFY | `frontend/src/components/cad/ThreeDViewer.tsx` — use `partKey` from scene manifest for selection, isolation, ghost |
| MODIFY | `frontend/src/components/cad/MaterialPanel.tsx` — show provenance, unmatched/unassigned sections |
| MODIFY | `frontend/src/api/cad.ts` — update `PartMaterialsMap` interface to `{ [partKey: string]: string }` |
| MODIFY | `backend/app/api/routers/cad.py` — `GET/PUT /cad/{id}/part-materials` keyed by partKey with provenance |
| ADD util | `render-worker/scripts/export_step_to_gltf.py` — embed `partKey` into mesh extras during GLB export |

**Acceptance gates:**
- `mesh.userData.partKey` exists on every mesh object in the Three.js scene after GLB load
- Select a part → DevTools shows the assignment payload contains `part_key: "ring_outer"`, not `mesh_name: "RingOuter_AF0"`
- Upload file with 3 unmatched Excel rows → MaterialPanel shows "3 unmatched source rows"
- After full page reload: all manual material assignments are restored correctly
- Isolation, hide, and ghost still work as before (no regression)

### Priority 5 — Canonical USD Export and Render Migration

**Goal:** Switch Blender still/turntable renders to consume the canonical USD stage, retiring the production GLB as an intermediate render artifact.

**Milestones:**
- M1: `render-worker/scripts/import_usd.py` — Blender can import USD + restore seam/sharp from primvars
- M2: Still render from USD matches current production GLB render quality (side-by-side comparison)
- M3: Turntable render from USD works end-to-end
- M4: `generate_production_glb_task` bypassed; renders consume `usd_master` directly
- M5: Material metadata as USD primvars — canonical material names baked into the USD asset
- M6: Geometry transform fix for assembly hierarchy — correct transforms for multi-level assemblies
- M7: Material map resolution at USD export time — USD becomes self-contained with canonical material names

**M5: Material metadata as USD primvars**

The current USD render path matches 0/25 parts for material assignment because Blender has no way to resolve canonical material names from the imported USD prims. This milestone embeds that metadata directly into the USD file.

- Pass resolved `material_map` to `export_step_to_usd.py` via `--material_map` CLI arg (JSON string)
- Write `schaeffler:canonicalMaterialName` as a STRING primvar on each Mesh prim during USD export
- `import_usd.py` reads the primvar after import and performs direct material lookup (no name-matching heuristics)
- Acceptance: Blender log shows `25/25 parts matched` for material assignment from USD

**M6: Geometry transform fix for assembly hierarchy**

Multi-level assemblies (3+ nesting levels) produce wrong geometry in USD because `_extract_mesh()` does not account for shape Location before face exploration and does not accumulate parent transforms through `_traverse_xcaf` recursion.

- `_extract_mesh()` must strip shape Location before face exploration, then apply both transforms sequentially
- For deeply nested assemblies (3+ levels): accumulate parent transforms through `_traverse_xcaf` recursion
- Acceptance: USD render matches STEP import geometry side-by-side (no displaced or rotated parts)

**M7: Material map resolution at USD export time**

Currently `generate_usd_master_task` does not resolve the material map before passing it to the export subprocess. This means the USD file has no material metadata, making it impossible for Blender to assign materials without external context.

- `generate_usd_master_task` must resolve `material_map` via `material_service.resolve_material_map()` and pass to subprocess
- This makes the USD file self-contained: canonical material names baked into the asset
- Acceptance: USD file inspected via `pxr` shows `schaeffler:canonicalMaterialName` on every mesh prim

**File targets:**

| Action | Path |
|---|---|
| CREATE | `render-worker/scripts/export_step_to_usd.py` — STEP→USD exporter (seam/sharp payload on mesh prims) |
| CREATE | `render-worker/scripts/import_usd.py` — Blender USD import helper: reads `primvars:schaeffler:seamEdgeVertexPairs`, marks seam+sharp |
| MODIFY | `render-worker/scripts/blender_render.py` — accept `--usd_path` flag alongside `--glb_path` |
| MODIFY | `backend/app/services/render_blender.py` — pass `usd_master` asset path when available |
| MODIFY | `backend/app/domains/pipeline/tasks/export_glb.py` — retire `generate_gltf_production_task` once USD path validated |
| KEEP (compat) | `render-worker/scripts/export_gltf.py` — retained as fallback until USD path confirmed stable |
| MODIFY (M5) | `render-worker/scripts/export_step_to_usd.py` — accept `--material_map` CLI arg, write `schaeffler:canonicalMaterialName` primvar on each Mesh prim |
| MODIFY (M5) | `render-worker/scripts/import_usd.py` — read `schaeffler:canonicalMaterialName` primvar after import, use for direct material lookup |
| MODIFY (M6) | `render-worker/scripts/export_step_to_usd.py` — fix `_extract_mesh()` to strip shape Location; accumulate parent transforms in `_traverse_xcaf` |
| MODIFY (M7) | `backend/app/domains/pipeline/tasks/export_glb.py` (or USD task) — call `resolve_material_map()` and pass result to export subprocess |

**Acceptance gates:**
- `render_still --usd_path usd_master.usd` → PNG output visually identical to current production GLB render (diff tolerance < 5% SSIM)
- Blender log shows `[USD_IMPORT] 25 parts imported, 5044 seam/sharp edges restored` (not reconstructed by angle)
- `GET /api/media?asset_type=gltf_production` returns 0 new entries after switch (old records preserved)
- Turntable MP4 plays without texture or material pop artifacts
- (M5) Blender log shows `[USD_IMPORT] 25/25 parts matched` for material assignment (not 0/25)
- (M6) USD render geometry matches STEP import geometry side-by-side for multi-level assemblies (no displaced/rotated parts)
- (M7) `python3 -c "from pxr import Usd; stage=Usd.Stage.Open('usd_master.usd'); [print(p.GetAttribute('primvars:schaeffler:canonicalMaterialName').Get()) for p in stage.Traverse()]"` → prints canonical material name for every mesh prim

### Priority 6 — Admin and Product Surface Simplification

**Goal:** Remove the geometry-GLB vs production-GLB mental model from admin, product detail, and repair flows.

**Milestones:**
- M1: Admin tessellation settings collapsed from 4 knobs to `scene_*` + `preview_*` (2 or 3 knobs max)
- M2: Bulk actions renamed from `generate-missing-geometry-glbs` → `generate-missing-canonical-scenes`
- M3: ProductDetail shows single canonical scene status card, not dual-GLB

**File targets:**

| Action | Path |
|---|---|
| MODIFY | `backend/app/api/routers/admin.py` — rename/collapse tessellation settings; new bulk action labels |
| CREATE migration | `backend/alembic/versions/06x_rename_tessellation_settings.py` — UPDATE system_settings SET key = 'scene_linear_deflection' WHERE key = 'gltf_production_linear_deflection' |
| MODIFY | `frontend/src/pages/Admin.tsx` — simplified tessellation panel: scene quality + optional preview override |
| MODIFY | `frontend/src/pages/ProductDetail.tsx` — single canonical scene card (status, regenerate button) |
| MODIFY | `backend/app/domains/media/models.py` — deprecate `gltf_geometry` / `gltf_production` (keep values, add `deprecated=True` metadata) |

**Acceptance gates:**
- Admin Settings page has max 3 tessellation quality fields (down from 4)
- ProductDetail page: one "Canonical Scene" status section, no separate geometry/production rows
- `GET /api/admin/settings` no longer exposes `gltf_preview_linear_deflection` key (replaced by `scene_linear_deflection`)

### Priority 7 — Render Job Tracking and Structured Logging

**Goal:** Fix broken render job cancellation (synthetic `render-{line_id}` ID never matches real Celery task ID) and establish structured per-step logging.

**Status:** Done, aside from any follow-up polish.

**Milestones:**
- M1: `RenderJobDocument` schema + migration; tasks write real `self.request.id` to DB
- M2: Cancel endpoint reads `celery_task_id` from job doc and calls `revoke()` — actually stops task
- M3: `PipelineLogger` integrated in all task files; every step emits `[STEP] start/done/error` with duration

**File targets:**

| Action | Path |
|---|---|
| CREATE | `backend/app/domains/rendering/job_document.py` — `RenderJobDocument` Pydantic model, `update_step()`, `set_state()` |
| CREATE | `backend/app/core/pipeline_logger.py` — `PipelineLogger(step_start/done/error)` writing to logging + Redis SSE |
| DONE | `backend/alembic/versions/048_render_job_document.py` — adds `render_job_doc JSONB` to `order_lines` |
| MODIFY | `backend/app/domains/pipeline/tasks/render_order_line.py` — write `celery_task_id` + step events to job doc |
| MODIFY | `backend/app/domains/pipeline/tasks/render_thumbnail.py` — same |
| MODIFY | `backend/app/api/routers/orders.py` — cancel reads `render_job_doc.celery_task_id`, calls `celery.control.revoke()` |

**Acceptance gates:**
- Start a 60s render task → click Cancel → `celery inspect active` shows task is gone within 15s
- `GET /api/orders/{id}/lines/{line_id}` response includes `render_job_doc.steps[]` with per-step `duration_s`
- Worker log shows `[THUMBNAIL] done in 34.2s` format (not bare f-strings)

### Priority 8 — Tenant Isolation Completion

**Goal:** Finish tenant isolation hardening, especially for non-HTTP execution paths.

**Status:** In progress. HTTP-side RLS enforcement is now real; task-side propagation is the remaining gap.

**Milestones:**
- M1: `TenantContextMiddleware` registered; all HTTP requests set RLS context from JWT
- M2: All Celery tasks call `set_tenant_context(db, tenant_id)` at task start
- M3: `global_admin` + `tenant_admin` roles in DB; `require_admin()` → `require_global_admin()`

**File targets:**

| Action | Path |
|---|---|
| CREATE | `backend/app/core/middleware.py` — `TenantContextMiddleware(BaseHTTPMiddleware)` |
| MODIFY | `backend/app/main.py` — `app.add_middleware(TenantContextMiddleware)` |
| MODIFY | `backend/app/utils/auth.py` — `create_access_token()` embeds `tenant_id` in JWT claims |
| MODIFY | `backend/app/tasks/pipeline/thumbnail.py`, `extract.py`, `stills.py`, `turntable.py` — `set_tenant_context()` at start |
| DONE | `backend/alembic/versions/049_role_hierarchy.py` — adds `global_admin` and `tenant_admin` |
| PARTIAL | Routers are mixed between new `require_global_admin()` usage and backward-compatible `require_admin()` aliases |

**Acceptance gates:**
- Login as tenant A user → `GET /api/products` returns 0 results when tenant A has no products, even if tenant B has 50
- Verify via: `SELECT count(*) FROM products WHERE tenant_id != '<tenantA_id>'` returns 0 from within tenant A session
- Celery task logs show `[TENANT] context set: tenant_id=<uuid>` at start
- `GET /api/admin/users` returns 403 for `tenant_admin` role (only `global_admin` can list all users)

### Priority 9 — Hash-Based Scene Conversion Caching

**Goal:** Extend the existing STEP-hash plumbing so canonical scene + preview derivatives can skip unnecessary re-tessellation.

**Status:** Partial foundation already exists.

**Milestones:**
- M1: Existing `cad_files.step_file_hash` is reused or renamed for canonical-scene caching
- M2: Export task checks hash before processing — returns cached asset UUID on hit
- M3: Hash invalidated correctly when admin forces reprocess or deflection settings change

**File targets:**

| Action | Path |
|---|---|
| DONE | `backend/app/domains/products/models.py` — `CadFile.step_file_hash: str \| None` already exists |
| DONE | `backend/app/domains/products/cache_service.py` — `compute_step_hash(file_path)` already exists |
| OPEN | `backend/app/domains/pipeline/tasks/export_glb.py` (or future USD task) — hash check before subprocess call |
| OPEN | optional migration to rename `step_file_hash` → `step_hash` only if naming consistency is worth the churn |

**Acceptance gates:**
- Upload same STEP file twice → second task completes in < 2s (cache hit logged: `[CACHE] hash match, skipping tessellation`)
- Change deflection setting → force reprocess → new export runs fresh (hash same but settings changed, cache bypassed)
- `GET /api/cad/{id}` response includes `step_hash` field

### Priority 10 — UI/UX Polish

**Goal:** Address independent UI items from `visual-audit-report.md` that require no backend changes.

**Status:** Partial. Some milestones are already shipped and should be removed from the active queue.

**Milestones:**
- M1: Tooltip/help text on every Admin settings input — mostly done
- M2: Empty state messages in MediaBrowser, ProductLibrary, Orders — partially done
- M3: Notification batching — group per-render noise into job summaries
- M4: Mobile navigation — hamburger menu at < 768px — done
- M5: Kanban rejection flow — drag-to-reject with reason field

**File targets:**

| Action | Path |
|---|---|
| MODIFY | `frontend/src/pages/Admin.tsx` — `title` attributes on all inputs; help text below complex settings |
| MODIFY | `frontend/src/pages/MediaBrowser.tsx` — empty state: "No assets yet — upload a STEP file to get started" |
| MODIFY | `frontend/src/pages/ProductLibrary.tsx` — empty state |
| MODIFY | `frontend/src/pages/Orders.tsx` — empty state |
| MODIFY | `frontend/src/components/layout/Layout.tsx` — hamburger + slide-in nav for mobile |
| MODIFY | `frontend/src/pages/OrderDetail.tsx` — reject button with reason modal |
| CREATE | `frontend/src/components/shared/Tooltip.tsx` — reusable tooltip wrapper |

**Acceptance gates:**
- All Admin settings inputs have visible help text or tooltip (manual check: no input label without explanation)
- Mobile viewport (375px): no horizontal scroll, nav accessible via hamburger
- Submit a render → NotificationCenter shows one "Render complete (3 files)" summary, not 3 individual toasts

---

## Dependency Graph

```
Priority 1 (Pipeline Cleanup Foundation)
  └── Priority 2 (USD Foundation Without Viewer Regression)
        └── Priority 4 (Viewer Migration to Canonical Part Identity)
              └── Priority 5 (Canonical USD Export and Render Migration)
                    └── Priority 6 (Admin and Product Surface Simplification)
                          └── Priority 9 (Hash-Based Scene Conversion Caching)

Priority 3 (Tessellation and Topology Quality)
  └── Priority 5 (Canonical USD Export and Render Migration)

Priority 8 remaining work (Celery tenant context)      — can run in parallel
Priority 10 remaining polish                           — independent
```

---

## What To Do Next

**All 10 original priorities are complete.** Additionally completed as of 2026-03-13:
- **Performance profiling and optimization** — 12-task plan fully implemented (see `plan.md`)
- **Part-materials input validation** — prevents bogus keys from being persisted
- **Dead code cleanup** — verified all legacy directories, endpoints, and imports are gone

**Remaining future work (not yet planned):**
- Automated test suite (currently no tests)
- Batch material assignment UI improvements
- Additional USD features (instancing, LOD)
- Production deployment hardening (health checks, monitoring)
- Draw call batching for 100+ part assemblies (deferred from performance plan — high risk, needs real test data)
- Merge dual STEP parse into single read (deferred — OCP/OCC.Core API compatibility concerns)

---

## Archive

Old planning files are kept for reference but superseded by this document:
- `PLAN.md` — original Phase A–F plan (Phases A–E complete, Phase F = Priority 9 here)
- `PLAN_REFACTOR.md` — 1,173-line architectural plan (Phases 1–8 mapped to Priorities 2–8 above)
- `plan.md` — GMSH tessellation implementation plan (Priority 3)
- `docs/rfcs/0001-step-to-usd-workflow.md` — USD RFC (Priorities 2, 4, and 5)
- `docs/plans/0001-step-to-usd-implementation.md` — actionable USD implementation plan
- `review-report.md` — latest code review results
- `visual-audit-report.md` — UX audit results
