feat(N): workflow pipeline, 3D viewer, worker management, QC tests
- workflow_builder.py: fix broken stubs, add render_order_line_still_task
(resolves step_path from DB instead of passing order_line_id as step_path)
- domains/rendering/tasks.py: add render_order_line_still_task,
export_gltf_for_order_line_task, export_blend_for_order_line_task,
generate_gltf_geometry_task (trimesh STL→GLB, no Blender needed)
- tasks/step_tasks.py: add generate_gltf_geometry_task for CadFile GLB export
- cad router: POST /{id}/generate-gltf-geometry endpoint (admin/PM)
- worker router: GET /celery-workers + POST /scale (docker compose subprocess)
- Dockerfile: pip install -e "[dev]" to enable pytest
- docker-compose.yml: docker socket + compose file mount on backend
- ThreeDViewer.tsx: mode toggle (geometry/production), wireframe, env presets,
download buttons (GLB + .blend)
- CadPreview.tsx: load gltf_geometry/gltf_production/blend_production assets
from MediaAsset table and pass URLs to ThreeDViewer
- ProductDetail.tsx: "View 3D" button → /cad/:id, "Generate GLB" button
- media router/service: cad_file_id filter on GET /api/media
- WorkerManagement.tsx: new page with worker status, queue depth, scale controls
- App.tsx + Layout.tsx: /workers route + sidebar link (admin/PM)
- tests: test_rendering_service.py, test_orders_service.py (backend)
- tests: WorkerActivity.test.tsx, WorkerManagement.test.tsx (frontend)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,420 +1,365 @@
|
||||
# Plan: Phase J (WebSocket) + Turntable Bug + Phase K (Asset Library)
|
||||
# Plan: Phase N — Workflow-Pipeline, 3D-Viewer Production-Modus, Worker-Management, QC-Tests
|
||||
|
||||
## Kontext
|
||||
|
||||
Analyse des aktuellen Codestands ergab: **Phasen F, G, H, I, L sind bereits vollständig implementiert.**
|
||||
Vier offene Bereiche aus dem PLAN.md müssen abgeschlossen werden:
|
||||
|
||||
| Phase | Status | Beleg |
|
||||
|-------|--------|-------|
|
||||
| F - Hash-Caching | DONE | `domains/products/cache_service.py` + migration 041 |
|
||||
| G - Billing | DONE | `domains/billing/` vollständig, WeasyPrint in Dockerfile |
|
||||
| H - Excel Sanity-Check | DONE | `domains/imports/service.py run_sanity_check()` + Upload.tsx Dialog |
|
||||
| I - Notification-Config | DONE | `notification_configs` migration 044, NotificationSettings.tsx |
|
||||
| L - Dashboard | DONE | AdminDashboard.tsx + ClientDashboard.tsx vollständig |
|
||||
| **J - WebSocket** | **FEHLT** | Kein `core/websocket.py`, alle Polls noch aktiv |
|
||||
1. **Workflow-Pipeline verdrahten**: `workflow_builder.py` enthält nur defekte Stubs. `_build_still` übergibt `order_line_id` als `step_path` an `render_still_task` → würde crashen. Der neue `still_with_exports`-Workflow (still + gltf_export + blend_export) ist nicht implementiert. Die Celery-Tasks für export_gltf/export_blend fehlen in `domains/rendering/tasks.py`.
|
||||
|
||||
Zusätzlich: **Kritischer Bug in `render_blender.py`** — ffmpeg-Overlay-Befehl haengt bei endlicher Frame-Sequenz (kein `shortest=1`) -> Timeout -> Turntable-Render schlaegt fehl.
|
||||
2. **K6: 3D-Viewer Production-Modus**: `ThreeDViewer.tsx` hat keinen Mode-Toggle, Wireframe, Env-Preset oder Download-Buttons. Für Testdaten wird `POST /api/cad/{id}/generate-gltf-geometry` benötigt (trimesh STL→GLB, kein Blender nötig).
|
||||
|
||||
---
|
||||
3. **L3: Worker-Management UI**: `WorkerManagement.tsx` fehlt. Backend braucht `/celery-workers` (Celery inspect) und `/scale` (docker compose subprocess). Backend-Container bekommt Docker-Socket-Mount.
|
||||
|
||||
## Bug Fix: Turntable ffmpeg Timeout
|
||||
|
||||
**Root cause**: In `backend/app/services/render_blender.py:507`:
|
||||
```python
|
||||
"-filter_complex", "[1:v][0:v]overlay=0:0",
|
||||
```
|
||||
Der `lavfi color`-Quell-Stream hat keine definierte Laenge. Ohne `shortest=1` wartet ffmpeg auf
|
||||
weitere Frames vom Farb-Stream nachdem die PNG-Sequenz endet -> haengt bis Timeout (300s).
|
||||
|
||||
**Fix**: `overlay=0:0` -> `overlay=0:0:shortest=1`
|
||||
|
||||
---
|
||||
|
||||
## Phase J: WebSocket Backend + Frontend
|
||||
|
||||
### Architektur (ADR-05: FastAPI nativ + Redis Pub/Sub)
|
||||
|
||||
```
|
||||
Backend Task/Router:
|
||||
-> redis.publish(f"tenant:{tenant_id}", json.dumps(event))
|
||||
|
||||
core/websocket.py:
|
||||
ConnectionManager: tenant_id -> set[WebSocket]
|
||||
background_task: asyncio.Task (redis subscribe loop)
|
||||
|
||||
Frontend:
|
||||
useWebSocket() hook -> WebSocket('/api/ws')
|
||||
Empfaengt Events, invalidiert React Query caches
|
||||
```
|
||||
|
||||
### Events die gesendet werden:
|
||||
| Event | Sender | Daten |
|
||||
|-------|--------|-------|
|
||||
| `render_complete` | step_tasks.py | order_line_id, status, thumbnail_url |
|
||||
| `render_failed` | step_tasks.py | order_line_id, error |
|
||||
| `cad_processing_complete` | step_tasks.py | cad_file_id, status |
|
||||
| `order_status_change` | orders router | order_id, new_status |
|
||||
| `queue_update` | beat task (alle 10s) | depth per queue |
|
||||
4. **M: QC-Tests**: `pytest` ist im Backend-Container nicht installiert. Dockerfile: `pip install -e ".[dev]"`. Neue Service-Tests für rendering und orders domains. 2 neue Vitest-Dateien.
|
||||
|
||||
---
|
||||
|
||||
## Betroffene Dateien
|
||||
|
||||
### Neu erstellen:
|
||||
- `backend/app/core/websocket.py` -- ConnectionManager + Redis Pub/Sub Loop
|
||||
- `frontend/src/hooks/useWebSocket.ts` -- WebSocket hook mit Auto-Reconnect
|
||||
- `frontend/src/contexts/WebSocketContext.tsx` -- Context Provider
|
||||
|
||||
### Aendern:
|
||||
- `backend/app/services/render_blender.py` -- ffmpeg shortest=1 Bug-Fix
|
||||
- `backend/app/main.py` -- WebSocket-Endpoint registrieren (`/api/ws`)
|
||||
- `backend/app/tasks/step_tasks.py` -- WebSocket-Events emittieren
|
||||
- `backend/app/domains/orders/router.py` -- Order-Status-Events emittieren
|
||||
- `backend/app/tasks/celery_app.py` -- `broadcast_queue_status` Beat-Task hinzufuegen
|
||||
- `frontend/src/App.tsx` -- WebSocketProvider wrappen
|
||||
- `frontend/src/pages/WorkerActivity.tsx` -- polling durch WS ersetzen
|
||||
- `frontend/src/pages/OrderDetail.tsx` -- polling durch WS ersetzen
|
||||
- `frontend/src/pages/Orders.tsx` -- polling reduzieren
|
||||
- `frontend/src/components/layout/Layout.tsx` -- polling reduzieren
|
||||
- `frontend/src/components/layout/NotificationCenter.tsx` -- polling durch WS ersetzen
|
||||
|
||||
### Nach Phase J Commit -- Phase K:
|
||||
- `backend/alembic/versions/045_asset_libraries.py` -- asset_libraries Tabelle
|
||||
- `backend/app/domains/materials/models.py` -- AssetLibrary Model hinzufuegen
|
||||
- `backend/app/domains/materials/router.py` -- Asset Library CRUD + Upload
|
||||
- `render-worker/scripts/asset_library.py` -- Materialien + Node-Groups aus .blend laden
|
||||
- `render-worker/scripts/catalog_assets.py` -- Katalog aus .blend lesen
|
||||
- `render-worker/scripts/export_gltf.py` -- GLB Export mit Materialien
|
||||
- `render-worker/scripts/export_blend.py` -- .blend Export mit pack_all()
|
||||
- `backend/app/domains/rendering/workflow_builder.py` -- Asset Library Nodes
|
||||
- `frontend/src/pages/Admin.tsx` -- Asset Library Manager UI
|
||||
- `frontend/src/api/assetLibraries.ts` -- API Client
|
||||
| Datei | Änderung |
|
||||
|-------|----------|
|
||||
| `backend/app/domains/rendering/tasks.py` | 3 neue Tasks: `render_order_line_still_task`, `export_gltf_for_order_line_task`, `export_blend_for_order_line_task` |
|
||||
| `backend/app/domains/rendering/workflow_builder.py` | Stubs ersetzen durch order-line-aware Tasks, `still_with_exports` hinzufügen |
|
||||
| `backend/app/api/routers/cad.py` | `POST /{id}/generate-gltf-geometry` Endpoint |
|
||||
| `backend/app/api/routers/worker.py` | `GET /celery-workers`, `POST /scale` Endpoints |
|
||||
| `backend/Dockerfile` | `pip install -e ".[dev]"` |
|
||||
| `docker-compose.yml` | Backend + Worker: Docker-Socket + Compose-File-Mount |
|
||||
| `frontend/src/components/cad/ThreeDViewer.tsx` | Mode-Toggle, Wireframe, Env-Preset, Download-Buttons |
|
||||
| `frontend/src/pages/WorkerManagement.tsx` | NEU: Worker-Liste, Queue-Stats, Scale-Button |
|
||||
| `frontend/src/api/worker.ts` | Neue Interfaces + API-Funktionen |
|
||||
| `frontend/src/App.tsx` | Route für /workers |
|
||||
| `frontend/src/components/layout/Layout.tsx` | Sidebar-Link Workers |
|
||||
| `backend/tests/domains/test_rendering_service.py` | NEU: ≥5 Tests für Rendering-Tasks und Workflow-Builder |
|
||||
| `backend/tests/domains/test_orders_service.py` | NEU: ≥5 Tests für Orders-Endpoints |
|
||||
| `frontend/src/__tests__/pages/WorkerActivity.test.tsx` | NEU: Vitest-Tests |
|
||||
| `frontend/src/__tests__/pages/WorkerManagement.test.tsx` | NEU: Vitest-Tests |
|
||||
|
||||
---
|
||||
|
||||
## Tasks (in Reihenfolge)
|
||||
|
||||
### Task 1: Bug-Fix ffmpeg Turntable Timeout [x]
|
||||
- **Datei**: `backend/app/services/render_blender.py:507`
|
||||
- **Was**: `"[1:v][0:v]overlay=0:0"` -> `"[1:v][0:v]overlay=0:0:shortest=1"`
|
||||
- **Akzeptanzkriterium**: Turntable-Render fuer Order f0436188 kann erneut gestartet werden und produziert MP4
|
||||
- **Abhaengigkeiten**: keine
|
||||
### Task 1: Backend — Neue order-line-aware Rendering-Tasks
|
||||
- **Datei**: `backend/app/domains/rendering/tasks.py`
|
||||
- **Was**: Drei neue Celery-Tasks hinzufügen (UNTER den bestehenden Tasks):
|
||||
|
||||
### Task 2: WebSocket Backend -- core/websocket.py [x]
|
||||
- **Datei**: `backend/app/core/websocket.py` (neu)
|
||||
- **Was**:
|
||||
```python
|
||||
class ConnectionManager:
|
||||
_connections: dict[str, set[WebSocket]] # tenant_id -> sockets
|
||||
async def connect(ws, tenant_id)
|
||||
def disconnect(ws, tenant_id)
|
||||
async def broadcast_to_tenant(tenant_id, event: dict)
|
||||
async def start_redis_subscriber() # asyncio background task
|
||||
**`render_order_line_still_task(order_line_id, **params)`** — Queue `thumbnail_rendering`:
|
||||
- Lädt OrderLine + CadFile via sync SQLAlchemy (wie `publish_asset`)
|
||||
- Setzt `render_status = 'processing'`
|
||||
- Ruft `render_still()` aus `app.services.render_blender` auf
|
||||
- Setzt `render_status = 'completed'`, speichert `render_log`
|
||||
- Bei Fehler: `render_status = 'failed'`
|
||||
- Returns dict mit `output_path`
|
||||
|
||||
def publish_event_sync(tenant_id: str, event: dict):
|
||||
# Sync version fuer Celery tasks -- redis.publish()
|
||||
```
|
||||
- Redis Pub/Sub: subscribe auf `tenant:*` Channels
|
||||
- Bei Nachricht: alle WebSockets des Tenants benachrichtigen
|
||||
- Auto-Ping alle 30s gegen Disconnects
|
||||
- **Akzeptanzkriterium**: broadcast_to_tenant sendet an alle verbundenen WS des Tenants
|
||||
- **Abhaengigkeiten**: keine
|
||||
**`export_gltf_for_order_line_task(order_line_id)`** — Queue `thumbnail_rendering`:
|
||||
- Lädt OrderLine + CadFile sync
|
||||
- Sucht STL-Cache (`{step_stem}_low.stl`)
|
||||
- Ruft Blender subprocess mit `export_gltf.py` auf: `blender --background --python export_gltf.py -- --stl_path X --output_path Y`
|
||||
- Lädt GLB nach MinIO `production-exports/{cad_file_id}/{order_line_id}.glb`
|
||||
- Erstellt `MediaAsset(asset_type=gltf_production, storage_key=...)`
|
||||
- Returns `storage_key`
|
||||
|
||||
### Task 3: WebSocket Endpoint in main.py [x]
|
||||
- **Datei**: `backend/app/main.py`
|
||||
- **Was**:
|
||||
```python
|
||||
@app.websocket("/api/ws")
|
||||
async def ws_endpoint(websocket: WebSocket, token: str = Query(...)):
|
||||
user = await verify_ws_token(token)
|
||||
await manager.connect(websocket, str(user.tenant_id))
|
||||
try:
|
||||
while True:
|
||||
await websocket.receive_text() # Keep-alive pings
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(websocket, str(user.tenant_id))
|
||||
```
|
||||
- Token-Auth via Query-Parameter (WS kann keinen Authorization-Header senden)
|
||||
- `verify_ws_token`: JWT decode, User laden (analog zu get_current_user)
|
||||
- `manager` als globale Instanz, gestartet im lifespan
|
||||
- **Akzeptanzkriterium**: `ws://localhost:8888/api/ws?token=<jwt>` oeffnet Verbindung
|
||||
- **Abhaengigkeiten**: Task 2
|
||||
**`export_blend_for_order_line_task(order_line_id)`** — Queue `thumbnail_rendering`:
|
||||
- Analog zu export_gltf, aber mit `export_blend.py`
|
||||
- MediaAsset type: `blend_production`
|
||||
|
||||
### Task 4: WebSocket Events in step_tasks.py [x]
|
||||
- **Datei**: `backend/app/tasks/step_tasks.py`
|
||||
- **Was**: In render_order_line_task und render_step_thumbnail nach Erfolg/Fehler:
|
||||
```python
|
||||
from app.core.websocket import publish_event_sync
|
||||
# bei render complete:
|
||||
publish_event_sync(tenant_id, {"type": "render_complete", "order_line_id": str(line.id), "status": "completed"})
|
||||
# bei render failed:
|
||||
publish_event_sync(tenant_id, {"type": "render_failed", "order_line_id": str(line.id), "error": str(exc)})
|
||||
# bei CAD processing complete:
|
||||
publish_event_sync(tenant_id, {"type": "cad_processing_complete", "cad_file_id": str(cad_file.id), "status": "completed"})
|
||||
```
|
||||
- tenant_id aus cad_file.tenant_id bzw. order_line -> order -> user.tenant_id laden
|
||||
- **Akzeptanzkriterium**: Render fertig -> WebSocket-Client empfaengt Event
|
||||
- **Abhaengigkeiten**: Task 2
|
||||
- **Akzeptanzkriterium**: Tasks in `domains/rendering/tasks.py` vorhanden, keine Import-Fehler
|
||||
- **Abhängigkeiten**: keine
|
||||
|
||||
### Task 5: WebSocket Events in orders router [x]
|
||||
- **Datei**: `backend/app/domains/orders/router.py`
|
||||
- **Was**: Bei Order-Status-Aenderung (submit, complete, cancel):
|
||||
```python
|
||||
from app.core.websocket import manager
|
||||
await manager.broadcast_to_tenant(
|
||||
str(current_user.tenant_id),
|
||||
{"type": "order_status_change", "order_id": str(order.id), "status": new_status}
|
||||
)
|
||||
```
|
||||
- **Akzeptanzkriterium**: Order-Submit -> WebSocket-Event geht an alle Browser-Tabs des Tenants
|
||||
- **Abhaengigkeiten**: Task 2
|
||||
|
||||
### Task 6: Queue-Update Beat-Task [x]
|
||||
- **Datei**: `backend/app/tasks/celery_app.py`
|
||||
- **Was**: Neuer Beat-Task alle 10s:
|
||||
```python
|
||||
@shared_task(name="beat.broadcast_queue_status", queue="step_processing")
|
||||
def broadcast_queue_status():
|
||||
from app.core.websocket import publish_event_sync
|
||||
from redis import Redis
|
||||
r = Redis.from_url(settings.redis_url)
|
||||
depths = {
|
||||
"step_processing": r.llen("step_processing"),
|
||||
"thumbnail_rendering": r.llen("thumbnail_rendering"),
|
||||
}
|
||||
# Broadcast an alle Tenants (broadcast_all)
|
||||
r.publish("__broadcast__", json.dumps({"type": "queue_update", "depths": depths}))
|
||||
```
|
||||
- `__broadcast__` Channel: wird an ALLE verbundenen WS gesendet (nicht tenant-spezifisch)
|
||||
- ConnectionManager subscribt auch auf `__broadcast__`
|
||||
- **Akzeptanzkriterium**: WorkerActivity-Queue-Tiefe aktualisiert alle 10s automatisch
|
||||
- **Abhaengigkeiten**: Task 2
|
||||
|
||||
### Task 7: Frontend WebSocket Hook [x]
|
||||
- **Datei**: `frontend/src/hooks/useWebSocket.ts` (neu)
|
||||
- **Was**:
|
||||
```typescript
|
||||
export function useWebSocketConnection() {
|
||||
// Verbindet zu ws://localhost:8888/api/ws?token=<jwt>
|
||||
// Auto-Reconnect: 1s, 2s, 4s, 8s, ... max 30s
|
||||
// Emittiert Events via onMessage callback
|
||||
// Pings alle 25s (keep-alive)
|
||||
// Trennt Verbindung bei Logout
|
||||
}
|
||||
```
|
||||
- **Akzeptanzkriterium**: Verbindung bleibt offen, reconnected nach Netzwerktrennung
|
||||
- **Abhaengigkeiten**: keine
|
||||
|
||||
### Task 8: Frontend WebSocket Context [x]
|
||||
- **Datei**: `frontend/src/contexts/WebSocketContext.tsx` (neu), `frontend/src/App.tsx` aendern
|
||||
- **Was**:
|
||||
```typescript
|
||||
export function WebSocketProvider({ children }) {
|
||||
const queryClient = useQueryClient()
|
||||
// on 'render_complete': invalidateQueries(['orders', order_line_id])
|
||||
// on 'render_failed': invalidateQueries(['orders', order_line_id])
|
||||
// on 'cad_processing_complete': invalidateQueries(['cad-activity'])
|
||||
// on 'order_status_change': invalidateQueries(['orders'])
|
||||
// on 'queue_update': queryClient.setQueryData(['queue-status'], ...)
|
||||
}
|
||||
// App.tsx: <WebSocketProvider> um <Router> wrappen
|
||||
```
|
||||
- **Akzeptanzkriterium**: render_complete Event -> OrderDetail aktualisiert ohne Poll-Interval
|
||||
- **Abhaengigkeiten**: Task 7
|
||||
|
||||
### Task 9: Polling ersetzen -- WorkerActivity.tsx [x]
|
||||
- **Datei**: `frontend/src/pages/WorkerActivity.tsx`
|
||||
- **Was**:
|
||||
- `refetchInterval: 5000` entfernen -- bei `cad_processing_complete` invalidieren
|
||||
- `refetchInterval: 3000` fuer Queue-Status entfernen -- bei `queue_update` setQueryData
|
||||
- **Akzeptanzkriterium**: Keine automatischen HTTP-Requests im Network-Tab (nur WS-Frames)
|
||||
- **Abhaengigkeiten**: Task 8
|
||||
|
||||
### Task 10: Polling ersetzen -- OrderDetail.tsx [x]
|
||||
- **Datei**: `frontend/src/pages/OrderDetail.tsx`
|
||||
- **Was**:
|
||||
- `refetchInterval: (query) => {...}` entfernen
|
||||
- Stattdessen: bei `render_complete` / `render_failed` fuer matching order_line_id -> invalidate
|
||||
- **Akzeptanzkriterium**: Render-Status in OrderDetail aktualisiert live ohne Poll
|
||||
- **Abhaengigkeiten**: Task 8
|
||||
|
||||
### Task 11: Polling reduzieren -- Layout.tsx + NotificationCenter.tsx [x]
|
||||
- **Dateien**: `frontend/src/components/layout/Layout.tsx`, `NotificationCenter.tsx`
|
||||
- **Was**:
|
||||
- Layout: `refetchInterval: 8000` -> 60000 (1min)
|
||||
- NotificationCenter: `refetchInterval: 15_000` -> 60000; bei `order_status_change` zusaetzlich invalidieren
|
||||
- **Akzeptanzkriterium**: Signifikant weniger Poll-Requests im Network-Tab
|
||||
- **Abhaengigkeiten**: Task 8
|
||||
|
||||
### Task 12: PLAN.md + LEARNINGS.md + Commit [x]
|
||||
- **Was**:
|
||||
- PLAN.md: Phase J als ABGESCHLOSSEN markieren, Status auf "Phase K als naechstes"
|
||||
- LEARNINGS.md: ffmpeg `shortest=1` Learning + WebSocket Auth via Query-Param Learning
|
||||
- `git commit -m "feat(J): WebSocket live-events + replace polling + fix ffmpeg turntable timeout"`
|
||||
- **Abhaengigkeiten**: Tasks 1-11
|
||||
|
||||
---
|
||||
|
||||
## Phase K Tasks (nach Commit)
|
||||
|
||||
### Task K1: Migration 045 + AssetLibrary Model [x]
|
||||
- **Datei**: `backend/alembic/versions/045_asset_libraries.py` (neu, autogenerate), `domains/materials/models.py`
|
||||
- **Was**:
|
||||
```python
|
||||
class AssetLibrary(Base):
|
||||
id: UUID PK, tenant_id FK nullable, name VARCHAR(200)
|
||||
blend_file_key TEXT, # MinIO key
|
||||
catalog JSONB, # {materials: [...], node_groups: [...]}
|
||||
description TEXT, is_active BOOL, created_at TIMESTAMP
|
||||
```
|
||||
- `render_templates.asset_library_id` FK optional (nullable)
|
||||
- `output_types.asset_library_id` FK optional (nullable)
|
||||
- **Akzeptanzkriterium**: `alembic upgrade head` erfolgreich, `asset_libraries` Tabelle in DB
|
||||
|
||||
### Task K2: Asset Library CRUD Backend [x]
|
||||
- **Datei**: `backend/app/domains/materials/router.py` + `service.py` + `schemas.py`
|
||||
- **Was**:
|
||||
- `POST /api/asset-libraries` -- .blend Upload -> MinIO `asset-libraries/{id}.blend` -> queut Katalog-Refresh
|
||||
- `GET /api/asset-libraries` -- Liste
|
||||
- `GET /api/asset-libraries/{id}/catalog` -- Materialien + Node-Groups
|
||||
- `DELETE /api/asset-libraries/{id}` -- nur wenn nicht in Verwendung (FK-Check)
|
||||
- `AssetLibraryOut` Schema mit `catalog` field
|
||||
- **Akzeptanzkriterium**: POST + GET funktionieren, .blend in MinIO gespeichert
|
||||
|
||||
### Task K3: Katalog-Refresh Celery Task + Blender Script [x]
|
||||
- **Datei**: `backend/app/domains/materials/tasks.py` (neu), `render-worker/scripts/catalog_assets.py` (neu)
|
||||
- **Was**:
|
||||
- Celery Task `refresh_asset_library_catalog(asset_library_id)` auf Queue `thumbnail_rendering`
|
||||
- Laedt .blend aus MinIO in tmpdir
|
||||
- Startet `blender --background --python catalog_assets.py -- <blend_path>`
|
||||
- `catalog_assets.py`: oeffnet .blend, liest alle markierten Assets:
|
||||
```python
|
||||
import bpy, json, sys
|
||||
blend_path = sys.argv[sys.argv.index('--') + 1]
|
||||
bpy.ops.wm.open_mainfile(filepath=blend_path)
|
||||
catalog = {
|
||||
"materials": [m.name for m in bpy.data.materials if m.asset_data],
|
||||
"node_groups": [ng.name for ng in bpy.data.node_groups if ng.asset_data],
|
||||
}
|
||||
print(json.dumps(catalog))
|
||||
```
|
||||
- Schreibt Katalog in `asset_libraries.catalog JSONB`
|
||||
- **Akzeptanzkriterium**: Nach .blend-Upload enthaelt `catalog` JSONB die Asset-Namen
|
||||
|
||||
### Task K4: Blender Asset Library Apply Script [x]
|
||||
- **Datei**: `render-worker/scripts/asset_library.py` (neu)
|
||||
- **Was**:
|
||||
```python
|
||||
def apply_asset_library_materials(blend_path: str, material_map: dict) -> None:
|
||||
"""Laedt Materialien aus Asset-Library .blend, wendet auf Mesh-Parts an."""
|
||||
with bpy.data.libraries.load(blend_path, link=True, assets_only=True) as (src, dst):
|
||||
dst.materials = [n for n in src.materials if n in material_map.values()]
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type == 'MESH':
|
||||
for slot in obj.material_slots:
|
||||
resolved = material_map.get(slot.material.name if slot.material else '')
|
||||
if resolved and resolved in bpy.data.materials:
|
||||
slot.material = bpy.data.materials[resolved]
|
||||
|
||||
def apply_asset_library_modifiers(blend_path: str, modifier_map: dict) -> None:
|
||||
"""Laedt Geometry-Node-Gruppen, wendet als Modifier an."""
|
||||
with bpy.data.libraries.load(blend_path, link=True, assets_only=True) as (src, dst):
|
||||
dst.node_groups = [n for n in src.node_groups if n in modifier_map.values()]
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type == 'MESH':
|
||||
for part_name, mod_name in modifier_map.items():
|
||||
if part_name.lower() in obj.name.lower():
|
||||
mod = obj.modifiers.new(name=mod_name, type='NODES')
|
||||
mod.node_group = bpy.data.node_groups.get(mod_name)
|
||||
```
|
||||
- **Akzeptanzkriterium**: Render mit Asset-Library zeigt korrekte Produktionsmaterialien
|
||||
|
||||
### Task K5: export_gltf + export_blend Scripts [x]
|
||||
- **Dateien**: `render-worker/scripts/export_gltf.py` (neu), `render-worker/scripts/export_blend.py` (neu)
|
||||
- **Was**:
|
||||
- `export_gltf.py`:
|
||||
1. STL importieren (`bpy.ops.import_mesh.stl`)
|
||||
2. Asset Library laden via `apply_asset_library_materials` + `apply_asset_library_modifiers`
|
||||
3. `bpy.ops.export_scene.gltf(filepath=out, export_format='GLB', export_apply=True, export_draco_mesh_compression_enable=True)`
|
||||
4. Output nach MinIO `production-exports/{cad_file_id}/{run_id}.glb`
|
||||
5. MediaAsset-Record mit `asset_type=gltf_production`
|
||||
- `export_blend.py`:
|
||||
1. STL + Asset Library laden (wie export_gltf)
|
||||
2. `bpy.ops.file.pack_all()`
|
||||
3. `bpy.ops.wm.save_as_mainfile(filepath=out, compress=True, copy=True)`
|
||||
4. MediaAsset-Record mit `asset_type=blend_production`
|
||||
- **Akzeptanzkriterium**: GLB-Download oeffnet sich im Three.js Viewer mit Materialien
|
||||
|
||||
### Task K6: Workflow-Builder -- Asset Library Nodes [x]
|
||||
### Task 2: Backend — workflow_builder.py reparieren + still_with_exports
|
||||
- **Datei**: `backend/app/domains/rendering/workflow_builder.py`
|
||||
- **Was**:
|
||||
- Neue Celery Tasks: `apply_asset_library_materials_task`, `apply_asset_library_modifiers_task`, `export_gltf_task`, `export_blend_task`
|
||||
- Neuer Workflow-Typ `still_production`:
|
||||
|
||||
- `_build_still`: Nutzt `render_order_line_still_task` statt `render_still_task`
|
||||
- `_build_turntable`: Bleibt vorerst mit `render_turntable_task` (file-path-basiert, funktioniert via legacy path)
|
||||
- `_build_multi_angle`: Nutzt `render_order_line_still_task` mit `camera_angle` param
|
||||
- **NEU** `_build_still_with_exports(order_line_id, params)`:
|
||||
```python
|
||||
chain(
|
||||
convert_step.si(order_line_id),
|
||||
from celery import chain, group
|
||||
return chain(
|
||||
render_order_line_still_task.si(order_line_id, **params),
|
||||
group(
|
||||
chain(apply_asset_library_materials.si(order_line_id), render_still.si(order_line_id)),
|
||||
chain(apply_asset_library_materials.si(order_line_id), export_gltf.si(order_line_id)),
|
||||
chain(apply_asset_library_materials.si(order_line_id), export_blend.si(order_line_id)),
|
||||
),
|
||||
generate_thumbnail.si(order_line_id),
|
||||
publish_asset.si(order_line_id),
|
||||
export_gltf_for_order_line_task.si(order_line_id),
|
||||
export_blend_for_order_line_task.si(order_line_id),
|
||||
)
|
||||
)
|
||||
```
|
||||
- **Akzeptanzkriterium**: Dispatch eines `still_production` Workflows -> PNG + GLB + .blend erzeugt
|
||||
- `dispatch_workflow()`: `"still_with_exports"` zu `builders` hinzufügen
|
||||
|
||||
### Task K7: Asset Library Management UI [x]
|
||||
- **Dateien**: `frontend/src/api/assetLibraries.ts` (neu), `frontend/src/pages/Admin.tsx` erweitern
|
||||
- **Was**:
|
||||
- API Client: `getAssetLibraries`, `uploadAssetLibrary` (multipart), `deleteAssetLibrary`, `getAssetLibraryCatalog`
|
||||
- Admin.tsx: neues Panel "Asset Libraries" (nach Render Templates)
|
||||
- Upload-Button + Drag-Drop
|
||||
- Tabelle: Name, Materialien-Anzahl, Node-Groups-Anzahl, Aktionen
|
||||
- Katalog-Detail: Material-Badge-Liste (gruen) + Node-Group-Badge-Liste (blau)
|
||||
- OutputTypeTable: Asset-Library-Dropdown-Spalte
|
||||
- **Akzeptanzkriterium**: Admin kann .blend hochladen, Katalog sehen, OutputType zuweisen
|
||||
- **Akzeptanzkriterium**: `dispatch_workflow("still_with_exports", order_line_id)` löst keine Exception aus
|
||||
- **Abhängigkeiten**: Task 1
|
||||
|
||||
### Task K8: PLAN.md + LEARNINGS.md + Commit [x]
|
||||
### Task 3: Backend — generate-gltf-geometry Endpoint (Testdaten für K6)
|
||||
- **Datei**: `backend/app/api/routers/cad.py`
|
||||
- **Was**: Neuer Endpoint `POST /api/cad/{id}/generate-gltf-geometry` (require_admin_or_pm):
|
||||
- Prüft ob CadFile existiert + STL-Cache vorhanden (`{step_dir}/{stem}_low.stl`)
|
||||
- Queut neuen Celery-Task `generate_gltf_geometry_task.delay(str(cad_file.id))`
|
||||
- Returns `{"task_id": ..., "message": "GLB generation queued"}`
|
||||
|
||||
Neuer Task `generate_gltf_geometry_task` in `domains/rendering/tasks.py` (Queue `thumbnail_rendering`):
|
||||
- Lädt CadFile sync, findet STL-Cache
|
||||
- **Nutzt trimesh** (kein Blender): `import trimesh; mesh = trimesh.load(stl_path); mesh.export(glb_path)`
|
||||
→ Warum trimesh: Schnell, kein Blender nötig, läuft auf worker-Container (trimesh in pyproject.toml cad-extras)
|
||||
- Lädt GLB nach MinIO `uploads/{cad_file_id}/geometry.glb`
|
||||
- Erstellt/aktualisiert `MediaAsset(asset_type=gltf_geometry, storage_key=..., cad_file_id=...)`
|
||||
→ `MediaAsset` braucht `cad_file_id` FK — prüfen ob vorhanden
|
||||
|
||||
**Wichtig**: Prüfen ob `media_assets.cad_file_id` existiert. Falls nicht: Migration 047 notwendig.
|
||||
|
||||
- **Akzeptanzkriterium**: `POST /api/cad/{id}/generate-gltf-geometry` gibt 202 zurück, nach Task-Ausführung existiert MediaAsset mit type=gltf_geometry
|
||||
- **Abhängigkeiten**: Task 1
|
||||
|
||||
### Task 4: Migration 047 — media_assets.cad_file_id (wenn nötig)
|
||||
- **Datei**: `backend/alembic/versions/047_media_assets_cad_file_id.py`
|
||||
- **Was**: Nullable FK `cad_file_id UUID REFERENCES cad_files(id) ON DELETE SET NULL` auf `media_assets`
|
||||
- **Prüfen**: `grep -n "cad_file_id" backend/app/domains/media/models.py` — falls schon vorhanden: Task überspringen
|
||||
- **Akzeptanzkriterium**: `alembic upgrade head` erfolgreich
|
||||
- **Abhängigkeiten**: keine
|
||||
|
||||
### Task 5: ThreeDViewer.tsx — Production-Modus, Wireframe, Env-Preset, Downloads
|
||||
- **Datei**: `frontend/src/components/cad/ThreeDViewer.tsx`
|
||||
- **Was**: Props erweitern + Toolbar-Erweiterung:
|
||||
|
||||
```typescript
|
||||
interface ThreeDViewerProps {
|
||||
cadFileId: string
|
||||
onClose: () => void
|
||||
productionGltfUrl?: string // wenn vorhanden: Mode-Toggle anzeigen
|
||||
downloadUrls?: { glb?: string; blend?: string }
|
||||
}
|
||||
```
|
||||
|
||||
**Neuer State:**
|
||||
- `mode: 'geometry' | 'production'` (default: 'geometry')
|
||||
- `wireframe: boolean` (default: false)
|
||||
- `envPreset: 'city' | 'studio' | 'sunset'` (default: 'city')
|
||||
|
||||
**Toolbar** (neu, rechts vom "Capture Angle"-Button):
|
||||
- Mode-Toggle (nur wenn `productionGltfUrl` gesetzt): Button-Gruppe "Geometry | Production"
|
||||
- Wireframe-Toggle: Button
|
||||
- Env-Preset-Dropdown: `<select>` mit city/studio/sunset
|
||||
- Download-Buttons (wenn `downloadUrls` gesetzt): Download-Icon + "GLB" + optional "BLEND"
|
||||
|
||||
**Canvas-Änderungen:**
|
||||
- `Environment preset={envPreset}` (jetzt konfigurierbar, bisher hardcoded "city")
|
||||
- `WireframeToggle`-Komponente: setzt `material.wireframe = wireframe` auf allen Mesh-Children
|
||||
- Model-URL: `mode === 'production' && productionGltfUrl ? productionGltfUrl : modelUrl`
|
||||
|
||||
**GltfErrorBoundary**: Reset bei mode-Wechsel (key prop ändern)
|
||||
|
||||
- **Akzeptanzkriterium**: Mode-Toggle erscheint wenn `productionGltfUrl` vorhanden, Wireframe-Toggle schaltet um, Env-Preset ändert Beleuchtung
|
||||
- **Abhängigkeiten**: keine
|
||||
|
||||
### Task 6: CadPreview.tsx — Production-Asset-URLs übergeben
|
||||
- **Datei**: `frontend/src/pages/CadPreview.tsx`
|
||||
- **Was**: Beim Öffnen des ThreeDViewers:
|
||||
- `GET /api/media-assets?cad_file_id={id}&asset_type=gltf_geometry` (oder gltf_production falls vorhanden)
|
||||
- Download-URLs für GLB + BLEND laden
|
||||
- `<ThreeDViewer productionGltfUrl={...} downloadUrls={...} />`
|
||||
- "Generate GLB" Button (admin/PM): ruft `POST /api/cad/{id}/generate-gltf-geometry` auf + Toast + Reload
|
||||
- **Akzeptanzkriterium**: Vorhandene MediaAssets werden als Production-URLs übergeben
|
||||
- **Abhängigkeiten**: Task 3, Task 5
|
||||
|
||||
### Task 7: Media-API — assets by cad_file_id Query-Parameter
|
||||
- **Datei**: `backend/app/domains/media/router.py`
|
||||
- **Was**: `GET /api/media-assets?cad_file_id={uuid}` — Query-Param zu `list_assets` hinzufügen (optional, nullable)
|
||||
- `list_media_assets(db, cad_file_id=...)` in service.py erweitern
|
||||
- **Akzeptanzkriterium**: `GET /api/media-assets?cad_file_id=abc` gibt nur Assets dieses CadFile zurück
|
||||
- **Abhängigkeiten**: Task 4
|
||||
|
||||
### Task 8: Frontend API — media.ts + cad.ts erweitern
|
||||
- **Datei**: `frontend/src/api/media.ts`, `frontend/src/api/cad.ts`
|
||||
- **Was**:
|
||||
- PLAN.md: Phase K als ABGESCHLOSSEN markieren
|
||||
- LEARNINGS.md: Asset Library link=True Pattern, GLB-Export Blender API
|
||||
- `git commit -m "feat(K): Blender Asset Library + production exports (GLB + .blend)"`
|
||||
- `media.ts`: `listMediaAssets(params: {cad_file_id?: string, asset_type?: string}): Promise<MediaAsset[]>`
|
||||
- `cad.ts`: `generateGltfGeometry(cadFileId: string): Promise<{task_id: string}>`
|
||||
- Interface `MediaAsset` um `cad_file_id?: string` ergänzen (falls noch nicht vorhanden)
|
||||
- **Akzeptanzkriterium**: TypeScript-Kompilierung fehlerfrei
|
||||
- **Abhängigkeiten**: Task 7
|
||||
|
||||
### Task 9: Backend — Worker-Management Endpoints
|
||||
- **Datei**: `backend/app/api/routers/worker.py`
|
||||
- **Was**: Zwei neue Endpoints (require_admin):
|
||||
|
||||
**`GET /api/worker/celery-workers`**:
|
||||
```python
|
||||
from app.tasks.celery_app import celery_app
|
||||
inspect = celery_app.control.inspect()
|
||||
active = inspect.active() or {}
|
||||
stats = inspect.stats() or {}
|
||||
# Aggregiere: worker_name, hostname, active_tasks_count, queues
|
||||
```
|
||||
Response: `list[CeleryWorkerInfo]` mit Feldern: `worker_name, hostname, active_tasks, status`
|
||||
|
||||
**`POST /api/worker/scale`** (Body: `{service: "render-worker"|"worker", count: int}`):
|
||||
```python
|
||||
import subprocess, shutil
|
||||
compose_file = os.environ.get("COMPOSE_FILE", "/docker-compose.yml")
|
||||
result = subprocess.run(
|
||||
["docker", "compose", "-f", compose_file,
|
||||
"up", "--scale", f"{service}={count}", "--no-deps", "-d"],
|
||||
capture_output=True, text=True, timeout=60
|
||||
)
|
||||
```
|
||||
- Erfordert Docker-Socket-Mount (docker-compose.yml Änderung, Task 10)
|
||||
- Validierung: count zwischen 0 und 10, service in erlaubte Liste
|
||||
|
||||
- **Akzeptanzkriterium**: `GET /api/worker/celery-workers` gibt Worker-Liste zurück (leer wenn keine aktiv)
|
||||
- **Abhängigkeiten**: keine
|
||||
|
||||
### Task 10: docker-compose.yml — Docker-Socket + Compose-File-Mount
|
||||
- **Datei**: `docker-compose.yml`
|
||||
- **Was**: Im `backend`-Service:
|
||||
```yaml
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- uploads:/app/uploads
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./docker-compose.yml:/docker-compose.yml
|
||||
environment:
|
||||
- COMPOSE_FILE=/docker-compose.yml
|
||||
```
|
||||
Außerdem `docker-cli` im Backend-Dockerfile installieren:
|
||||
```dockerfile
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
... docker.io \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
```
|
||||
- **Akzeptanzkriterium**: `docker compose exec backend docker compose version` funktioniert
|
||||
- **Abhängigkeiten**: Task 9
|
||||
|
||||
### Task 11: Frontend — WorkerManagement.tsx
|
||||
- **Datei**: `frontend/src/pages/WorkerManagement.tsx` (NEU)
|
||||
- **Was**: Seite mit 3 Bereichen:
|
||||
|
||||
**Section 1 — Worker-Status** (useQuery `['celery-workers']`, refetchInterval 15s):
|
||||
- Tabelle: Worker-Name, Hostname, Aktive Tasks, Status-Dot (grün=online, grau=keine Tasks)
|
||||
- Leerer Zustand: "No active workers"
|
||||
|
||||
**Section 2 — Queue-Tiefe** (aus `GET /api/worker/activity`, bestehend):
|
||||
- Karten: `step_processing` + `thumbnail_rendering` Queue-Tiefe
|
||||
- Nutzt vorhandene WorkerActivity-Daten
|
||||
|
||||
**Section 3 — Scale-Worker** (require admin):
|
||||
- Zwei Slider/Spinner: "step-worker (worker)" 1-8, "render-worker" 1-4
|
||||
- Button "Scale" → `POST /api/worker/scale`
|
||||
- Warnung: "Scaling down kills active renders"
|
||||
- Toast bei Erfolg/Fehler
|
||||
|
||||
- **Akzeptanzkriterium**: Seite lädt, Worker-Liste zeigt laufende Worker, Scale-Button sendet Request
|
||||
- **Abhängigkeiten**: Task 9, Task 12
|
||||
|
||||
### Task 12: Frontend — worker.ts API-Client
|
||||
- **Datei**: `frontend/src/api/worker.ts` (NEU oder ergänzen)
|
||||
- **Was**:
|
||||
```typescript
|
||||
export interface CeleryWorkerInfo {
|
||||
worker_name: string
|
||||
hostname: string
|
||||
active_tasks: number
|
||||
status: 'online' | 'idle'
|
||||
}
|
||||
export async function getCeleryWorkers(): Promise<CeleryWorkerInfo[]>
|
||||
export async function scaleWorker(service: string, count: number): Promise<void>
|
||||
```
|
||||
- **Akzeptanzkriterium**: TypeScript kompiliert
|
||||
- **Abhängigkeiten**: Task 9
|
||||
|
||||
### Task 13: Frontend — Route + Sidebar-Link für WorkerManagement
|
||||
- **Datei**: `frontend/src/App.tsx`, `frontend/src/components/layout/Layout.tsx`
|
||||
- **Was**:
|
||||
- App.tsx: Route `/workers` → `<WorkerManagement />`
|
||||
- Layout.tsx: Sidebar-Link "Workers" mit `Server`-Icon (admin only)
|
||||
- **Akzeptanzkriterium**: `/workers` erreichbar, Link erscheint für Admins
|
||||
- **Abhängigkeiten**: Task 11
|
||||
|
||||
### Task 14: Dockerfile — pytest installieren
|
||||
- **Datei**: `backend/Dockerfile`
|
||||
- **Was**: `pip install --no-cache-dir -e .` → `pip install --no-cache-dir -e ".[dev]"`
|
||||
- **Akzeptanzkriterium**: `docker compose exec backend pytest --version` gibt Versionsnummer aus (nach Rebuild)
|
||||
- **Abhängigkeiten**: keine
|
||||
|
||||
### Task 15: Backend-Tests — test_rendering_service.py
|
||||
- **Datei**: `backend/tests/domains/test_rendering_service.py` (NEU)
|
||||
- **Was**: ≥5 Tests:
|
||||
1. `test_dispatch_workflow_unknown_type_raises` — ValueError bei unbekanntem Typ
|
||||
2. `test_dispatch_workflow_still_builds_chain` — `_build_still` gibt Celery-Chain zurück (ohne apply_async)
|
||||
3. `test_dispatch_workflow_still_with_exports_builds_chain` — group in chain
|
||||
4. `test_publish_asset_creates_media_asset(db, admin_user)` — async, erstellt MediaAsset
|
||||
5. `test_publish_asset_nonexistent_order_line_returns_none` — graceful None
|
||||
6. (Bonus) `test_legacy_dispatch_queues_task(monkeypatch)` — mock_celery, prüft Task wurde eingereicht
|
||||
- **Akzeptanzkriterium**: `pytest tests/domains/test_rendering_service.py` → alles grün
|
||||
- **Abhängigkeiten**: Task 14
|
||||
|
||||
### Task 16: Backend-Tests — test_orders_service.py
|
||||
- **Datei**: `backend/tests/domains/test_orders_service.py` (NEU)
|
||||
- **Was**: ≥5 Tests gegen `GET/POST /api/orders` und Orders-Service-Funktionen:
|
||||
1. `test_create_order_returns_201(client, auth_headers)` — POST /api/orders
|
||||
2. `test_list_orders_empty(client, auth_headers)` — leere Liste zurück
|
||||
3. `test_get_order_404_for_unknown_id(client, auth_headers)` — 404 bei unbekannter ID
|
||||
4. `test_order_submit_status_change(client, auth_headers)` — Submit ändert Status
|
||||
5. `test_order_requires_auth(client)` — 401 ohne Token
|
||||
- **Akzeptanzkriterium**: `pytest tests/domains/test_orders_service.py` → alles grün
|
||||
- **Abhängigkeiten**: Task 14
|
||||
|
||||
### Task 17: Frontend-Tests — WorkerActivity.test.tsx + WorkerManagement.test.tsx
|
||||
- **Datei**: `frontend/src/__tests__/pages/WorkerActivity.test.tsx` (NEU), `WorkerManagement.test.tsx` (NEU)
|
||||
- **Was**:
|
||||
- WorkerActivity: Test render + "No recent activity" leerer Zustand, Mock-API-Response
|
||||
- WorkerManagement: Test render Header "Worker Management", Scale-Button vorhanden
|
||||
- Nutzen MSW handlers aus `mocks/`
|
||||
- **Akzeptanzkriterium**: `npm run test` → 0 Failures (≥5 Tests total neu)
|
||||
- **Abhängigkeiten**: Task 11
|
||||
|
||||
---
|
||||
|
||||
## Migrations-Check
|
||||
|
||||
| Migration | Phase | Status |
|
||||
|-----------|-------|--------|
|
||||
| 041 step_file_hash | F | existiert |
|
||||
| 042 invoices | G | existiert |
|
||||
| 043 import_validations | H | existiert |
|
||||
| 044 notification_configs | I | existiert |
|
||||
| **045 asset_libraries** | **K** | **fehlt** |
|
||||
| Migration | Beschreibung | Notwendig? |
|
||||
|-----------|-------------|------------|
|
||||
| 047 | `media_assets.cad_file_id FK` | **Prüfen**: `grep cad_file_id backend/app/domains/media/models.py` — wenn fehlt → ja |
|
||||
|
||||
Vor Implementierung prüfen: `cat backend/app/domains/media/models.py | grep cad_file_id`
|
||||
|
||||
---
|
||||
|
||||
## Reihenfolge-Empfehlung
|
||||
|
||||
```
|
||||
Task 1 (Bug-Fix, sofort)
|
||||
Tasks 2-6 parallel (Backend WebSocket)
|
||||
Tasks 7-8 parallel (Frontend Hook + Context)
|
||||
Tasks 9-11 (Polling ersetzen, nach 8)
|
||||
Task 12 (Commit)
|
||||
Tasks K1-K3 parallel (Datenmodell + Backend + Blender-Katalog)
|
||||
Tasks K4-K5 parallel (Blender Scripts)
|
||||
Tasks K6-K7 (Workflow + UI, nach K1-K5)
|
||||
Task K8 (Commit)
|
||||
Parallel-Gruppe 1 (keine gegenseitigen Abhängigkeiten):
|
||||
Task 1 (neue Celery-Tasks)
|
||||
Task 4 (Migration 047 prüfen + ggf. erstellen)
|
||||
Task 5 (ThreeDViewer Props)
|
||||
Task 9 (Worker-Endpoints Backend)
|
||||
Task 14 (Dockerfile pytest)
|
||||
|
||||
Nach Gruppe 1:
|
||||
Task 2 (workflow_builder reparieren) — braucht Task 1
|
||||
Task 3 (generate-gltf-geometry Endpoint) — braucht Task 1 + 4
|
||||
Task 10 (docker-compose Mount) — braucht Task 9
|
||||
Task 12 (worker.ts API) — braucht Task 9
|
||||
|
||||
Nach Gruppe 2:
|
||||
Task 6 (CadPreview anpassen) — braucht Task 3, 5
|
||||
Task 7 (media router cad_file_id param) — braucht Task 4
|
||||
Task 8 (frontend API) — braucht Task 7
|
||||
Task 11 (WorkerManagement.tsx) — braucht Task 9, 12
|
||||
|
||||
Nach Gruppe 3:
|
||||
Task 13 (Route + Sidebar) — braucht Task 11
|
||||
Task 15 (test_rendering_service.py) — braucht Task 14
|
||||
Task 16 (test_orders_service.py) — braucht Task 14
|
||||
Task 17 (frontend tests) — braucht Task 11
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Risiken / Offene Fragen
|
||||
|
||||
- **WebSocket Auth via Query-Param**: Token in Server-Logs sichtbar. Fuer v2 akzeptabel. In v3: kurzlebigen WS-Token (TTL 30s) aus JWT generieren.
|
||||
- **Redis Pub/Sub Skalierung**: Bei vielen Tenants/Tabs kann Subscriber-Loop Bottleneck werden. Fuer v2 OK. In v3: Redis Streams.
|
||||
- **Phase K -- MinIO Bucket**: `asset-libraries` Bucket muss beim Startup erstellt werden (lifespan in main.py).
|
||||
- **Phase K -- link=True** bedeutet .blend muss vor Render via MinIO heruntergeladen werden (in tmpdir). Bereits einkalkuliert in K3.
|
||||
- **Bestehende material_libraries**: Die alte `material_libraries` Tabelle/Feature bleibt parallel bestehen -- kein Breaking Change. Asset Libraries sind additiv.
|
||||
1. **media_assets.cad_file_id**: Muss vor Implementierung geprüft werden. Wenn schon vorhanden → Migration 047 entfällt.
|
||||
|
||||
2. **trimesh auf render-worker**: `trimesh` ist in `pyproject.toml` als optionale `cad`-Dependency gelistet (`trimesh>=4.2.0`). Der worker-Container muss sie installiert haben. Im render-worker Dockerfile prüfen: `pip install trimesh`.
|
||||
|
||||
3. **docker compose in Backend-Container**: Das scale-Feature setzt voraus, dass `docker.io` + compose-Plugin im Backend-Image installiert sind. Build-Zeit steigt ~30MB. Alternativ: Nur die Celery-Worker-Ansicht implementieren, Scale als Hinweis-Text mit dem CLI-Befehl.
|
||||
|
||||
4. **render_order_line_still_task vs. legacy render_order_line_task**: Beide tun ähnliches. Langfristig sollte `step_tasks.render_order_line_task` durch den neuen Task ersetzt werden. Für jetzt: Neuer Task läuft parallel, Legacy bleibt erhalten (backward-compat).
|
||||
|
||||
5. **Celery inspect Timeout**: `celery_app.control.inspect(timeout=2)` kann hängen wenn kein Worker läuft. Timeout setzen + leere Liste zurückgeben.
|
||||
|
||||
Reference in New Issue
Block a user