From b795f0e6d62019b51638d58b8f2f4c2c0c2ce660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Mon, 6 Apr 2026 12:45:47 +0200 Subject: [PATCH] refactor: rebrand project to HartOMat --- .claude/commands/db-migrate.md | 4 +- .claude/commands/debug-render.md | 12 +- .claude/commands/excel-import.md | 6 +- .claude/commands/frontend.md | 2 +- .claude/commands/implement.md | 2 +- .claude/commands/plan.md | 4 +- .claude/commands/render-pipeline.md | 6 +- .claude/commands/review.md | 4 +- .claude/commands/tenant-audit.md | 12 +- .claude/commands/usd-export.md | 34 +++--- .claude/commands/visualtesting.md | 2 +- .claude/settings.json | 2 +- .env.example | 6 +- .mcp.json | 8 +- CLAUDE.md | 10 +- LEARNINGS.md | 8 +- MaterialNamingSchema/generate_blend.py | 76 ++++++------- ROADMAP.md | 20 ++-- backend/alembic.ini | 2 +- ...materials.py => 019_hartomat_materials.py} | 16 +-- backend/alembic/versions/035_tenants.py | 2 +- backend/alembic/versions/036_tenant_rls.py | 4 +- .../versions/050_tenant_feature_flags.py | 2 +- .../versions/063_hartomat_rebrand_backfill.py | 106 ++++++++++++++++++ backend/app/api/routers/admin.py | 4 +- backend/app/api/routers/asset_libraries.py | 2 +- backend/app/api/routers/materials.py | 28 ++--- backend/app/api/routers/worker.py | 4 +- backend/app/config.py | 6 +- backend/app/data/hartomat_materials.py | 48 ++++++++ backend/app/data/material_alias_seeds.py | 72 ++++++------ backend/app/data/schaeffler_materials.py | 48 -------- backend/app/domains/materials/models.py | 2 +- backend/app/domains/materials/service.py | 16 +-- backend/app/domains/notifications/service.py | 2 +- .../app/domains/pipeline/tasks/export_glb.py | 2 +- .../pipeline/tasks/render_order_line.py | 2 +- .../app/domains/rendering/workflow_router.py | 2 +- backend/app/domains/tenants/models.py | 2 +- backend/app/main.py | 6 +- backend/app/services/azure_ai.py | 4 +- backend/app/services/chat_service.py | 18 +-- backend/app/services/excel_parser.py | 4 +- backend/app/services/step_processor.py | 4 +- backend/app/tasks/celery_app.py | 2 +- backend/app/utils/seed_templates.py | 6 +- backend/pyproject.toml | 2 +- backend/tests/conftest.py | 4 +- docker-compose.worker.yml | 6 +- docker-compose.yml | 38 +++---- docs/mcp-server.md | 32 +++--- docs/plans/0001-step-to-usd-implementation.md | 34 +++--- docs/rfcs/0001-step-to-usd-workflow.md | 82 +++++++------- frontend/index.html | 4 +- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- frontend/public/hartomat.svg | 5 + frontend/src/api/client.ts | 2 +- frontend/src/api/materials.ts | 10 +- frontend/src/components/MaterialWizard.tsx | 10 +- .../src/components/admin/OutputTypeTable.tsx | 4 +- frontend/src/components/cad/MaterialPanel.tsx | 2 +- frontend/src/components/cad/cadUtils.ts | 2 +- frontend/src/components/chat/ChatPanel.tsx | 6 +- .../orders/UnmappedMaterialsDialog.tsx | 2 +- .../src/components/shared/MaterialInput.tsx | 10 +- frontend/src/help/helpTexts.ts | 4 +- frontend/src/index.css | 2 +- frontend/src/main.tsx | 2 +- frontend/src/pages/Admin.tsx | 2 +- frontend/src/pages/AssetLibrary.tsx | 2 +- frontend/src/pages/Login.tsx | 4 +- frontend/src/pages/Materials.tsx | 36 +++--- frontend/src/pages/NewProductOrder.tsx | 6 +- frontend/src/pages/OrderDetail.tsx | 12 +- frontend/src/pages/Preferences.tsx | 2 +- frontend/src/pages/Tenants.tsx | 8 +- frontend/src/store/auth.ts | 2 +- frontend/src/store/theme.ts | 4 +- ...er_mcp_server.py => hartomat_mcp_server.py | 22 ++-- render-worker/scripts/_blender_import.py | 2 +- render-worker/scripts/_blender_materials.py | 6 +- render-worker/scripts/_blender_scene_setup.py | 4 +- render-worker/scripts/asset_library.py | 2 +- render-worker/scripts/cinematic_render.py | 4 +- render-worker/scripts/export_step_to_gltf.py | 4 +- render-worker/scripts/export_step_to_usd.py | 32 +++--- render-worker/scripts/import_usd.py | 18 +-- render-worker/scripts/still_render.py | 2 +- render-worker/scripts/turntable_render.py | 6 +- scripts/start.sh | 2 +- scripts/stop.sh | 2 +- scripts/test_render_pipeline.py | 4 +- tools/restore_sharp_marks.py | 10 +- visual-audit-report.md | 4 +- 95 files changed, 608 insertions(+), 497 deletions(-) rename backend/alembic/versions/{019_schaeffler_materials.py => 019_hartomat_materials.py} (56%) create mode 100644 backend/alembic/versions/063_hartomat_rebrand_backfill.py create mode 100644 backend/app/data/hartomat_materials.py delete mode 100644 backend/app/data/schaeffler_materials.py create mode 100644 frontend/public/hartomat.svg rename schaeffler_mcp_server.py => hartomat_mcp_server.py (97%) diff --git a/.claude/commands/db-migrate.md b/.claude/commands/db-migrate.md index dfbae44..0b3a748 100644 --- a/.claude/commands/db-migrate.md +++ b/.claude/commands/db-migrate.md @@ -1,6 +1,6 @@ # Database Migration Agent -You are a specialist for Alembic migrations in the Schaeffler Automat project. You create, verify, and apply database migrations safely. +You are a specialist for Alembic migrations in the HartOMat project. You create, verify, and apply database migrations safely. ## Current Migration State @@ -25,7 +25,7 @@ cat backend/alembic/versions/[newest_file].py docker compose exec backend alembic upgrade head # 5. Verify schema -docker compose exec postgres psql -U schaeffler -d schaeffler -c "\d tablename" +docker compose exec postgres psql -U hartomat -d hartomat -c "\d tablename" ``` ## Pre-Apply Checklist diff --git a/.claude/commands/debug-render.md b/.claude/commands/debug-render.md index 529cb81..2fea02a 100644 --- a/.claude/commands/debug-render.md +++ b/.claude/commands/debug-render.md @@ -1,6 +1,6 @@ # Debug Render Agent -You are a specialist for render pipeline problems in the Schaeffler Automat project. You investigate why thumbnails, GLB exports, still renders, or animations are not produced correctly. +You are a specialist for render pipeline problems in the HartOMat project. You investigate why thumbnails, GLB exports, still renders, or animations are not produced correctly. ## Architecture Overview (current) @@ -27,19 +27,19 @@ render_step_thumbnail [queue: asset_pipeline, render-worker container] ```bash # CadFile status -docker compose exec postgres psql -U schaeffler -d schaeffler -c " +docker compose exec postgres psql -U hartomat -d hartomat -c " SELECT id, original_name, processing_status, step_file_hash, render_job_doc->>'state' AS job_state FROM cad_files WHERE id = '[cad_file_id]';" # MediaAssets for a CadFile -docker compose exec postgres psql -U schaeffler -d schaeffler -c " +docker compose exec postgres psql -U hartomat -d hartomat -c " SELECT asset_type, storage_key, file_size_bytes, is_archived, created_at FROM media_assets WHERE cad_file_id = '[cad_file_id]' ORDER BY created_at DESC;" # OrderLine render status and job document -docker compose exec postgres psql -U schaeffler -d schaeffler -c " +docker compose exec postgres psql -U hartomat -d hartomat -c " SELECT id, render_status, render_backend_used, render_job_doc->>'celery_task_id' AS celery_id, render_job_doc->>'state' AS job_state, @@ -47,7 +47,7 @@ SELECT id, render_status, render_backend_used, FROM order_lines WHERE id = '[order_line_id]';" # Material alias lookup -docker compose exec postgres psql -U schaeffler -d schaeffler -c " +docker compose exec postgres psql -U hartomat -d hartomat -c " SELECT m.name AS canonical, ma.alias FROM materials m JOIN material_aliases ma ON ma.material_id = m.id WHERE lower(ma.alias) = lower('[material_name]');" @@ -85,7 +85,7 @@ docker compose exec render-worker find /app/uploads/[cad_file_id]/ -name "*.stp" docker compose exec render-worker find /app/uploads/[cad_file_id]/ -name "*.glb" # MinIO contents (via mc alias) -docker compose exec minio mc ls local/schaeffler/[cad_file_id]/ +docker compose exec minio mc ls local/hartomat/[cad_file_id]/ ``` ## Step 4: Test Export Scripts Directly diff --git a/.claude/commands/excel-import.md b/.claude/commands/excel-import.md index a3671f8..5f43e2a 100644 --- a/.claude/commands/excel-import.md +++ b/.claude/commands/excel-import.md @@ -1,12 +1,12 @@ # Excel Import Agent -You are a specialist for the Excel import parser in the Schaeffler Automat project. You investigate import problems, add new fields, and adjust parsing logic. +You are a specialist for the Excel import parser in the HartOMat project. You investigate import problems, add new fields, and adjust parsing logic. ## Parser Overview **File**: `backend/app/services/excel_parser.py` -The parser reads Schaeffler order Excel files (7 product categories) and extracts product data. +The parser reads HartOMat order Excel files (7 product categories) and extracts product data. ### Header Detection - Searches rows 0–4 for `"Ebene1"` in any column @@ -46,7 +46,7 @@ for r in rows[:3]: " # Check material aliases seeded -docker compose exec postgres psql -U schaeffler -d schaeffler -c " +docker compose exec postgres psql -U hartomat -d hartomat -c " SELECT m.name, ma.alias FROM materials m JOIN material_aliases ma ON ma.material_id = m.id LIMIT 20;" diff --git a/.claude/commands/frontend.md b/.claude/commands/frontend.md index caca49a..65c85a8 100644 --- a/.claude/commands/frontend.md +++ b/.claude/commands/frontend.md @@ -1,6 +1,6 @@ # Frontend Agent -You are a specialist for the React/TypeScript frontend of the Schaeffler Automat project. You implement new UI pages, components, and API bindings. +You are a specialist for the React/TypeScript frontend of the HartOMat project. You implement new UI pages, components, and API bindings. ## Tech Stack diff --git a/.claude/commands/implement.md b/.claude/commands/implement.md index bff0aaa..db270d3 100644 --- a/.claude/commands/implement.md +++ b/.claude/commands/implement.md @@ -1,6 +1,6 @@ # Implementer Agent -You are the implementer for the Schaeffler Automat project. You read `plan.md` and execute tasks one at a time. +You are the implementer for the HartOMat project. You read `plan.md` and execute tasks one at a time. ## Your Workflow diff --git a/.claude/commands/plan.md b/.claude/commands/plan.md index c080878..ae2dad0 100644 --- a/.claude/commands/plan.md +++ b/.claude/commands/plan.md @@ -1,6 +1,6 @@ # Planner Agent -You are the planner for the Schaeffler Automat project. Your only job is analysis and planning — you implement **nothing**. +You are the planner for the HartOMat project. Your only job is analysis and planning — you implement **nothing**. ## Your Workflow @@ -106,7 +106,7 @@ Key-value store in `system_settings` table. Updated via direct SQL UPDATE (SQLAl ### USD Work - Library: `usd-core` pip → `pxr` module -- Seam/sharp: index-space primvars (`primvars:schaeffler:seamEdgeVertexPairs`) +- Seam/sharp: index-space primvars (`primvars:hartomat:seamEdgeVertexPairs`) - Layer strategy: canonical geometry layer + override layer, flatten via `UsdUtils.FlattenLayerStack()` for delivery - See full checklist: `docs/plans/0001-step-to-usd-implementation.md` diff --git a/.claude/commands/render-pipeline.md b/.claude/commands/render-pipeline.md index 3fafceb..174995d 100644 --- a/.claude/commands/render-pipeline.md +++ b/.claude/commands/render-pipeline.md @@ -1,6 +1,6 @@ # Render Pipeline Agent -You are a specialist for the render script chain in the Schaeffler Automat project. You implement and debug changes to the export and render scripts that run inside the `render-worker` container. +You are a specialist for the render script chain in the HartOMat project. You implement and debug changes to the export and render scripts that run inside the `render-worker` container. ## Pipeline Overview @@ -93,7 +93,7 @@ material_name = mat_map_lower.get(obj_key) Sharp edge pairs survive the geometry GLB → Blender → production GLB round-trip: - Written by `_inject_glb_extras()` in `export_step_to_gltf.py` into `scenes[0].extras` -- Read by Blender's glTF importer as `bpy.context.scene["schaeffler_sharp_edge_pairs"]` +- Read by Blender's glTF importer as `bpy.context.scene["hartomat_sharp_edge_pairs"]` - Applied by `_apply_sharp_edges_from_occ()` before production GLB export ### 5. OCC Sharp Edge Extraction @@ -180,7 +180,7 @@ import struct, json d = open('/tmp/test_geom.glb', 'rb').read() jl = struct.unpack_from('.click()`. Gilt für alle geschützten Download-Endpoints. ### 2026-03-07 | PostgreSQL RLS | SET LOCAL muss in jeder Transaktion gesetzt werden -`GRANT BYPASSRLS TO schaeffler` schlug still fehl → Admin-Endpoints bekamen 0 Zeilen. +`GRANT BYPASSRLS TO hartomat` schlug still fehl → Admin-Endpoints bekamen 0 Zeilen. **Lösung:** `await db.execute(text("SET LOCAL app.current_tenant_id = 'bypass'"))` direkt vor jede RLS-geschützte Query in internen/Admin-Endpoints. ### 2026-03-07 | trimesh | GLB-Export-Scale: STL in mm → Three.js in Metern @@ -378,7 +378,7 @@ Bei Mesh-Name-Mismatch-Bugs: GLB-Datei direkt parsen statt im Browser debuggen. ```python import urllib.request, json, struct # Login -data = json.dumps({'email':'admin@schaeffler.com','password':'Admin1234!'}).encode() +data = json.dumps({'email':'admin@hartomat.com','password':'Admin1234!'}).encode() req = urllib.request.Request('http://localhost:8888/api/auth/login', data=data, headers={'Content-Type':'application/json'}) token = json.load(urllib.request.urlopen(req))['access_token'] # Media-Assets für CAD-File diff --git a/MaterialNamingSchema/generate_blend.py b/MaterialNamingSchema/generate_blend.py index d7c519b..48791d0 100644 --- a/MaterialNamingSchema/generate_blend.py +++ b/MaterialNamingSchema/generate_blend.py @@ -1,4 +1,4 @@ -"""Generate material_library.blend with all 35 Schaeffler standard materials. +"""Generate material_library.blend with all 35 HartOMat standard materials. Run with: blender --background --python generate_blend.py """ @@ -9,51 +9,51 @@ import os # Format: (R, G, B, A) linear color, metallic, roughness MATERIALS = [ # --- 01 Metals --- - ("SCHAEFFLER_010101_Steel-Bare", (0.55, 0.56, 0.58, 1.0), 1.0, 0.35), - ("SCHAEFFLER_010102_Steel-Burnished", (0.15, 0.12, 0.10, 1.0), 1.0, 0.25), - ("SCHAEFFLER_010103_Steel-Galvanized", (0.65, 0.67, 0.70, 1.0), 1.0, 0.40), - ("SCHAEFFLER_010104_Steel-Casted", (0.35, 0.33, 0.31, 1.0), 1.0, 0.60), - ("SCHAEFFLER_010105_Steel-Plate", (0.50, 0.51, 0.53, 1.0), 1.0, 0.30), - ("SCHAEFFLER_010201_Niro", (0.70, 0.72, 0.74, 1.0), 1.0, 0.20), - ("SCHAEFFLER_010301_Tin", (0.75, 0.75, 0.73, 1.0), 1.0, 0.30), - ("SCHAEFFLER_010401_Aluminium", (0.80, 0.80, 0.82, 1.0), 1.0, 0.25), - ("SCHAEFFLER_010501_Brass", (0.70, 0.55, 0.20, 1.0), 1.0, 0.25), - ("SCHAEFFLER_010601_Bronze", (0.55, 0.35, 0.15, 1.0), 1.0, 0.30), + ("HARTOMAT_010101_Steel-Bare", (0.55, 0.56, 0.58, 1.0), 1.0, 0.35), + ("HARTOMAT_010102_Steel-Burnished", (0.15, 0.12, 0.10, 1.0), 1.0, 0.25), + ("HARTOMAT_010103_Steel-Galvanized", (0.65, 0.67, 0.70, 1.0), 1.0, 0.40), + ("HARTOMAT_010104_Steel-Casted", (0.35, 0.33, 0.31, 1.0), 1.0, 0.60), + ("HARTOMAT_010105_Steel-Plate", (0.50, 0.51, 0.53, 1.0), 1.0, 0.30), + ("HARTOMAT_010201_Niro", (0.70, 0.72, 0.74, 1.0), 1.0, 0.20), + ("HARTOMAT_010301_Tin", (0.75, 0.75, 0.73, 1.0), 1.0, 0.30), + ("HARTOMAT_010401_Aluminium", (0.80, 0.80, 0.82, 1.0), 1.0, 0.25), + ("HARTOMAT_010501_Brass", (0.70, 0.55, 0.20, 1.0), 1.0, 0.25), + ("HARTOMAT_010601_Bronze", (0.55, 0.35, 0.15, 1.0), 1.0, 0.30), # --- 02 Coatings --- - ("SCHAEFFLER_020101_Durotect-Blue", (0.15, 0.25, 0.50, 1.0), 0.8, 0.20), - ("SCHAEFFLER_020102_Durotect-Black", (0.05, 0.05, 0.06, 1.0), 0.8, 0.15), - ("SCHAEFFLER_020201_Coat-Black", (0.03, 0.03, 0.03, 1.0), 0.6, 0.10), + ("HARTOMAT_020101_Durotect-Blue", (0.15, 0.25, 0.50, 1.0), 0.8, 0.20), + ("HARTOMAT_020102_Durotect-Black", (0.05, 0.05, 0.06, 1.0), 0.8, 0.15), + ("HARTOMAT_020201_Coat-Black", (0.03, 0.03, 0.03, 1.0), 0.6, 0.10), # --- 03 Non-metals --- - ("SCHAEFFLER_030101_Elastomer-Brown", (0.30, 0.18, 0.08, 1.0), 0.0, 0.55), - ("SCHAEFFLER_030102_Elastomer-Green", (0.10, 0.30, 0.10, 1.0), 0.0, 0.55), - ("SCHAEFFLER_030103_Elastomer-Black", (0.04, 0.04, 0.04, 1.0), 0.0, 0.55), - ("SCHAEFFLER_030201_Plastic-Brown", (0.35, 0.22, 0.10, 1.0), 0.0, 0.40), - ("SCHAEFFLER_030202_Plastic-Green", (0.08, 0.35, 0.12, 1.0), 0.0, 0.40), - ("SCHAEFFLER_030203_Plastic-Black", (0.02, 0.02, 0.02, 1.0), 0.0, 0.40), - ("SCHAEFFLER_030204_Plastic-Blue", (0.10, 0.20, 0.50, 1.0), 0.0, 0.40), - ("SCHAEFFLER_030205_Plastic-White", (0.85, 0.85, 0.85, 1.0), 0.0, 0.40), - ("SCHAEFFLER_030301_Plastic-Clear", (0.90, 0.90, 0.92, 1.0), 0.0, 0.10), # + transmission - ("SCHAEFFLER_030302_Plastic-Translucent-White", (0.80, 0.80, 0.82, 1.0), 0.0, 0.20), # + transmission - ("SCHAEFFLER_030401_TPU-Blue", (0.12, 0.25, 0.55, 1.0), 0.0, 0.45), - ("SCHAEFFLER_030501_Ceramic-Black", (0.03, 0.03, 0.04, 1.0), 0.0, 0.15), + ("HARTOMAT_030101_Elastomer-Brown", (0.30, 0.18, 0.08, 1.0), 0.0, 0.55), + ("HARTOMAT_030102_Elastomer-Green", (0.10, 0.30, 0.10, 1.0), 0.0, 0.55), + ("HARTOMAT_030103_Elastomer-Black", (0.04, 0.04, 0.04, 1.0), 0.0, 0.55), + ("HARTOMAT_030201_Plastic-Brown", (0.35, 0.22, 0.10, 1.0), 0.0, 0.40), + ("HARTOMAT_030202_Plastic-Green", (0.08, 0.35, 0.12, 1.0), 0.0, 0.40), + ("HARTOMAT_030203_Plastic-Black", (0.02, 0.02, 0.02, 1.0), 0.0, 0.40), + ("HARTOMAT_030204_Plastic-Blue", (0.10, 0.20, 0.50, 1.0), 0.0, 0.40), + ("HARTOMAT_030205_Plastic-White", (0.85, 0.85, 0.85, 1.0), 0.0, 0.40), + ("HARTOMAT_030301_Plastic-Clear", (0.90, 0.90, 0.92, 1.0), 0.0, 0.10), # + transmission + ("HARTOMAT_030302_Plastic-Translucent-White", (0.80, 0.80, 0.82, 1.0), 0.0, 0.20), # + transmission + ("HARTOMAT_030401_TPU-Blue", (0.12, 0.25, 0.55, 1.0), 0.0, 0.45), + ("HARTOMAT_030501_Ceramic-Black", (0.03, 0.03, 0.04, 1.0), 0.0, 0.15), # --- 04 Compounds --- - ("SCHAEFFLER_040101_E40", (0.25, 0.22, 0.18, 1.0), 0.0, 0.50), - ("SCHAEFFLER_040102_E50", (0.28, 0.25, 0.20, 1.0), 0.0, 0.50), - ("SCHAEFFLER_040201_Elgoglide", (0.20, 0.22, 0.25, 1.0), 0.0, 0.35), - ("SCHAEFFLER_040202_Elgotex", (0.05, 0.05, 0.06, 1.0), 0.0, 0.35), - ("SCHAEFFLER_040301_PTFE-Niro-Compound", (0.60, 0.62, 0.65, 1.0), 0.3, 0.25), - ("SCHAEFFLER_040302_PTFE-Foil", (0.85, 0.85, 0.82, 1.0), 0.0, 0.15), - ("SCHAEFFLER_040303_PTFE-Compound-Black", (0.04, 0.04, 0.05, 1.0), 0.0, 0.30), - ("SCHAEFFLER_040304_PTFE-Compound-Orange", (0.70, 0.35, 0.08, 1.0), 0.0, 0.30), - ("SCHAEFFLER_040305_GFK-PTFE-Compound", (0.08, 0.10, 0.08, 1.0), 0.0, 0.45), + ("HARTOMAT_040101_E40", (0.25, 0.22, 0.18, 1.0), 0.0, 0.50), + ("HARTOMAT_040102_E50", (0.28, 0.25, 0.20, 1.0), 0.0, 0.50), + ("HARTOMAT_040201_Elgoglide", (0.20, 0.22, 0.25, 1.0), 0.0, 0.35), + ("HARTOMAT_040202_Elgotex", (0.05, 0.05, 0.06, 1.0), 0.0, 0.35), + ("HARTOMAT_040301_PTFE-Niro-Compound", (0.60, 0.62, 0.65, 1.0), 0.3, 0.25), + ("HARTOMAT_040302_PTFE-Foil", (0.85, 0.85, 0.82, 1.0), 0.0, 0.15), + ("HARTOMAT_040303_PTFE-Compound-Black", (0.04, 0.04, 0.05, 1.0), 0.0, 0.30), + ("HARTOMAT_040304_PTFE-Compound-Orange", (0.70, 0.35, 0.08, 1.0), 0.0, 0.30), + ("HARTOMAT_040305_GFK-PTFE-Compound", (0.08, 0.10, 0.08, 1.0), 0.0, 0.45), # --- 05 Misc --- - ("SCHAEFFLER_059999_FailedMaterial", (1.00, 0.00, 0.50, 1.0), 0.0, 0.50), + ("HARTOMAT_059999_FailedMaterial", (1.00, 0.00, 0.50, 1.0), 0.0, 0.50), ] # Translucent materials that need transmission TRANSLUCENT = { - "SCHAEFFLER_030301_Plastic-Clear": 0.9, - "SCHAEFFLER_030302_Plastic-Translucent-White": 0.5, + "HARTOMAT_030301_Plastic-Clear": 0.9, + "HARTOMAT_030302_Plastic-Translucent-White": 0.5, } diff --git a/ROADMAP.md b/ROADMAP.md index 04a9fa9..2c72bac 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,4 +1,4 @@ -# Schaeffler Automat — Master Roadmap +# HartOMat — Master Roadmap > **Consolidated:** 2026-03-11 > **Branch:** `refactor/v2` @@ -53,7 +53,7 @@ Verified against the repository on `2026-03-13`. 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: +The key architectural clarification from [docs/rfcs/0001-step-to-usd-workflow.md](/home/hartmut/Documents/Copilot/hartomat/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 @@ -112,7 +112,7 @@ This priority combines dead-code deletion and task decomposition because both ar **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 +- M1: `export_step_to_usd.py` produces valid USD with part hierarchy and `hartomat: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 @@ -139,7 +139,7 @@ This priority combines dead-code deletion and task decomposition because both ar - 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 +- `python3 export_step_to_usd.py --step_path 81113-l_cut.stp` → valid `.usd` file, 25 part prims, each has `hartomat: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 @@ -226,7 +226,7 @@ This priority combines dead-code deletion and task decomposition because both ar 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 +- Write `hartomat: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 @@ -244,20 +244,20 @@ Currently `generate_usd_master_task` does not resolve the material map before pa - `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 +- Acceptance: USD file inspected via `pxr` shows `hartomat: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 | +| CREATE | `render-worker/scripts/import_usd.py` — Blender USD import helper: reads `primvars:hartomat: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 (M5) | `render-worker/scripts/export_step_to_usd.py` — accept `--material_map` CLI arg, write `hartomat:canonicalMaterialName` primvar on each Mesh prim | +| MODIFY (M5) | `render-worker/scripts/import_usd.py` — read `hartomat: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 | @@ -268,7 +268,7 @@ Currently `generate_usd_master_task` does not resolve the material map before pa - 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 +- (M7) `python3 -c "from pxr import Usd; stage=Usd.Stage.Open('usd_master.usd'); [print(p.GetAttribute('primvars:hartomat:canonicalMaterialName').Get()) for p in stage.Traverse()]"` → prints canonical material name for every mesh prim ### Priority 6 — Admin and Product Surface Simplification diff --git a/backend/alembic.ini b/backend/alembic.ini index f0c3c38..f3acbab 100644 --- a/backend/alembic.ini +++ b/backend/alembic.ini @@ -2,7 +2,7 @@ script_location = alembic prepend_sys_path = . version_path_separator = os -sqlalchemy.url = postgresql://schaeffler:schaeffler@localhost:5432/schaeffler +sqlalchemy.url = postgresql://hartomat:hartomat@localhost:5432/hartomat [post_write_hooks] diff --git a/backend/alembic/versions/019_schaeffler_materials.py b/backend/alembic/versions/019_hartomat_materials.py similarity index 56% rename from backend/alembic/versions/019_schaeffler_materials.py rename to backend/alembic/versions/019_hartomat_materials.py index ca06bc4..eee51d8 100644 --- a/backend/alembic/versions/019_schaeffler_materials.py +++ b/backend/alembic/versions/019_hartomat_materials.py @@ -1,4 +1,4 @@ -"""Schaeffler standard materials — add schaeffler_code column and seed 35 materials +"""HartOMat standard materials — add hartomat_code column and seed 35 materials Revision ID: 019 Revises: 018 @@ -16,23 +16,23 @@ depends_on = None def upgrade() -> None: - op.add_column("materials", sa.Column("schaeffler_code", sa.Integer(), nullable=True)) + op.add_column("materials", sa.Column("hartomat_code", sa.Integer(), nullable=True)) - from app.data.schaeffler_materials import SCHAEFFLER_MATERIALS + from app.data.hartomat_materials import HARTOMAT_MATERIALS conn = op.get_bind() now = datetime.utcnow().isoformat() - for mat in SCHAEFFLER_MATERIALS: + for mat in HARTOMAT_MATERIALS: desc = mat["description"].replace("'", "''") name = mat["name"].replace("'", "''") conn.execute(sa.text( - f"INSERT INTO materials (id, name, description, source, schaeffler_code, created_at, updated_at) " + f"INSERT INTO materials (id, name, description, source, hartomat_code, created_at, updated_at) " f"VALUES ('{uuid.uuid4()}', '{name}', '{desc}', '{mat['source']}', " - f"{mat['schaeffler_code']}, '{now}', '{now}') " + f"{mat['hartomat_code']}, '{now}', '{now}') " f"ON CONFLICT (name) DO NOTHING" )) def downgrade() -> None: - op.execute("DELETE FROM materials WHERE source = 'schaeffler_standard'") - op.drop_column("materials", "schaeffler_code") + op.execute("DELETE FROM materials WHERE source = 'hartomat_standard'") + op.drop_column("materials", "hartomat_code") diff --git a/backend/alembic/versions/035_tenants.py b/backend/alembic/versions/035_tenants.py index f814a7c..a1ced47 100644 --- a/backend/alembic/versions/035_tenants.py +++ b/backend/alembic/versions/035_tenants.py @@ -28,7 +28,7 @@ def upgrade(): # Seed default tenant — all existing data will be assigned to this tenant op.execute(""" INSERT INTO tenants (name, slug, is_active) - VALUES ('Schaeffler', 'schaeffler', true) + VALUES ('HartOMat', 'hartomat', true) """) diff --git a/backend/alembic/versions/036_tenant_rls.py b/backend/alembic/versions/036_tenant_rls.py index 7f5373e..90e9dbf 100644 --- a/backend/alembic/versions/036_tenant_rls.py +++ b/backend/alembic/versions/036_tenant_rls.py @@ -74,10 +74,10 @@ def upgrade(): ), ) - # 2. Backfill with the default 'schaeffler' tenant + # 2. Backfill with the default 'hartomat' tenant op.execute( f"UPDATE {table} " - "SET tenant_id = (SELECT id FROM tenants WHERE slug = 'schaeffler')" + "SET tenant_id = (SELECT id FROM tenants WHERE slug = 'hartomat')" ) # 3. Make NOT NULL now that every row has a value diff --git a/backend/alembic/versions/050_tenant_feature_flags.py b/backend/alembic/versions/050_tenant_feature_flags.py index 211bb5c..97b4c0d 100644 --- a/backend/alembic/versions/050_tenant_feature_flags.py +++ b/backend/alembic/versions/050_tenant_feature_flags.py @@ -20,7 +20,7 @@ _DEFAULT_CONFIG = """{ "max_concurrent_renders": 3, "render_engines_allowed": ["cycles", "eevee"], "max_order_size": 500, - "fallback_material": "SCHAEFFLER_059999_FailedMaterial", + "fallback_material": "HARTOMAT_059999_FailedMaterial", "notifications_enabled": true, "invoice_prefix": "INV" }""" diff --git a/backend/alembic/versions/063_hartomat_rebrand_backfill.py b/backend/alembic/versions/063_hartomat_rebrand_backfill.py new file mode 100644 index 0000000..d653c7d --- /dev/null +++ b/backend/alembic/versions/063_hartomat_rebrand_backfill.py @@ -0,0 +1,106 @@ +"""Backfill persisted Schaeffler branding to HartOMat. + +Revision ID: 063 +Revises: 062 +""" +from alembic import op +import sqlalchemy as sa + +revision = "063" +down_revision = "062" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + material_columns = {column["name"] for column in inspector.get_columns("materials")} + + if "schaeffler_code" in material_columns and "hartomat_code" not in material_columns: + op.alter_column("materials", "schaeffler_code", new_column_name="hartomat_code") + + op.execute( + """ + UPDATE materials + SET name = REPLACE(name, 'SCHAEFFLER_', 'HARTOMAT_') + WHERE name LIKE 'SCHAEFFLER_%' + """ + ) + op.execute( + """ + UPDATE materials + SET source = 'hartomat_standard' + WHERE source = 'schaeffler_standard' + """ + ) + + op.execute( + """ + UPDATE tenants + SET name = 'HartOMat', + slug = 'hartomat' + WHERE lower(name) = 'schaeffler' + OR lower(slug) = 'schaeffler' + """ + ) + + op.execute( + """ + UPDATE tenants + SET tenant_config = jsonb_set( + tenant_config, + '{fallback_material}', + '\"HARTOMAT_059999_FailedMaterial\"'::jsonb, + true + ) + WHERE tenant_config ? 'fallback_material' + """ + ) + + +def downgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + material_columns = {column["name"] for column in inspector.get_columns("materials")} + + op.execute( + """ + UPDATE materials + SET name = REPLACE(name, 'HARTOMAT_', 'SCHAEFFLER_') + WHERE name LIKE 'HARTOMAT_%' + """ + ) + op.execute( + """ + UPDATE materials + SET source = 'schaeffler_standard' + WHERE source = 'hartomat_standard' + """ + ) + + op.execute( + """ + UPDATE tenants + SET name = 'Schaeffler', + slug = 'schaeffler' + WHERE lower(name) = 'hartomat' + OR lower(slug) = 'hartomat' + """ + ) + + op.execute( + """ + UPDATE tenants + SET tenant_config = jsonb_set( + tenant_config, + '{fallback_material}', + '\"SCHAEFFLER_059999_FailedMaterial\"'::jsonb, + true + ) + WHERE tenant_config ? 'fallback_material' + """ + ) + + if "hartomat_code" in material_columns and "schaeffler_code" not in material_columns: + op.alter_column("materials", "hartomat_code", new_column_name="schaeffler_code") diff --git a/backend/app/api/routers/admin.py b/backend/app/api/routers/admin.py index 9872458..c31d4d7 100644 --- a/backend/app/api/routers/admin.py +++ b/backend/app/api/routers/admin.py @@ -1014,9 +1014,9 @@ async def get_dashboard_stats( if isinstance(entry, dict) and entry.get("material"): all_mat_names.add(entry["material"]) - # Library materials (name starts with SCHAEFFLER_) + # Library materials (name starts with HARTOMAT_) lib_count_result = await db.execute( - select(func.count(Material.id)).where(Material.name.like("SCHAEFFLER_%")) + select(func.count(Material.id)).where(Material.name.like("HARTOMAT_%")) ) library_material_count = lib_count_result.scalar() or 0 diff --git a/backend/app/api/routers/asset_libraries.py b/backend/app/api/routers/asset_libraries.py index d9eabb9..a106d3f 100644 --- a/backend/app/api/routers/asset_libraries.py +++ b/backend/app/api/routers/asset_libraries.py @@ -89,7 +89,7 @@ async def get_material_pbr_map(db: AsyncSession = Depends(get_db)): } # Also index by aliases so frontend can look up by raw Excel names - # (e.g. "Steel--Stahl" → same PBR as "SCHAEFFLER_010101_Steel-Bare") + # (e.g. "Steel--Stahl" → same PBR as "HARTOMAT_010101_Steel-Bare") # Bypass RLS — this is public data and aliases may have NULL tenant_id if pbr_map: await db.execute(text("SET LOCAL app.current_tenant_id = 'bypass'")) diff --git a/backend/app/api/routers/materials.py b/backend/app/api/routers/materials.py index 8adcc49..7c8ab00 100644 --- a/backend/app/api/routers/materials.py +++ b/backend/app/api/routers/materials.py @@ -23,7 +23,7 @@ class MaterialOut(BaseModel): name: str description: str | None source: str - schaeffler_code: int | None = None + hartomat_code: int | None = None created_by_name: str | None = None aliases: list[str] = [] created_at: datetime @@ -42,7 +42,7 @@ class MaterialCreate(BaseModel): name: str description: str | None = None source: str = "manual" - schaeffler_code: int | None = None + hartomat_code: int | None = None class MaterialUpdate(BaseModel): @@ -64,7 +64,7 @@ def _to_out(mat: Material) -> MaterialOut: name=mat.name, description=mat.description, source=mat.source, - schaeffler_code=mat.schaeffler_code, + hartomat_code=mat.hartomat_code, created_by_name=creator_name, aliases=alias_names, created_at=mat.created_at, @@ -94,9 +94,9 @@ async def get_next_code( range_end = prefix_int + 99 result = await db.execute( - select(func.max(Material.schaeffler_code)).where( - Material.schaeffler_code >= range_start, - Material.schaeffler_code <= range_end, + select(func.max(Material.hartomat_code)).where( + Material.hartomat_code >= range_start, + Material.hartomat_code <= range_end, ) ) max_code = result.scalar_one_or_none() @@ -113,16 +113,16 @@ async def get_next_code( } -@router.post("/seed-schaeffler") -async def seed_schaeffler_materials( +@router.post("/seed-hartomat") +async def seed_hartomat_materials( user: User = Depends(require_admin_or_pm), db: AsyncSession = Depends(get_db), ): - """Bulk-create the 35 standard Schaeffler materials. Skips existing by name.""" - from app.data.schaeffler_materials import SCHAEFFLER_MATERIALS + """Bulk-create the 35 standard HartOMat materials. Skips existing by name.""" + from app.data.hartomat_materials import HARTOMAT_MATERIALS inserted = 0 - for mat_data in SCHAEFFLER_MATERIALS: + for mat_data in HARTOMAT_MATERIALS: existing = await db.execute( select(Material).where(Material.name == mat_data["name"]) ) @@ -132,14 +132,14 @@ async def seed_schaeffler_materials( name=mat_data["name"], description=mat_data["description"], source=mat_data["source"], - schaeffler_code=mat_data["schaeffler_code"], + hartomat_code=mat_data["hartomat_code"], created_by=user.id, ) db.add(mat) inserted += 1 await db.commit() - return {"inserted": inserted, "total": len(SCHAEFFLER_MATERIALS)} + return {"inserted": inserted, "total": len(HARTOMAT_MATERIALS)} @router.post("/seed-aliases") @@ -273,7 +273,7 @@ async def create_material( name=body.name, description=body.description, source=body.source, - schaeffler_code=body.schaeffler_code, + hartomat_code=body.hartomat_code, created_by=user.id, ) db.add(mat) diff --git a/backend/app/api/routers/worker.py b/backend/app/api/routers/worker.py index 3966099..2184bfe 100644 --- a/backend/app/api/routers/worker.py +++ b/backend/app/api/routers/worker.py @@ -475,8 +475,8 @@ async def scale_workers( compose_file = os.path.join(compose_dir, "docker-compose.yml") # Derive project name from compose dir on host (directory name = project name). # Inside the container the compose file is at /compose, but the host project - # dir name determines the container naming prefix (e.g. "schaefflerautomat"). - compose_project = os.environ.get("COMPOSE_PROJECT_NAME", "schaefflerautomat") + # dir name determines the container naming prefix (e.g. "hartomat"). + compose_project = os.environ.get("COMPOSE_PROJECT_NAME", "hartomat") def _scale() -> subprocess.CompletedProcess: return subprocess.run( diff --git a/backend/app/config.py b/backend/app/config.py index 7043f2b..88b85c8 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -4,9 +4,9 @@ from typing import Optional class Settings(BaseSettings): # Database - postgres_db: str = "schaeffler" - postgres_user: str = "schaeffler" - postgres_password: str = "schaeffler" + postgres_db: str = "hartomat" + postgres_user: str = "hartomat" + postgres_password: str = "hartomat" postgres_host: str = "localhost" postgres_port: int = 5432 diff --git a/backend/app/data/hartomat_materials.py b/backend/app/data/hartomat_materials.py new file mode 100644 index 0000000..bf973a8 --- /dev/null +++ b/backend/app/data/hartomat_materials.py @@ -0,0 +1,48 @@ +"""HartOMat standard materials — single source of truth. + +Naming convention: HARTOMAT_[TypeCode(2)][SubType(2)][Consecutive(2)]_[Name-Parts-Dashed] +Type codes: 01=Metals, 02=Coatings, 03=Non-metals, 04=Compounds, 05=Misc +""" + +HARTOMAT_MATERIALS: list[dict] = [ + # --- 01 Metals --- + {"name": "HARTOMAT_010101_Steel-Bare", "description": "Stahl / Stahl, glänzend / Stahl, konserviert", "hartomat_code": 10101, "source": "hartomat_standard"}, + {"name": "HARTOMAT_010102_Steel-Burnished", "description": "Stahl, brüniert", "hartomat_code": 10102, "source": "hartomat_standard"}, + {"name": "HARTOMAT_010103_Steel-Galvanized", "description": "Stahl, verzinkt", "hartomat_code": 10103, "source": "hartomat_standard"}, + {"name": "HARTOMAT_010104_Steel-Casted", "description": "Stahl Körnung", "hartomat_code": 10104, "source": "hartomat_standard"}, + {"name": "HARTOMAT_010105_Steel-Plate", "description": "Stahlblech", "hartomat_code": 10105, "source": "hartomat_standard"}, + {"name": "HARTOMAT_010201_Niro", "description": "Niro", "hartomat_code": 10201, "source": "hartomat_standard"}, + {"name": "HARTOMAT_010301_Tin", "description": "MU-Stahl, Zinnüberzug / MX-Stahl, Zinnüberzug", "hartomat_code": 10301, "source": "hartomat_standard"}, + {"name": "HARTOMAT_010401_Aluminium", "description": "Aluminium", "hartomat_code": 10401, "source": "hartomat_standard"}, + {"name": "HARTOMAT_010501_Brass", "description": "Messing", "hartomat_code": 10501, "source": "hartomat_standard"}, + {"name": "HARTOMAT_010601_Bronze", "description": "MU-B, Bronze", "hartomat_code": 10601, "source": "hartomat_standard"}, + # --- 02 Coatings --- + {"name": "HARTOMAT_020101_Durotect-Blue", "description": "Stahl, Durotect CMT", "hartomat_code": 20101, "source": "hartomat_standard"}, + {"name": "HARTOMAT_020102_Durotect-Black", "description": "Stahl, Durotect M", "hartomat_code": 20102, "source": "hartomat_standard"}, + {"name": "HARTOMAT_020201_Coat-Black", "description": "", "hartomat_code": 20201, "source": "hartomat_standard"}, + # --- 03 Non-metals --- + {"name": "HARTOMAT_030101_Elastomer-Brown", "description": "Elastomer, braun", "hartomat_code": 30101, "source": "hartomat_standard"}, + {"name": "HARTOMAT_030102_Elastomer-Green", "description": "Elastomer, grün", "hartomat_code": 30102, "source": "hartomat_standard"}, + {"name": "HARTOMAT_030103_Elastomer-Black", "description": "Elastomer, schwarz", "hartomat_code": 30103, "source": "hartomat_standard"}, + {"name": "HARTOMAT_030201_Plastic-Brown", "description": "Kunststoff, braun", "hartomat_code": 30201, "source": "hartomat_standard"}, + {"name": "HARTOMAT_030202_Plastic-Green", "description": "Kunststoff, grün", "hartomat_code": 30202, "source": "hartomat_standard"}, + {"name": "HARTOMAT_030203_Plastic-Black", "description": "Kunststoff, schwarz", "hartomat_code": 30203, "source": "hartomat_standard"}, + {"name": "HARTOMAT_030204_Plastic-Blue", "description": "Kunststoff, blau", "hartomat_code": 30204, "source": "hartomat_standard"}, + {"name": "HARTOMAT_030205_Plastic-White", "description": "Kunststoff, weiß", "hartomat_code": 30205, "source": "hartomat_standard"}, + {"name": "HARTOMAT_030301_Plastic-Clear", "description": "Kunststoff, durchsichtig", "hartomat_code": 30301, "source": "hartomat_standard"}, + {"name": "HARTOMAT_030302_Plastic-Translucent-White", "description": "", "hartomat_code": 30302, "source": "hartomat_standard"}, + {"name": "HARTOMAT_030401_TPU-Blue", "description": "TPU, blau", "hartomat_code": 30401, "source": "hartomat_standard"}, + {"name": "HARTOMAT_030501_Ceramic-Black", "description": "Keramik, schwarz", "hartomat_code": 30501, "source": "hartomat_standard"}, + # --- 04 Compounds --- + {"name": "HARTOMAT_040101_E40", "description": "E40", "hartomat_code": 40101, "source": "hartomat_standard"}, + {"name": "HARTOMAT_040102_E50", "description": "E50", "hartomat_code": 40102, "source": "hartomat_standard"}, + {"name": "HARTOMAT_040201_Elgoglide", "description": "Elgoglide", "hartomat_code": 40201, "source": "hartomat_standard"}, + {"name": "HARTOMAT_040202_Elgotex", "description": "Elgotex, schwarz", "hartomat_code": 40202, "source": "hartomat_standard"}, + {"name": "HARTOMAT_040301_PTFE-Niro-Compound", "description": "PTFE-Compound, Niro-Verbund", "hartomat_code": 40301, "source": "hartomat_standard"}, + {"name": "HARTOMAT_040302_PTFE-Foil", "description": "PTFE-Folie", "hartomat_code": 40302, "source": "hartomat_standard"}, + {"name": "HARTOMAT_040303_PTFE-Compound-Black", "description": "PTFE-Verbund, schwarz", "hartomat_code": 40303, "source": "hartomat_standard"}, + {"name": "HARTOMAT_040304_PTFE-Compound-Orange", "description": "PTFE-Verbundwerkstoff", "hartomat_code": 40304, "source": "hartomat_standard"}, + {"name": "HARTOMAT_040305_GFK-PTFE-Compound", "description": "GFK+PTFE Verbundwerkstoff, schwarz / TPU, schwarz", "hartomat_code": 40305, "source": "hartomat_standard"}, + # --- 05 Misc --- + {"name": "HARTOMAT_059999_FailedMaterial", "description": "", "hartomat_code": 59999, "source": "hartomat_standard"}, +] diff --git a/backend/app/data/material_alias_seeds.py b/backend/app/data/material_alias_seeds.py index 803f605..c73165c 100644 --- a/backend/app/data/material_alias_seeds.py +++ b/backend/app/data/material_alias_seeds.py @@ -1,9 +1,9 @@ """Material alias seed data — derived from naming_scheme.xlsx Materialmapping sheet. -Each entry maps a SCHAEFFLER library material name to its known aliases: +Each entry maps a HARTOMAT library material name to its known aliases: - German description (Col A from Materialmapping) - Intermediate identifier (Col B, e.g. "Steel_black_oxided--Stahl_brueniert") -- Schaeffler code as string (e.g. "10102") +- HartOMat code as string (e.g. "10102") - German variants (singular/plural, abbreviations, industry terms, DIN/EN standards) - English equivalents commonly used in German engineering contexts """ @@ -13,7 +13,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ # --- 01 Metals --- # ===================================================================== { - "material_name": "SCHAEFFLER_010101_Steel-Bare", + "material_name": "HARTOMAT_010101_Steel-Bare", "aliases": [ "Stahl", "Stahl, glänzend", @@ -66,7 +66,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_010102_Steel-Burnished", + "material_name": "HARTOMAT_010102_Steel-Burnished", "aliases": [ "Stahl, brüniert", "Steel_black_oxided--Stahl_brueniert", @@ -94,7 +94,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_010103_Steel-Galvanized", + "material_name": "HARTOMAT_010103_Steel-Galvanized", "aliases": [ "Stahl, verzinkt", "Steel_galvanized--Stahl_verzinkt", @@ -130,7 +130,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_010104_Steel-Casted", + "material_name": "HARTOMAT_010104_Steel-Casted", "aliases": [ "Stahl Körnung", "Guss", @@ -169,7 +169,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_010105_Steel-Plate", + "material_name": "HARTOMAT_010105_Steel-Plate", "aliases": [ "Stahlblech", "Steel_sheet--Stahlblech", @@ -204,7 +204,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_010201_Niro", + "material_name": "HARTOMAT_010201_Niro", "aliases": [ "Niro", "Steel_stainless--Niro", @@ -248,7 +248,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_010301_Tin", + "material_name": "HARTOMAT_010301_Tin", "aliases": [ "Zinnüberzug", "Tin--Zinn", @@ -278,7 +278,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_010401_Aluminium", + "material_name": "HARTOMAT_010401_Aluminium", "aliases": [ "Aluminium", "Aluminium--Aluminium", @@ -319,7 +319,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_010501_Brass", + "material_name": "HARTOMAT_010501_Brass", "aliases": [ "Messing", "Brass--Messing", @@ -351,7 +351,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_010601_Bronze", + "material_name": "HARTOMAT_010601_Bronze", "aliases": [ "MU-B; Bronze", "Bronze", @@ -393,7 +393,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ # --- 02 Coatings --- # ===================================================================== { - "material_name": "SCHAEFFLER_020101_Durotect-Blue", + "material_name": "HARTOMAT_020101_Durotect-Blue", "aliases": [ "Stahl, Durotect CMT", "Durotect_CMT--Durotect_CMT", @@ -414,7 +414,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_020102_Durotect-Black", + "material_name": "HARTOMAT_020102_Durotect-Black", "aliases": [ "Stahl, Durotect M", "Stahl; Durotect M", @@ -435,7 +435,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_020201_Coat-Black", + "material_name": "HARTOMAT_020201_Coat-Black", "aliases": [ "Stahl, schwarz", "Steel_coated_black--Stahl_beschichtet_schwarz", @@ -468,7 +468,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ # --- 03 Non-metals --- # ===================================================================== { - "material_name": "SCHAEFFLER_030101_Elastomer-Brown", + "material_name": "HARTOMAT_030101_Elastomer-Brown", "aliases": [ "Elastomer, braun", "Elastomer_brown--Elastomer_braun", @@ -493,7 +493,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_030102_Elastomer-Green", + "material_name": "HARTOMAT_030102_Elastomer-Green", "aliases": [ "Elastomer, grün", "Elastomer_green--Elastomer_gruen", @@ -518,7 +518,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_030103_Elastomer-Black", + "material_name": "HARTOMAT_030103_Elastomer-Black", "aliases": [ "Elastomer, schwarz", "Eslastomer_black--Elastomer_schwarz", @@ -557,7 +557,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_030201_Plastic-Brown", + "material_name": "HARTOMAT_030201_Plastic-Brown", "aliases": [ "Kunststoff, braun", "Plastic_brown--Kunststoff_braun", @@ -585,7 +585,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_030202_Plastic-Green", + "material_name": "HARTOMAT_030202_Plastic-Green", "aliases": [ "Kunststoff, grün", "Plastic_green--Kunststoff_gruen", @@ -612,7 +612,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_030203_Plastic-Black", + "material_name": "HARTOMAT_030203_Plastic-Black", "aliases": [ "Kunststoff, schwarz", "Plastic_black--Kunststoff_schwarz", @@ -642,7 +642,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_030204_Plastic-Blue", + "material_name": "HARTOMAT_030204_Plastic-Blue", "aliases": [ "Kunststoff, blau", "Plastic_blue--Kunststoff_blau", @@ -668,7 +668,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_030205_Plastic-White", + "material_name": "HARTOMAT_030205_Plastic-White", "aliases": [ "Kunststoff, weiß", "Plastic_white--Kunststoff_weiss", @@ -702,7 +702,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_030301_Plastic-Clear", + "material_name": "HARTOMAT_030301_Plastic-Clear", "aliases": [ "Kunststoff, durchsichtig", "Plastic_clear--Kunststoff_durchsichtig", @@ -738,7 +738,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_030302_Plastic-Translucent-White", + "material_name": "HARTOMAT_030302_Plastic-Translucent-White", "aliases": [ "Plastic_translucent_white--Kunststoff_transluzent_weiss", "30302", @@ -769,7 +769,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_030401_TPU-Blue", + "material_name": "HARTOMAT_030401_TPU-Blue", "aliases": [ "TPU, blau", "Elastomer_blue--Elastomer_blau", @@ -798,7 +798,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_030501_Ceramic-Black", + "material_name": "HARTOMAT_030501_Ceramic-Black", "aliases": [ "Keramik, schwarz", "Ceramics_black--Keramik_schwarz", @@ -834,7 +834,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ # --- 04 Compounds --- # ===================================================================== { - "material_name": "SCHAEFFLER_040101_E40", + "material_name": "HARTOMAT_040101_E40", "aliases": [ "E40", "E40--E40", @@ -851,7 +851,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_040102_E50", + "material_name": "HARTOMAT_040102_E50", "aliases": [ "E50", "E50--E50", @@ -868,7 +868,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_040201_Elgoglide", + "material_name": "HARTOMAT_040201_Elgoglide", "aliases": [ "Elgoglide", "Elgoglide--Elgoglide", @@ -886,7 +886,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_040202_Elgotex", + "material_name": "HARTOMAT_040202_Elgotex", "aliases": [ "Elgotex, schwarz", "Elgotex--Elgotex", @@ -907,7 +907,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_040301_PTFE-Niro-Compound", + "material_name": "HARTOMAT_040301_PTFE-Niro-Compound", "aliases": [ "PTFE-Compound, Niro-Verbund", "PTFE_compound_stainless_steel_composite--PTFE_Compound_Niro_Verbund", @@ -933,7 +933,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_040302_PTFE-Foil", + "material_name": "HARTOMAT_040302_PTFE-Foil", "aliases": [ "PTFE-Folie", "PTFE_film--PTFE_Folie", @@ -962,7 +962,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_040303_PTFE-Compound-Black", + "material_name": "HARTOMAT_040303_PTFE-Compound-Black", "aliases": [ "PTFE-Verbund, schwarz", "PTFE_compound_black--PTFE_Verbund_schwarz", @@ -987,7 +987,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_040304_PTFE-Compound-Orange", + "material_name": "HARTOMAT_040304_PTFE-Compound-Orange", "aliases": [ "PTFE-Verbundwerkstoff", "PTFE_composite_material_orange--PTFE_Verbundwerkstoff_orange", @@ -1014,7 +1014,7 @@ MATERIAL_ALIAS_SEEDS: list[dict] = [ ], }, { - "material_name": "SCHAEFFLER_040305_GFK-PTFE-Compound", + "material_name": "HARTOMAT_040305_GFK-PTFE-Compound", "aliases": [ "GFK+PTFE Verbundwerkstoff, schwarz", "GFK_PTFE_compound--GFK_PTFE_Verbundwerkstoff", diff --git a/backend/app/data/schaeffler_materials.py b/backend/app/data/schaeffler_materials.py deleted file mode 100644 index 1d36a69..0000000 --- a/backend/app/data/schaeffler_materials.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Schaeffler standard materials — single source of truth. - -Naming convention: SCHAEFFLER_[TypeCode(2)][SubType(2)][Consecutive(2)]_[Name-Parts-Dashed] -Type codes: 01=Metals, 02=Coatings, 03=Non-metals, 04=Compounds, 05=Misc -""" - -SCHAEFFLER_MATERIALS: list[dict] = [ - # --- 01 Metals --- - {"name": "SCHAEFFLER_010101_Steel-Bare", "description": "Stahl / Stahl, glänzend / Stahl, konserviert", "schaeffler_code": 10101, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_010102_Steel-Burnished", "description": "Stahl, brüniert", "schaeffler_code": 10102, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_010103_Steel-Galvanized", "description": "Stahl, verzinkt", "schaeffler_code": 10103, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_010104_Steel-Casted", "description": "Stahl Körnung", "schaeffler_code": 10104, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_010105_Steel-Plate", "description": "Stahlblech", "schaeffler_code": 10105, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_010201_Niro", "description": "Niro", "schaeffler_code": 10201, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_010301_Tin", "description": "MU-Stahl, Zinnüberzug / MX-Stahl, Zinnüberzug", "schaeffler_code": 10301, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_010401_Aluminium", "description": "Aluminium", "schaeffler_code": 10401, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_010501_Brass", "description": "Messing", "schaeffler_code": 10501, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_010601_Bronze", "description": "MU-B, Bronze", "schaeffler_code": 10601, "source": "schaeffler_standard"}, - # --- 02 Coatings --- - {"name": "SCHAEFFLER_020101_Durotect-Blue", "description": "Stahl, Durotect CMT", "schaeffler_code": 20101, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_020102_Durotect-Black", "description": "Stahl, Durotect M", "schaeffler_code": 20102, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_020201_Coat-Black", "description": "", "schaeffler_code": 20201, "source": "schaeffler_standard"}, - # --- 03 Non-metals --- - {"name": "SCHAEFFLER_030101_Elastomer-Brown", "description": "Elastomer, braun", "schaeffler_code": 30101, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_030102_Elastomer-Green", "description": "Elastomer, grün", "schaeffler_code": 30102, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_030103_Elastomer-Black", "description": "Elastomer, schwarz", "schaeffler_code": 30103, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_030201_Plastic-Brown", "description": "Kunststoff, braun", "schaeffler_code": 30201, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_030202_Plastic-Green", "description": "Kunststoff, grün", "schaeffler_code": 30202, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_030203_Plastic-Black", "description": "Kunststoff, schwarz", "schaeffler_code": 30203, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_030204_Plastic-Blue", "description": "Kunststoff, blau", "schaeffler_code": 30204, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_030205_Plastic-White", "description": "Kunststoff, weiß", "schaeffler_code": 30205, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_030301_Plastic-Clear", "description": "Kunststoff, durchsichtig", "schaeffler_code": 30301, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_030302_Plastic-Translucent-White", "description": "", "schaeffler_code": 30302, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_030401_TPU-Blue", "description": "TPU, blau", "schaeffler_code": 30401, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_030501_Ceramic-Black", "description": "Keramik, schwarz", "schaeffler_code": 30501, "source": "schaeffler_standard"}, - # --- 04 Compounds --- - {"name": "SCHAEFFLER_040101_E40", "description": "E40", "schaeffler_code": 40101, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_040102_E50", "description": "E50", "schaeffler_code": 40102, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_040201_Elgoglide", "description": "Elgoglide", "schaeffler_code": 40201, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_040202_Elgotex", "description": "Elgotex, schwarz", "schaeffler_code": 40202, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_040301_PTFE-Niro-Compound", "description": "PTFE-Compound, Niro-Verbund", "schaeffler_code": 40301, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_040302_PTFE-Foil", "description": "PTFE-Folie", "schaeffler_code": 40302, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_040303_PTFE-Compound-Black", "description": "PTFE-Verbund, schwarz", "schaeffler_code": 40303, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_040304_PTFE-Compound-Orange", "description": "PTFE-Verbundwerkstoff", "schaeffler_code": 40304, "source": "schaeffler_standard"}, - {"name": "SCHAEFFLER_040305_GFK-PTFE-Compound", "description": "GFK+PTFE Verbundwerkstoff, schwarz / TPU, schwarz", "schaeffler_code": 40305, "source": "schaeffler_standard"}, - # --- 05 Misc --- - {"name": "SCHAEFFLER_059999_FailedMaterial", "description": "", "schaeffler_code": 59999, "source": "schaeffler_standard"}, -] diff --git a/backend/app/domains/materials/models.py b/backend/app/domains/materials/models.py index 6310fb3..4057dff 100644 --- a/backend/app/domains/materials/models.py +++ b/backend/app/domains/materials/models.py @@ -17,7 +17,7 @@ class Material(Base): name: Mapped[str] = mapped_column(String(200), nullable=False, unique=True) description: Mapped[str] = mapped_column(Text, nullable=True) source: Mapped[str] = mapped_column(String(20), nullable=False, default="manual") - schaeffler_code: Mapped[int | None] = mapped_column(Integer, nullable=True) + hartomat_code: Mapped[int | None] = mapped_column(Integer, nullable=True) created_by: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True ) diff --git a/backend/app/domains/materials/service.py b/backend/app/domains/materials/service.py index 48d9f75..eda8334 100644 --- a/backend/app/domains/materials/service.py +++ b/backend/app/domains/materials/service.py @@ -1,7 +1,7 @@ """Material alias resolution service. Used from Celery tasks (sync context) to resolve raw material names -(from Excel / user input) to SCHAEFFLER library material names via aliases. +(from Excel / user input) to HARTOMAT library material names via aliases. Resolution chain: 1. Alias lookup (case-insensitive) → use alias.material.name @@ -31,7 +31,7 @@ def _get_engine(): def resolve_material_map(raw_map: dict[str, str]) -> dict[str, str]: - """Resolve raw material names to SCHAEFFLER library names via aliases. + """Resolve raw material names to HARTOMAT library names via aliases. For each value in raw_map: 1. Alias lookup (case-insensitive) → return alias.material.name @@ -66,7 +66,7 @@ def resolve_material_map(raw_map: dict[str, str]) -> dict[str, str]: raw_lower = raw_material.lower() # 1. Alias lookup first — aliases explicitly map intermediate/display names - # to the canonical SCHAEFFLER library names + # to the canonical HARTOMAT library names if raw_lower in alias_lookup: target = alias_lookup[raw_lower] logger.info("resolved '%s' → '%s' (alias match)", raw_material, target) @@ -147,7 +147,7 @@ async def find_unmapped_materials( """Find material names that have no alias or library match. Returns a list of {"raw_name": str, "suggestions": [...]} for each - unmapped name. Suggestions are the top 5 SCHAEFFLER library materials + unmapped name. Suggestions are the top 5 HARTOMAT library materials by string similarity. """ if not material_names: @@ -159,8 +159,8 @@ async def find_unmapped_materials( # Load all materials mat_rows = (await db.execute(select(Material))).scalars().all() - # Library materials have a schaeffler_code - library_mats = [m for m in mat_rows if m.schaeffler_code is not None] + # Library materials have a hartomat_code + library_mats = [m for m in mat_rows if m.hartomat_code is not None] # All material names (case-insensitive) for exact-match check name_lookup: dict[str, Material] = {m.name.lower(): m for m in mat_rows} @@ -179,7 +179,7 @@ async def find_unmapped_materials( # 2. Exact name match with a library material → mapped matched_mat = name_lookup.get(raw_lower) - if matched_mat and matched_mat.schaeffler_code is not None: + if matched_mat and matched_mat.hartomat_code is not None: continue # Unmapped — compute suggestions from library materials @@ -194,7 +194,7 @@ async def find_unmapped_materials( { "id": str(m.id), "name": m.name, - "schaeffler_code": str(m.schaeffler_code), + "hartomat_code": str(m.hartomat_code), } for _, m in scored[:5] ] diff --git a/backend/app/domains/notifications/service.py b/backend/app/domains/notifications/service.py index 8fcf66b..2bc3c0f 100644 --- a/backend/app/domains/notifications/service.py +++ b/backend/app/domains/notifications/service.py @@ -235,7 +235,7 @@ def send_email_notification_stub( from email.mime.text import MIMEText msg = MIMEText(body, "plain", "utf-8") msg["Subject"] = subject - msg["From"] = cfg.get("smtp_from_address") or cfg.get("smtp_user", "noreply@schaeffler.com") + msg["From"] = cfg.get("smtp_from_address") or cfg.get("smtp_user", "noreply@hartomat.com") msg["To"] = to_address port = int(cfg.get("smtp_port", "587")) with smtplib.SMTP(smtp_host, port) as smtp: diff --git a/backend/app/domains/pipeline/tasks/export_glb.py b/backend/app/domains/pipeline/tasks/export_glb.py index 3094d7f..a29d370 100644 --- a/backend/app/domains/pipeline/tasks/export_glb.py +++ b/backend/app/domains/pipeline/tasks/export_glb.py @@ -330,7 +330,7 @@ def generate_usd_master_task(self, cad_file_id: str) -> dict: if part_name and raw_material: raw_mat_map[part_name] = raw_material - # Resolve raw material names to SCHAEFFLER library names via aliases + # Resolve raw material names to HARTOMAT library names via aliases material_map: dict[str, str] = {} if raw_mat_map: material_map = resolve_material_map(raw_mat_map) diff --git a/backend/app/domains/pipeline/tasks/render_order_line.py b/backend/app/domains/pipeline/tasks/render_order_line.py index a222aca..50636e2 100644 --- a/backend/app/domains/pipeline/tasks/render_order_line.py +++ b/backend/app/domains/pipeline/tasks/render_order_line.py @@ -244,7 +244,7 @@ def render_order_line_task(self, order_line_id: str): if m.get("part_name") and m.get("material") } - # Resolve raw material names to SCHAEFFLER library names via aliases + # Resolve raw material names to HARTOMAT library names via aliases from app.services.material_service import resolve_material_map material_map = resolve_material_map(material_map) diff --git a/backend/app/domains/rendering/workflow_router.py b/backend/app/domains/rendering/workflow_router.py index e9fd447..4b7fd37 100644 --- a/backend/app/domains/rendering/workflow_router.py +++ b/backend/app/domains/rendering/workflow_router.py @@ -61,7 +61,7 @@ _STEP_DESCRIPTIONS: dict[StepName, str] = { StepName.OCC_OBJECT_EXTRACT: "Extract part objects and metadata from the STEP file using cadquery/OCC", StepName.OCC_GLB_EXPORT: "Convert STEP geometry to glTF/GLB via cadquery", StepName.GLB_BBOX: "Compute bounding-box from the exported GLB for camera framing", - StepName.MATERIAL_MAP_RESOLVE: "Resolve raw part-material names to SCHAEFFLER library materials via alias table", + StepName.MATERIAL_MAP_RESOLVE: "Resolve raw part-material names to HARTOMAT library materials via alias table", StepName.AUTO_POPULATE_MATERIALS: "Auto-create Material records for any newly discovered part names", StepName.BLENDER_RENDER: "Render a thumbnail PNG using Blender (Cycles or EEVEE)", StepName.THREEJS_RENDER: "Render a thumbnail PNG using Three.js / Playwright headless browser", diff --git a/backend/app/domains/tenants/models.py b/backend/app/domains/tenants/models.py index 9f6e937..477fd2d 100644 --- a/backend/app/domains/tenants/models.py +++ b/backend/app/domains/tenants/models.py @@ -9,7 +9,7 @@ _DEFAULT_TENANT_CONFIG = { "max_concurrent_renders": 3, "render_engines_allowed": ["cycles", "eevee"], "max_order_size": 500, - "fallback_material": "SCHAEFFLER_059999_FailedMaterial", + "fallback_material": "HARTOMAT_059999_FailedMaterial", "notifications_enabled": True, "invoice_prefix": "INV", # Azure AI validation (per-tenant) diff --git a/backend/app/main.py b/backend/app/main.py index 8695ec0..f9623d6 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -41,9 +41,9 @@ async def lifespan(app: FastAPI): app = FastAPI( - title="Schaeffler Automat API", + title="HartOMat API", version="0.1.0", - description="Media-creation pipeline for Schaeffler CAD/bearing product orders", + description="Media-creation pipeline for HartOMat CAD/bearing product orders", lifespan=lifespan, ) @@ -101,7 +101,7 @@ app.include_router(chat_router, prefix="/api") @app.get("/health") async def health(): - return {"status": "ok", "service": "schaefflerautomat-backend"} + return {"status": "ok", "service": "hartomat-backend"} @app.websocket("/api/ws") diff --git a/backend/app/services/azure_ai.py b/backend/app/services/azure_ai.py index 7a9f4bd..6dfd30e 100644 --- a/backend/app/services/azure_ai.py +++ b/backend/app/services/azure_ai.py @@ -8,12 +8,12 @@ from pathlib import Path logger = logging.getLogger(__name__) -VALIDATION_PROMPT = """You are a quality control expert for Schaeffler bearing product catalog images. +VALIDATION_PROMPT = """You are a quality control expert for HartOMat bearing product catalog images. Analyze this thumbnail of a bearing/mechanical component and evaluate: 1. Is the component orientation correct for a standard product catalog? (typically isometric view, 30° elevation, 45° rotation) 2. Are the key features visible? (rolling elements, rings, cage if present) -3. Does it match standard Schaeffler catalog angle conventions? +3. Does it match standard HartOMat catalog angle conventions? Respond in JSON with exactly these fields: { diff --git a/backend/app/services/chat_service.py b/backend/app/services/chat_service.py index 5620e5f..e543653 100644 --- a/backend/app/services/chat_service.py +++ b/backend/app/services/chat_service.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) # ── System prompt ──────────────────────────────────────────────────────────── -SYSTEM_PROMPT = """You are the Schaeffler Automat AI assistant. You help users manage their automated render pipeline for Schaeffler product images. +SYSTEM_PROMPT = """You are the HartOMat AI assistant. You help users manage their automated render pipeline for HartOMat product images. You can: - List and search orders and products @@ -39,8 +39,8 @@ RULES: 7. Respond in the same language the user writes in. 8. Be concise — short answers are better than long ones. 9. When the user says "beliebig", "any", "random", "irgendein" — just pick one yourself, don't ask back. -10. Material system: Materials have SCHAEFFLER library names (e.g. SCHAEFFLER_020101_Durotect-Blue). Common names like "Durotect", "Stahl", "Bronze" are aliases that map to these library names. When the user asks for a material by a common name, use list_materials to find the correct SCHAEFFLER name, then use that for material_override. -11. When setting material_override, always use the full SCHAEFFLER library name (e.g. SCHAEFFLER_020101_Durotect-Blue), never the alias. +10. Material system: Materials have HARTOMAT library names (e.g. HARTOMAT_020101_Durotect-Blue). Common names like "Durotect", "Stahl", "Bronze" are aliases that map to these library names. When the user asks for a material by a common name, use list_materials to find the correct HARTOMAT name, then use that for material_override. +11. When setting material_override, always use the full HARTOMAT library name (e.g. HARTOMAT_020101_Durotect-Blue), never the alias. 12. When mentioning a product, ALWAYS link to it: [ProductName](/products/UUID). When mentioning an order, link to it: [OrderNumber](/orders/UUID). This makes the response navigable. 13. Materials exist at TWO levels: (a) product_material — materials assigned to the product's CAD parts (from STEP/Excel import, e.g. "Durotect_M", "Stahl"), and (b) material_override — a single material applied to ALL parts at render time. When user asks for a product "with Durotect material", search product_material FIRST (products that naturally have Durotect parts). Only use material_override filter if they specifically say "override" or "alle Teile in". 14. NEVER say "no renders found" when renders DO exist. If no exact match, show the closest match and explain what's different.""" @@ -120,7 +120,7 @@ TOOLS = [ }, "material_override": { "type": "string", - "description": "Optional SCHAEFFLER library material name to apply to all lines.", + "description": "Optional HARTOMAT library material name to apply to all lines.", "default": "", }, }, @@ -176,7 +176,7 @@ TOOLS = [ }, "material_name": { "type": "string", - "description": "SCHAEFFLER library material name, or empty string to clear.", + "description": "HARTOMAT library material name, or empty string to clear.", }, }, "required": ["order_id", "material_name"], @@ -250,7 +250,7 @@ TOOLS = [ "type": "function", "function": { "name": "list_materials", - "description": "List available SCHAEFFLER library materials with their aliases. Use this to find the correct material name for material_override. Materials have names like SCHAEFFLER_010101_Steel-Bare. Aliases map common names (Stahl, Bronze, Durotect, etc.) to these library materials. When user asks for a material by a common name, search aliases to find the correct SCHAEFFLER library material name.", + "description": "List available HARTOMAT library materials with their aliases. Use this to find the correct material name for material_override. Materials have names like HARTOMAT_010101_Steel-Bare. Aliases map common names (Stahl, Bronze, Durotect, etc.) to these library materials. When user asks for a material by a common name, search aliases to find the correct HARTOMAT library material name.", "parameters": { "type": "object", "properties": { @@ -671,13 +671,13 @@ async def _tool_check_materials(db: AsyncSession, tenant_id: str, user_id: str = async def _tool_list_materials(db: AsyncSession, tenant_id: str, query: str = "") -> str: """List library materials with their aliases.""" sql = """ - SELECT m.id, m.name, m.schaeffler_code, m.description, + SELECT m.id, m.name, m.hartomat_code, m.description, COALESCE( (SELECT json_agg(ma.alias) FROM material_aliases ma WHERE ma.material_id = m.id), '[]'::json ) AS aliases FROM materials m - WHERE m.schaeffler_code IS NOT NULL + WHERE m.hartomat_code IS NOT NULL """ params: dict = {} if query: @@ -698,7 +698,7 @@ async def _tool_list_materials(db: AsyncSession, tenant_id: str, query: str = "" aliases = r["aliases"] if isinstance(r["aliases"], list) else [] materials.append({ "name": r["name"], - "schaeffler_code": r["schaeffler_code"], + "hartomat_code": r["hartomat_code"], "description": r["description"], "aliases": aliases[:10], # cap to avoid token bloat }) diff --git a/backend/app/services/excel_parser.py b/backend/app/services/excel_parser.py index 6f2a3fe..c68a0aa 100644 --- a/backend/app/services/excel_parser.py +++ b/backend/app/services/excel_parser.py @@ -1,5 +1,5 @@ """ -Excel parser for Schaeffler CAD order lists. +Excel parser for HartOMat CAD order lists. Supports two formats: @@ -294,7 +294,7 @@ def _parse_material_mapping(wb) -> list[dict]: def parse_excel(file_path: str | Path) -> ParsedExcel: """ - Parse a Schaeffler order list Excel file. + Parse a HartOMat order list Excel file. Returns a ParsedExcel with all data rows extracted. Header-driven: finds "Ebene1" in any column within first 5 rows, diff --git a/backend/app/services/step_processor.py b/backend/app/services/step_processor.py index 51b630e..4d1ed5a 100644 --- a/backend/app/services/step_processor.py +++ b/backend/app/services/step_processor.py @@ -25,9 +25,9 @@ def build_part_colors( """ Build {part_name: material_name} for Blender rendering. - Returns a mapping of part name → Schaeffler material name (e.g. SCHAEFFLER_010101_Steel-Bare). + Returns a mapping of part name → HartOMat material name (e.g. HARTOMAT_010101_Steel-Bare). Parts with no material assignment are omitted; Blender will use the fallback material - (SCHAEFFLER_059999_FailedMaterial) for unrecognised parts. + (HARTOMAT_059999_FailedMaterial) for unrecognised parts. Args: cad_parsed_objects: List of part names from cad_file.parsed_objects["objects"]. diff --git a/backend/app/tasks/celery_app.py b/backend/app/tasks/celery_app.py index 0877dd4..bbeb299 100644 --- a/backend/app/tasks/celery_app.py +++ b/backend/app/tasks/celery_app.py @@ -3,7 +3,7 @@ from celery.schedules import crontab from app.config import settings celery_app = Celery( - "schaefflerautomat", + "hartomat", broker=settings.redis_url, backend=settings.redis_url, include=[ diff --git a/backend/app/utils/seed_templates.py b/backend/app/utils/seed_templates.py index cc79adf..f784022 100644 --- a/backend/app/utils/seed_templates.py +++ b/backend/app/utils/seed_templates.py @@ -1,4 +1,4 @@ -"""Seed database with 7 Schaeffler product category templates.""" +"""Seed database with 7 HartOMat product category templates.""" import asyncio import uuid from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker @@ -133,7 +133,7 @@ TEMPLATES = [ ] -async def seed(db_url: str, admin_email: str = "admin@schaeffler.com", admin_password: str = "Admin1234!"): +async def seed(db_url: str, admin_email: str = "admin@hartomat.com", admin_password: str = "Admin1234!"): from app.models.template import Template from app.models.user import User, UserRole from app.utils.auth import hash_password @@ -162,7 +162,7 @@ async def seed(db_url: str, admin_email: str = "admin@schaeffler.com", admin_pas email=admin_email, password_hash=hash_password(admin_password), role=UserRole.global_admin, - full_name="Schaeffler Admin", + full_name="HartOMat Admin", ) session.add(admin) print(f" + Admin user: {admin_email}") diff --git a/backend/pyproject.toml b/backend/pyproject.toml index eba7f11..39dd378 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" packages = ["app"] [project] -name = "schaefflerautomat-backend" +name = "hartomat-backend" version = "0.1.0" requires-python = ">=3.11" dependencies = [ diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index d894f5e..968082a 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,5 +1,5 @@ """ -Pytest fixtures for the Schaeffler Automat backend test suite. +Pytest fixtures for the HartOMat backend test suite. The tests in this suite are divided into: - Unit tests (no DB / network required): excel_parser, models, schemas @@ -122,7 +122,7 @@ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sess TEST_DB_URL = os.environ.get( "TEST_DATABASE_URL", - "postgresql+asyncpg://schaeffler:schaeffler@localhost:5432/schaeffler_test" + "postgresql+asyncpg://hartomat:hartomat@localhost:5432/hartomat_test" ) diff --git a/docker-compose.worker.yml b/docker-compose.worker.yml index d5723cc..102517d 100644 --- a/docker-compose.worker.yml +++ b/docker-compose.worker.yml @@ -16,13 +16,13 @@ services: render-worker: - image: schaefflerautomat-render-worker:latest + image: hartomat-render-worker:latest # Or build locally: build: { context: ./render-worker, dockerfile: Dockerfile } environment: - REDIS_URL=${REDIS_URL:?Set REDIS_URL to the main server Redis URL} - POSTGRES_HOST=${POSTGRES_HOST:?Set POSTGRES_HOST to the main server DB host} - - POSTGRES_DB=${POSTGRES_DB:-schaeffler} - - POSTGRES_USER=${POSTGRES_USER:-schaeffler} + - POSTGRES_DB=${POSTGRES_DB:-hartomat} + - POSTGRES_USER=${POSTGRES_USER:-hartomat} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD} - POSTGRES_PORT=${POSTGRES_PORT:-5432} - MINIO_URL=${MINIO_URL:?Set MINIO_URL to the main server MinIO URL} diff --git a/docker-compose.yml b/docker-compose.yml index c5176c2..7a2678c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,15 +2,15 @@ services: postgres: image: postgres:16-alpine environment: - POSTGRES_DB: ${POSTGRES_DB:-schaeffler} - POSTGRES_USER: ${POSTGRES_USER:-schaeffler} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-schaeffler} + POSTGRES_DB: ${POSTGRES_DB:-hartomat} + POSTGRES_USER: ${POSTGRES_USER:-hartomat} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-hartomat} volumes: - pgdata:/var/lib/postgresql/data ports: - "5432:5432" healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-schaeffler}"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-hartomat}"] interval: 5s timeout: 5s retries: 5 @@ -48,9 +48,9 @@ services: dockerfile: Dockerfile command: /start.sh environment: - - POSTGRES_DB=${POSTGRES_DB:-schaeffler} - - POSTGRES_USER=${POSTGRES_USER:-schaeffler} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-schaeffler} + - POSTGRES_DB=${POSTGRES_DB:-hartomat} + - POSTGRES_USER=${POSTGRES_USER:-hartomat} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-hartomat} - POSTGRES_HOST=postgres - POSTGRES_PORT=5432 - REDIS_URL=${REDIS_URL:-redis://redis:6379/0} @@ -89,9 +89,9 @@ services: dockerfile: Dockerfile command: celery -A app.tasks.celery_app worker --loglevel=info -Q step_processing,ai_validation --autoscale=${MAX_CONCURRENCY:-8},${MIN_CONCURRENCY:-2} --concurrency=${MIN_CONCURRENCY:-2} environment: - - POSTGRES_DB=${POSTGRES_DB:-schaeffler} - - POSTGRES_USER=${POSTGRES_USER:-schaeffler} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-schaeffler} + - POSTGRES_DB=${POSTGRES_DB:-hartomat} + - POSTGRES_USER=${POSTGRES_USER:-hartomat} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-hartomat} - POSTGRES_HOST=postgres - POSTGRES_PORT=5432 - REDIS_URL=${REDIS_URL:-redis://redis:6379/0} @@ -123,9 +123,9 @@ services: - BLENDER_VERSION=${BLENDER_VERSION:-5.0.1} command: bash -c "python3 /check_version.py && celery -A app.tasks.celery_app worker --loglevel=info -Q asset_pipeline --autoscale=1,1 --concurrency=1" environment: - - POSTGRES_DB=${POSTGRES_DB:-schaeffler} - - POSTGRES_USER=${POSTGRES_USER:-schaeffler} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-schaeffler} + - POSTGRES_DB=${POSTGRES_DB:-hartomat} + - POSTGRES_USER=${POSTGRES_USER:-hartomat} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-hartomat} - POSTGRES_HOST=postgres - POSTGRES_PORT=5432 - REDIS_URL=${REDIS_URL:-redis://redis:6379/0} @@ -165,9 +165,9 @@ services: - BLENDER_VERSION=${BLENDER_VERSION:-5.0.1} command: bash -c "python3 /check_version.py && celery -A app.tasks.celery_app worker --loglevel=info -Q asset_pipeline_light --autoscale=2,2 --concurrency=2" environment: - - POSTGRES_DB=${POSTGRES_DB:-schaeffler} - - POSTGRES_USER=${POSTGRES_USER:-schaeffler} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-schaeffler} + - POSTGRES_DB=${POSTGRES_DB:-hartomat} + - POSTGRES_USER=${POSTGRES_USER:-hartomat} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-hartomat} - POSTGRES_HOST=postgres - POSTGRES_PORT=5432 - REDIS_URL=${REDIS_URL:-redis://redis:6379/0} @@ -204,9 +204,9 @@ services: dockerfile: Dockerfile command: celery -A app.tasks.celery_app beat --loglevel=info environment: - - POSTGRES_DB=${POSTGRES_DB:-schaeffler} - - POSTGRES_USER=${POSTGRES_USER:-schaeffler} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-schaeffler} + - POSTGRES_DB=${POSTGRES_DB:-hartomat} + - POSTGRES_USER=${POSTGRES_USER:-hartomat} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-hartomat} - POSTGRES_HOST=postgres - POSTGRES_PORT=5432 - REDIS_URL=${REDIS_URL:-redis://redis:6379/0} diff --git a/docs/mcp-server.md b/docs/mcp-server.md index 4298018..c6b51e3 100644 --- a/docs/mcp-server.md +++ b/docs/mcp-server.md @@ -1,6 +1,6 @@ -# Schaeffler Automat MCP Server +# HartOMat MCP Server -An MCP (Model Context Protocol) server that gives Claude Code direct access to the Schaeffler Automat render pipeline, product library, and database. +An MCP (Model Context Protocol) server that gives Claude Code direct access to the HartOMat render pipeline, product library, and database. ## Quick Start @@ -17,9 +17,9 @@ The project includes `.mcp.json` which automatically registers the MCP server wh ### Setup (manual) ```bash -claude mcp add schaeffler -- uv run \ +claude mcp add hartomat -- uv run \ --with "mcp[cli]" --with psycopg2-binary --with httpx \ - python schaeffler_mcp_server.py + python hartomat_mcp_server.py ``` ### Verify @@ -29,7 +29,7 @@ Inside Claude Code, run: /mcp ``` -You should see `schaeffler` listed with status "connected". +You should see `hartomat` listed with status "connected". ## Available Tools @@ -61,8 +61,8 @@ You should see `schaeffler` listed with status "connected". | Resource URI | Description | |---|---| -| `schaeffler://schema` | Full database schema (tables + columns) | -| `schaeffler://output-types` | All configured output types | +| `hartomat://schema` | Full database schema (tables + columns) | +| `hartomat://output-types` | All configured output types | ## Usage Examples @@ -98,9 +98,9 @@ The server connects to your local Docker services by default. Override via envir | Variable | Default | Description | |---|---|---| -| `DATABASE_URL` | `postgresql://schaeffler:schaeffler@localhost:5432/schaeffler` | PostgreSQL connection string | +| `DATABASE_URL` | `postgresql://hartomat:hartomat@localhost:5432/hartomat` | PostgreSQL connection string | | `API_URL` | `http://localhost:8888` | Backend API base URL | -| `API_EMAIL` | `admin@schaeffler.com` | API login email | +| `API_EMAIL` | `admin@hartomat.com` | API login email | | `API_PASSWORD` | `Admin1234!` | API login password | ### Custom configuration @@ -108,11 +108,11 @@ The server connects to your local Docker services by default. Override via envir Edit `.mcp.json` in the project root to change defaults, or use `claude mcp add` with `--env` flags: ```bash -claude mcp add schaeffler \ +claude mcp add hartomat \ --env DATABASE_URL=postgresql://user:pass@host/db \ --env API_URL=https://staging.example.com \ -- uv run --with "mcp[cli]" --with psycopg2-binary --with httpx \ - python schaeffler_mcp_server.py + python hartomat_mcp_server.py ``` ## Security Notes @@ -127,7 +127,7 @@ claude mcp add schaeffler \ ### Server not connecting 1. Check Docker services are running: `docker compose ps` -2. Check PostgreSQL is accessible: `psql postgresql://schaeffler:schaeffler@localhost:5432/schaeffler -c "SELECT 1"` +2. Check PostgreSQL is accessible: `psql postgresql://hartomat:hartomat@localhost:5432/hartomat -c "SELECT 1"` 3. Check backend API is up: `curl http://localhost:8888/api/auth/login` ### Dependencies missing @@ -141,14 +141,14 @@ uv pip install "mcp[cli]" psycopg2-binary httpx ```bash # Run manually to see errors uv run --with "mcp[cli]" --with psycopg2-binary --with httpx \ - python schaeffler_mcp_server.py + python hartomat_mcp_server.py ``` ### Reset MCP connection ```bash -claude mcp remove schaeffler -claude mcp add schaeffler -- uv run \ +claude mcp remove hartomat +claude mcp add hartomat -- uv run \ --with "mcp[cli]" --with psycopg2-binary --with httpx \ - python schaeffler_mcp_server.py + python hartomat_mcp_server.py ``` diff --git a/docs/plans/0001-step-to-usd-implementation.md b/docs/plans/0001-step-to-usd-implementation.md index 46c9f1d..9c29a2a 100644 --- a/docs/plans/0001-step-to-usd-implementation.md +++ b/docs/plans/0001-step-to-usd-implementation.md @@ -12,7 +12,7 @@ - [ ] `blender_render.py` decomposition is still pending; current file remains monolithic - [ ] Legacy STL-era cleanup is still pending (`stl_quality`, STL endpoints, orphaned directories) - [x] Decision: USD authoring library → **`usd-core` (pip)** — provides `pxr` module, no GPU tools needed, pip-installable in render-worker -- [x] Decision: seam/sharp payload encoding → **index-space primvars** (`primvars:schaeffler:seamEdgeVertexPairs`, `primvars:schaeffler:sharpEdgeVertexPairs`) — survives transforms, no KD-tree needed +- [x] Decision: seam/sharp payload encoding → **index-space primvars** (`primvars:hartomat:seamEdgeVertexPairs`, `primvars:hartomat:sharpEdgeVertexPairs`) — survives transforms, no KD-tree needed - [x] Decision: preview GLB derivation → **co-author from same tessellation pass** during migration (avoid round-trip loss from USD→GLB export) - [x] Decision: single-file vs override layers → **Option B: canonical geometry layer + material override layer, flattened via `UsdUtils.FlattenLayerStack()` for delivery** — preserves hierarchy AND allows instancing later (`FlattenLayerStack` keeps `instanceable` prims; `UsdStage.Flatten` would expand them). Note: Phase 1 uses no instancing (matching current GLB pipeline), but the delivery path is already instancing-safe. @@ -63,14 +63,14 @@ Add after the `gmsh` line. `usd-core` is the Pixar-maintained pip distribution o | Attribute | Value source | |---|---| -| `schaeffler:partKey` | `generate_part_key(xcaf_label_path)` | -| `schaeffler:sourceName` | XCAF `TDataStd_Name` attribute | -| `schaeffler:sourceColor` | XCAF embedded color (hex string) | -| `schaeffler:rawMaterialName` | from `CadFile.part_materials` if available | -| `schaeffler:tessellation:linearDeflectionMm` | CLI arg value | -| `schaeffler:tessellation:angularDeflectionRad` | CLI arg value | -| `primvars:schaeffler:seamEdgeVertexPairs` | OCC B-rep seam edges (index pairs in mesh-local space) | -| `primvars:schaeffler:sharpEdgeVertexPairs` | sharp edges from `_extract_sharp_edge_pairs()` | +| `hartomat:partKey` | `generate_part_key(xcaf_label_path)` | +| `hartomat:sourceName` | XCAF `TDataStd_Name` attribute | +| `hartomat:sourceColor` | XCAF embedded color (hex string) | +| `hartomat:rawMaterialName` | from `CadFile.part_materials` if available | +| `hartomat:tessellation:linearDeflectionMm` | CLI arg value | +| `hartomat:tessellation:angularDeflectionRad` | CLI arg value | +| `primvars:hartomat:seamEdgeVertexPairs` | OCC B-rep seam edges (index pairs in mesh-local space) | +| `primvars:hartomat:sharpEdgeVertexPairs` | sharp edges from `_extract_sharp_edge_pairs()` | **CLI interface:** @@ -86,7 +86,7 @@ python3 export_step_to_usd.py \ **Acceptance gate:** `python3 export_step_to_usd.py --step_path 81113-l_cut.stp --output_path /tmp/test.usd` → - File exists, parseable -- 25 part prims with `schaeffler:partKey` attribute +- 25 part prims with `hartomat:partKey` attribute - Part count matches `export_step_to_gltf.py` output for same file ### Task 1.2 — `usd_master` MediaAsset type @@ -150,7 +150,7 @@ def build_scene_manifest(cad_file: CadFile, usd_asset: MediaAsset) -> dict: "part_key": "ring_outer", "source_name": "RingOuter_AF0", "prim_path": "/Root/Assembly/Bearing/RingOuter", - "effective_material": "SCHAEFFLER_010102_...", + "effective_material": "HARTOMAT_010102_...", "assignment_provenance": "manual|auto|default", "is_unassigned": false } @@ -198,7 +198,7 @@ New endpoint returning `SceneManifest`. Calls `build_scene_manifest()` — reads **File:** `backend/app/api/routers/cad.py` -Accept `{ "part_key": "ring_outer", "material": "SCHAEFFLER_010102_..." }` body (or bulk map). Write to `manual_material_overrides` column (not the old `part_materials` column). +Accept `{ "part_key": "ring_outer", "material": "HARTOMAT_010102_..." }` body (or bulk map). Write to `manual_material_overrides` column (not the old `part_materials` column). **Acceptance gate:** PUT with `partKey` → subsequent GET `/scene-manifest` shows that part's `assignment_provenance: "manual"`. @@ -218,10 +218,10 @@ After tessellation (OCC or GMSH), for each mesh face: Write to USD mesh prim: ```python -mesh_prim.GetPrimvar("schaeffler:seamEdgeVertexPairs").Set( +mesh_prim.GetPrimvar("hartomat:seamEdgeVertexPairs").Set( Vt.Vec2iArray(seam_pairs), # [(vi0, vi1), ...] ) -mesh_prim.GetPrimvar("schaeffler:sharpEdgeVertexPairs").Set( +mesh_prim.GetPrimvar("hartomat:sharpEdgeVertexPairs").Set( Vt.Vec2iArray(sharp_pairs), ) ``` @@ -243,8 +243,8 @@ def import_usd_and_restore_topology(usd_path: str) -> list: if obj.type != 'MESH': continue # Read custom attributes set by USD importer - seam_pairs = obj.get("schaeffler_seamEdgeVertexPairs") or [] - sharp_pairs = obj.get("schaeffler_sharpEdgeVertexPairs") or [] + seam_pairs = obj.get("hartomat_seamEdgeVertexPairs") or [] + sharp_pairs = obj.get("hartomat_sharpEdgeVertexPairs") or [] _mark_seams_from_index_pairs(obj, seam_pairs) _mark_sharp_from_index_pairs(obj, sharp_pairs) ... @@ -262,7 +262,7 @@ def import_usd_and_restore_topology(usd_path: str) -> list: Add `--usd_path` argument. When provided: 1. Call `import_usd.py` instead of `export_gltf.py` GLB import -2. Read `schaeffler:partKey` and `schaeffler:canonicalMaterialName` per mesh object after import +2. Read `hartomat:partKey` and `hartomat:canonicalMaterialName` per mesh object after import 3. Apply materials by `partKey → material library name` lookup instead of object-name heuristics **Migration:** Keep `--glb_path` working in parallel; switch production task to prefer `--usd_path` when `usd_master` asset exists. diff --git a/docs/rfcs/0001-step-to-usd-workflow.md b/docs/rfcs/0001-step-to-usd-workflow.md index 4ed88a0..088846c 100644 --- a/docs/rfcs/0001-step-to-usd-workflow.md +++ b/docs/rfcs/0001-step-to-usd-workflow.md @@ -35,12 +35,12 @@ That split introduces avoidable duplication, fragility, and impedance mismatches The code confirms this architecture: -- Geometry export task: [backend/app/domains/pipeline/tasks/export_glb.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/pipeline/tasks/export_glb.py#L16) -- Production export task: [backend/app/domains/pipeline/tasks/export_glb.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/pipeline/tasks/export_glb.py#L176) -- Blender production export script: [render-worker/scripts/export_gltf.py](/home/hartmut/Documents/Copilot/schaefflerautomat/render-worker/scripts/export_gltf.py#L106) -- OCC GLB exporter with XCAF name/color preservation and sharp-edge extras: [render-worker/scripts/export_step_to_gltf.py](/home/hartmut/Documents/Copilot/schaefflerautomat/render-worker/scripts/export_step_to_gltf.py#L301) -- Media asset model: [backend/app/domains/media/models.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/media/models.py#L11) -- Frontend viewer contract: [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/components/cad/ThreeDViewer.tsx#L40) +- Geometry export task: [backend/app/domains/pipeline/tasks/export_glb.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/domains/pipeline/tasks/export_glb.py#L16) +- Production export task: [backend/app/domains/pipeline/tasks/export_glb.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/domains/pipeline/tasks/export_glb.py#L176) +- Blender production export script: [render-worker/scripts/export_gltf.py](/home/hartmut/Documents/Copilot/hartomat/render-worker/scripts/export_gltf.py#L106) +- OCC GLB exporter with XCAF name/color preservation and sharp-edge extras: [render-worker/scripts/export_step_to_gltf.py](/home/hartmut/Documents/Copilot/hartomat/render-worker/scripts/export_step_to_gltf.py#L301) +- Media asset model: [backend/app/domains/media/models.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/domains/media/models.py#L11) +- Frontend viewer contract: [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/hartomat/frontend/src/components/cad/ThreeDViewer.tsx#L40) ## Goals @@ -95,18 +95,18 @@ However, this data is not represented in a durable scene data model. Some of it Material mapping already exists conceptually in the domain model: - product-level `cad_part_materials` -- canonical material/alias resolution in [backend/app/domains/materials/service.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/materials/service.py#L32) +- canonical material/alias resolution in [backend/app/domains/materials/service.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/domains/materials/service.py#L32) -The weak point is the last mile: materials are currently assigned in Blender by matching imported object names from a GLB round-trip in [render-worker/scripts/export_gltf.py](/home/hartmut/Documents/Copilot/schaefflerautomat/render-worker/scripts/export_gltf.py#L192). +The weak point is the last mile: materials are currently assigned in Blender by matching imported object names from a GLB round-trip in [render-worker/scripts/export_gltf.py](/home/hartmut/Documents/Copilot/hartomat/render-worker/scripts/export_gltf.py#L192). ### Admin and Settings Surface The admin/backend model still mirrors the dual-GLB architecture: -- separate preview and production tessellation settings in [backend/app/api/routers/admin.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/api/routers/admin.py#L24) -- a bulk action specifically for missing geometry GLBs in [backend/app/api/routers/admin.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/api/routers/admin.py#L536) -- an admin UI that exposes preview-vs-production GLB tessellation controls in [frontend/src/pages/Admin.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/pages/Admin.tsx#L1400) -- product detail logic that queries both `gltf_geometry` and `gltf_production` assets in [frontend/src/pages/ProductDetail.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/pages/ProductDetail.tsx#L182) +- separate preview and production tessellation settings in [backend/app/api/routers/admin.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/api/routers/admin.py#L24) +- a bulk action specifically for missing geometry GLBs in [backend/app/api/routers/admin.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/api/routers/admin.py#L536) +- an admin UI that exposes preview-vs-production GLB tessellation controls in [frontend/src/pages/Admin.tsx](/home/hartmut/Documents/Copilot/hartomat/frontend/src/pages/Admin.tsx#L1400) +- product detail logic that queries both `gltf_geometry` and `gltf_production` assets in [frontend/src/pages/ProductDetail.tsx](/home/hartmut/Documents/Copilot/hartomat/frontend/src/pages/ProductDetail.tsx#L182) That duplication is operationally expensive and should be reduced as part of the refactor, not carried forward under new names. @@ -216,17 +216,17 @@ Object names imported into Blender must no longer be the primary identity mechan Each part prim should carry: -- `schaeffler:partKey` -- `schaeffler:sourceName` -- `schaeffler:sourceAssemblyPath` -- `schaeffler:sourceColor` -- `schaeffler:rawMaterialName` -- `schaeffler:canonicalMaterialName` -- `schaeffler:tessellation:linearDeflectionMm` -- `schaeffler:tessellation:angularDeflectionRad` -- `schaeffler:cadFileId` -- `schaeffler:productId` when available -- `schaeffler:mesh:topologyHash` +- `hartomat:partKey` +- `hartomat:sourceName` +- `hartomat:sourceAssemblyPath` +- `hartomat:sourceColor` +- `hartomat:rawMaterialName` +- `hartomat:canonicalMaterialName` +- `hartomat:tessellation:linearDeflectionMm` +- `hartomat:tessellation:angularDeflectionRad` +- `hartomat:cadFileId` +- `hartomat:productId` when available +- `hartomat:mesh:topologyHash` Each mesh prim should carry: @@ -241,10 +241,10 @@ Each mesh prim should carry: USD does not have a first-class standard seam concept for Blender UV editing, so this RFC proposes storing authored topology support data as custom primvars or custom attributes on the mesh prim: -- `primvars:schaeffler:seamEdgeVertexPairs` -- `primvars:schaeffler:sharpEdgeVertexPairs` -- `primvars:schaeffler:faceBatchIds` -- `primvars:schaeffler:sourceUv` +- `primvars:hartomat:seamEdgeVertexPairs` +- `primvars:hartomat:sharpEdgeVertexPairs` +- `primvars:hartomat:faceBatchIds` +- `primvars:hartomat:sourceUv` The exact encoding can evolve, but the initial implementation should optimize for deterministic Blender reconstruction rather than elegance. @@ -321,10 +321,10 @@ The current viewer supports several behaviors that must not regress: Those behaviors currently operate on GLB mesh objects in: -- [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/components/cad/ThreeDViewer.tsx#L488) -- [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/components/cad/ThreeDViewer.tsx#L553) -- [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/components/cad/ThreeDViewer.tsx#L675) -- [frontend/src/components/cad/MaterialPanel.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/components/cad/MaterialPanel.tsx#L123) +- [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/hartomat/frontend/src/components/cad/ThreeDViewer.tsx#L488) +- [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/hartomat/frontend/src/components/cad/ThreeDViewer.tsx#L553) +- [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/hartomat/frontend/src/components/cad/ThreeDViewer.tsx#L675) +- [frontend/src/components/cad/MaterialPanel.tsx](/home/hartmut/Documents/Copilot/hartomat/frontend/src/components/cad/MaterialPanel.tsx#L123) The USD refactor must preserve these capabilities. The replacement is not "browser renders USD directly". The replacement is: @@ -393,10 +393,10 @@ That makes missing assignments visible instead of silently failing. The current backend already has the right conceptual split, but the naming is misleading: -- [backend/app/domains/products/models.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/products/models.py#L62) `Product.cad_part_materials` behaves like imported or product-authored source material rows -- [backend/app/domains/products/models.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/products/models.py#L34) `CadFile.part_materials` behaves like viewer-side manual assignments or overrides -- [backend/app/api/routers/cad.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/api/routers/cad.py#L395) still presents those overrides as a part-name keyed map -- [backend/app/api/routers/products.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/api/routers/products.py#L433) performs Excel reconciliation directly into the product-side list +- [backend/app/domains/products/models.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/domains/products/models.py#L62) `Product.cad_part_materials` behaves like imported or product-authored source material rows +- [backend/app/domains/products/models.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/domains/products/models.py#L34) `CadFile.part_materials` behaves like viewer-side manual assignments or overrides +- [backend/app/api/routers/cad.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/api/routers/cad.py#L395) still presents those overrides as a part-name keyed map +- [backend/app/api/routers/products.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/api/routers/products.py#L433) performs Excel reconciliation directly into the product-side list The USD refactor should formalize this into three explicit layers: @@ -567,7 +567,7 @@ The existing `export_step_to_gltf.py` can remain temporarily for migration and f ## 2. Media Asset Model -Extend [backend/app/domains/media/models.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/media/models.py#L11) with at least: +Extend [backend/app/domains/media/models.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/domains/media/models.py#L11) with at least: - `usd_master` @@ -580,7 +580,7 @@ The important change is that `usd_master` becomes the canonical CAD scene artifa ## 3. Pipeline Tasks -Replace the current dual-export mental model in [backend/app/domains/pipeline/tasks/export_glb.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/domains/pipeline/tasks/export_glb.py#L16) with: +Replace the current dual-export mental model in [backend/app/domains/pipeline/tasks/export_glb.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/domains/pipeline/tasks/export_glb.py#L16) with: - `generate_usd_master_task` - optional `generate_preview_glb_task` @@ -590,7 +590,7 @@ The production GLB task should be retired once Blender can render from USD and b ## 4. Render Service -The render service in [backend/app/services/render_blender.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/services/render_blender.py#L18) currently converts `STEP -> GLB -> Blender render`. +The render service in [backend/app/services/render_blender.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/services/render_blender.py#L18) currently converts `STEP -> GLB -> Blender render`. Target flow: @@ -608,7 +608,7 @@ This removes the need for a production GLB as an intermediate render artifact. ## 5. Frontend -The current viewer API in [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/schaefflerautomat/frontend/src/components/cad/ThreeDViewer.tsx#L40) assumes: +The current viewer API in [frontend/src/components/cad/ThreeDViewer.tsx](/home/hartmut/Documents/Copilot/hartomat/frontend/src/components/cad/ThreeDViewer.tsx#L40) assumes: - one geometry GLB - one production GLB @@ -633,7 +633,7 @@ The frontend should stop encoding the architectural distinction between geometry ## 6. Viewer Assignment API -The current viewer override endpoint in [backend/app/api/routers/cad.py](/home/hartmut/Documents/Copilot/schaefflerautomat/backend/app/api/routers/cad.py#L395) should evolve from a raw part-name keyed map into a canonical scene-assignment endpoint. +The current viewer override endpoint in [backend/app/api/routers/cad.py](/home/hartmut/Documents/Copilot/hartomat/backend/app/api/routers/cad.py#L395) should evolve from a raw part-name keyed map into a canonical scene-assignment endpoint. Target behavior: @@ -720,7 +720,7 @@ Example shape: "part_key": "ring_outer", "source_name": "RingOuter_AF0", "prim_path": "/Root/Assembly/Bearing/RingOuter", - "effective_material": "SCHAEFFLER_010102_Steel-Polished", + "effective_material": "HARTOMAT_010102_Steel-Polished", "assignment_provenance": "manual", "is_unassigned": false } diff --git a/frontend/index.html b/frontend/index.html index 792fe10..c029716 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,9 +2,9 @@ - + - Hart.O.Mat — Hartomatisierung + HartOMat diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fd5f6ef..1d9de06 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,11 +1,11 @@ { - "name": "schaefflerautomat-frontend", + "name": "hartomat-frontend", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "schaefflerautomat-frontend", + "name": "hartomat-frontend", "version": "0.1.0", "dependencies": { "@react-three/drei": "^9.102.3", diff --git a/frontend/package.json b/frontend/package.json index 9f24190..1d55750 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "schaefflerautomat-frontend", + "name": "hartomat-frontend", "private": true, "version": "0.1.0", "type": "module", diff --git a/frontend/public/hartomat.svg b/frontend/public/hartomat.svg new file mode 100644 index 0000000..e682e55 --- /dev/null +++ b/frontend/public/hartomat.svg @@ -0,0 +1,5 @@ + + HartOMat + + + diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 94d3a43..d3c6093 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -9,7 +9,7 @@ const api = axios.create({ api.interceptors.request.use((config) => { const token = useAuthStore.getState().token if (token) config.headers.Authorization = `Bearer ${token}` - const tenantId = localStorage.getItem('schaeffler_tenant_id') + const tenantId = localStorage.getItem('hartomat_tenant_id') if (tenantId) config.headers['X-Tenant-ID'] = tenantId return config }) diff --git a/frontend/src/api/materials.ts b/frontend/src/api/materials.ts index aceaafc..be5851f 100644 --- a/frontend/src/api/materials.ts +++ b/frontend/src/api/materials.ts @@ -5,7 +5,7 @@ export interface Material { name: string description: string | null source: string - schaeffler_code: number | null + hartomat_code: number | null created_by_name: string | null aliases: string[] created_at: string @@ -27,7 +27,7 @@ export async function createMaterial(data: { name: string description?: string source?: string - schaeffler_code?: number | null + hartomat_code?: number | null }) { const res = await api.post('/materials', data) return res.data @@ -54,8 +54,8 @@ export async function saveCadPartMaterials( return res.data } -export async function seedSchaefflerMaterials() { - const res = await api.post<{ inserted: number; total: number }>('/materials/seed-schaeffler') +export async function seedHartOMatMaterials() { + const res = await api.post<{ inserted: number; total: number }>('/materials/seed-hartomat') return res.data } @@ -93,7 +93,7 @@ export async function seedAliases(): Promise<{ inserted: number; total: number } export interface MaterialSuggestion { id: string name: string - schaeffler_code: string + hartomat_code: string } export interface UnmappedMaterial { diff --git a/frontend/src/components/MaterialWizard.tsx b/frontend/src/components/MaterialWizard.tsx index 84dfd80..fe191de 100644 --- a/frontend/src/components/MaterialWizard.tsx +++ b/frontend/src/components/MaterialWizard.tsx @@ -98,10 +98,10 @@ export default function MaterialWizard({ open, onClose, onCreated }: Props) { .replace(/^-|-$/g, '') const fullMaterialName = fullCode && sanitizedName - ? `SCHAEFFLER_${fullCode}_${sanitizedName}` + ? `HARTOMAT_${fullCode}_${sanitizedName}` : null - const schaefflerCodeInt = fullCode ? parseInt(fullCode, 10) : null + const hartomatCodeInt = fullCode ? parseInt(fullCode, 10) : null const createMut = useMutation({ mutationFn: () => @@ -109,7 +109,7 @@ export default function MaterialWizard({ open, onClose, onCreated }: Props) { name: fullMaterialName!, description: description.trim() || undefined, source: 'manual', - schaeffler_code: schaefflerCodeInt, + hartomat_code: hartomatCodeInt, }), onSuccess: () => { toast.success('Material created') @@ -133,7 +133,7 @@ export default function MaterialWizard({ open, onClose, onCreated }: Props) { {/* Header */}
-

Schaeffler Material Wizard

+

HartOMat Material Wizard

Step {step} of 3

diff --git a/frontend/src/pages/AssetLibrary.tsx b/frontend/src/pages/AssetLibrary.tsx index 910698c..d558743 100644 --- a/frontend/src/pages/AssetLibrary.tsx +++ b/frontend/src/pages/AssetLibrary.tsx @@ -55,7 +55,7 @@ function UploadModal({ onClose }: { onClose: () => void }) { setName(e.target.value)} /> diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 27e5cab..b03e494 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -36,7 +36,7 @@ export default function LoginPage() {
S
-

Schaeffler Automat

+

HartOMat

Media Creation Pipeline

@@ -49,7 +49,7 @@ export default function LoginPage() { onChange={(e) => setEmail(e.target.value)} required className="input-base w-full" - placeholder="admin@schaeffler.com" + placeholder="admin@hartomat.com" />
diff --git a/frontend/src/pages/Materials.tsx b/frontend/src/pages/Materials.tsx index df7fbc2..a622f7b 100644 --- a/frontend/src/pages/Materials.tsx +++ b/frontend/src/pages/Materials.tsx @@ -8,7 +8,7 @@ import { } from 'lucide-react' import { listMaterials, createMaterial, updateMaterial, deleteMaterial, - seedSchaefflerMaterials, addAlias, deleteAlias, seedAliases, + seedHartOMatMaterials, addAlias, deleteAlias, seedAliases, batchCreateAliases, } from '../api/materials' import type { Material } from '../api/materials' @@ -24,8 +24,8 @@ const TYPE_GROUPS = [ ] as const function getTypeCode(mat: Material): string | null { - if (mat.schaeffler_code == null) return null - return String(mat.schaeffler_code).padStart(6, '0').slice(0, 2) + if (mat.hartomat_code == null) return null + return String(mat.hartomat_code).padStart(6, '0').slice(0, 2) } interface MaterialGroup { @@ -90,12 +90,12 @@ export default function MaterialsPage() { }) const seedMut = useMutation({ - mutationFn: seedSchaefflerMaterials, + mutationFn: seedHartOMatMaterials, onSuccess: (data) => { if (data.inserted > 0) { - toast.success(`Imported ${data.inserted} of ${data.total} Schaeffler standard materials`) + toast.success(`Imported ${data.inserted} of ${data.total} HartOMat standard materials`) } else { - toast.info('All Schaeffler standard materials already exist') + toast.info('All HartOMat standard materials already exist') } qc.invalidateQueries({ queryKey: ['materials'] }) }, @@ -147,9 +147,9 @@ export default function MaterialsPage() { onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to create alias'), }) - // Library materials (have schaeffler_code) for quick-map dropdown + // Library materials (have hartomat_code) for quick-map dropdown const libraryMaterials = useMemo( - () => materials.filter((m) => m.schaeffler_code !== null).sort((a, b) => a.name.localeCompare(b.name)), + () => materials.filter((m) => m.hartomat_code !== null).sort((a, b) => a.name.localeCompare(b.name)), [materials] ) @@ -203,7 +203,7 @@ export default function MaterialsPage() { buckets.delete(tg.code) } } - // Custom / non-schaeffler materials + // Custom / non-hartomat materials const custom = buckets.get(null) if (custom && custom.length > 0) { result.push({ code: null, label: 'Custom', icon: Plus, bg: 'bg-surface-alt', border: 'border-border-default', text: 'text-content-secondary', items: custom }) @@ -239,7 +239,7 @@ export default function MaterialsPage() { setConfirmState({ open: true, title: 'Import Standard Materials', - message: 'Import 35 Schaeffler standard materials? Existing entries will be skipped.', + message: 'Import 35 HartOMat standard materials? Existing entries will be skipped.', onConfirm: () => { seedMut.mutate() setConfirmState((s) => ({ ...s, open: false })) @@ -248,7 +248,7 @@ export default function MaterialsPage() { }} disabled={seedMut.isPending} className="btn-secondary text-sm flex items-center gap-1.5" - title="Import the 35 standard Schaeffler SCHAEFFLER_... materials used in Blender material libraries. Existing entries are skipped." + title="Import the 35 standard HartOMat HARTOMAT_... materials used in Blender material libraries. Existing entries are skipped." > {seedMut.isPending ? 'Importing...' : 'Import Standards'} @@ -266,16 +266,16 @@ export default function MaterialsPage() { }} disabled={seedAliasMut.isPending} className="btn-secondary text-sm flex items-center gap-1.5" - title="Seed ~100 material aliases from the Schaeffler naming scheme (German descriptions, intermediate codes → SCHAEFFLER_... library names). Existing aliases are skipped." + title="Seed ~100 material aliases from the HartOMat naming scheme (German descriptions, intermediate codes → HARTOMAT_... library names). Existing aliases are skipped." > {seedAliasMut.isPending ? 'Seeding...' : 'Seed Aliases'}
@@ -677,7 +677,7 @@ export default function TenantsPage() { rows={4} value={aiForm.ai_validation_prompt ?? ''} onChange={(e) => setAIForm((prev) => ({ ...prev, ai_validation_prompt: e.target.value || null }))} - placeholder="Optional: override the default Schaeffler bearing analysis prompt" + placeholder="Optional: override the default HartOMat bearing analysis prompt" className="w-full px-3 py-2 rounded-md border border-border-default bg-surface text-content text-sm resize-y focus:outline-none focus:ring-2 focus:ring-accent/50" /> diff --git a/frontend/src/store/auth.ts b/frontend/src/store/auth.ts index 1edc7a9..b1471a6 100644 --- a/frontend/src/store/auth.ts +++ b/frontend/src/store/auth.ts @@ -32,6 +32,6 @@ export const useAuthStore = create()( setAuth: (token, user) => set({ token, user }), logout: () => set({ token: null, user: null }), }), - { name: 'schaeffler-auth' }, + { name: 'hartomat-auth' }, ), ) diff --git a/frontend/src/store/theme.ts b/frontend/src/store/theme.ts index c5658bd..a44e9ea 100644 --- a/frontend/src/store/theme.ts +++ b/frontend/src/store/theme.ts @@ -5,7 +5,7 @@ export type ThemeMode = 'light' | 'dark' | 'system' export type AccentKey = 'green' | 'blue' | 'purple' | 'amber' | 'teal' | 'custom' export const ACCENT_PRESETS: { key: AccentKey; label: string; hex: string }[] = [ - { key: 'green', label: 'Schaeffler Green', hex: '#00893d' }, + { key: 'green', label: 'HartOMat Green', hex: '#00893d' }, { key: 'blue', label: 'Blue', hex: '#2563eb' }, { key: 'purple', label: 'Purple', hex: '#7c3aed' }, { key: 'amber', label: 'Amber', hex: '#d97706' }, @@ -115,6 +115,6 @@ export const useThemeStore = create()( applyTheme(get().mode, 'custom', hex) }, }), - { name: 'schaeffler-theme' }, + { name: 'hartomat-theme' }, ), ) diff --git a/schaeffler_mcp_server.py b/hartomat_mcp_server.py similarity index 97% rename from schaeffler_mcp_server.py rename to hartomat_mcp_server.py index 85c20ef..1d4c6a9 100644 --- a/schaeffler_mcp_server.py +++ b/hartomat_mcp_server.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Schaeffler Automat MCP Server. +"""HartOMat MCP Server. Exposes the render pipeline, product library, material system, and order management as MCP tools for Claude Code. @@ -8,7 +8,7 @@ Requirements (install once): uv pip install "mcp[cli]" psycopg2-binary httpx Register in Claude Code: - claude mcp add schaeffler -- python schaeffler_mcp_server.py + claude mcp add hartomat -- python hartomat_mcp_server.py """ import json import os @@ -23,18 +23,18 @@ from mcp.server.fastmcp import FastMCP DB_URL = os.environ.get( "DATABASE_URL", - "postgresql://schaeffler:schaeffler@localhost:5432/schaeffler", + "postgresql://hartomat:hartomat@localhost:5432/hartomat", ) API_URL = os.environ.get("API_URL", "http://localhost:8888") -API_EMAIL = os.environ.get("API_EMAIL", "admin@schaeffler.com") +API_EMAIL = os.environ.get("API_EMAIL", "admin@hartomat.com") API_PASSWORD = os.environ.get("API_PASSWORD", "Admin1234!") # ── Server setup ───────────────────────────────────────────────────────────── mcp = FastMCP( - "Schaeffler Automat", + "HartOMat", instructions=( - "MCP server for the Schaeffler Automat render pipeline. " + "MCP server for the HartOMat render pipeline. " "Provides tools to query orders, products, materials, render status, " "worker health, and run read-only SQL against the PostgreSQL database." ), @@ -112,7 +112,7 @@ def _api_post(path: str, body: dict | None = None) -> dict | list: @mcp.tool() def query_database(sql: str) -> str: - """Execute a read-only SQL query against the Schaeffler PostgreSQL database. + """Execute a read-only SQL query against the HartOMat PostgreSQL database. Only SELECT queries are allowed. The database contains tables for orders, order_lines, products, cad_files, materials, material_aliases, @@ -285,7 +285,7 @@ def set_material_override(order_id: str, material_name: str = "") -> str: Args: order_id: UUID of the order. - material_name: SCHAEFFLER library material name, or empty to clear. + material_name: HARTOMAT library material name, or empty to clear. """ data = _api_post( f"/api/orders/{order_id}/batch-material-override", @@ -314,7 +314,7 @@ def create_order( output_type_id: UUID of the output type (takes priority over name). output_type_name: Name of the output type (used if output_type_id is empty). render_overrides: Optional dict of render setting overrides (e.g. {"output_format": "webp", "width": 2048, "height": 2048}). - material_override: Optional SCHAEFFLER library material name to apply to all lines. + material_override: Optional HARTOMAT library material name to apply to all lines. notes: Optional notes for the order. """ # Resolve output_type_id from name if needed @@ -609,7 +609,7 @@ def get_failed_renders(limit: int = 20) -> str: # ── Resources ──────────────────────────────────────────────────────────────── -@mcp.resource("schaeffler://schema") +@mcp.resource("hartomat://schema") def get_database_schema() -> str: """Database schema overview — table names and column types.""" rows = _db_query(""" @@ -634,7 +634,7 @@ def get_database_schema() -> str: return "\n".join(lines) -@mcp.resource("schaeffler://output-types") +@mcp.resource("hartomat://output-types") def get_output_types_resource() -> str: """All configured output types with settings.""" data = _api_get("/api/output-types?include_inactive=true") diff --git a/render-worker/scripts/_blender_import.py b/render-worker/scripts/_blender_import.py index e6b2df1..c328ca7 100644 --- a/render-worker/scripts/_blender_import.py +++ b/render-worker/scripts/_blender_import.py @@ -89,7 +89,7 @@ def import_usd_file(usd_path: str) -> tuple[list, dict]: """Import USD stage into current Blender scene — delegates to import_usd module. Returns (parts, material_lookup) where material_lookup maps - blender_object_name → canonical SCHAEFFLER material name (from USD primvars). + blender_object_name → canonical HARTOMAT material name (from USD primvars). """ from import_usd import import_usd_file as _impl result = _impl(usd_path) diff --git a/render-worker/scripts/_blender_materials.py b/render-worker/scripts/_blender_materials.py index 09d8b0c..74754b1 100644 --- a/render-worker/scripts/_blender_materials.py +++ b/render-worker/scripts/_blender_materials.py @@ -5,7 +5,7 @@ import os import re as _re import time as _time -FAILED_MATERIAL_NAME = "SCHAEFFLER_059999_FailedMaterial" +FAILED_MATERIAL_NAME = "HARTOMAT_059999_FailedMaterial" def _find_material_with_nodes(base_name: str): @@ -100,7 +100,7 @@ def _batch_append_materials(mat_lib_path: str, names: set[str]) -> dict: def assign_failed_material(part_obj) -> None: """Assign the standard fallback material (magenta) when no library material matches. - Reuses SCHAEFFLER_059999_FailedMaterial if already loaded; otherwise + Reuses HARTOMAT_059999_FailedMaterial if already loaded; otherwise creates a simple magenta Principled BSDF node tree. """ import bpy # type: ignore[import] @@ -157,7 +157,7 @@ def apply_material_library_direct( """Assign materials from library using a direct object_name → material_name mapping. This bypasses all name-matching heuristics — the mapping comes from USD - customData (schaeffler:canonicalMaterialName) read via pxr after Blender import. + customData (hartomat:canonicalMaterialName) read via pxr after Blender import. Parts not present in material_lookup receive FAILED_MATERIAL_NAME. material_lookup: {blender_object_name: canonical_material_name} diff --git a/render-worker/scripts/_blender_scene_setup.py b/render-worker/scripts/_blender_scene_setup.py index 827b586..98a798c 100644 --- a/render-worker/scripts/_blender_scene_setup.py +++ b/render-worker/scripts/_blender_scene_setup.py @@ -83,7 +83,7 @@ def _setup_mode_b(args, lap_fn: Callable[[str], None]) -> None: _unassigned = [p for p in parts if not p.data.materials or (len(p.data.materials) == 1 and p.data.materials[0] and - p.data.materials[0].name == "SCHAEFFLER_059999_FailedMaterial")] + p.data.materials[0].name == "HARTOMAT_059999_FailedMaterial")] if _unassigned: print(f"[blender_render] {len(_unassigned)} parts without USD primvar — " f"falling back to name-matching", flush=True) @@ -162,7 +162,7 @@ def _setup_mode_a(args) -> None: _unassigned = [p for p in parts if not p.data.materials or (len(p.data.materials) == 1 and p.data.materials[0] and - p.data.materials[0].name == "SCHAEFFLER_059999_FailedMaterial")] + p.data.materials[0].name == "HARTOMAT_059999_FailedMaterial")] if _unassigned: apply_material_library( _unassigned, args.material_library_path, diff --git a/render-worker/scripts/asset_library.py b/render-worker/scripts/asset_library.py index e611c6f..a730733 100644 --- a/render-worker/scripts/asset_library.py +++ b/render-worker/scripts/asset_library.py @@ -19,7 +19,7 @@ def apply_asset_library_materials(blend_path: str, material_map: dict, link: boo Args: blend_path: Absolute path to the .blend library file. material_map: Mapping of current slot material name -> library material name. - E.g. {"Steel--Stahl": "SCHAEFFLER_010101_Steel-Bare"} + E.g. {"Steel--Stahl": "HARTOMAT_010101_Steel-Bare"} link: If True (default), link materials (external reference, good for rendering). If False, append materials (local copy — required for GLB/GLTF export so that the exporter can traverse Principled BSDF node trees for PBR values). diff --git a/render-worker/scripts/cinematic_render.py b/render-worker/scripts/cinematic_render.py index 55cee72..32a6cba 100644 --- a/render-worker/scripts/cinematic_render.py +++ b/render-worker/scripts/cinematic_render.py @@ -577,7 +577,7 @@ def main(): if material_map: _unassigned = [p for p in parts if not p.data.materials or (len(p.data.materials) == 1 and p.data.materials[0] and - p.data.materials[0].name == "SCHAEFFLER_059999_FailedMaterial")] + p.data.materials[0].name == "HARTOMAT_059999_FailedMaterial")] if _unassigned: print(f"[cinematic_render] {len(_unassigned)} parts without USD primvar -- " f"falling back to name-matching", flush=True) @@ -654,7 +654,7 @@ def main(): if material_map: _unassigned = [p for p in parts if not p.data.materials or (len(p.data.materials) == 1 and p.data.materials[0] and - p.data.materials[0].name == "SCHAEFFLER_059999_FailedMaterial")] + p.data.materials[0].name == "HARTOMAT_059999_FailedMaterial")] if _unassigned: _apply_material_library_shared( _unassigned, material_library_path, diff --git a/render-worker/scripts/export_step_to_gltf.py b/render-worker/scripts/export_step_to_gltf.py index 24f0bcf..e02c272 100644 --- a/render-worker/scripts/export_step_to_gltf.py +++ b/render-worker/scripts/export_step_to_gltf.py @@ -754,8 +754,8 @@ def main() -> None: try: extras_payload: dict = {} if sharp_pairs: - extras_payload["schaeffler_sharp_edge_pairs"] = sharp_pairs - extras_payload["schaeffler_sharp_threshold_deg"] = args.sharp_threshold + extras_payload["hartomat_sharp_edge_pairs"] = sharp_pairs + extras_payload["hartomat_sharp_threshold_deg"] = args.sharp_threshold if part_key_map: extras_payload["partKeyMap"] = part_key_map if extras_payload: diff --git a/render-worker/scripts/export_step_to_usd.py b/render-worker/scripts/export_step_to_usd.py index 4bc7944..321c44d 100644 --- a/render-worker/scripts/export_step_to_usd.py +++ b/render-worker/scripts/export_step_to_usd.py @@ -1,4 +1,4 @@ -"""STEP → USD exporter for Schaeffler Automat. +"""STEP → USD exporter for HartOMat. Reads a STEP file via OCP/XCAF (preserving part names + embedded colors), tessellates with BRepMesh, builds a USD stage mirroring the full XCAF @@ -18,7 +18,7 @@ Usage: [--color_map '{"Ring": "#4C9BE8"}'] \\ [--sharp_threshold 20.0] \\ [--cad_file_id uuid] \\ - [--material_map '{"part_name": "SCHAEFFLER_010101_Steel-Bare", ...}'] + [--material_map '{"part_name": "HARTOMAT_010101_Steel-Bare", ...}'] Exit 0 on success, exit 1 on failure. Prints MANIFEST_JSON: {...} to stdout before exit. @@ -418,8 +418,8 @@ def _author_xcaf_to_usd( _occ_trsf_to_usd_matrix(local_loc.Transformation())) prim = xform.GetPrim() - prim.SetCustomDataByKey("schaeffler:sourceName", source_name) - prim.SetCustomDataByKey("schaeffler:sourceAssemblyPath", xcaf_path) + prim.SetCustomDataByKey("hartomat:sourceName", source_name) + prim.SetCustomDataByKey("hartomat:sourceAssemblyPath", xcaf_path) print(f" {' ' * depth}[asm] {source_name} → {xform_path}" f"{' (transform)' if has_local_trsf else ''}") @@ -484,16 +484,16 @@ def _author_xcaf_to_usd( _occ_trsf_to_usd_matrix(local_loc.Transformation())) prim = xform.GetPrim() - prim.SetCustomDataByKey("schaeffler:partKey", part_key) - prim.SetCustomDataByKey("schaeffler:sourceName", source_name) - prim.SetCustomDataByKey("schaeffler:sourceAssemblyPath", xcaf_path) - prim.SetCustomDataByKey("schaeffler:sourceColor", hex_color) - prim.SetCustomDataByKey("schaeffler:tessellation:linearDeflectionMm", + prim.SetCustomDataByKey("hartomat:partKey", part_key) + prim.SetCustomDataByKey("hartomat:sourceName", source_name) + prim.SetCustomDataByKey("hartomat:sourceAssemblyPath", xcaf_path) + prim.SetCustomDataByKey("hartomat:sourceColor", hex_color) + prim.SetCustomDataByKey("hartomat:tessellation:linearDeflectionMm", args.linear_deflection) - prim.SetCustomDataByKey("schaeffler:tessellation:angularDeflectionRad", + prim.SetCustomDataByKey("hartomat:tessellation:angularDeflectionRad", args.angular_deflection) if args.cad_file_id: - prim.SetCustomDataByKey("schaeffler:cadFileId", args.cad_file_id) + prim.SetCustomDataByKey("hartomat:cadFileId", args.cad_file_id) # ── UsdGeomMesh ──────────────────────────────────────────── mesh = UsdGeom.Mesh.Define(stage, mesh_path) @@ -525,13 +525,13 @@ def _author_xcaf_to_usd( # ── Material metadata on mesh prim (customData) ─────────── mesh_prim = mesh.GetPrim() - mesh_prim.SetCustomDataByKey("schaeffler:partKey", part_key) - mesh_prim.SetCustomDataByKey("schaeffler:sourceName", source_name) + mesh_prim.SetCustomDataByKey("hartomat:partKey", part_key) + mesh_prim.SetCustomDataByKey("hartomat:sourceName", source_name) canonical_mat = _lookup_material(source_name, part_key, mat_map_lower) if canonical_mat: mesh_prim.SetCustomDataByKey( - "schaeffler:canonicalMaterialName", canonical_mat) + "hartomat:canonicalMaterialName", canonical_mat) primvars_api = UsdGeom.PrimvarsAPI(mesh) @@ -542,7 +542,7 @@ def _author_xcaf_to_usd( idx_pairs = _world_to_index_pairs(vertices, sharp_pairs) if idx_pairs: pv = primvars_api.CreatePrimvar( - "schaeffler:sharpEdgeVertexPairs", + "hartomat:sharpEdgeVertexPairs", Sdf.ValueTypeNames.Int2Array, UsdGeom.Tokens.constant, ) @@ -556,7 +556,7 @@ def _author_xcaf_to_usd( seam_idx_pairs = _world_to_index_pairs(vertices, seam_pairs) if seam_idx_pairs: pv_seam = primvars_api.CreatePrimvar( - "schaeffler:seamEdgeVertexPairs", + "hartomat:seamEdgeVertexPairs", Sdf.ValueTypeNames.Int2Array, UsdGeom.Tokens.constant, ) diff --git a/render-worker/scripts/import_usd.py b/render-worker/scripts/import_usd.py index 968f1e6..c1a9d5f 100644 --- a/render-worker/scripts/import_usd.py +++ b/render-worker/scripts/import_usd.py @@ -2,7 +2,7 @@ Runs inside Blender's Python environment (bpy available). Imports a USD stage and restores seam + sharp edges from -schaeffler:*EdgeVertexPairs primvars. Blender's built-in USD importer does +hartomat:*EdgeVertexPairs primvars. Blender's built-in USD importer does NOT map arbitrary custom primvars (constant Int2Array) to mesh attributes, so we read them directly via the pxr module and apply via bmesh. @@ -22,7 +22,7 @@ def import_usd_file(usd_path: str) -> list | tuple: Returns a tuple of (parts, material_lookup) where: - parts: list of imported mesh objects, centred at world origin - material_lookup: dict mapping blender_object_name → canonical_material_name - (populated from schaeffler:canonicalMaterialName customData, empty dict if absent) + (populated from hartomat:canonicalMaterialName customData, empty dict if absent) USD stage is mm Y-up with metersPerUnit=0.001 — Blender scales to metres. """ @@ -49,20 +49,20 @@ def import_usd_file(usd_path: str) -> list | tuple: for prim in stage.Traverse(): if prim.GetTypeName() != "Mesh": continue - part_key = prim.GetCustomDataByKey("schaeffler:partKey") or "" - mat_name = prim.GetCustomDataByKey("schaeffler:canonicalMaterialName") or "" + part_key = prim.GetCustomDataByKey("hartomat:partKey") or "" + mat_name = prim.GetCustomDataByKey("hartomat:canonicalMaterialName") or "" if not part_key or not mat_name: parent = prim.GetParent() if parent: - part_key = part_key or (parent.GetCustomDataByKey("schaeffler:partKey") or "") - mat_name = mat_name or (parent.GetCustomDataByKey("schaeffler:canonicalMaterialName") or "") + part_key = part_key or (parent.GetCustomDataByKey("hartomat:partKey") or "") + mat_name = mat_name or (parent.GetCustomDataByKey("hartomat:canonicalMaterialName") or "") if part_key and mat_name: material_lookup[part_key] = mat_name # Read seam/sharp primvars from USD mesh prim pvs_api = UsdGeom.PrimvarsAPI(prim) - sharp_pv = pvs_api.GetPrimvar("schaeffler:sharpEdgeVertexPairs") - seam_pv = pvs_api.GetPrimvar("schaeffler:seamEdgeVertexPairs") + sharp_pv = pvs_api.GetPrimvar("hartomat:sharpEdgeVertexPairs") + seam_pv = pvs_api.GetPrimvar("hartomat:seamEdgeVertexPairs") sharp_list = [] seam_list = [] if sharp_pv and sharp_pv.HasValue(): @@ -83,7 +83,7 @@ def import_usd_file(usd_path: str) -> list | tuple: print(f"[import_usd] pxr material lookup: {len(material_lookup)}/{len(parts)} parts", flush=True) else: - print("[import_usd] no schaeffler:canonicalMaterialName metadata found (legacy USD)", + print("[import_usd] no hartomat:canonicalMaterialName metadata found (legacy USD)", flush=True) if edge_data: diff --git a/render-worker/scripts/still_render.py b/render-worker/scripts/still_render.py index 6b19d07..b7310d1 100644 --- a/render-worker/scripts/still_render.py +++ b/render-worker/scripts/still_render.py @@ -345,7 +345,7 @@ def main(): part_colors_json = args[6] if len(args) > 6 else "{}" transparent_bg = args[7] == "1" if len(args) > 7 else False - # Template + material library args (passed by schaeffler-still.js) + # Template + material library args (passed by hartomat-still.js) template_path = args[8] if len(args) > 8 and args[8] else "" target_collection = args[9] if len(args) > 9 else "Product" material_library_path = args[10] if len(args) > 10 and args[10] else "" diff --git a/render-worker/scripts/turntable_render.py b/render-worker/scripts/turntable_render.py index 51a9f4f..2a56476 100644 --- a/render-worker/scripts/turntable_render.py +++ b/render-worker/scripts/turntable_render.py @@ -317,7 +317,7 @@ def main(): samples = int(args[7]) part_colors_json = args[8] if len(args) > 8 else "{}" - # Template + material library args (passed by schaeffler-turntable.js) + # Template + material library args (passed by hartomat-turntable.js) template_path = args[9] if len(args) > 9 and args[9] else "" target_collection = args[10] if len(args) > 10 else "Product" material_library_path = args[11] if len(args) > 11 and args[11] else "" @@ -468,7 +468,7 @@ def main(): if material_map: _unassigned = [p for p in parts if not p.data.materials or (len(p.data.materials) == 1 and p.data.materials[0] and - p.data.materials[0].name == "SCHAEFFLER_059999_FailedMaterial")] + p.data.materials[0].name == "HARTOMAT_059999_FailedMaterial")] if _unassigned: print(f"[turntable_render] {len(_unassigned)} parts without USD primvar — " f"falling back to name-matching", flush=True) @@ -559,7 +559,7 @@ def main(): if material_map: _unassigned = [p for p in parts if not p.data.materials or (len(p.data.materials) == 1 and p.data.materials[0] and - p.data.materials[0].name == "SCHAEFFLER_059999_FailedMaterial")] + p.data.materials[0].name == "HARTOMAT_059999_FailedMaterial")] if _unassigned: _apply_material_library_shared( _unassigned, material_library_path, diff --git a/scripts/start.sh b/scripts/start.sh index 9f215f1..f50b9ab 100755 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -2,7 +2,7 @@ set -euo pipefail cd "$(dirname "$0")/.." -echo "Starting Schaeffler Automat..." +echo "Starting HartOMat..." docker compose up -d echo "" diff --git a/scripts/stop.sh b/scripts/stop.sh index 41727dc..1bc1b0a 100755 --- a/scripts/stop.sh +++ b/scripts/stop.sh @@ -2,7 +2,7 @@ set -euo pipefail cd "$(dirname "$0")/.." -echo "Stopping Schaeffler Automat..." +echo "Stopping HartOMat..." docker compose down echo "" diff --git a/scripts/test_render_pipeline.py b/scripts/test_render_pipeline.py index 7ade6ad..db99ea8 100644 --- a/scripts/test_render_pipeline.py +++ b/scripts/test_render_pipeline.py @@ -16,7 +16,7 @@ Usage: # Custom credentials / host python scripts/test_render_pipeline.py --sample --host http://localhost:8888 \ - --email admin@schaeffler.com --password Admin1234! + --email admin@hartomat.com --password Admin1234! Environment variables (alternative to flags): TEST_HOST, TEST_EMAIL, TEST_PASSWORD @@ -34,7 +34,7 @@ from pathlib import Path # --------------------------------------------------------------------------- DEFAULT_HOST = os.environ.get("TEST_HOST", "http://localhost:8888") -DEFAULT_EMAIL = os.environ.get("TEST_EMAIL", "admin@schaeffler.com") +DEFAULT_EMAIL = os.environ.get("TEST_EMAIL", "admin@hartomat.com") DEFAULT_PASSWORD = os.environ.get("TEST_PASSWORD", "Admin1234!") SAMPLE_STEP = Path(__file__).parent.parent / "step-sample-file" / "81113-l_cut.stp" diff --git a/tools/restore_sharp_marks.py b/tools/restore_sharp_marks.py index b758d2e..98c1354 100644 --- a/tools/restore_sharp_marks.py +++ b/tools/restore_sharp_marks.py @@ -1,6 +1,6 @@ """Blender companion script: restore sharp + seam edge marks after importing a production GLB. -After importing a Schaeffler production GLB in Blender, run this script once via +After importing a HartOMat production GLB in Blender, run this script once via the Scripting workspace (Text Editor → Run Script). It reads the sharp angle AND the OCC B-rep sharp edge pairs baked into the GLB at export time, and re-applies mark_sharp() + mark_seam() on every mesh object. @@ -26,7 +26,7 @@ if not mesh_objects: print("No mesh objects found in scene.") else: # --- Pass 1: dihedral-angle-based sharp/seam marks --- - angle_deg = bpy.context.scene.get("schaeffler_sharp_angle_deg", 30.0) + angle_deg = bpy.context.scene.get("hartomat_sharp_angle_deg", 30.0) smooth_rad = math.radians(float(angle_deg)) total_sharp = 0 @@ -48,7 +48,7 @@ else: print(f"Pass 1 (dihedral {angle_deg}°): {total_sharp} sharp/seam edges across {len(mesh_objects)} objects.") # --- Pass 2: OCC B-rep sharp edges from GLB extras --- - # The production GLB embeds schaeffler_sharp_edge_pairs (OCC B-rep topology, + # The production GLB embeds hartomat_sharp_edge_pairs (OCC B-rep topology, # dense curve samples at 0.3mm) in scenes[0].extras, which Blender maps to # scene custom properties on import. These cover geometrically sharp edges # that the dihedral-angle pass misses due to tessellation noise. @@ -56,7 +56,7 @@ else: # Coordinate convention (mirrors export_gltf.py _apply_sharp_edges_from_occ): # OCC STEP space (Z-up, mm) → Blender (Z-up, m): # Blender(X, Y, Z) = OCC(X*0.001, -Z*0.001, Y*0.001) - occ_pairs = bpy.context.scene.get("schaeffler_sharp_edge_pairs") or [] + occ_pairs = bpy.context.scene.get("hartomat_sharp_edge_pairs") or [] if occ_pairs: print(f"Pass 2 (OCC B-rep): applying {len(occ_pairs)} sharp edge segment pairs...") @@ -99,4 +99,4 @@ else: print(f"Pass 2 (OCC B-rep): {marked_total} additional edges marked across {len(mesh_objects)} objects.") else: - print("Pass 2 (OCC B-rep): no schaeffler_sharp_edge_pairs in GLB extras — skipped.") + print("Pass 2 (OCC B-rep): no hartomat_sharp_edge_pairs in GLB extras — skipped.") diff --git a/visual-audit-report.md b/visual-audit-report.md index 24de323..b0f8fb6 100644 --- a/visual-audit-report.md +++ b/visual-audit-report.md @@ -1,4 +1,4 @@ -# Schaeffler Automat — UX & Quality Audit Report +# HartOMat — UX & Quality Audit Report **Date**: 2026-03-08 **Overall Score**: 6.5/10 @@ -6,7 +6,7 @@ ## Executive Summary -Schaeffler Automat is a functionally complete internal tool with a solid design system foundation (semantic CSS variables, Tailwind tokens, dark/light theming, role-aware navigation). The core workflows — Excel import → STEP upload → render dispatch — are implemented end-to-end. However, the UI suffers from **information density without hierarchy**: the Admin page is an undifferentiated ~3000px scroll, the Upload wizard has no progress indicator, and the Activity page mixes live queue data with historical records without clear separation. The most impactful improvements are a redesigned Admin hub with tab navigation, an Upload wizard progress bar, and standardized interaction patterns (replacing `window.confirm()`, adding debounce to search, and skeleton loading states). +HartOMat is a functionally complete internal tool with a solid design system foundation (semantic CSS variables, Tailwind tokens, dark/light theming, role-aware navigation). The core workflows — Excel import → STEP upload → render dispatch — are implemented end-to-end. However, the UI suffers from **information density without hierarchy**: the Admin page is an undifferentiated ~3000px scroll, the Upload wizard has no progress indicator, and the Activity page mixes live queue data with historical records without clear separation. The most impactful improvements are a redesigned Admin hub with tab navigation, an Upload wizard progress bar, and standardized interaction patterns (replacing `window.confirm()`, adding debounce to search, and skeleton loading states). ---