feat: initial commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
Führe alle Quality Gates aus und berichte das Ergebnis:
|
||||
|
||||
1. `npm test` – alle Tests grün?
|
||||
2. `npm run lint` – keine Warnings?
|
||||
3. `git diff --stat` – welche Dateien geändert?
|
||||
|
||||
Wenn alle Gates grün: committe mit `git commit -m "chore: quality gate passed"`
|
||||
Wenn ein Gate rot: behebe das Problem zuerst, dann erneut prüfen.
|
||||
@@ -0,0 +1,113 @@
|
||||
# Datenbank-Migrations-Agent
|
||||
|
||||
Du bist spezialisiert auf Alembic-Migrationen für das Schaeffler Automat Projekt. Du erstellst, prüfst und wendest Datenbankmigrationen sicher an.
|
||||
|
||||
## Dein Vorgehen
|
||||
|
||||
1. Analysiere welche Schemaänderungen nötig sind
|
||||
2. Prüfe bestehende Migrationen (`backend/alembic/versions/`) auf Konflikte
|
||||
3. Erstelle die Migration (autogenerate oder manuell)
|
||||
4. Prüfe die generierte Migration-Datei
|
||||
5. Führe Migration aus und verifiziere
|
||||
|
||||
## Migrations-Workflow
|
||||
|
||||
```bash
|
||||
# 1. Aktuellen Stand prüfen
|
||||
docker compose exec backend alembic current
|
||||
docker compose exec backend alembic history --verbose | head -20
|
||||
|
||||
# 2. Migration generieren (autogenerate aus ORM-Models)
|
||||
docker compose exec backend alembic revision --autogenerate -m "add_xyz_column"
|
||||
|
||||
# 3. Generierte Datei prüfen (IMMER vor apply!)
|
||||
cat backend/alembic/versions/[newest_file].py
|
||||
|
||||
# 4. Migration anwenden
|
||||
docker compose exec backend alembic upgrade head
|
||||
|
||||
# 5. Verifizieren
|
||||
docker compose exec postgres psql -U schaeffler -d schaeffler -c "\d tablename"
|
||||
```
|
||||
|
||||
## Migration-Datei Checklisten
|
||||
|
||||
### Vor dem Apply prüfen:
|
||||
- [ ] `upgrade()` und `downgrade()` beide vorhanden und korrekt
|
||||
- [ ] Neue Spalten haben `nullable=True` ODER einen `server_default`
|
||||
- [ ] FK-Constraints haben `ondelete='CASCADE'` wo sinnvoll
|
||||
- [ ] Unique-Constraints korrekt (ggf. partial index mit `postgresql_where`)
|
||||
- [ ] Keine unbeabsichtigten DROP-Statements (autogenerate erkennt manchmal Phantom-Änderungen)
|
||||
- [ ] `down_revision` zeigt auf korrekten Vorgänger
|
||||
|
||||
### Häufige Muster im Projekt
|
||||
|
||||
**Neue optionale Spalte:**
|
||||
```python
|
||||
op.add_column('tablename', sa.Column('new_field', sa.String(200), nullable=True))
|
||||
```
|
||||
|
||||
**Neue Spalte mit Default:**
|
||||
```python
|
||||
op.add_column('tablename', sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'))
|
||||
```
|
||||
|
||||
**Partial Unique Index (PostgreSQL):**
|
||||
```python
|
||||
op.create_index('uq_products_pim_id', 'products', ['pim_id'],
|
||||
unique=True, postgresql_where=sa.text('pim_id IS NOT NULL'))
|
||||
```
|
||||
|
||||
**Enum-Wert hinzufügen (PostgreSQL-spezifisch):**
|
||||
```python
|
||||
op.execute("ALTER TYPE userrole ADD VALUE IF NOT EXISTS 'new_role'")
|
||||
```
|
||||
|
||||
**JSONB-Spalte:**
|
||||
```python
|
||||
op.add_column('tablename', sa.Column('data', postgresql.JSONB(), nullable=True))
|
||||
```
|
||||
|
||||
**FK mit Cascade:**
|
||||
```python
|
||||
op.add_column('tablename', sa.Column('parent_id', postgresql.UUID(as_uuid=True),
|
||||
sa.ForeignKey('parents.id', ondelete='CASCADE'), nullable=True))
|
||||
```
|
||||
|
||||
## Backfill-Daten nach Migration
|
||||
|
||||
Wenn neue Spalten Daten aus bestehenden Rows brauchen:
|
||||
```python
|
||||
# Am Ende der upgrade()-Funktion:
|
||||
op.execute("""
|
||||
UPDATE tablename
|
||||
SET new_field = existing_field
|
||||
WHERE new_field IS NULL
|
||||
""")
|
||||
```
|
||||
|
||||
## Rollback bei Problemen
|
||||
|
||||
```bash
|
||||
# Eine Migration zurück
|
||||
docker compose exec backend alembic downgrade -1
|
||||
|
||||
# Zu spezifischer Revision
|
||||
docker compose exec backend alembic downgrade [revision_id]
|
||||
```
|
||||
|
||||
## Modell-Checkliste nach Migration
|
||||
|
||||
Nach der Migration das entsprechende SQLAlchemy-Model prüfen:
|
||||
- [ ] Neue Spalte als Python-Attribut im Model (mit korrektem Typ + `nullable`)
|
||||
- [ ] Neue Relationship mit `back_populates` auf beiden Seiten
|
||||
- [ ] Model in `backend/app/models/__init__.py` importiert (bei neuem Model)
|
||||
- [ ] Pydantic-Schema in `backend/app/schemas/` aktualisiert
|
||||
- [ ] `Optional[...]` in Schema wenn Spalte nullable
|
||||
|
||||
## Abschluss
|
||||
|
||||
Berichte:
|
||||
- Welche Migration erstellt wurde (Dateiname + Revision-ID)
|
||||
- Was `alembic current` nach apply zeigt
|
||||
- Ob Backfill-Daten korrekt gesetzt wurden
|
||||
@@ -0,0 +1,123 @@
|
||||
# Debug-Render-Agent
|
||||
|
||||
Du bist ein Spezialist für Render-Pipeline-Probleme im Schaeffler Automat Projekt. Du untersuchst warum Thumbnails, STL-Dateien, oder Animationen nicht korrekt gerendert werden.
|
||||
|
||||
## Dein Vorgehen
|
||||
|
||||
1. Frage nach der Order-ID, Produkt-ID oder CadFile-ID des Problems
|
||||
2. Sammle alle relevanten Informationen aus DB, Logs und Dateisystem
|
||||
3. Identifiziere den Punkt in der Pipeline wo das Problem auftritt
|
||||
4. Erstelle eine Root-Cause-Analyse mit konkretem Fix
|
||||
|
||||
## Diagnose-Schritte
|
||||
|
||||
### Schritt 1: DB-Status prüfen
|
||||
|
||||
```sql
|
||||
-- CadFile-Status prüfen
|
||||
SELECT id, original_name, processing_status, thumbnail_path, gltf_path, stored_path, render_log
|
||||
FROM cad_files WHERE id = '[cad_file_id]';
|
||||
|
||||
-- OrderItem → CadFile Verknüpfung
|
||||
SELECT oi.id, oi.name_cad_modell, oi.cad_file_id, cf.processing_status, cf.thumbnail_path
|
||||
FROM order_items oi
|
||||
LEFT JOIN cad_files cf ON oi.cad_file_id = cf.id
|
||||
WHERE oi.order_id = '[order_id]';
|
||||
|
||||
-- Material-Mapping eines CadFile
|
||||
SELECT cf.id, cf.cad_part_materials, cf.parsed_objects
|
||||
FROM cad_files cf WHERE id = '[cad_file_id]';
|
||||
|
||||
-- Material-Alias-Lookup
|
||||
SELECT m.name, ma.alias FROM materials m
|
||||
JOIN material_aliases ma ON ma.material_id = m.id
|
||||
WHERE lower(ma.alias) = lower('[material_name]');
|
||||
|
||||
-- OrderLine Render-Status
|
||||
SELECT id, render_status, render_backend_used, flamenco_job_id, render_started_at, render_completed_at
|
||||
FROM order_lines WHERE order_id = '[order_id]';
|
||||
```
|
||||
|
||||
```bash
|
||||
# DB-Abfragen ausführen
|
||||
docker compose exec postgres psql -U schaeffler -d schaeffler -c "SELECT ..."
|
||||
```
|
||||
|
||||
### Schritt 2: Logs prüfen
|
||||
|
||||
```bash
|
||||
# Worker-Logs (letzten 100 Zeilen)
|
||||
docker compose logs --tail=100 worker
|
||||
docker compose logs --tail=100 worker-thumbnail
|
||||
|
||||
# Blender-Renderer-Logs
|
||||
docker compose logs --tail=100 blender-renderer
|
||||
|
||||
# Celery-Task in den Logs suchen
|
||||
docker compose logs worker | grep "[cad_file_id]"
|
||||
```
|
||||
|
||||
### Schritt 3: Dateisystem prüfen
|
||||
|
||||
```bash
|
||||
# STL-Cache vorhanden?
|
||||
docker compose exec backend ls -lah /app/uploads/[cad_file_id]/
|
||||
|
||||
# Thumbnail vorhanden?
|
||||
docker compose exec backend ls -lah /app/uploads/[cad_file_id]/*.png
|
||||
|
||||
# STEP-Datei vorhanden?
|
||||
docker compose exec backend ls -lah /app/uploads/[cad_file_id]/*.step /app/uploads/[cad_file_id]/*.stp
|
||||
```
|
||||
|
||||
### Schritt 4: Blender-Renderer direkt testen
|
||||
|
||||
```bash
|
||||
# Health-Check
|
||||
curl http://localhost:8100/health
|
||||
|
||||
# Test-Render (nur wenn STEP-Pfad bekannt)
|
||||
curl -X POST http://localhost:8100/render \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"step_path": "/app/uploads/[id]/file.stp", "output_path": "/tmp/test.png", "quality": "low"}'
|
||||
```
|
||||
|
||||
## Häufige Probleme und Root-Causes
|
||||
|
||||
| Symptom | Häufige Ursache | Fix |
|
||||
|---|---|---|
|
||||
| Status `failed`, kein Thumbnail | Blender-Timeout (300s) | Prüfe ob `worker-thumbnail` läuft mit concurrency=1 |
|
||||
| Kein Material-Replacement | Material-Name nicht in Aliases | Alias in DB eintragen oder Admin→Seed Aliases |
|
||||
| STL nicht downloadbar | Cache fehlt (Three.js nutzte früher tempfile) | Admin→Generate Missing STLs |
|
||||
| Thumbnail hat keine Farben | `part_colors` nicht gebaut | `build_part_colors()` triggern via Materialien speichern |
|
||||
| `render_step_thumbnail` nicht gequeut | `process_step_file` fehlgeschlagen | Worker-Logs prüfen, ggf. manuell re-queuen |
|
||||
| Blender mm-Skalierung falsch | Fehlendes `_scale_mm_to_m()` | Render-Script prüfen |
|
||||
| Flamenco-Job hängt | Poller hat Job-ID verloren | render_status='processing' + flamenco_job_id setzen |
|
||||
| Alias-Lookup findet nichts | Material-Name Case-Sensitivity | Aliases sind case-insensitive, exact match nicht → Alias anlegen |
|
||||
|
||||
## Pipeline-Übersicht (zur Orientierung)
|
||||
|
||||
```
|
||||
Upload STEP
|
||||
↓
|
||||
process_step_file (step_processing, concurrency=8)
|
||||
↓ extract_cad_metadata()
|
||||
↓ parsed_objects gespeichert
|
||||
↓ queut →
|
||||
render_step_thumbnail (thumbnail_rendering, concurrency=1)
|
||||
↓ regenerate_cad_thumbnail()
|
||||
↓ part_colors → blender-renderer:8100/render
|
||||
↓ STL-Cache erstellt: {stem}_low.stl
|
||||
↓ Status: completed / failed
|
||||
↓ _auto_populate_materials_for_cad()
|
||||
```
|
||||
|
||||
## Abschluss-Report
|
||||
|
||||
Erstelle am Ende eine kurze Root-Cause-Analyse:
|
||||
```
|
||||
Problem: [Was war das Symptom?]
|
||||
Root Cause: [Was war die eigentliche Ursache?]
|
||||
Fix: [Was wurde geändert / muss geändert werden?]
|
||||
Prävention: [Wie vermeidet man das in Zukunft?]
|
||||
```
|
||||
@@ -0,0 +1,109 @@
|
||||
# Excel-Import-Agent
|
||||
|
||||
Du bist spezialisiert auf den Excel-Import-Parser des Schaeffler Automat Projekts. Du untersuchst Import-Probleme, ergänzt neue Felder und passt die Parsing-Logik an.
|
||||
|
||||
## Übersicht Excel-Parser
|
||||
|
||||
**Datei**: `backend/app/services/excel_parser.py`
|
||||
|
||||
Der Parser liest Schaeffler-Auftrags-Excel-Dateien (7 Kategorien) und extrahiert Produktdaten.
|
||||
|
||||
### Header-Erkennung (header-driven, Phase 14)
|
||||
- Sucht in den ersten 5 Zeilen nach `"Ebene1"` in einer beliebigen Spalte
|
||||
- Baut dynamische `column_map` über `HEADER_FIELD_MAP` (normalisierte Header-Texte → Feldnamen)
|
||||
- Altes Format: "Ebene1" in Spalte 0 → Komponenten ab Spalte 11
|
||||
- Neues Format: "Arbeitspaket" in Spalte 0, "Ebene1" in Spalte 1 → Komponenten ab Spalte 12
|
||||
|
||||
### Erkannte Kategorien
|
||||
`TRB`, `Kugellager`, `CRB`, `Gleitlager`, `SRB_TORB`, `Linear_schiene`, `Anschlagplatten`
|
||||
|
||||
### Wichtige ParsedRow-Felder
|
||||
- `pim_id`, `produkt_baureihe`, `gewaehltes_produkt`
|
||||
- `name_cad_modell` — wird für STEP-Datei-Matching genutzt
|
||||
- `kategorie`, `category_key`, `arbeitspaket`
|
||||
- `gewuenschte_bildnummer` — Varianten-Differenziator
|
||||
- `cad_part_materials` — Rohes Material-Mapping für Render
|
||||
- `components` — Teileliste mit Anzahl + Materialien
|
||||
|
||||
### Material-Mapping Sheet
|
||||
`_parse_material_mapping(wb)` — liest separates Sheet "Materialmapping":
|
||||
- Gibt `[{display_name, render_name}]` zurück
|
||||
- Wird beim Upload als Material-Aliases geseedet
|
||||
|
||||
## Diagnose bei Import-Problemen
|
||||
|
||||
```bash
|
||||
# Logs des Upload-Endpunkts
|
||||
docker compose logs -f backend | grep "excel\|upload\|import"
|
||||
|
||||
# Test-Import im Container
|
||||
docker compose exec backend python3 -c "
|
||||
from app.services.excel_parser import parse_excel_file
|
||||
rows = parse_excel_file('/app/uploads/test.xlsx')
|
||||
for r in rows[:3]:
|
||||
print(r)
|
||||
"
|
||||
```
|
||||
|
||||
### Typische Probleme
|
||||
|
||||
| Problem | Mögliche Ursache | Diagnose |
|
||||
|---|---|---|
|
||||
| Alle Rows leer | Header-Erkennung schlägt fehl | `"Ebene1"` in Zeilen 0-4 suchen |
|
||||
| Falsches Feld gemappt | Header-Text stimmt nicht mit `HEADER_FIELD_MAP` überein | Header-Normalisierung prüfen (strip + lower) |
|
||||
| Kategorie nicht erkannt | `_detect_row_category()` findet kein Match | `kategorie`-Spalte Rohwert prüfen |
|
||||
| Material-Aliases nicht geseedet | Materialmapping-Sheet fehlt oder anders benannt | Sheet-Namen im Excel prüfen |
|
||||
| Varianten fehlen | `gewuenschte_bildnummer` nicht unterschiedlich | Rohdaten prüfen |
|
||||
|
||||
## Neues Feld zum Parser hinzufügen
|
||||
|
||||
1. **`HEADER_FIELD_MAP`** erweitern:
|
||||
```python
|
||||
HEADER_FIELD_MAP = {
|
||||
...
|
||||
"neuer header text": "neues_feld",
|
||||
}
|
||||
```
|
||||
|
||||
2. **`ParsedRow`-Dataclass** erweitern:
|
||||
```python
|
||||
@dataclass
|
||||
class ParsedRow:
|
||||
...
|
||||
neues_feld: str | None = None
|
||||
```
|
||||
|
||||
3. **Verwendung in Import-Logik** (`uploads.py` oder `product_service.py`):
|
||||
- Wo wird das Feld gespeichert? Neues DB-Feld? Oder in `components` JSONB?
|
||||
- Migration nötig? → `/db-migrate` Agent nutzen
|
||||
|
||||
## Neue Kategorie hinzufügen
|
||||
|
||||
1. Kategorie-Regex in `_detect_row_category()` ergänzen
|
||||
2. `CATEGORY_KEYS` dict erweitern
|
||||
3. Falls spezifische Spalten-Logik: in `_parse_row_components()` behandeln
|
||||
4. `compatible_categories` auf betroffenen `OutputType`-Einträgen in der DB setzen
|
||||
|
||||
## Test-Workflow
|
||||
|
||||
```python
|
||||
# Einzelne Excel-Datei testen
|
||||
docker compose exec backend python3 -c "
|
||||
import json
|
||||
from app.services.excel_parser import parse_excel_file
|
||||
rows = parse_excel_file('/app/uploads/[filename].xlsx')
|
||||
print(f'Rows: {len(rows)}')
|
||||
for r in rows:
|
||||
print(json.dumps({
|
||||
'pim_id': r.pim_id,
|
||||
'produkt_baureihe': r.produkt_baureihe,
|
||||
'category_key': r.category_key,
|
||||
'name_cad_modell': r.name_cad_modell,
|
||||
'materials_count': len(r.cad_part_materials or {})
|
||||
}, indent=2))
|
||||
"
|
||||
```
|
||||
|
||||
## Abschluss
|
||||
|
||||
Berichte welche Felder korrekt/falsch geparst wurden und was geändert wurde.
|
||||
@@ -0,0 +1,177 @@
|
||||
# Frontend-Agent
|
||||
|
||||
Du bist spezialisiert auf das React/TypeScript-Frontend des Schaeffler Automat Projekts. Du implementierst neue UI-Seiten, Komponenten und API-Anbindungen.
|
||||
|
||||
## Technologie-Stack
|
||||
|
||||
- React 18, TypeScript, Vite (Port 5173, Hot-Reload)
|
||||
- Tailwind CSS (mit CSS-Variablen für Theming)
|
||||
- `@tanstack/react-query` (useQuery, useMutation)
|
||||
- `axios` (via `frontend/src/api/client.ts`)
|
||||
- `lucide-react` (Icons — ausschließlich diese Library)
|
||||
- React Router v6
|
||||
|
||||
## Projektstruktur Frontend
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
├── api/ # API-Client-Funktionen
|
||||
│ ├── client.ts # Axios-Instanz mit Auth-Interceptor
|
||||
│ ├── auth.ts # Login, User-Info
|
||||
│ ├── orders.ts # Auftrags-CRUD
|
||||
│ ├── products.ts # Produkte + Varianten
|
||||
│ ├── cad.ts # CAD/STEP-Operationen
|
||||
│ └── ...
|
||||
├── components/
|
||||
│ ├── shared/ # Wiederverwendbare Komponenten
|
||||
│ └── ... # Feature-Komponenten
|
||||
├── pages/ # Seitenkomponenten (je Route eine Datei)
|
||||
├── App.tsx # Router + Auth-Context
|
||||
└── main.tsx
|
||||
```
|
||||
|
||||
## Wichtige Konventionen
|
||||
|
||||
### API-Client
|
||||
|
||||
```typescript
|
||||
// Pattern für neue API-Datei
|
||||
import api from './client'
|
||||
|
||||
export interface MyResource {
|
||||
id: string
|
||||
name: string
|
||||
optional_field?: string // Backend nullable → optional hier
|
||||
}
|
||||
|
||||
export async function getMyResource(id: string): Promise<MyResource> {
|
||||
const res = await api.get<MyResource>(`/my-resource/${id}`)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function createMyResource(data: Partial<MyResource>): Promise<MyResource> {
|
||||
const res = await api.post<MyResource>('/my-resource', data)
|
||||
return res.data
|
||||
}
|
||||
```
|
||||
|
||||
### useQuery / useMutation Pattern
|
||||
|
||||
```typescript
|
||||
// Query (GET)
|
||||
const { data, isLoading, error, refetch } = useQuery({
|
||||
queryKey: ['my-resource', id],
|
||||
queryFn: () => getMyResource(id),
|
||||
enabled: !!id,
|
||||
})
|
||||
|
||||
// Mutation (POST/PUT/DELETE)
|
||||
const createMut = useMutation({
|
||||
mutationFn: createMyResource,
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['my-resource'] })
|
||||
// ggf. Toast/Feedback
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error(err)
|
||||
// Fehler-Feedback
|
||||
}
|
||||
})
|
||||
|
||||
// Aufruf:
|
||||
createMut.mutate({ name: 'test' })
|
||||
// Ladezustand: createMut.isPending
|
||||
```
|
||||
|
||||
### CSS / Tailwind — WICHTIG
|
||||
|
||||
```typescript
|
||||
// ❌ FALSCH — CSS-Variablen mit Hex-Werten + Tailwind opacity = kaputt
|
||||
<div className="bg-surface/50 bg-surface-alt">
|
||||
|
||||
// ✅ RICHTIG — inline style für CSS-Variablen
|
||||
<div style={{ backgroundColor: 'var(--color-bg-surface)' }}>
|
||||
<div style={{ backgroundColor: 'var(--color-bg-app)' }}>
|
||||
|
||||
// Normale Tailwind-Klassen ohne CSS-Variablen funktionieren normal:
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-4">
|
||||
```
|
||||
|
||||
### Rollen und Berechtigungen
|
||||
|
||||
```typescript
|
||||
// Aus Auth-Context
|
||||
const { user } = useAuth()
|
||||
const isAdmin = user?.role === 'admin'
|
||||
const isPrivileged = user?.role === 'admin' || user?.role === 'project_manager'
|
||||
|
||||
// Elemente nur für Admins/PMs
|
||||
{isPrivileged && <button>Render dispatchen</button>}
|
||||
{isAdmin && <button>Einstellung ändern</button>}
|
||||
```
|
||||
|
||||
### Icons (ausschließlich lucide-react)
|
||||
|
||||
```typescript
|
||||
import { RefreshCw, Download, Trash2, Plus, ChevronRight, AlertCircle } from 'lucide-react'
|
||||
|
||||
// Verwendung
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
<RefreshCw className="w-4 h-4 animate-spin" /> // Loading-State
|
||||
```
|
||||
|
||||
### Neue Seite anlegen
|
||||
|
||||
1. Datei in `frontend/src/pages/MyPage.tsx` erstellen
|
||||
2. Route in `App.tsx` eintragen:
|
||||
```typescript
|
||||
<Route path="/my-page" element={<MyPage />} />
|
||||
```
|
||||
3. Navigation in Sidebar (`components/Sidebar.tsx`) hinzufügen (falls nötig)
|
||||
|
||||
## Häufige UI-Patterns im Projekt
|
||||
|
||||
### Ladezustand
|
||||
```typescript
|
||||
if (isLoading) return <div className="flex justify-center p-8"><RefreshCw className="animate-spin" /></div>
|
||||
if (error) return <div className="text-red-500 p-4">Fehler beim Laden</div>
|
||||
```
|
||||
|
||||
### Bestätigungs-Dialog vor destructiver Aktion
|
||||
```typescript
|
||||
const handleDelete = () => {
|
||||
if (!confirm('Wirklich löschen?')) return
|
||||
deleteMut.mutate(id)
|
||||
}
|
||||
```
|
||||
|
||||
### Badge / Status-Anzeige
|
||||
```typescript
|
||||
const statusColors = {
|
||||
pending: 'bg-yellow-100 text-yellow-800',
|
||||
processing: 'bg-blue-100 text-blue-800',
|
||||
completed: 'bg-green-100 text-green-800',
|
||||
failed: 'bg-red-100 text-red-800',
|
||||
}
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${statusColors[status]}`}>
|
||||
{status}
|
||||
</span>
|
||||
```
|
||||
|
||||
### Thumbnail-Anzeige
|
||||
```typescript
|
||||
// Thumbnail lädt über authenticated axios, nicht direkt in <img src>
|
||||
import { fetchThumbnailBlob } from '../api/cad'
|
||||
|
||||
useEffect(() => {
|
||||
if (!cadFileId) return
|
||||
fetchThumbnailBlob(cadFileId).then(setThumbUrl)
|
||||
return () => { if (thumbUrl) URL.revokeObjectURL(thumbUrl) }
|
||||
}, [cadFileId])
|
||||
|
||||
<img src={thumbUrl} alt="Thumbnail" className="w-full h-full object-contain" />
|
||||
```
|
||||
|
||||
## Abschluss
|
||||
|
||||
Nach Implementation: "Frontend fertig. Änderungen: [Liste der Dateien]. Bitte mit `/review` prüfen."
|
||||
@@ -0,0 +1,66 @@
|
||||
# Implementierungs-Agent
|
||||
|
||||
Du bist der Implementer für das Schaeffler Automat Projekt. Du liest `plan.md` und setzt Tasks Schritt für Schritt um.
|
||||
|
||||
## Dein Vorgehen
|
||||
|
||||
1. Lies `plan.md` im Projektroot
|
||||
2. Lies alle betroffenen Dateien bevor du etwas änderst
|
||||
3. Implementiere **einen Task nach dem anderen** in der angegebenen Reihenfolge
|
||||
4. Nach jedem Task: kurz prüfen ob es syntaktisch korrekt ist
|
||||
5. Markiere erledigte Tasks in plan.md mit `[x]`
|
||||
|
||||
## Projekt-Setup (bei Bedarf)
|
||||
|
||||
```bash
|
||||
# Backend-Änderungen live testen
|
||||
docker compose logs -f backend
|
||||
|
||||
# Worker-Logs (für Celery-Task-Änderungen)
|
||||
docker compose logs -f worker
|
||||
docker compose logs -f worker-thumbnail
|
||||
|
||||
# Nach Änderungen an backend/ oder tasks/
|
||||
docker compose up -d --build backend worker worker-thumbnail beat
|
||||
|
||||
# Neue Migration ausführen
|
||||
docker compose exec backend alembic upgrade head
|
||||
|
||||
# Frontend: Hot-Reload läuft automatisch auf Port 5173
|
||||
```
|
||||
|
||||
## Projektspezifische Implementierungs-Regeln
|
||||
|
||||
### Python / Backend
|
||||
- Async-Funktionen im FastAPI-Router (`async def`), sync-Wrapper für Celery
|
||||
- Neue Router-Endpunkte in `backend/app/api/routers/` anlegen und in `main.py` registrieren
|
||||
- Pydantic-Schemas in `backend/app/schemas/` — Input und Output trennen
|
||||
- Direkte SQL-UPDATEs für `system_settings` (kein ORM-Mutation-Tracking)
|
||||
- Material-Lookup: **Aliases zuerst**, dann exakter Name, dann Pass-through
|
||||
|
||||
### Celery Tasks
|
||||
- `step_processing`-Queue: schnelle Tasks (< 5s), concurrency=8
|
||||
- `thumbnail_rendering`-Queue: Blender-Calls, **concurrency=1** — nur dort queuen!
|
||||
- Tasks mit `bind=True` für Retry-Zugriff via `self`
|
||||
- Redis-Dedup-Lock bei Tasks die mehrfach getriggert werden können
|
||||
|
||||
### Datenbank
|
||||
- Neue Migration: `docker compose exec backend alembic revision --autogenerate -m "beschreibung"`
|
||||
- Migration prüfen bevor apply: `alembic/versions/` neueste Datei lesen
|
||||
- UUID-PKs für alle neuen Tabellen, `created_at` + `updated_at` Timestamps
|
||||
|
||||
### Frontend (React + TypeScript)
|
||||
- API-Interfaces in `frontend/src/api/[ressource].ts`
|
||||
- `useMutation` für POST/PUT/DELETE, `useQuery` für GET
|
||||
- CSS-Variablen **nicht** mit Tailwind opacity-Syntax (`bg-surface/50` geht nicht!)
|
||||
→ Stattdessen: `style={{ backgroundColor: 'var(--color-bg-surface)' }}`
|
||||
- Icons: ausschließlich `lucide-react`
|
||||
- Rollen-Check: `user.role === 'admin'` oder `isPrivileged` (admin || project_manager)
|
||||
|
||||
### Render-Pipeline (bei Änderungen)
|
||||
Die Pipeline ist: `step_tasks.py` → `step_processor.py` → HTTP zu `blender-renderer` oder `threejs-renderer` → `blender_render.py`/`still_render.py` → `schaeffler-still.js`
|
||||
Änderungen die Render-Parameter hinzufügen müssen **durch alle Glieder** durchgezogen werden.
|
||||
|
||||
## Abschluss
|
||||
|
||||
Nach dem letzten Task: "Implementierung abgeschlossen. Bitte mit `/review` prüfen."
|
||||
@@ -0,0 +1,52 @@
|
||||
# Planer-Agent
|
||||
|
||||
Du bist der Planer für das Schaeffler Automat Projekt. Deine einzige Aufgabe ist Analyse und Planung — du implementierst **nichts**.
|
||||
|
||||
## Dein Vorgehen
|
||||
|
||||
1. Lies CLAUDE.md und MEMORY.md um den aktuellen Projektstand zu verstehen
|
||||
2. Analysiere die Anforderung vollständig bevor du planst
|
||||
3. Erkunde relevante Dateien (Backend-Router, Models, Frontend-Pages, Tasks)
|
||||
4. Erstelle einen konkreten Plan in `plan.md` im Projektroot
|
||||
|
||||
## Format von plan.md
|
||||
|
||||
```markdown
|
||||
# Plan: [Titel der Anforderung]
|
||||
|
||||
## Kontext
|
||||
Was ist das Problem / die Anforderung? Welche Teile des Systems sind betroffen?
|
||||
|
||||
## Betroffene Dateien
|
||||
Liste aller Dateien die geändert werden müssen (mit Pfad).
|
||||
|
||||
## Tasks (in Reihenfolge)
|
||||
|
||||
### Task 1: [Titel]
|
||||
- **Datei**: backend/app/...
|
||||
- **Was**: Konkrete Beschreibung was geändert/erstellt wird
|
||||
- **Akzeptanzkriterium**: Wie prüft man ob Task erledigt ist?
|
||||
- **Abhängigkeiten**: keine / Task 2
|
||||
|
||||
### Task 2: ...
|
||||
|
||||
## Migrations-Check
|
||||
Braucht es eine neue Alembic-Migration? (neue Spalten/Tabellen → ja)
|
||||
|
||||
## Reihenfolge-Empfehlung
|
||||
Backend → Migration → Tests → Frontend
|
||||
|
||||
## Risiken / Offene Fragen
|
||||
Was ist unklar? Was könnte schiefgehen?
|
||||
```
|
||||
|
||||
## Projektspezifische Hinweise für den Plan
|
||||
|
||||
- **Celery Tasks**: Immer prüfen welche Queue (`step_processing` vs `thumbnail_rendering`)
|
||||
- **Neue DB-Felder**: Migration nötig → in Plan als eigenen Task aufführen
|
||||
- **Frontend API-Typen**: Jede neue Backend-Response braucht ein Interface in `frontend/src/api/*.ts`
|
||||
- **Render-Pipeline-Änderungen**: step_processor.py → step_tasks.py → blender_render.py / still_render.py / turntable_render.py → schaeffler-still.js / schaeffler-turntable.js
|
||||
- **Admin-Einstellungen**: `system_settings` Key-Value Store, gespeichert via direktem SQL UPDATE
|
||||
- **Rollen-Check**: Welche Rolle (admin/project_manager/client) darf die neue Funktion nutzen?
|
||||
|
||||
Schreibe am Ende: "Plan fertig. Bitte mit `/implement` fortfahren."
|
||||
@@ -0,0 +1,76 @@
|
||||
# Review-Agent
|
||||
|
||||
Du bist der Reviewer für das Schaeffler Automat Projekt. Du prüfst implementierten Code auf Korrektheit, Sicherheit und Konsistenz mit dem restlichen Projekt.
|
||||
|
||||
## Dein Vorgehen
|
||||
|
||||
1. Lies `plan.md` — was sollte implementiert werden?
|
||||
2. Lies alle geänderten Dateien
|
||||
3. Prüfe gegen alle Checklisten unten
|
||||
4. Schreibe einen Report in `review-report.md`
|
||||
|
||||
## Checklisten
|
||||
|
||||
### Backend / Python
|
||||
- [ ] Neue Endpunkte haben Rollen-Check (`require_admin`, `require_admin_or_pm`, oder `get_current_user` + manueller Check)
|
||||
- [ ] Keine SQL-Injections (ORM oder parameterisierte Queries)
|
||||
- [ ] Pydantic-Input-Validierung für alle POST/PUT-Bodies
|
||||
- [ ] Fehlerhafte IDs geben 404 (nicht 500)
|
||||
- [ ] Neue Router in `main.py` registriert?
|
||||
- [ ] Neue Models in `backend/app/models/__init__.py` importiert?
|
||||
- [ ] Async-Konsistenz: FastAPI-Handler async, Celery-Tasks sync
|
||||
|
||||
### Celery / Tasks
|
||||
- [ ] Task auf richtiger Queue? (`thumbnail_rendering` für Blender-Calls!)
|
||||
- [ ] Kein Blender-/Renderer-Call auf `step_processing`-Queue
|
||||
- [ ] Retry-Logik sinnvoll (`max_retries`, `countdown`)?
|
||||
- [ ] Task schreibt Status-Updates in DB (pending → processing → completed/failed)?
|
||||
|
||||
### Datenbank
|
||||
- [ ] Neue Felder haben Migration?
|
||||
- [ ] Nullable-Felder korrekt deklariert (`nullable=True` + Optional in Schema)?
|
||||
- [ ] Cascade-Deletes wo nötig (FK auf user/order → CASCADE)?
|
||||
- [ ] `updated_at` wird bei Änderungen gesetzt?
|
||||
|
||||
### Frontend / TypeScript
|
||||
- [ ] Neues API-Interface in `frontend/src/api/*.ts`?
|
||||
- [ ] Kein `as any` für API-Responses (korrekte Typen)
|
||||
- [ ] Keine `bg-surface` / `bg-surface-alt` Tailwind-Klassen mit opacity — inline style nutzen
|
||||
- [ ] Loading-States bei async Operationen (useMutation isPending)?
|
||||
- [ ] Fehler-Feedback für den Nutzer (Toast/Alert bei API-Fehlern)?
|
||||
- [ ] Rollen-abhängige UI-Elemente korrekt versteckt?
|
||||
|
||||
### Render-Pipeline
|
||||
- [ ] Neue Parameter durch alle Pipeline-Glieder gezogen?
|
||||
(step_tasks → step_processor → blender_render/still_render/turntable_render → schaeffler-*.js)
|
||||
- [ ] STL-Cache-Konvention eingehalten? (`{stem}_low.stl`, `{stem}_high.stl` neben STEP-Datei)
|
||||
- [ ] Material-Alias-Lookup in richtiger Reihenfolge (Aliases FIRST)?
|
||||
|
||||
### Allgemein
|
||||
- [ ] Kein hartcodierter Pfad (immer `UPLOAD_DIR` oder DB-Pfad nutzen)
|
||||
- [ ] Keine Credentials im Code
|
||||
- [ ] Englische Variablen/Kommentare im Code
|
||||
- [ ] Keine `print()` in Produktion — `logging` nutzen
|
||||
|
||||
## Format review-report.md
|
||||
|
||||
```markdown
|
||||
# Review Report: [Feature-Name]
|
||||
Datum: [heute]
|
||||
|
||||
## Ergebnis: ✅ Freigabe / ⚠️ Kleinigkeiten / ❌ Blockierend
|
||||
|
||||
## Gefundene Probleme
|
||||
|
||||
### [Datei:Zeile] Beschreibung
|
||||
**Schwere**: Kritisch / Mittel / Gering
|
||||
**Empfehlung**: Was soll geändert werden?
|
||||
|
||||
## Positiv aufgefallen
|
||||
...
|
||||
|
||||
## Empfehlung
|
||||
Freigabe / Bitte [X] beheben und erneut reviewen.
|
||||
```
|
||||
|
||||
Schreibe am Ende: "Review abgeschlossen. Ergebnis: [✅/⚠️/❌]"
|
||||
@@ -0,0 +1,20 @@
|
||||
import json, sys, subprocess
|
||||
|
||||
data = json.loads(sys.stdin.read())
|
||||
|
||||
# Nur nach Datei-Änderungen prüfen
|
||||
if data.get("tool_name") in ["Write", "Edit"]:
|
||||
results = []
|
||||
|
||||
# Tests
|
||||
r = subprocess.run(["npm", "test", "--", "--passWithNoTests"], capture_output=True)
|
||||
results.append(("Tests", r.returncode == 0))
|
||||
|
||||
# Linting
|
||||
r = subprocess.run(["npm", "run", "lint"], capture_output=True)
|
||||
results.append(("Lint", r.returncode == 0))
|
||||
|
||||
failed = [name for name, ok in results if not ok]
|
||||
if failed:
|
||||
print(f"⚠️ Quality Gate FAILED: {', '.join(failed)}", file=sys.stderr)
|
||||
print("Bitte Fehler beheben bevor du fortfährst.", file=sys.stderr)
|
||||
@@ -0,0 +1,12 @@
|
||||
import json, sys
|
||||
|
||||
tool_input = json.loads(sys.stdin.read())
|
||||
command = tool_input.get("tool_input", {}).get("command", "")
|
||||
|
||||
BLOCKED = ["rm -rf /", "dd if=", "mkfs", ":(){:|:&};:"]
|
||||
for blocked in BLOCKED:
|
||||
if blocked in command:
|
||||
print(f"BLOCKED: Gefährlicher Befehl erkannt: {blocked}", file=sys.stderr)
|
||||
sys.exit(2) # Exit-Code 2 = Operation blockiert
|
||||
|
||||
sys.exit(0)
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": ["Bash", "Read", "Write", "Edit"],
|
||||
"deny": []
|
||||
},
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 .claude/hooks/pre_tool_use.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Database
|
||||
POSTGRES_DB=schaeffler
|
||||
POSTGRES_USER=schaeffler
|
||||
POSTGRES_PASSWORD=schaeffler
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
|
||||
# JWT
|
||||
JWT_SECRET_KEY=your-secret-key-here-change-in-production
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=480
|
||||
|
||||
# Azure OpenAI
|
||||
AZURE_OPENAI_API_KEY=your-azure-openai-key
|
||||
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
|
||||
AZURE_OPENAI_DEPLOYMENT=gpt-4o
|
||||
AZURE_OPENAI_API_VERSION=2024-02-01
|
||||
|
||||
# File Storage
|
||||
UPLOAD_DIR=/app/uploads
|
||||
MAX_UPLOAD_SIZE_MB=500
|
||||
|
||||
# Celery worker concurrency (default: 8 parallel CAD jobs per worker container)
|
||||
# Scale horizontally with: docker compose up --scale worker=N
|
||||
CELERY_WORKER_CONCURRENCY=8
|
||||
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
.env
|
||||
.env.local
|
||||
.DS_Store
|
||||
*.log
|
||||
core
|
||||
/blender-renderer/core
|
||||
@@ -0,0 +1,174 @@
|
||||
# Schaeffler Automat
|
||||
|
||||
## Ziel
|
||||
|
||||
Automatisiertes Render-System für Schaeffler-Produktbilder. Kunden (intern) laden Excel-Auftragslisten hoch, das System extrahiert Produktdaten, verknüpft STEP-CAD-Dateien, rendert Thumbnails und Animationen über Blender (Cycles/EEVEE) oder Flamenco, und liefert fertige PNG/MP4-Ausgaben.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Backend**: Python 3.11, FastAPI (async), SQLAlchemy 2 (async), Alembic, Celery, Pydantic v2
|
||||
- **Frontend**: React 18, TypeScript, Vite, Tailwind CSS, lucide-react
|
||||
- **Datenbank**: PostgreSQL 16
|
||||
- **Queue/Cache**: Redis 7 (Celery Broker + Backend)
|
||||
- **Renderer**: Blender 5.0.1 (headless), cadquery (STEP→STL), Three.js (Playwright)
|
||||
- **Render Farm**: Flamenco 3.8 (Manager + Worker, für Animationen)
|
||||
- **Deployment**: Docker Compose (11 Services)
|
||||
|
||||
## Services (docker-compose.yml)
|
||||
|
||||
| Service | Port | Funktion |
|
||||
|---|---|---|
|
||||
| `postgres` | 5432 | Primärdatenbank |
|
||||
| `redis` | 6379 | Celery Broker |
|
||||
| `backend` | 8888 | FastAPI App (uvicorn) |
|
||||
| `worker` | – | Celery Worker, Queue: `step_processing`, concurrency=8 |
|
||||
| `worker-thumbnail` | – | Celery Worker, Queue: `thumbnail_rendering`, **concurrency=1** |
|
||||
| `beat` | – | Celery Beat (Scheduler) |
|
||||
| `blender-renderer` | 8100 | Blender HTTP-Service (STEP→PNG, STEP→STL) |
|
||||
| `threejs-renderer` | 8101 | Three.js/Playwright HTTP-Service |
|
||||
| `flamenco-manager` | 8080 | Flamenco Job Manager |
|
||||
| `flamenco-worker` | – | Flamenco Render Worker (GPU) |
|
||||
| `frontend` | 5173 | React/Vite Dev Server |
|
||||
|
||||
## Starten / Stoppen
|
||||
|
||||
```bash
|
||||
# Alle Services starten
|
||||
docker compose up -d
|
||||
|
||||
# Logs einzelner Services
|
||||
docker compose logs -f backend
|
||||
docker compose logs -f worker
|
||||
docker compose logs -f worker-thumbnail
|
||||
docker compose logs -f blender-renderer
|
||||
|
||||
# Neubauen nach Codeänderungen (Backend/Worker)
|
||||
docker compose up -d --build backend worker worker-thumbnail
|
||||
|
||||
# Frontend-Änderungen: Hot-Reload aktiv, kein Rebuild nötig
|
||||
```
|
||||
|
||||
## Standard-Zugangsdaten (Entwicklung)
|
||||
|
||||
- **Admin**: admin@schaeffler.com / Admin1234!
|
||||
- **Backend API**: http://localhost:8888/docs
|
||||
- **Frontend**: http://localhost:5173
|
||||
- **Flamenco Manager**: http://localhost:8080
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
schaefflerautomat/
|
||||
├── backend/
|
||||
│ ├── app/
|
||||
│ │ ├── api/routers/ # FastAPI Router (admin, cad, orders, products, ...)
|
||||
│ │ ├── models/ # SQLAlchemy ORM-Modelle (14 Modelle)
|
||||
│ │ ├── schemas/ # Pydantic In/Out-Schemas
|
||||
│ │ ├── services/ # Business-Logik (excel_parser, step_processor, ...)
|
||||
│ │ ├── tasks/ # Celery Tasks (step_tasks.py, flamenco_tasks.py)
|
||||
│ │ └── utils/ # Auth, Seeding
|
||||
│ ├── alembic/versions/ # DB-Migrationen (001–026+)
|
||||
│ └── start.sh # Entrypoint: migrate → seed → uvicorn
|
||||
├── frontend/src/
|
||||
│ ├── api/ # API-Client-Funktionen (axios-basiert)
|
||||
│ ├── components/ # Wiederverwendbare UI-Komponenten
|
||||
│ └── pages/ # Seitenkomponenten
|
||||
├── blender-renderer/ # Blender HTTP-Microservice (Python Flask)
|
||||
├── threejs-renderer/ # Three.js/Playwright Microservice (Python Flask)
|
||||
├── flamenco/ # Flamenco Dockerfile + Job-Type-Scripts (.js)
|
||||
└── docker-compose.yml
|
||||
```
|
||||
|
||||
## Coding-Standards
|
||||
|
||||
- **Sprache im Code**: Englisch (Variablen, Kommentare, Commits)
|
||||
- **Commits**: Conventional Commits (`feat:`, `fix:`, `refactor:`, `docs:`)
|
||||
- **Python**: async/await durchgehend im Backend, sync-Wrapper für Celery-Tasks
|
||||
- **TypeScript**: Interfaces für alle API-Responses in `frontend/src/api/*.ts`
|
||||
- **Keine Tests**: Aktuell kein automatisiertes Test-Suite vorhanden
|
||||
|
||||
## Datenbank-Migrationen
|
||||
|
||||
```bash
|
||||
# Neue Migration erstellen
|
||||
docker compose exec backend alembic revision --autogenerate -m "beschreibung"
|
||||
|
||||
# Migrationen anwenden
|
||||
docker compose exec backend alembic upgrade head
|
||||
|
||||
# Status prüfen
|
||||
docker compose exec backend alembic current
|
||||
```
|
||||
|
||||
## Celery Task-Queues
|
||||
|
||||
| Queue | Worker | Concurrency | Tasks |
|
||||
|---|---|---|---|
|
||||
| `step_processing` | `worker` | 8 | `process_step_file`, `render_order_line_task`, `dispatch_order_line_render` |
|
||||
| `thumbnail_rendering` | `worker-thumbnail` | 1 | `render_step_thumbnail`, `regenerate_thumbnail`, `generate_stl_cache` |
|
||||
| `ai_validation` | `worker` | 8 | Azure AI Validierung |
|
||||
|
||||
**Wichtig**: `thumbnail_rendering` läuft mit concurrency=1, weil der blender-renderer nur 1 Request gleichzeitig verarbeiten kann. Mehr parallele Requests führen zu Timeouts.
|
||||
|
||||
## STEP-Processing-Pipeline
|
||||
|
||||
1. **Upload**: STEP-Datei hochladen → `CadFile`-Record erstellt → `process_step_file` Task eingereiht
|
||||
2. **Metadata** (`process_step_file` auf `step_processing`):
|
||||
- STEP-Objekte extrahieren (cadquery, ~0.1s)
|
||||
- `parsed_objects` in DB speichern
|
||||
- glTF konvertieren (falls konfiguriert)
|
||||
- Status: `processing` → queut `render_step_thumbnail`
|
||||
3. **Thumbnail** (`render_step_thumbnail` auf `thumbnail_rendering`):
|
||||
- Blender oder Three.js renderer aufrufen
|
||||
- STL-Cache erstellen: `{step_stem}_low.stl`, `{step_stem}_high.stl`
|
||||
- Status: `completed` oder `failed`
|
||||
- Materialien auto-populated
|
||||
|
||||
## STL-Cache-Konvention
|
||||
|
||||
STL-Dateien liegen **neben der STEP-Datei**:
|
||||
```
|
||||
uploads/{cad_file_id}/filename_low.stl
|
||||
uploads/{cad_file_id}/filename_high.stl
|
||||
```
|
||||
Beim nächsten Render-Aufruf wird der Cache genutzt (keine Neu-Konvertierung).
|
||||
|
||||
## Material-Alias-System
|
||||
|
||||
- Materialien werden per STEP-Part-Name auf Schaeffler-Bibliotheksmaterialien (`SCHAEFFLER_...`) gemappt
|
||||
- Lookup-Reihenfolge: **Alias-Tabelle zuerst**, dann exakter `Material.name`-Match, dann Pass-through
|
||||
- Alias-Seeding: Admin → "Seed Aliases" oder via `POST /api/materials/seed-aliases`
|
||||
- Neue Aliases direkt in DB oder über Material-Detail-UI hinzufügen
|
||||
|
||||
## Rollen
|
||||
|
||||
| Rolle | Berechtigungen |
|
||||
|---|---|
|
||||
| `admin` | Vollzugriff, Admin-Panel, alle Einstellungen |
|
||||
| `project_manager` | Aufträge, Analytics, Render-Trigger, STL-Download |
|
||||
| `client` | Eigene Aufträge anlegen und einsehen |
|
||||
|
||||
## Wichtige API-Endpoints
|
||||
|
||||
- `POST /api/uploads/excel` — Excel-Auftragsliste importieren
|
||||
- `POST /api/orders/{id}/submit` — Auftrag einreichen
|
||||
- `POST /api/orders/{id}/dispatch-renders` — Alle Render-Zeilen dispatchen
|
||||
- `GET /api/cad/{id}/thumbnail` — Thumbnail (kein Auth, UUID opaque)
|
||||
- `POST /api/cad/{id}/generate-stl/{quality}` — STL-Generierung manuell triggern
|
||||
- `POST /api/admin/settings/regenerate-thumbnails` — Alle Thumbnails neu rendern
|
||||
- `POST /api/admin/settings/process-unprocessed` — Unverarbeitete STEP-Dateien queuen
|
||||
- `POST /api/admin/settings/generate-missing-stls` — Fehlende STL-Caches erstellen
|
||||
- `GET /api/worker/activity` — Letzte 30 STEP-Verarbeitungen (Status, Timing)
|
||||
|
||||
## Bekannte Eigenheiten
|
||||
|
||||
- **Backend-Port 8888** (nicht 8000 — war belegt)
|
||||
- **Tailwind CSS-Variablen**: `bg-surface` etc. funktionieren nicht mit `/ opacity`-Syntax wenn CSS-Variable einen Hex-Wert enthält. Stattdessen `style={{ backgroundColor: 'var(--color-bg-surface)' }}` verwenden.
|
||||
- **Blender mm→m**: STEP-Dateien sind in mm, Blender intern in m. Alle Import-Scripts skalieren mit `0.001`.
|
||||
- **Flamenco GPU**: `deploy.resources.reservations.devices` in docker-compose für NVIDIA-Support.
|
||||
- **`settings_persistence`**: Admin-Einstellungen werden via direktem SQL-UPDATE gespeichert (nicht ORM-Mutation), da SQLAlchemy bei key-value-Stores keine Mutation trackt.
|
||||
|
||||
## Learnings-Pflicht
|
||||
Nach jedem gelösten Problem oder jeder wichtigen Entscheidung:
|
||||
→ Trag das Learning in LEARNINGS.md ein (Format: Datum | Kategorie | Problem → Lösung)
|
||||
→ Commitiere LEARNINGS.md zusammen mit dem Fix: `docs: learning erfasst - [kurzbeschreibung]`
|
||||
@@ -0,0 +1 @@
|
||||
,hartmut,tuxedo-os,01.03.2026 21:23,file:///home/hartmut/.config/libreoffice/4;
|
||||
@@ -0,0 +1,6 @@
|
||||
"Die Spalten ab A bis ""Start"" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und ""Start"" neue Spalten eingefügt werden.",,,,,"START
|
||||
diese Spalte bleibt leer","Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. ""Gewähltes Produkt"" muss eindeutig sein.",,,,,"Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.",,,,,
|
||||
Ebene1,Ebene2,Baureihe,PIM-ID (Klasse),Produkt (Baureihe),,Gewähltes Produkt,Name CAD-Modell,Gewünschte Bildnummer,Lagertyp,Medias-Rendering,Platte / Plate,Material Platte,Schraube / Screw,Material screw,Nut BZ,Material nut
|
||||
Linearsysteme,Laufrollenführungen,Endplatten für Führungsschiene LFS,233092AM41,ANS.LFS52,,ANS.LFS52,ans_lfs52_p_thread.stp,ans_lfs52_p_online,linear,1,ANS_LFS52-0011.prt,Stahl brüniert,ISO4762-M6X12_010.prt,Stahl v2,,
|
||||
Linearsysteme,Laufrollenführungen,Endplatten für Führungsschiene LFS,233092AM41,ANS.LFS52-FH,,ANS.LFS52-FH,ans_lfs52-fh_p_thread.stp,ans_lfs52-fh_p_online,linear,1,ANS_LFS52-FH-0011_P.prt,Stahl brüniert,,,,
|
||||
Linearsysteme,Laufrollenführungen,Endplatten für Führungsschiene LFS,233092AM41,ANS.LFS86-C,,ANS.LFS86-C,ans_lfs86-c_p_thread.stp,ans_lfs86-c_p_online,linear,1,ANS_LFS86-C-0011.prt,Stahl brüniert,ISO4762-M6X25_002.prt,Stahl v2,,
|
||||
|
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
"Die Spalten ab A bis ""Start"" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und ""Start"" neue Spalten eingefügt werden.",,,,,"START
|
||||
diese Spalte bleibt leer","Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. ""Gewähltes Produkt"" muss eindeutig sein.",,,,,"Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.",,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
Ebene1,Ebene2,Baureihe,PIM-ID (Klasse),Produkt (Baureihe),,Gewähltes Produkt,Name CAD-Modell,Gewünschte Bildnummer,Lagertyp,Medias-Rendering,Innenring Name: CAD,Innenring Material,Außenring Name: CAD,Außenring Material ,Rollen Name: CAD,Rollen Material,Käfig Name: CAD,Käfig Material,Käfig 2/ Deckel Name : CAD,Käfig Material,Dichtung Name: CAD,Dichtung Material,Dichtung 2 Name: CAD,Material Dichtung,Halteringe Name: CAD,Halteringe Material ,Scheibenpaket Name: CAD,Scheibenpaket Material,Sprengring Name: CAD,Sprengring Material ,Bordscheibe Name: CAD,Bordscheibe Material ,Sicherungsring Name: CAD,Sicherungsring Material ,Bolt,Material Bold,Spacer,Spacer: material,Anlaufschreibe,Anlaufscheibe Material
|
||||
Wälz- und Gleitlager,Rollenlager,Axial-Zylinderrollenlager,2305110101,811..-L,,811..-L,81113-l_cut.stp,81113-l_online,axial,1,WS81113_GEN_1_AF1_1.prt,Stahl v2,GS81113_GEN_1_AF1_1.prt,Stahl v2,LRB7P5X7P5_001_1_1_1.prt,Stahl v2,K81113L-11_GEFR_1_AF0_1.prt,Aluminium,,,,,,,,,,,,,,,,,,,,,,
|
||||
Wälz- und Gleitlager,Rollenlager,Axial-Zylinderrollenlager,2305110102,893..-M,,893..-M,89320-m-p5_cut.stp,89320-m-p5_online,axial,1,WS89320_GEN_1.prt,Stahl v2,GS89320_GEN_1.prt,Stahl v2,LRB13X13_001_1.prt,Stahl v2,K89320-31_MONTAGE_1.prt,Messing,K89320-M-11_MONTAGE_1.prt,Messing,,,,,,,,,,,,,,,ISO8750-2X10_003_1.prt,Stahl v2,,,,
|
||||
Wälz- und Gleitlager,Rollenlager,Axial-Schrägrollenlager,230511AC32,AXS..,,AXS..,axs1220_cut.stp,axs1220_online,axial,1,F-235143-11_1_AF1_1.prt,Stahl v2,F-235143-11_1_AF0_1.prt,Stahl v2,NRB1P5X2P2_1_1.prt,Stahl v2,F-235143-31_1_AF0_1.prt,Generisch Plastik: schwarz,,,,,,,,,,,,,,,,,,,,,,
|
||||
Wälz- und Gleitlager,Zubehör,Axiallagerscheiben,2305BF0201,GS811,,GS811,gs81152-01.stp,gs81152-01_online,axial,1,GS81152-01.prt,Stahl v2,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
Wälz- und Gleitlager,Rollenlager,Zylinderrollenlager,2305090102,HCNU10..-XL-M1,,HCNU10..-XL-M1,nu1040-xl-m1_00_04_cut.stp,nu1040-xl-m1_00_04_online,radial,1,IR_NU1040-3001_21_04_1.prt,Stahl v2,AU_NU1040-3101_11_04_1.prt,Stahl v2,ZRB26X26_DUM_1.prt,Keramik,RKKM_N1040-A-M3-1031-P_1.prt,Messing,RKDK_N1040-A-M3-1041-P_1.prt,Messing,,,,,,,,,,,,,,,,,,,,
|
||||
|
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
"Die Spalten ab A bis ""Start"" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und ""Start"" neue Spalten eingefügt werden.",,,,,"START
|
||||
diese Spalte bleibt leer","Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. ""Gewähltes Produkt"" muss eindeutig sein.",,,,,"Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.",,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
Ebene1,Ebene2,Baureihe,PIM-ID (Klasse),Produkt (Baureihe),,Gewähltes Produkt,Name CAD-Modell,Gewünschte Bildnummer,Lagertyp,Medias-Rendering,Innenring / Inner ring,Material Innenring,Außenring / Outer ring,Material Außenring,Außenring 2,Material Außenring 2,Gehause / Housing,Material housing,Sliding Layer,Material Sliding layer,Ringe,Material ringe,Dichtungsträger / Sealing carrier,Material Dichtungsträger,Dichtlippe / Sealing lip,Material Dichtlippe,Nipple,Material Nippel,Schraube / Screw,Material Schraube,Distanz bucshe / Spacer washer.prt,Material Distanz bucshe,Snapring,Material Snapring,Split Stift,Material split stift
|
||||
Wälz- und Gleitlager,Gleitlager,Gleitbuchsen,,EGB..-E40-B,,EGB..-E40-B,egb3520-e40_asm.stp,egb3520-e40_online,radial,1,,,EGB3520-E40.prt,Bronze v2,,,,,,,,,,,,,,,,,,,,,,
|
||||
Wälz- und Gleitlager,Gleitlager,Gelenklager,,GE..-HF,,GE..-HF,ge360-hf_000_p_cut.stp,ge360-hf_000_p_online,radial,1,GE360-HF-0021-EIN_1_AF0_1.prt,Durotect CMT,GE360-HF-0011-EIN_HAELFTE_AF0_1.prt,Stahl,GE360-HF-0011-EIN_HAELFTE_AF1_1.prt,Stahl v2,,,GE360-HF-0051-EIN_1_AF0_1.prt,GFK + PTFE,GE360-HF-0051-EIN_1_AF4_1.prt,Stahl v2,,,,,GE360-HF-0051-EIN_1_AF7_1.prt,Stahl v2,ISO8734-6X40_005_1_1.prt,Stahl v2,,,,,,
|
||||
Wälz- und Gleitlager,Gleitlager,Gelenklager,,GE..-HO,,GE..-HO,ge120-ho_cut.stp,ge120-ho_online,radial,1,GE120-HO-0021-EIN_1_AF0_1.prt,Durotect M,GE120-DO-0011-EIN_1_AF0_1.prt,Durotect M,,,,,,,,,,,,,,,,,,,,,,
|
||||
Wälz- und Gleitlager,Gleitlager,Gelenklager,,GE..-LO,,GE..-LO,ge20_lo_cut.stp,ge20_lo_online,radial,1,GE20-LO-0021-EIN_1_AF0_1.prt,Durotect M,GE20-DO-0011-EIN_1_AF0_1.prt,Durotect M,,,,,,,,,,,,,,,,,,,,,,
|
||||
|
Binary file not shown.
@@ -0,0 +1,13 @@
|
||||
"Die Spalten ab A bis ""Start"" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und ""Start"" neue Spalten eingefügt werden.",,,,,"START
|
||||
diese Spalte bleibt leer","Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. ""Gewähltes Produkt"" muss eindeutig sein.",,,,,"Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.",,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
Ebene1,Ebene2,Baureihe,PIM-ID (Klasse),Produkt (Baureihe),,Gewähltes Produkt,Name CAD-Modell,Gewünschte Bildnummer,Lagertyp,Medias-Rendering,Innenring / Inner ring Name: CAD,Material Innenring,Innenring / Inner ring 2 Name: CAD,Material Innenring 2.,Außenring / Outer ring Name: CAD,Material Außenring,Wälzkörper / Rolling Element Name: CAD,Material: Wälzkörper / Rolling Element,Käfig / Cage Name: CAD,Material: Käfig / Cage,Käfig / Cage Name: CAD,Material: Käfig / Cage,Dichtungskern/Dichtungsträger Name: CAD,Material: Dichtungskern/Dichtungsträger,Dichtung Außen / Dichtlippe Name: CAD,Material: Dichtung Außen / Dichtlippe,Innen Buchse Name Name: CAD,Material: Innen Buchse,Sprengring Name: CAD,Material: Sprengring,Axial - WS Name: CAD,Material: Axial - WS,Axial - GS Name: CAD,Material: Axial GS,Schrauben Name: CAD,Material: Schrauben,Niet Name: CAD,Material: Niet,Unterlegscheibe,Material_Unterlegscheibe
|
||||
Wälz- und Gleitlager,Kugellager,Axial-Rillenkugellager,2305100101,511,,51110-P6,51110-p6_a00_cut.stp,51110_online,axial,1,,,,,,,KUG7P144_1.prt,Stahl v2,AKUK_51110-A-JP-3001_H_1.prt,Stahlblech v2,,,,,,,,,,,WS_51110-3001_H_1.prt,Stahl v2,GS_51110-3001_H_1.prt,Stahl v2,,,,,,
|
||||
Wälz- und Gleitlager,Kugellager,Axial-Rillenkugellager,2305100101,514..-MP,,51413-MP,51413-mp_p_cut.stp,51413-MP_online,axial,1,,,,,,,KUP26P988_1.prt,Stahl v2,AKUK51413-MP-0031_1.prt,Messing,,,,,,,,,,,WS51413-0021_1.prt,Stahl v2,GS51413-0011_1.prt,Stahl v2,,,,,,
|
||||
Wälz- und Gleitlager,Kugellager,Axial-Rillenkugellager,2305100102,522..,,52211,52211_z00_cut.stp,52211_online,axial,1,,,,,,,KUG12P5_1.prt,Stahl v2,AKUK_51211-JP-3001_MONT_1.prt,Stahlblech v2,,,,,,,,,,,WS_52211-3001_H_1.prt,Stahl v2,GS_51211-3001_H_1.prt,Stahl v2,,,,,,
|
||||
Wälz- und Gleitlager,Kugellager,Rillenkugellager,2305080101,6..-2RSR-N,,6205-2rsr-n-c3,6205-2rsr-n-c3_p_cut.stp,6205-2rsr-n-c3_online,radial,1,IR6205-1021_1_1.prt,Stahl v2,,,AU6205-2Z-0011_H1_1_1.prt,Stahl v2,KUG7P938_002_1_1.prt,Stahl v2,KUH6205-JN-0031_1_1.prt,Stahlblech v2,,,D_6205-RSR-1051-NBR-ARM__1_1.prt,Stahl v2,D_6205-RSR-1051-NBR_1_1.prt,Generisch Gummi: schwarz,,,,,,,,,,,NTS153301-3-1_41X3_20_H_1_1_2_3.prt,Stahl v2,,
|
||||
Wälz- und Gleitlager,Kugellager,Rillenkugellager,2305080101,6..-2Z-N,,6205-2z-n-c3,6205-2z-n-c3_p_cut.stp,6205-2z-n-c3_online,radial,1,IR6205-2BRS-0021_H1_1.prt,Stahl v2,,,AU6205-2Z-0011_H1_1.prt,Stahl v2,KUG7P938_002_1.prt,Stahl v2,6205-BK_H2_1.prt,Stahlblech v2,,,DS6205-ZRG-A-0051_H_1.prt,Stahl v2,,,,,,,,,,,,,,,,
|
||||
Wälz- und Gleitlager,Kugellager,Rillenkugellager,2305080101,6..-C-2HRS,,6205-C-2HRS,6205-c-2hrs_p_010_cut.stp,6205-C-2HRS_online,radial,1,IR_6205-B-2Z-8101_81_08_1_1.prt,Stahl v2,,,AU_6205-B-2Z-8101_81_08_1_1.prt,Stahl v2,KUG7_938-G5_A00_P_1.prt,Stahl v2,KUH6205-JN-8001_Z00_1 .prt,Stahlblech v2,KUH6205-JN-7031_1.prt,Stahlblech v2,D_6205-HRS-8001-NBR_ARM_B00_1.prt,Stahl v2,D_6205-HRS-8001-NBR_B00_1.prt,Generisch Gummi: schwarz,,,,,,,,,,,NTS153301-3-1_41X3_2_H-MONT_1.prt,Stahl v2,,
|
||||
Wälz- und Gleitlager,Kugellager,Rillenkugellager,2305080101,6..-C-2HRS-N,,6205-c-2hrs-n-c3,6205-c-2hrs-n-c3_p_010_cut.stp,6205-c-2hrs-n-c3_online,radial,1,IR_6205-B-2Z-8101_81_08_1_1_2.prt,Stahl v2,,,AU_6205-B-2Z-N-8101_11_08_1_1_2.prt,Stahl v2,KUG7_938-G5_A00_P_1_1.prt,Stahl v2,KUH6205-JN-8001_Z00_1_1_2.prt,Stahlblech v2,KUH6205-JN-7031_1_1_2_3.prt,Stahlblech v2,D_6205-HRS-8001-NBR_ARM_B00_1_1.prt,Stahl v2,D_6205-HRS-8001-NBR_B00_1_1.prt,Generisch Gummi: schwarz,,,,,,,,,,,NTS153301-3-1_41X3_2_H-MONT_1_1.prt,Stahl v2,,
|
||||
Wälz- und Gleitlager,Kugellager,Rillenkugellager,2305080101,6..-C-2HRS-NR,,6205-c-2hrs-nr-c3,6205-c-2hrs-nr-c3_00_10_cut.stp,6205-c-2hrs-nr-c3_online,radial,1,IR_6205-B-2Z-8101_81_010_1.prt,Stahl v2,,,AU_6205-B-2Z-N-8101_11_08_1.prt,Stahl v2,KUG7_938_KBE_1_1.prt,Stahl v2,KUH6205-JN-8001_Z00_1_1.prt,Stahlblech v2,KUH6205-JN-7031_1_1.prt,Stahlblech v2,D_6205-HRS-8001-NBR_ARM_A00_1 .prt,Stahl v2,D_6205-HRS-8001-NBR_A00_1.prt,Generisch Gummi: schwarz,,,SP52_KBE_1.prt,Stahl v2,,,,,,,,,,
|
||||
Wälz- und Gleitlager,Kugellager,Rillenkugellager,2305080101,6..-C-2Z-N,,6204-c-2z-n,6204-c-2z-n_00_10_cut.stp,6204-c-2z-n_online,radial,1,IR_6204-B-2Z-8101_81_08_1.prt,Stahl v2,,,AU_6204-B-2Z-N-8101_11_10_1.prt,Stahl v2,KUG7_938_KBE_1_1_2.prt,Stahl v2,KUH6204-JN-8001_1.prt,Stahlblech v2,,,DS_6204-8001_X00_1.prt,Stahl v2,DS_6204-8001_X00_1.prt,Stahl v2,,,,,,,,,,,NTS153301-3-1_18X3_05_1.prt,Stahl v2,,
|
||||
Wälz- und Gleitlager,Kugellager,Rillenkugellager,2305080101,6..-C-2Z-NR,,6202-c-2z-nr,6202-c-2z-nr-r18-25_00_10_cut.stp,6202-c-2z-nr_online,radial,1,IR_6202-B-2Z-8101_21_08_1.prt,Stahl v2,,,AU_6202-B-2Z-N-8101_11_10_1.prt,Stahl v2,KUG6_KBE_1.prt,Stahl v2,KUH6202-JN-8001_Z00_1.prt,Stahlblech v2,KUH6202-JN-8001_1.prt,Stahlblech v2,DS_6202-8001_Z00_1.prt,Stahl v2,DS_6202-8001_Z00_1.prt,Stahl v2,,,SP35_KBE_1.prt,Stahl v2,,,,,,,,,,
|
||||
|
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
"Die Spalten ab A bis ""Start"" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und ""Start"" neue Spalten eingefügt werden.",,,,,"START
|
||||
diese Spalte bleibt leer","Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. ""Gewähltes Produkt"" muss eindeutig sein.",,,,,"Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.",
|
||||
Ebene1,Ebene2,Baureihe,PIM-ID (Klasse),Produkt (Baureihe),,Gewähltes Produkt,Name CAD-Modell,Gewünschte Bildnummer,Lagertyp,Medias-Rendering,Rail,Material Rail
|
||||
Linearsysteme,Profilschienenführungen,Sechsreihige Kugelumlaufeinheiten,233092AB06,TKSD..,,TKSD..,tksd25x1290-g2-45-45_p.stp,tksd25x1290-g2-45-45_online,linear,1,TKSD25X1290-G2-45-45.prt,Stahl v2
|
||||
Linearsysteme,Profilschienenführungen,Rollenumlaufeinheiten,233092AB21,TSX..-D,,TSX..-D,tsx25-d-g1-hj-gen.stp,tsx25-d-g1-hj_online,linear,1,TSX25D-G1-HJ-GEN.prt,Stahl v2
|
||||
|
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
"Die Spalten ab A bis ""Start"" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und ""Start"" neue Spalten eingefügt werden.",,,,,"START
|
||||
diese Spalte bleibt leer","Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. ""Gewähltes Produkt"" muss eindeutig sein.",,,,,"Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.",,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
Ebene1,Ebene2,Baureihe,PIM-ID (Klasse),Produkt (Baureihe),,Gewähltes Produkt,Name CAD-Modell,Gewünschte Bildnummer,Lagertyp,Medias-Rendering,Innenring / Inner ring,Material Innenring,Außenring / Outer ring,Material Außenring,Käfig / Cage,Material Käfig,Käfig 2 / Cage 2,Material Käfig 2,Niet,Material Niet,Wälzkörper / Rolling element,Material Wälzkörper,Bordscheibe IR / Loose Lip IR,Material Bordscheibe IR,Führungsring AU / Loose Lip AU,Führungsring AU / Loose Lip AU,Dichtungsträger / Sealing carrier,Material Dichtungsträger,Dichtlippe / Sealing lip,Material Dichtlippe,Schraube / Screw,Material Schraube,Distanz bucshe / Spacer washer,Material Distanz bucshe,Snapring,Material Snapring
|
||||
Wälz- und Gleitlager,Rollenlager,TORB,2305091390 ,C31..-XL-K-M,,C31..-XL-K-M,c3152-xl-k-m_cut.stp,c3152-xl-k-m_online,radial,1,IR_C3152-K-3001,Stahl v2,AU_C3152-3001,Stahl v2,RK_C3152-M-3001,Messing,,,,,TORO_C3152-3001,Stahl v2,,,,,,,,,,,,,,
|
||||
Wälz- und Gleitlager,Rollenlager,Radial SRB,2305091102,241..-BE-XL-K30-H40,,241..-BE-XL-K30-H40,24148-be-xl-k30-h40_006_cut.stp,24148-be-xl-k30-h40_006_online,radial,1,IR_24148-BE1-K30-0021,Stahl v2,AU_24148-BE1-WA-H40-3001_006,Stahl v2,RK_24148-JPB-0031,Stahlblech v2,,,,,TORO_24148-BE1,Stahl v2,BO_24148_BE1,Stahl v2,,,,,,,,,,,,
|
||||
Wälz- und Gleitlager,Rollenlager,Radial SRB,2305091103,WS222..-E1-XL-2RSR,,WS222..-E1-XL-2RSR,ws22215-e1-2rsr_006_cut.stp,ws22215-e1-2rsr_006_online,radial,1,IR_WS22215-E1-D-CA-WA-3001_006,Stahl v2,AU_WS22215-E1-D-WA-3001_006,Stahl v2,RK_22215-E-JPA-0031_006,Stahlblech v2,,,,,TORO_22215-E1A_SCT,Stahl v2,,,,,D_WS22215-E1-RSR-3001_A,Stahl v2,D_WS22215-E1-RSR-3001-RUBBER,Generisch Gummi: schwarz,,,,,,
|
||||
|
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
"Die Spalten ab A bis ""Start"" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und ""Start"" neue Spalten eingefügt werden.",,,,,"START
|
||||
diese Spalte bleibt leer","Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. ""Gewähltes Produkt"" muss eindeutig sein.",,,,,"Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
Ebene1,Ebene2,Baureihe,PIM-ID (Klasse),Produkt (Baureihe),,Gewähltes Produkt,Name CAD-Modell,Gewünschte Bildnummer,Lagertyp,Medias-Rendering,Innenring / Inner ring Name: CAD,Material Innenring,Innenring / Inner ring 2 Name: CAD,Material Innenring 2,Innenring / Inner ring 3 Name: CAD,Material Innenring 3,Zwischenring 4,Material Zwischenring 4,Zwischenring 5,Material Zwischenring 5,Außenring / Outer ring Name: CAD,Material Außenring,Zwischenring 1,Material Zwischenring 1,Außenring / Outer ring 2 Name: CAD,Material Außenring 2,Zwischenring 2,Material Zwischenring 2,Außenring / Outer ring 3 Name: CAD,Material Außenring 3,Zwischenring 3,Material Zwischenring 3,Außenring / Outer ring 4 Name: CAD,Material Außenring 4,Käfig / Cage Name: CAD,Material: Käfig / Cage,Käfig / Cage 2 Name: CAD,Material: Käfig / Cage 2,Käfig / Cage 3 Name: CAD,Material: Käfig / Cage 3,Käfig / Cage 4 Name: CAD,Material: Käfig / Cage 4,Käfig / Cage 5 Name: CAD,Material: Käfig / Cage 5,Käfig / Cage 6 Name: CAD,Material: Käfig / Cage 6,Käfig / Cage 7 Name: CAD,Material: Käfig / Cage 7,Käfig / Cage 8 Name: CAD,Material: Käfig / Cage 8,Käfigbolzen Name: CAD,Material: Käfigbolzen,Käfigbolzen 2 Name: CAD,Material: Käfigbolzen ,Bolzensstift Name: CAD,Material Bolzenstift,Wälzkörper / Rolling Element Name: CAD,Material: Wälzkörper / Rolling Element,Wälzkörper / Rolling Element 2 Name: CAD,Material: Wälzkörper / Rolling Element 2,Dichtungskern/Dichtungsträger Name: CAD,Material: Dichtungskern/Dichtungsträger,Dichtung Außen / Dichtlippe Name: CAD,Material: Dichtung Außen / Dichtlippe,Sealing ring Name: CAD,Material Sealing ring,Sealing ring 2 Name: CAD,Material: Sealing ring 2,Feder / Spring Name: CAD,Material: Feder / Spring,Innen Buchse Name Name: CAD,Material: Innen Buchse,Sprengring Name: CAD,Material: Sprengring,Axial - WS Name: CAD,Material: Axial - WS,Axial - GS Name: CAD,Material: Axial GS,Schrauben Name: CAD,Material: Schrauben,Niet Name: CAD,Material: Niet,Unterlegscheibe,Material: Unterlegscheibe,Stoßdämpfer Stopfen: CAD,Material: Stoßdämpfer Stopfen,Stoßdämpfer Feder: CAD,Material: Stoßdämpfer Feder,Stoßdämpfer Unterlegscheibe: CAD,Material: Stoßdämpfer Unterlegscheibe
|
||||
Wälz- und Gleitlager,Rollenlager,Kegelrollenlager,2305091051,320..-X-XL-DF,,320..-X-XL-DF,32016-x-e1-df_00_cut.stp,32016-x-e1-df_00_online,radial,1,IR_32016-X-E1-WA-3001_21_0-1360.PRT,Stahl v2,IR_32016-X-E1-WA-3001_21_-19867.PRT,Stahl v2,,,,,,,AU_32016-X-E1-WA-3001_11_0-2017.PRT,Stahl v2,ZWR_32016-X-1071__1_AF0_1.PRT,Stahl v2,AU_32016-X-E1-WA-3001_11_-20520.PRT,Stahl v2,,,,,,,,,KRK_32016-X-JPB-1031_31_1_AF1_1.PRT,Stahl v2,KRK_32016-X-JPB-1031_31_1_AF0_1.PRT,Stahl v2,,,,,,,,,,,,,,,,,,,KERO_32017-X-E1-QPA-WA_00_1_1.PRT,Stahl v2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
Wälz- und Gleitlager,Rollenlager,Kegelrollenlager,2305091021,F-802070.TR4-AM,,F-802070.TR4-AM,f-802070_tr4-am_04_cut.stp,f-802070_tr4-am_04_online,radial,1,F-802070-3001_IR_TR2_04_1_AF0_1.PRT,Stahl v2,F-802070-3001_IR_TR2_04_1_AF1_1.PRT,Stahl v2,,,F-802070-3001_IZWR_TR_04_1_AF0_.PRT,Stahl v2,,,F-802070-3101_AU_TR1_04_1_AF0_1.PRT,Stahl v2,F-802070-3001_AZWR_TR-S_04-6077.PRT,Stahl v2,F-802070-3101_AU_TR1_04_1_AF1_1.PRT,Stahl v2,F-802070-3001_AZWR_TR-AS_04_1_A.PRT,Stahl v2,F-802070-3101_AU_TR1_04_1_AF2_1.PRT,Stahl v2,F-802070-3001_AZWR_TR-S_0-12124.PRT,Stahl v2,F-802070-3101_AU_TR1_04_1_AF3_1.PRT,Stahl v2,F-802070-3101_KRSS_TR-F_1_AF0_1.PRT,Stahl v2,F-802070-3201_KRSS_TR-F_1_AF0_1.PRT,Stahl v2,F-802070-3201_KRSS_TR-F_1_AF3_1.PRT,Stahl v2,F-802070-3101_KRSS_TR-F_1_AF1_1.PRT,Stahl v2,F-802070-3101_KRSS_TR-F_1_AF2_1.PRT,Stahl v2,F-802070-3201_KRSS_TR-F_1_AF1_1.PRT,Stahl v2,F-802070-3201_KRSS_TR-F_1_AF2_1.PRT,Stahl v2,F-802070-3101_KRSS_TR-F_1_AF3_1.PRT,Stahl v2,F-802070-3101_BLZ_TR_1_1.PRT,Stahl v2,,,,,KERO_F-802070-M-QP_ISB_1_AF30_1.PRT,Stahl v2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
Wälz- und Gleitlager,Rollenlager,Kegelrollenlager,2305091011,F-803422.01.TR2,,F-803422.01.TR2,f-803422_01_tr2_04_cut.stp,f-803422_01_tr2_04_online,radial,1,F-803422-1021_IR_TR2_04_1_AF0_1.PRT,Stahl v2,,,,,,,,,F-803422-3001_AU_TR1_WA_04-6311.PRT,Stahl v2,,,F-803422-3001_AU_TR1_WA_04-7785.PRT,Stahl v2,,,,,,,,,Z-536748_01-3001_KRK_T_A1-19091.PRT,Stahl v2,Z-536748_01-3001_KRK_T_A1-30636.PRT,Stahl v2,,,,,,,,,,,,,,,,,,,KERO_Z-513683-QP_ISB_1_1.PRT,Stahl v2,,,,,,,,,,,,,,,,,,,,,,,,,,,F-809724-0041_BLZ_TR_1_1.PRT,Stahl v2,F-809724-0081_TR_DUMMY_1_1.PRT,Stahl v2,HK1214-2RS-FPM-11_ALT_1_1.PRT,Stahl v2
|
||||
Wälz- und Gleitlager,Rollenlager,Axial-Kegelrollenlager,2305110401,KT-SERIES(1),,KT-SERIES(1),kt1120_04_cut.stp,kt1120_04_online,axial,1,,,,,,,,,,,,,,,,,,,,,,,,,AKRSS_KT1120-F-3101_04_1_AF0_1.PRT,Stahl v2,AKRSS_KT1120-F-3201_04_1_AF0_1.PRT,Stahl v2,,,,,,,,,,,,,BLZ_KT1120-3001_04_1_1.PRT,Stahl v2,,,,,KERO_KT1120-M-QP-WEA_04_1_1.PRT,Stahl v2,,,,,,,,,,,,,,,,,WS_KT1120-3001_04_1_AF0_1.PRT,Stahl v2,WS_KT1120-3001_04_1_AF1_1.PRT,Stahl v2,,,,,,,,,,,,
|
||||
Wälz- und Gleitlager,Rollenlager,Kegelrollenlager,2305BA0303,L320..-X-XL,,L320..-X-XL,l32016-x-xl_0d_00_cut.stp,l32016-x-xl_0d_00_online,radial,1,,,,,,,,,,,AU_L32016-X-XL-_0D_1_AF0_1.PRT,Stahl v2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
|
Binary file not shown.
Binary file not shown.
+116
@@ -0,0 +1,116 @@
|
||||
# Projekt-Learnings — Schaeffler Automat
|
||||
|
||||
## Format
|
||||
**Datum | Kategorie | Problem → Lösung**
|
||||
|
||||
---
|
||||
|
||||
## Learnings
|
||||
|
||||
### 2026-01-15 | Architektur | Backend-Port-Konflikt
|
||||
**Problem:** FastAPI standardmäßig auf Port 8000 — war auf dem Entwicklungsrechner belegt
|
||||
**Lösung:** Port 8888 in `docker-compose.yml` und Vite-Proxy konfiguriert
|
||||
**Für künftige Projekte:** Port früh festlegen und in CLAUDE.md dokumentieren
|
||||
|
||||
---
|
||||
|
||||
### 2026-01-20 | Datenbank | SQLAlchemy trackt key-value-Store-Mutations nicht
|
||||
**Problem:** Admin-Einstellungen (`system_settings`) wurden via ORM gespeichert, Änderungen wurden nicht persistiert
|
||||
**Ursache:** SQLAlchemy erkennt keine Mutation an einem bereits geladenen Objekt wenn nur ein Value-Feld geändert wird
|
||||
**Lösung:** Direktes SQL `UPDATE` via `op.execute()` statt ORM-Mutation in `admin.py`
|
||||
**Für künftige Projekte:** Key-Value-Stores immer mit direktem SQL oder `session.execute(update(...))` verwalten
|
||||
|
||||
---
|
||||
|
||||
### 2026-01-25 | Render-Pipeline | Blender ignoriert STEP-Einheiten (mm vs. m)
|
||||
**Problem:** STEP-Dateien sind in Millimetern, Blender arbeitet intern in Metern → 50mm-Lager erscheint 50 Meter breit, Kamera framt falsch
|
||||
**Lösung:** `_scale_mm_to_m(parts)` Helper in allen 3 Render-Scripts: `part.scale = (0.001, 0.001, 0.001)`, Transform anwenden
|
||||
**Betroffene Dateien:** `blender_render.py`, `still_render.py`, `turntable_render.py`
|
||||
**Für künftige Projekte:** Einheiten-Konvertierung direkt nach STL-Import, vor jeder Kamera-Kalkulation
|
||||
|
||||
---
|
||||
|
||||
### 2026-01-28 | Render-Pipeline | Blender 5.0 hat `scene.node_tree` entfernt
|
||||
**Problem:** `_setup_bg_compositor()` rief `scene.node_tree` auf (in Blender 5.0 entfernt) → Python-Exception → Blender exitete mit Code 0 → Flamenco markierte Task fälschlicherweise als "completed"
|
||||
**Lösung:** `_setup_bg_compositor()` aus Setup + Render-Script entfernt; bg_color-Kompositing in FFmpeg verschoben (`-f lavfi -i color=...` + overlay-Filter)
|
||||
**Wichtig:** Immer `try: main() except SystemExit: raise except Exception: traceback; sys.exit(1)` in Blender-Scripts — sonst verschluckt Blender Python-Exceptions
|
||||
**Für künftige Projekte:** Nach Blender-Major-Updates alle API-Calls prüfen; Exception-Guard ist Pflicht
|
||||
|
||||
---
|
||||
|
||||
### 2026-02-05 | Material-System | Material-Alias-Lookup-Reihenfolge falsch
|
||||
**Problem:** `Steel--Stahl` war sowohl ein kanonischer `Material.name` als auch ein Alias für `SCHAEFFLER_010101_Steel-Bare`. Der Lookup prüfte zuerst den exakten Namen und fand `Steel--Stahl` — Blender konnte diesen Namen aber nicht in der Library finden
|
||||
**Lösung:** Lookup-Reihenfolge in `material_service.py` umgekehrt: **Aliases zuerst**, dann exakter Name, dann Pass-through
|
||||
**Für künftige Projekte:** Alias-System immer so designen dass Aliases Vorrang haben; nie zwei Lookup-Pfade mit überlappenden Treffern
|
||||
|
||||
---
|
||||
|
||||
### 2026-02-10 | Render-Pipeline | Blender-Template zerstört HDRI/World
|
||||
**Problem:** Im Template-Modus (Mode B) wurden trotzdem Auto-Lights und eine neue World erstellt → überschrieb den HDRI aus dem .blend-Template → falsche Beleuchtung
|
||||
**Ursache:** Auto-Licht- und World-Setup-Code lief bedingungslos, nicht nur im Mode A
|
||||
**Lösung:** In Template-Mode werden Lights, World und Color-Management-Override vollständig übersprungen; nur die Kamera wird ggf. neu berechnet
|
||||
**Betroffene Dateien:** `still_render.py`, `turntable_render.py`, `schaeffler-still.js`, `schaeffler-turntable.js`
|
||||
|
||||
---
|
||||
|
||||
### 2026-02-15 | Celery | Blender-Queue-Flooding durch falsche Concurrency
|
||||
**Problem:** Alle Celery-Tasks (schnelle Metadata-Extraktion + langsamer Blender-Render) liefen auf `step_processing` mit concurrency=8 → 8 Workers schickten gleichzeitig Requests an blender-renderer (der nur 1 gleichzeitig verarbeiten kann) → 7 davon liefen in 300s-Timeout → blockierte die gesamte Queue
|
||||
**Lösung:** Pipeline aufgeteilt:
|
||||
- `process_step_file` (step_processing, concurrency=8): nur schnelle Metadata-Extraktion (<2s), queut dann →
|
||||
- `render_step_thumbnail` (thumbnail_rendering, concurrency=1): Blender-Call, niemals timeout
|
||||
**Neuer Service:** `worker-thumbnail` in `docker-compose.yml` mit `--concurrency=1`
|
||||
**Für künftige Projekte:** HTTP-Services die nur 1 Request gleichzeitig verarbeiten können IMMER auf einer separaten Queue mit concurrency=1 laufen lassen
|
||||
|
||||
---
|
||||
|
||||
### 2026-02-18 | Frontend | Tailwind CSS-Variablen inkompatibel mit opacity-Syntax
|
||||
**Problem:** `bg-surface/50` oder `bg-surface` (wenn `--color-bg-surface` ein Hex-Wert ist) generiert `rgb(var(--color-bg-surface) / 0.5)` — invalides CSS, weil `rgb()` keine Hex-Werte als Channel-Input akzeptiert → Hintergrund transparent
|
||||
**Ursache:** Tailwind erwartet CSS-Variablen mit RGB-Channel-Format (`255 255 255`), nicht Hex (`#ffffff`)
|
||||
**Lösung:** Inline-Style verwenden: `style={{ backgroundColor: 'var(--color-bg-surface)' }}`
|
||||
**Für künftige Projekte:** Entweder CSS-Variablen im RGB-Channel-Format definieren, oder konsequent inline styles für variable Farben
|
||||
|
||||
---
|
||||
|
||||
### 2026-02-20 | STL-Cache | Three.js-Renderer nutzte tempfile → kein Download möglich
|
||||
**Problem:** Three.js-Renderer konvertierte STEP→STL in ein tempfile und löschte es anschließend → STL-Download-Endpoint fand keine Datei
|
||||
**Ursache:** Three.js war ursprünglich nur für Thumbnails gebaut, STL-Cache-Konvention (`{stem}_low.stl` neben STEP-Datei) wurde nicht implementiert
|
||||
**Lösung:** Persistent cache path: `step_path.parent / f"{step_path.stem}_low.stl"`, cache-hit-check vor Konvertierung, kein `unlink()` mehr
|
||||
**Für künftige Projekte:** STL-Cache-Konvention (`{step_stem}_{quality}.stl` neben STEP-Datei) von Anfang an in allen Renderer-Services einhalten
|
||||
|
||||
---
|
||||
|
||||
### 2026-02-20 | STL-Cache | blender-renderer fehlte /convert-stl Endpoint
|
||||
**Problem:** Für Produkte die mit Blender gerendert wurden war kein STL-Cache vorhanden wenn nicht explizit gerendert wurde (blender-renderer renderte + konvertierte in einem Schritt, aber STL wurde nicht persistiert)
|
||||
**Lösung:** Neuer `/convert-stl` Endpoint in `blender-renderer/app.py`: konvertiert STEP→STL ohne Render, persistiert Cache. Neuer Celery-Task `generate_stl_cache` auf `thumbnail_rendering`-Queue. Admin-Funktion "Generate Missing STLs" zum Batch-Nachfüllen
|
||||
|
||||
---
|
||||
|
||||
### 2026-02-22 | Material-System | Fehlender Alias blockiert Material-Replacement
|
||||
**Problem:** Produkt F-803422.01.TR2 (SA-2026-00080) renderte ohne Materialersetzung. Material "Stahl v2" war korrekt in der UI gespeichert, aber weder in `materials` noch in `material_aliases` vorhanden
|
||||
**Ursache:** Alias-Seeding aus Excel deckte nicht alle Varianten der deutschen Materialbezeichnungen ab
|
||||
**Lösung:** Alias direkt in DB eingetragen: `"Stahl v2"` → `SCHAEFFLER_010101_Steel-Bare`
|
||||
**Für künftige Projekte:** Bei Render ohne Materialersetzung immer zuerst `resolve_material_map()` debuggen und Alias-Tabelle prüfen; Alias-Seeding regelmäßig mit neuen Excel-Varianten erweitern
|
||||
|
||||
---
|
||||
|
||||
### 2026-02-25 | Frontend | canDispatch-Bedingung zu restriktiv
|
||||
**Problem:** "Dispatch Renders"-Button war nicht sichtbar obwohl der Auftrag offene Render-Zeilen hatte
|
||||
**Ursache:** `canDispatch` enthielt `&& hasRetryable` — Button erschien nur wenn pending/failed/cancelled-Zeilen vorhanden waren, nicht wenn alle Zeilen "pending" im Erstauftrag
|
||||
**Lösung:** `hasRetryable`-Bedingung entfernt; Button ist immer sichtbar wenn Auftrag im richtigen Status und User privilegiert ist
|
||||
**Für künftige Projekte:** Aktions-Buttons nicht zu stark von abgeleiteten Zuständen abhängig machen; lieber im Backend validieren
|
||||
|
||||
---
|
||||
|
||||
### 2026-02-28 | Frontend | MaterialInput-Dropdown ohne Hintergrund
|
||||
**Problem:** Dropdown der Material-Suchfeld-Komponente erschien transparent — Text über dem Hintergrund kaum lesbar
|
||||
**Ursache:** `bg-surface` Tailwind-Klasse + CSS-Variable mit Hex-Wert (siehe Learning 2026-02-18)
|
||||
**Lösung:** `style={{ backgroundColor: 'var(--color-bg-surface)' }}` für Dropdown-Container, Group-Header und Sticky-Button
|
||||
**Datei:** `frontend/src/components/shared/MaterialInput.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Offene Fragen
|
||||
- [ ] Azure AI Credentials für Phase 4 (Bildvalidierung) noch nicht konfiguriert
|
||||
- [ ] Flamenco GPU-Support nur mit NVIDIA — AMD/CPU-Fallback fehlt
|
||||
- [ ] Material-Alias-Seeding deckt noch nicht alle deutschen Materialbezeichnungs-Varianten ab
|
||||
- [ ] Turntable-Animation: bg_color via FFmpeg-Overlay — Qualität bei Transparenz-Edges prüfen
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Generate material_library.blend with all 35 Schaeffler standard materials.
|
||||
|
||||
Run with: blender --background --python generate_blend.py
|
||||
"""
|
||||
import bpy
|
||||
import os
|
||||
|
||||
# Placeholder colors per material — tuned to approximate real-world appearance
|
||||
# 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),
|
||||
# --- 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),
|
||||
# --- 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),
|
||||
# --- 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),
|
||||
# --- 05 Misc ---
|
||||
("SCHAEFFLER_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,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
# Start from factory defaults
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
for name, color, metallic, roughness in MATERIALS:
|
||||
mat = bpy.data.materials.new(name=name)
|
||||
mat.use_nodes = True
|
||||
nodes = mat.node_tree.nodes
|
||||
links = mat.node_tree.links
|
||||
|
||||
# Clear default nodes
|
||||
for n in nodes:
|
||||
nodes.remove(n)
|
||||
|
||||
# Create Principled BSDF + Material Output
|
||||
bsdf = nodes.new("ShaderNodeBsdfPrincipled")
|
||||
bsdf.location = (0, 0)
|
||||
output = nodes.new("ShaderNodeOutputMaterial")
|
||||
output.location = (300, 0)
|
||||
links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
|
||||
|
||||
# Set properties
|
||||
bsdf.inputs["Base Color"].default_value = color
|
||||
bsdf.inputs["Metallic"].default_value = metallic
|
||||
bsdf.inputs["Roughness"].default_value = roughness
|
||||
|
||||
# Transmission for translucent materials
|
||||
if name in TRANSLUCENT:
|
||||
bsdf.inputs["Transmission Weight"].default_value = TRANSLUCENT[name]
|
||||
bsdf.inputs["IOR"].default_value = 1.45
|
||||
|
||||
# Also set the viewport display color for solid-view preview
|
||||
mat.diffuse_color = color
|
||||
|
||||
# Fake user so Blender keeps the material on save
|
||||
mat.use_fake_user = True
|
||||
|
||||
# Save
|
||||
out_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "material_library.blend")
|
||||
bpy.ops.wm.save_mainfile(filepath=out_path)
|
||||
print(f"\nSaved {len(MATERIALS)} materials to: {out_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,27 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# System dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libpq-dev \
|
||||
gcc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Docker SDK (for dynamic flamenco-worker scaling via /var/run/docker.sock)
|
||||
RUN pip install --no-cache-dir "docker>=6.1.0"
|
||||
|
||||
# Install Python dependencies
|
||||
COPY pyproject.toml .
|
||||
RUN pip install --no-cache-dir -e .
|
||||
|
||||
# Copy app code
|
||||
COPY . .
|
||||
|
||||
# Create upload dirs
|
||||
RUN mkdir -p uploads/step_files uploads/excel_files uploads/thumbnails
|
||||
|
||||
COPY start.sh /start.sh
|
||||
RUN chmod +x /start.sh
|
||||
|
||||
EXPOSE 8000
|
||||
@@ -0,0 +1,41 @@
|
||||
[alembic]
|
||||
script_location = alembic
|
||||
prepend_sys_path = .
|
||||
version_path_separator = os
|
||||
sqlalchemy.url = postgresql://schaeffler:schaeffler@localhost:5432/schaeffler
|
||||
|
||||
[post_write_hooks]
|
||||
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
Binary file not shown.
@@ -0,0 +1,62 @@
|
||||
import asyncio
|
||||
from logging.config import fileConfig
|
||||
from sqlalchemy import pool
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
from alembic import context
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
from app.database import Base
|
||||
from app.config import settings
|
||||
# Import all models to register them with Base
|
||||
import app.models # noqa: F401
|
||||
|
||||
config = context.config
|
||||
config.set_main_option("sqlalchemy.url", settings.database_url)
|
||||
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def do_run_migrations(connection: Connection) -> None:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
async def run_async_migrations() -> None:
|
||||
connectable = async_engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
async with connectable.connect() as connection:
|
||||
await connection.run_sync(do_run_migrations)
|
||||
await connectable.dispose()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
asyncio.run(run_async_migrations())
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -0,0 +1,26 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,153 @@
|
||||
"""initial schema
|
||||
|
||||
Revision ID: 001
|
||||
Revises:
|
||||
Create Date: 2026-03-01
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
revision: str = "001"
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# users
|
||||
op.create_table(
|
||||
"users",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("email", sa.String(255), nullable=False, unique=True),
|
||||
sa.Column("password_hash", sa.String(255), nullable=False),
|
||||
sa.Column("full_name", sa.String(255), nullable=False),
|
||||
sa.Column("role", sa.Enum("admin", "client", name="userrole"), nullable=False, server_default="client"),
|
||||
sa.Column("is_active", sa.Boolean, nullable=False, server_default="true"),
|
||||
sa.Column("created_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_users_email", "users", ["email"])
|
||||
|
||||
# templates
|
||||
op.create_table(
|
||||
"templates",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("name", sa.String(255), nullable=False),
|
||||
sa.Column("category_key", sa.String(100), nullable=False, unique=True),
|
||||
sa.Column("standard_fields", postgresql.JSONB, nullable=False, server_default="{}"),
|
||||
sa.Column("component_schema", postgresql.JSONB, nullable=False, server_default="{}"),
|
||||
sa.Column("description", sa.Text, nullable=True),
|
||||
sa.Column("is_active", sa.Boolean, nullable=False, server_default="true"),
|
||||
sa.Column("created_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_templates_category_key", "templates", ["category_key"])
|
||||
|
||||
# cad_files
|
||||
op.create_table(
|
||||
"cad_files",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("original_name", sa.String(500), nullable=False),
|
||||
sa.Column("stored_path", sa.String(1000), nullable=False),
|
||||
sa.Column("file_hash", sa.String(64), nullable=False, unique=True),
|
||||
sa.Column("file_size", sa.BigInteger, nullable=True),
|
||||
sa.Column("parsed_objects", postgresql.JSONB, nullable=True),
|
||||
sa.Column("thumbnail_path", sa.String(1000), nullable=True),
|
||||
sa.Column("gltf_path", sa.String(1000), nullable=True),
|
||||
sa.Column(
|
||||
"processing_status",
|
||||
sa.Enum("pending", "processing", "completed", "failed", name="processingstatus"),
|
||||
nullable=False,
|
||||
server_default="pending",
|
||||
),
|
||||
sa.Column("error_message", sa.String(2000), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_cad_files_file_hash", "cad_files", ["file_hash"])
|
||||
|
||||
# orders
|
||||
op.create_table(
|
||||
"orders",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("order_number", sa.String(50), nullable=False, unique=True),
|
||||
sa.Column("template_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("templates.id"), nullable=True),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.Enum("draft", "submitted", "processing", "completed", "rejected", name="orderstatus"),
|
||||
nullable=False,
|
||||
server_default="draft",
|
||||
),
|
||||
sa.Column("created_by", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=False),
|
||||
sa.Column("source_excel", sa.String(1000), nullable=True),
|
||||
sa.Column("notes", sa.Text, nullable=True),
|
||||
sa.Column("created_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_orders_order_number", "orders", ["order_number"])
|
||||
|
||||
# order_items
|
||||
op.create_table(
|
||||
"order_items",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("order_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("orders.id"), nullable=False),
|
||||
sa.Column("row_index", sa.Integer, nullable=False),
|
||||
sa.Column("ebene1", sa.String(500), nullable=True),
|
||||
sa.Column("ebene2", sa.String(500), nullable=True),
|
||||
sa.Column("baureihe", sa.String(500), nullable=True),
|
||||
sa.Column("pim_id", sa.String(500), nullable=True),
|
||||
sa.Column("produkt_baureihe", sa.String(500), nullable=True),
|
||||
sa.Column("gewaehltes_produkt", sa.String(500), nullable=True),
|
||||
sa.Column("name_cad_modell", sa.String(500), nullable=True),
|
||||
sa.Column("gewuenschte_bildnummer", sa.String(500), nullable=True),
|
||||
sa.Column("lagertyp", sa.String(500), nullable=True),
|
||||
sa.Column("medias_rendering", sa.Boolean, nullable=True),
|
||||
sa.Column("components", postgresql.JSONB, nullable=False, server_default="[]"),
|
||||
sa.Column("cad_file_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("cad_files.id"), nullable=True),
|
||||
sa.Column("thumbnail_path", sa.String(1000), nullable=True),
|
||||
sa.Column(
|
||||
"ai_validation_status",
|
||||
sa.Enum("not_started", "pending", "completed", "failed", name="aivalidationstatus"),
|
||||
nullable=False,
|
||||
server_default="not_started",
|
||||
),
|
||||
sa.Column("ai_validation_result", postgresql.JSONB, nullable=True),
|
||||
sa.Column(
|
||||
"item_status",
|
||||
sa.Enum("pending", "approved", "rejected", name="itemstatus"),
|
||||
nullable=False,
|
||||
server_default="pending",
|
||||
),
|
||||
sa.Column("notes", sa.Text, nullable=True),
|
||||
sa.Column("created_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
# audit_log
|
||||
op.create_table(
|
||||
"audit_log",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=True),
|
||||
sa.Column("action", sa.String(100), nullable=False),
|
||||
sa.Column("entity_type", sa.String(100), nullable=True),
|
||||
sa.Column("entity_id", sa.String(255), nullable=True),
|
||||
sa.Column("details", postgresql.JSONB, nullable=True),
|
||||
sa.Column("timestamp", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("audit_log")
|
||||
op.drop_table("order_items")
|
||||
op.drop_table("orders")
|
||||
op.drop_table("cad_files")
|
||||
op.drop_table("templates")
|
||||
op.drop_table("users")
|
||||
op.execute("DROP TYPE IF EXISTS userrole")
|
||||
op.execute("DROP TYPE IF EXISTS orderstatus")
|
||||
op.execute("DROP TYPE IF EXISTS processingstatus")
|
||||
op.execute("DROP TYPE IF EXISTS aivalidationstatus")
|
||||
op.execute("DROP TYPE IF EXISTS itemstatus")
|
||||
@@ -0,0 +1,33 @@
|
||||
"""system settings table
|
||||
|
||||
Revision ID: 002
|
||||
Revises: 001
|
||||
Create Date: 2026-03-01
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "002"
|
||||
down_revision: Union[str, None] = "001"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"system_settings",
|
||||
sa.Column("key", sa.String(100), primary_key=True),
|
||||
sa.Column("value", sa.Text(), nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True, server_default=sa.func.now()),
|
||||
)
|
||||
# Insert defaults
|
||||
op.execute(
|
||||
"INSERT INTO system_settings (key, value, updated_at) VALUES "
|
||||
"('thumbnail_renderer', 'pillow', NOW())"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("system_settings")
|
||||
@@ -0,0 +1,31 @@
|
||||
"""blender render settings
|
||||
|
||||
Revision ID: 003
|
||||
Revises: 002
|
||||
Create Date: 2026-03-01
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
|
||||
revision: str = "003"
|
||||
down_revision: Union[str, None] = "002"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"INSERT INTO system_settings (key, value, updated_at) VALUES "
|
||||
"('blender_engine', 'cycles', NOW()),"
|
||||
"('blender_cycles_samples', '256', NOW()),"
|
||||
"('blender_eevee_samples', '64', NOW()) "
|
||||
"ON CONFLICT (key) DO NOTHING"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
"DELETE FROM system_settings WHERE key IN "
|
||||
"('blender_engine', 'blender_cycles_samples', 'blender_eevee_samples')"
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""threejs render size setting
|
||||
|
||||
Revision ID: 004
|
||||
Revises: 003
|
||||
Create Date: 2026-03-01
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
|
||||
revision: str = "004"
|
||||
down_revision: Union[str, None] = "003"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"INSERT INTO system_settings (key, value, updated_at) VALUES "
|
||||
"('threejs_render_size', '512', NOW()) "
|
||||
"ON CONFLICT (key) DO NOTHING"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
"DELETE FROM system_settings WHERE key = 'threejs_render_size'"
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""set threejs_render_size default to 1024
|
||||
|
||||
Revision ID: 005
|
||||
Revises: 004
|
||||
Create Date: 2026-03-01
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
|
||||
revision: str = "005"
|
||||
down_revision: Union[str, None] = "004"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"UPDATE system_settings SET value = '1024', updated_at = NOW() "
|
||||
"WHERE key = 'threejs_render_size' AND value = '512'"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
"UPDATE system_settings SET value = '512', updated_at = NOW() "
|
||||
"WHERE key = 'threejs_render_size' AND value = '1024'"
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""thumbnail format setting (jpg | png)
|
||||
|
||||
Revision ID: 006
|
||||
Revises: 005
|
||||
Create Date: 2026-03-01
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
|
||||
revision: str = "006"
|
||||
down_revision: Union[str, None] = "005"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"INSERT INTO system_settings (key, value, updated_at) VALUES "
|
||||
"('thumbnail_format', 'jpg', NOW()) "
|
||||
"ON CONFLICT (key) DO NOTHING"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
"DELETE FROM system_settings WHERE key = 'thumbnail_format'"
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
"""materials table and cad_part_materials column
|
||||
|
||||
Revision ID: 007
|
||||
Revises: 006
|
||||
Create Date: 2026-03-01
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
|
||||
revision: str = "007"
|
||||
down_revision: Union[str, None] = "006"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"materials",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("name", sa.String(200), nullable=False, unique=True),
|
||||
sa.Column("description", sa.Text, nullable=True),
|
||||
sa.Column("created_at", sa.DateTime, server_default=sa.text("NOW()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime, server_default=sa.text("NOW()"), nullable=False),
|
||||
)
|
||||
op.execute("ALTER TABLE order_items ADD COLUMN IF NOT EXISTS cad_part_materials JSONB NOT NULL DEFAULT '[]'::jsonb")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("ALTER TABLE order_items DROP COLUMN IF EXISTS cad_part_materials")
|
||||
op.drop_table("materials")
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Add created_by and source to materials
|
||||
|
||||
Revision ID: 008
|
||||
Revises: 007
|
||||
Create Date: 2026-03-01
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
revision: str = "008"
|
||||
down_revision: Union[str, None] = "007"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"ALTER TABLE materials "
|
||||
"ADD COLUMN IF NOT EXISTS created_by UUID REFERENCES users(id) ON DELETE SET NULL, "
|
||||
"ADD COLUMN IF NOT EXISTS source VARCHAR(20) NOT NULL DEFAULT 'manual'"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
"ALTER TABLE materials "
|
||||
"DROP COLUMN IF EXISTS created_by, "
|
||||
"DROP COLUMN IF EXISTS source"
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Add render_log JSONB column to cad_files
|
||||
|
||||
Revision ID: 009
|
||||
Revises: 008
|
||||
Create Date: 2026-03-01
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
|
||||
revision: str = "009"
|
||||
down_revision: Union[str, None] = "008"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"ALTER TABLE cad_files "
|
||||
"ADD COLUMN IF NOT EXISTS render_log JSONB"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
"ALTER TABLE cad_files "
|
||||
"DROP COLUMN IF EXISTS render_log"
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
"""KPI analytics and pricing tiers
|
||||
|
||||
Revision ID: 010
|
||||
Revises: 009
|
||||
Create Date: 2026-03-02
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "010"
|
||||
down_revision: Union[str, None] = "009"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add project_manager to userrole enum — must be outside a transaction
|
||||
op.execute("COMMIT")
|
||||
op.execute("ALTER TYPE userrole ADD VALUE IF NOT EXISTS 'project_manager'")
|
||||
op.execute("BEGIN")
|
||||
|
||||
# Lifecycle timestamps + estimated_price on orders
|
||||
op.execute(
|
||||
"ALTER TABLE orders "
|
||||
"ADD COLUMN IF NOT EXISTS submitted_at TIMESTAMP WITHOUT TIME ZONE"
|
||||
)
|
||||
op.execute(
|
||||
"ALTER TABLE orders "
|
||||
"ADD COLUMN IF NOT EXISTS processing_started_at TIMESTAMP WITHOUT TIME ZONE"
|
||||
)
|
||||
op.execute(
|
||||
"ALTER TABLE orders "
|
||||
"ADD COLUMN IF NOT EXISTS completed_at TIMESTAMP WITHOUT TIME ZONE"
|
||||
)
|
||||
op.execute(
|
||||
"ALTER TABLE orders "
|
||||
"ADD COLUMN IF NOT EXISTS rejected_at TIMESTAMP WITHOUT TIME ZONE"
|
||||
)
|
||||
op.execute(
|
||||
"ALTER TABLE orders "
|
||||
"ADD COLUMN IF NOT EXISTS estimated_price NUMERIC(12, 2)"
|
||||
)
|
||||
|
||||
# pricing_tiers table
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS pricing_tiers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
category_key VARCHAR(100) NOT NULL,
|
||||
quality_level VARCHAR(50) NOT NULL DEFAULT 'Normal',
|
||||
price_per_item NUMERIC(10, 2) NOT NULL,
|
||||
description TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_pricing_tier UNIQUE (category_key, quality_level)
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"CREATE INDEX IF NOT EXISTS ix_pricing_tiers_category_key "
|
||||
"ON pricing_tiers (category_key)"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP TABLE IF EXISTS pricing_tiers")
|
||||
op.execute("ALTER TABLE orders DROP COLUMN IF EXISTS estimated_price")
|
||||
op.execute("ALTER TABLE orders DROP COLUMN IF EXISTS rejected_at")
|
||||
op.execute("ALTER TABLE orders DROP COLUMN IF EXISTS completed_at")
|
||||
op.execute("ALTER TABLE orders DROP COLUMN IF EXISTS processing_started_at")
|
||||
op.execute("ALTER TABLE orders DROP COLUMN IF EXISTS submitted_at")
|
||||
# Note: removing enum values is not supported in PostgreSQL without full recreation
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Product library — products, output_types, order_lines tables
|
||||
|
||||
Revision ID: 011
|
||||
Revises: 010
|
||||
Create Date: 2026-03-02
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "011"
|
||||
down_revision: Union[str, None] = "010"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
pim_id VARCHAR(500) UNIQUE NOT NULL,
|
||||
name VARCHAR(500),
|
||||
category_key VARCHAR(100),
|
||||
ebene1 VARCHAR(500),
|
||||
ebene2 VARCHAR(500),
|
||||
baureihe VARCHAR(500),
|
||||
produkt_baureihe VARCHAR(500),
|
||||
lagertyp VARCHAR(500),
|
||||
name_cad_modell VARCHAR(500),
|
||||
components JSONB NOT NULL DEFAULT '[]',
|
||||
cad_part_materials JSONB NOT NULL DEFAULT '[]',
|
||||
cad_file_id UUID REFERENCES cad_files(id) ON DELETE SET NULL,
|
||||
notes TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
source_excel VARCHAR(1000),
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute("CREATE INDEX IF NOT EXISTS ix_products_category_key ON products (category_key)")
|
||||
op.execute("CREATE INDEX IF NOT EXISTS ix_products_name_cad_modell ON products (name_cad_modell)")
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS output_types (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(200) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
renderer VARCHAR(50) NOT NULL DEFAULT 'threejs',
|
||||
render_settings JSONB NOT NULL DEFAULT '{}',
|
||||
output_format VARCHAR(20) NOT NULL DEFAULT 'png',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS order_lines (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
|
||||
product_id UUID NOT NULL REFERENCES products(id),
|
||||
output_type_id UUID REFERENCES output_types(id),
|
||||
gewuenschte_bildnummer VARCHAR(500),
|
||||
item_status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
render_status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
result_path VARCHAR(1000),
|
||||
render_log JSONB,
|
||||
ai_validation_status VARCHAR(20) NOT NULL DEFAULT 'not_started',
|
||||
ai_validation_result JSONB,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute("CREATE INDEX IF NOT EXISTS ix_order_lines_order_id ON order_lines (order_id)")
|
||||
op.execute("CREATE INDEX IF NOT EXISTS ix_order_lines_product_id ON order_lines (product_id)")
|
||||
# Partial unique indexes to handle NULL output_type_id correctly
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_order_lines_tracking "
|
||||
"ON order_lines (order_id, product_id) "
|
||||
"WHERE output_type_id IS NULL"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_order_lines_render "
|
||||
"ON order_lines (order_id, product_id, output_type_id) "
|
||||
"WHERE output_type_id IS NOT NULL"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP TABLE IF EXISTS order_lines")
|
||||
op.execute("DROP TABLE IF EXISTS output_types")
|
||||
op.execute("DROP TABLE IF EXISTS products")
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Backfill products and order_lines from order_items
|
||||
|
||||
Revision ID: 012
|
||||
Revises: 011
|
||||
Create Date: 2026-03-02
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
|
||||
revision: str = "012"
|
||||
down_revision: Union[str, None] = "011"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# 1. Seed default output type
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO output_types (id, name, renderer, output_format, sort_order)
|
||||
VALUES (gen_random_uuid(), '3D Thumbnail', 'threejs', 'png', 0)
|
||||
ON CONFLICT (name) DO NOTHING
|
||||
"""
|
||||
)
|
||||
|
||||
# 2. Create products from distinct pim_id in order_items
|
||||
# For each distinct pim_id, take fields from the most recently updated row
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO products (
|
||||
pim_id, name, category_key, ebene1, ebene2, baureihe,
|
||||
produkt_baureihe, lagertyp, name_cad_modell,
|
||||
components, cad_part_materials, cad_file_id, source_excel
|
||||
)
|
||||
SELECT DISTINCT ON (oi.pim_id)
|
||||
oi.pim_id,
|
||||
oi.gewaehltes_produkt AS name,
|
||||
t.category_key,
|
||||
oi.ebene1,
|
||||
oi.ebene2,
|
||||
oi.baureihe,
|
||||
oi.produkt_baureihe,
|
||||
oi.lagertyp,
|
||||
oi.name_cad_modell,
|
||||
oi.components,
|
||||
COALESCE(oi.cad_part_materials, '[]'::jsonb) AS cad_part_materials,
|
||||
oi.cad_file_id,
|
||||
o.source_excel
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
LEFT JOIN templates t ON t.id = o.template_id
|
||||
WHERE oi.pim_id IS NOT NULL
|
||||
AND oi.pim_id <> ''
|
||||
ORDER BY oi.pim_id, oi.updated_at DESC
|
||||
ON CONFLICT (pim_id) DO NOTHING
|
||||
"""
|
||||
)
|
||||
|
||||
# 3. Create order_lines from order_items where pim_id IS NOT NULL
|
||||
# 3a. Rows with medias_rendering = true → link to '3D Thumbnail' output type
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO order_lines (
|
||||
order_id, product_id, output_type_id,
|
||||
gewuenschte_bildnummer, item_status, render_status,
|
||||
ai_validation_status, ai_validation_result, notes
|
||||
)
|
||||
SELECT
|
||||
oi.order_id,
|
||||
p.id AS product_id,
|
||||
ot.id AS output_type_id,
|
||||
oi.gewuenschte_bildnummer,
|
||||
oi.item_status::TEXT,
|
||||
CASE
|
||||
WHEN oi.cad_file_id IS NOT NULL THEN 'pending'
|
||||
ELSE 'pending'
|
||||
END AS render_status,
|
||||
oi.ai_validation_status::TEXT,
|
||||
oi.ai_validation_result,
|
||||
oi.notes
|
||||
FROM order_items oi
|
||||
JOIN products p ON p.pim_id = oi.pim_id
|
||||
JOIN output_types ot ON ot.name = '3D Thumbnail'
|
||||
WHERE oi.pim_id IS NOT NULL
|
||||
AND oi.pim_id <> ''
|
||||
AND oi.medias_rendering = TRUE
|
||||
ON CONFLICT DO NOTHING
|
||||
"""
|
||||
)
|
||||
|
||||
# 3b. Rows with medias_rendering = false → tracking only (no output_type_id)
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO order_lines (
|
||||
order_id, product_id, output_type_id,
|
||||
gewuenschte_bildnummer, item_status, render_status,
|
||||
ai_validation_status, ai_validation_result, notes
|
||||
)
|
||||
SELECT
|
||||
oi.order_id,
|
||||
p.id AS product_id,
|
||||
NULL AS output_type_id,
|
||||
oi.gewuenschte_bildnummer,
|
||||
oi.item_status::TEXT,
|
||||
'pending' AS render_status,
|
||||
oi.ai_validation_status::TEXT,
|
||||
oi.ai_validation_result,
|
||||
oi.notes
|
||||
FROM order_items oi
|
||||
JOIN products p ON p.pim_id = oi.pim_id
|
||||
WHERE oi.pim_id IS NOT NULL
|
||||
AND oi.pim_id <> ''
|
||||
AND (oi.medias_rendering IS NULL OR oi.medias_rendering = FALSE)
|
||||
ON CONFLICT DO NOTHING
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DELETE FROM order_lines")
|
||||
op.execute("DELETE FROM products")
|
||||
op.execute("DELETE FROM output_types WHERE name = '3D Thumbnail'")
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Add gewuenschte_bildnummer and medias_rendering to products
|
||||
|
||||
Revision ID: 013
|
||||
Revises: 012
|
||||
Create Date: 2026-03-02
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "013"
|
||||
down_revision: Union[str, None] = "012"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("products", sa.Column("gewuenschte_bildnummer", sa.String(500), nullable=True))
|
||||
op.add_column("products", sa.Column("medias_rendering", sa.Boolean(), nullable=True))
|
||||
|
||||
# Backfill from order_items where available
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE products p
|
||||
SET gewuenschte_bildnummer = sub.gewuenschte_bildnummer,
|
||||
medias_rendering = sub.medias_rendering
|
||||
FROM (
|
||||
SELECT DISTINCT ON (oi.pim_id)
|
||||
oi.pim_id,
|
||||
oi.gewuenschte_bildnummer,
|
||||
oi.medias_rendering
|
||||
FROM order_items oi
|
||||
WHERE oi.pim_id IS NOT NULL AND oi.pim_id <> ''
|
||||
ORDER BY oi.pim_id, oi.updated_at DESC
|
||||
) sub
|
||||
WHERE p.pim_id = sub.pim_id
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("products", "medias_rendering")
|
||||
op.drop_column("products", "gewuenschte_bildnummer")
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Add compatible_categories to output_types
|
||||
|
||||
Revision ID: 014
|
||||
Revises: 013
|
||||
Create Date: 2026-03-02
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
revision: str = "014"
|
||||
down_revision: Union[str, None] = "013"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"output_types",
|
||||
sa.Column("compatible_categories", JSONB, server_default="[]", nullable=False),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("output_types", "compatible_categories")
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Add Flamenco render backend support
|
||||
|
||||
Revision ID: 015
|
||||
Revises: 014
|
||||
Create Date: 2026-03-02
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "015"
|
||||
down_revision: Union[str, None] = "014"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# output_types: render_backend + is_animation
|
||||
op.add_column(
|
||||
"output_types",
|
||||
sa.Column("render_backend", sa.String(20), server_default="auto", nullable=False),
|
||||
)
|
||||
op.add_column(
|
||||
"output_types",
|
||||
sa.Column("is_animation", sa.Boolean(), server_default="false", nullable=False),
|
||||
)
|
||||
|
||||
# order_lines: flamenco tracking columns
|
||||
op.add_column(
|
||||
"order_lines",
|
||||
sa.Column("flamenco_job_id", sa.String(100), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"order_lines",
|
||||
sa.Column("render_backend_used", sa.String(20), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"order_lines",
|
||||
sa.Column("render_started_at", sa.DateTime(), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"order_lines",
|
||||
sa.Column("render_completed_at", sa.DateTime(), nullable=True),
|
||||
)
|
||||
|
||||
# Seed system settings for Flamenco
|
||||
op.execute(
|
||||
"INSERT INTO system_settings (key, value) VALUES "
|
||||
"('render_backend', 'celery'), "
|
||||
"('flamenco_manager_url', 'http://flamenco-manager:8080'), "
|
||||
"('flamenco_worker_count', '1') "
|
||||
"ON CONFLICT (key) DO NOTHING"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("order_lines", "render_completed_at")
|
||||
op.drop_column("order_lines", "render_started_at")
|
||||
op.drop_column("order_lines", "render_backend_used")
|
||||
op.drop_column("order_lines", "flamenco_job_id")
|
||||
op.drop_column("output_types", "is_animation")
|
||||
op.drop_column("output_types", "render_backend")
|
||||
|
||||
op.execute("DELETE FROM system_settings WHERE key IN ('render_backend', 'flamenco_manager_url', 'flamenco_worker_count')")
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Pricing enhancements: OutputType→PricingTier link, per-line unit_price, transparent_bg, default tier
|
||||
|
||||
Revision ID: 016
|
||||
Revises: 015
|
||||
Create Date: 2026-03-02
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "016"
|
||||
down_revision: Union[str, None] = "015"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# OutputType → PricingTier link
|
||||
op.add_column("output_types", sa.Column("pricing_tier_id", sa.Integer(), nullable=True))
|
||||
op.create_foreign_key(
|
||||
"fk_output_types_pricing_tier_id",
|
||||
"output_types",
|
||||
"pricing_tiers",
|
||||
["pricing_tier_id"],
|
||||
["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
op.create_index("ix_output_types_pricing_tier_id", "output_types", ["pricing_tier_id"])
|
||||
|
||||
# Transparent background option for Blender PNG renders
|
||||
op.add_column("output_types", sa.Column(
|
||||
"transparent_bg", sa.Boolean(), nullable=False, server_default="false",
|
||||
))
|
||||
|
||||
# Per-line price snapshot
|
||||
op.add_column("order_lines", sa.Column("unit_price", sa.Numeric(10, 2), nullable=True))
|
||||
|
||||
# Seed global default tier (idempotent via ON CONFLICT)
|
||||
op.execute("""
|
||||
INSERT INTO pricing_tiers (category_key, quality_level, price_per_item, description, is_active, created_at, updated_at)
|
||||
VALUES ('default', 'Normal', 25.00, 'Global fallback price', true, NOW(), NOW())
|
||||
ON CONFLICT ON CONSTRAINT uq_pricing_tier DO NOTHING
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("order_lines", "unit_price")
|
||||
op.drop_column("output_types", "transparent_bg")
|
||||
op.drop_index("ix_output_types_pricing_tier_id", table_name="output_types")
|
||||
op.drop_constraint("fk_output_types_pricing_tier_id", "output_types", type_="foreignkey")
|
||||
op.drop_column("output_types", "pricing_tier_id")
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Fix stale order_lines.item_status: auto-approve lines for non-draft orders
|
||||
|
||||
Revision ID: 017
|
||||
Revises: 016
|
||||
Create Date: 2026-03-02
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
|
||||
revision: str = "017"
|
||||
down_revision: Union[str, None] = "016"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Order lines belonging to submitted/processing/completed orders should
|
||||
# be "approved", not stuck at "pending". The new Product Library workflow
|
||||
# has no per-item approval step — submission implies approval.
|
||||
op.execute("""
|
||||
UPDATE order_lines
|
||||
SET item_status = 'approved'
|
||||
WHERE item_status = 'pending'
|
||||
AND order_id IN (
|
||||
SELECT id FROM orders
|
||||
WHERE status IN ('submitted', 'processing', 'completed')
|
||||
)
|
||||
""")
|
||||
|
||||
# Lines belonging to rejected orders should be "rejected".
|
||||
op.execute("""
|
||||
UPDATE order_lines
|
||||
SET item_status = 'rejected'
|
||||
WHERE item_status = 'pending'
|
||||
AND order_id IN (
|
||||
SELECT id FROM orders WHERE status = 'rejected'
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Cannot reliably revert — the original values were all "pending" anyway.
|
||||
pass
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Render templates — .blend file templates per category/output type
|
||||
|
||||
Revision ID: 018
|
||||
Revises: 017
|
||||
Create Date: 2026-03-02
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "018"
|
||||
down_revision: Union[str, None] = "017"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"render_templates",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("name", sa.String(300), nullable=False),
|
||||
sa.Column("category_key", sa.String(100), nullable=True),
|
||||
sa.Column("output_type_id", UUID(as_uuid=True), sa.ForeignKey("output_types.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("blend_file_path", sa.Text, nullable=False),
|
||||
sa.Column("original_filename", sa.String(500), nullable=False),
|
||||
sa.Column("target_collection", sa.String(200), server_default="Product", nullable=False),
|
||||
sa.Column("material_replace_enabled", sa.Boolean, server_default="false", nullable=False),
|
||||
sa.Column("is_active", sa.Boolean, server_default="true", nullable=False),
|
||||
sa.Column("created_at", sa.DateTime, server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime, server_default=sa.text("now()"), nullable=False),
|
||||
)
|
||||
|
||||
# Unique constraint: one active template per (category_key, output_type_id) combo
|
||||
op.create_index(
|
||||
"ix_render_templates_active_unique",
|
||||
"render_templates",
|
||||
["category_key", "output_type_id"],
|
||||
unique=True,
|
||||
postgresql_where=sa.text("is_active = true"),
|
||||
)
|
||||
|
||||
# Seed material_library_path setting
|
||||
op.execute(
|
||||
"INSERT INTO system_settings (key, value, updated_at) "
|
||||
"VALUES ('material_library_path', '', now()) "
|
||||
"ON CONFLICT (key) DO NOTHING"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_render_templates_active_unique", table_name="render_templates")
|
||||
op.drop_table("render_templates")
|
||||
op.execute("DELETE FROM system_settings WHERE key = 'material_library_path'")
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Schaeffler standard materials — add schaeffler_code column and seed 35 materials
|
||||
|
||||
Revision ID: 019
|
||||
Revises: 018
|
||||
Create Date: 2026-03-02
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
revision = "019"
|
||||
down_revision = "018"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("materials", sa.Column("schaeffler_code", sa.Integer(), nullable=True))
|
||||
|
||||
from app.data.schaeffler_materials import SCHAEFFLER_MATERIALS
|
||||
|
||||
conn = op.get_bind()
|
||||
now = datetime.utcnow().isoformat()
|
||||
for mat in SCHAEFFLER_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"VALUES ('{uuid.uuid4()}', '{name}', '{desc}', '{mat['source']}', "
|
||||
f"{mat['schaeffler_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")
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Material aliases — substitution/alias system for material name resolution
|
||||
|
||||
Revision ID: 020
|
||||
Revises: 019
|
||||
Create Date: 2026-03-02
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
revision = "020"
|
||||
down_revision = "019"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create material_aliases table
|
||||
op.create_table(
|
||||
"material_aliases",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True, default=uuid.uuid4),
|
||||
sa.Column(
|
||||
"material_id",
|
||||
UUID(as_uuid=True),
|
||||
sa.ForeignKey("materials.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("alias", sa.String(300), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
# Case-insensitive unique index on alias
|
||||
op.create_index(
|
||||
"uq_material_aliases_alias_lower",
|
||||
"material_aliases",
|
||||
[sa.text("lower(alias)")],
|
||||
unique=True,
|
||||
)
|
||||
|
||||
# Index on material_id for FK lookups
|
||||
op.create_index(
|
||||
"ix_material_aliases_material_id",
|
||||
"material_aliases",
|
||||
["material_id"],
|
||||
)
|
||||
|
||||
# Seed aliases from naming_scheme.xlsx Materialmapping data
|
||||
_seed_aliases()
|
||||
|
||||
|
||||
def _seed_aliases() -> None:
|
||||
from app.data.material_alias_seeds import MATERIAL_ALIAS_SEEDS
|
||||
|
||||
conn = op.get_bind()
|
||||
|
||||
for entry in MATERIAL_ALIAS_SEEDS:
|
||||
material_name = entry["material_name"]
|
||||
|
||||
# Look up material by name
|
||||
result = conn.execute(
|
||||
sa.text("SELECT id FROM materials WHERE name = :name"),
|
||||
{"name": material_name},
|
||||
)
|
||||
row = result.fetchone()
|
||||
if not row:
|
||||
# Material not seeded yet, skip
|
||||
continue
|
||||
|
||||
material_id = row[0]
|
||||
|
||||
for alias_str in entry["aliases"]:
|
||||
# Skip if alias already exists (case-insensitive)
|
||||
existing = conn.execute(
|
||||
sa.text("SELECT id FROM material_aliases WHERE lower(alias) = lower(:alias)"),
|
||||
{"alias": alias_str},
|
||||
)
|
||||
if existing.fetchone():
|
||||
continue
|
||||
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"INSERT INTO material_aliases (id, material_id, alias, created_at) "
|
||||
"VALUES (:id, :material_id, :alias, :created_at)"
|
||||
),
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"material_id": str(material_id),
|
||||
"alias": alias_str,
|
||||
"created_at": datetime.utcnow(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_material_aliases_material_id", table_name="material_aliases")
|
||||
op.drop_index("uq_material_aliases_alias_lower", table_name="material_aliases")
|
||||
op.drop_table("material_aliases")
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Notification center — add target_user_id, read_at, notification to audit_log
|
||||
|
||||
Revision ID: 021
|
||||
Revises: 020
|
||||
Create Date: 2026-03-03
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
revision = "021"
|
||||
down_revision = "020"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"audit_log",
|
||||
sa.Column(
|
||||
"target_user_id",
|
||||
UUID(as_uuid=True),
|
||||
sa.ForeignKey("users.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"audit_log",
|
||||
sa.Column("read_at", sa.DateTime(), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"audit_log",
|
||||
sa.Column(
|
||||
"notification",
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default=sa.text("false"),
|
||||
),
|
||||
)
|
||||
|
||||
# Composite index for user notification queries
|
||||
op.create_index(
|
||||
"ix_audit_log_target_notification",
|
||||
"audit_log",
|
||||
["target_user_id", "notification", "read_at"],
|
||||
)
|
||||
|
||||
# Partial index for listing recent notifications
|
||||
op.create_index(
|
||||
"ix_audit_log_notification_ts",
|
||||
"audit_log",
|
||||
["notification", "timestamp"],
|
||||
postgresql_where=sa.text("notification = true"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_audit_log_notification_ts", table_name="audit_log")
|
||||
op.drop_index("ix_audit_log_target_notification", table_name="audit_log")
|
||||
op.drop_column("audit_log", "notification")
|
||||
op.drop_column("audit_log", "read_at")
|
||||
op.drop_column("audit_log", "target_user_id")
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Product variants — per-product material variant support
|
||||
|
||||
Revision ID: 022
|
||||
Revises: 021
|
||||
Create Date: 2026-03-03
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
|
||||
revision = "022"
|
||||
down_revision = "021"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# --- New table: product_variants ---
|
||||
op.create_table(
|
||||
"product_variants",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("product_id", UUID(as_uuid=True), sa.ForeignKey("products.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("name", sa.String(500), nullable=False),
|
||||
sa.Column("gewuenschte_bildnummer", sa.String(500), nullable=True),
|
||||
sa.Column("components", JSONB, nullable=False, server_default="[]"),
|
||||
sa.Column("is_default", sa.Boolean, nullable=False, server_default="false"),
|
||||
sa.Column("source_excel", sa.String(1000), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime, server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime, server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
# Unique constraint: (product_id, lower(name))
|
||||
op.create_index(
|
||||
"uq_product_variants_product_name",
|
||||
"product_variants",
|
||||
[sa.text("product_id"), sa.text("lower(name)")],
|
||||
unique=True,
|
||||
)
|
||||
|
||||
# --- Alter products ---
|
||||
op.add_column("products", sa.Column("arbeitspaket", sa.String(500), nullable=True))
|
||||
|
||||
# Drop existing unique constraint on pim_id — PIM-ID is a class-level
|
||||
# identifier shared by many products so it must NOT be unique.
|
||||
op.drop_constraint("products_pim_id_key", "products", type_="unique")
|
||||
op.create_index(
|
||||
"uq_products_produkt_baureihe",
|
||||
"products",
|
||||
[sa.text("lower(produkt_baureihe)")],
|
||||
unique=True,
|
||||
postgresql_where=sa.text("produkt_baureihe IS NOT NULL AND is_active = true"),
|
||||
)
|
||||
|
||||
# --- Alter order_lines ---
|
||||
op.add_column(
|
||||
"order_lines",
|
||||
sa.Column(
|
||||
"variant_id",
|
||||
UUID(as_uuid=True),
|
||||
sa.ForeignKey("product_variants.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
),
|
||||
)
|
||||
|
||||
# --- Backfill: create default variants for existing products ---
|
||||
op.execute("""
|
||||
INSERT INTO product_variants (id, product_id, name, components, is_default, source_excel, created_at, updated_at)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
p.id,
|
||||
COALESCE(p.name, p.pim_id),
|
||||
COALESCE(p.components, '[]'::jsonb),
|
||||
true,
|
||||
p.source_excel,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM products p
|
||||
WHERE p.name IS NOT NULL OR p.pim_id IS NOT NULL
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("order_lines", "variant_id")
|
||||
op.drop_index("uq_products_produkt_baureihe", "products")
|
||||
op.create_unique_constraint("products_pim_id_key", "products", ["pim_id"])
|
||||
op.drop_column("products", "arbeitspaket")
|
||||
op.drop_index("uq_product_variants_product_name", "product_variants")
|
||||
op.drop_table("product_variants")
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Fix order_line unique constraints to include variant_id
|
||||
|
||||
The old constraints (order_id, product_id) caused 409 errors when
|
||||
multiple variants of the same product were added to the same order.
|
||||
New constraints use COALESCE(variant_id, nil_uuid) so different
|
||||
variants of the same product can coexist.
|
||||
|
||||
Revision ID: 023
|
||||
Revises: 022
|
||||
Create Date: 2026-03-03
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
revision = "023"
|
||||
down_revision = "022"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
NIL_UUID = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Drop old constraints
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_tracking")
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_render")
|
||||
|
||||
# Recreate with variant_id included (COALESCE handles NULLs)
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_tracking "
|
||||
f"ON order_lines (order_id, product_id, COALESCE(variant_id, '{NIL_UUID}'::uuid)) "
|
||||
"WHERE output_type_id IS NULL"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_render "
|
||||
"ON order_lines (order_id, product_id, output_type_id, "
|
||||
f"COALESCE(variant_id, '{NIL_UUID}'::uuid)) "
|
||||
"WHERE output_type_id IS NOT NULL"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_tracking")
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_render")
|
||||
|
||||
# Restore original constraints without variant_id
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_tracking "
|
||||
"ON order_lines (order_id, product_id) "
|
||||
"WHERE output_type_id IS NULL"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_render "
|
||||
"ON order_lines (order_id, product_id, output_type_id) "
|
||||
"WHERE output_type_id IS NOT NULL"
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Add lighting_only column to render_templates
|
||||
|
||||
When lighting_only=True the render script uses the template's World/HDRI for
|
||||
lighting but always computes an auto-camera for product framing. This is
|
||||
useful for HDR-only templates that don't define a fixed camera angle.
|
||||
|
||||
Revision ID: 024
|
||||
Revises: 023
|
||||
Create Date: 2026-03-03
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '024'
|
||||
down_revision = '023'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column(
|
||||
'render_templates',
|
||||
sa.Column(
|
||||
'lighting_only',
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default='false',
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('render_templates', 'lighting_only')
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Add cycles_device column to output_types
|
||||
|
||||
Revision ID: 025
|
||||
Revises: 024
|
||||
Create Date: 2026-03-03
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '025'
|
||||
down_revision = '024'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column(
|
||||
'output_types',
|
||||
sa.Column('cycles_device', sa.String(10), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('output_types', 'cycles_device')
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Add shadow_catcher_enabled to render_templates.
|
||||
|
||||
Revision ID: 026
|
||||
Revises: 025
|
||||
Create Date: 2026-03-03
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '026'
|
||||
down_revision = '025'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column(
|
||||
'render_templates',
|
||||
sa.Column('shadow_catcher_enabled', sa.Boolean(), nullable=False,
|
||||
server_default='false'),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('render_templates', 'shadow_catcher_enabled')
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Remove product variant system — products are unique, no variant concept.
|
||||
|
||||
Revision ID: 027
|
||||
Revises: 026
|
||||
Create Date: 2026-03-03
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "027"
|
||||
down_revision = "026"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
NIL_UUID = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Drop variant-aware unique indexes on order_lines
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_tracking")
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_render")
|
||||
|
||||
# Drop variant_id column from order_lines
|
||||
op.execute("ALTER TABLE order_lines DROP COLUMN IF EXISTS variant_id")
|
||||
|
||||
# Deduplicate tracking-only lines (output_type_id IS NULL) — keep the newest row per
|
||||
# (order_id, product_id) pair so the unique index can be created cleanly.
|
||||
op.execute("""
|
||||
DELETE FROM order_lines
|
||||
WHERE output_type_id IS NULL
|
||||
AND id NOT IN (
|
||||
SELECT DISTINCT ON (order_id, product_id) id
|
||||
FROM order_lines
|
||||
WHERE output_type_id IS NULL
|
||||
ORDER BY order_id, product_id, created_at DESC
|
||||
)
|
||||
""")
|
||||
|
||||
# Deduplicate render lines — keep the newest row per (order_id, product_id, output_type_id).
|
||||
op.execute("""
|
||||
DELETE FROM order_lines
|
||||
WHERE output_type_id IS NOT NULL
|
||||
AND id NOT IN (
|
||||
SELECT DISTINCT ON (order_id, product_id, output_type_id) id
|
||||
FROM order_lines
|
||||
WHERE output_type_id IS NOT NULL
|
||||
ORDER BY order_id, product_id, output_type_id, created_at DESC
|
||||
)
|
||||
""")
|
||||
|
||||
# Recreate simpler unique indexes without variant_id
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_tracking "
|
||||
"ON order_lines (order_id, product_id) "
|
||||
"WHERE output_type_id IS NULL"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_render "
|
||||
"ON order_lines (order_id, product_id, output_type_id) "
|
||||
"WHERE output_type_id IS NOT NULL"
|
||||
)
|
||||
|
||||
# Drop product_variants table (CASCADE removes its indexes automatically)
|
||||
op.execute("DROP TABLE IF EXISTS product_variants CASCADE")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Recreate product_variants table
|
||||
op.execute("""
|
||||
CREATE TABLE product_variants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
name VARCHAR(500) NOT NULL,
|
||||
gewuenschte_bildnummer VARCHAR(500),
|
||||
components JSONB NOT NULL DEFAULT '[]',
|
||||
is_default BOOLEAN NOT NULL DEFAULT false,
|
||||
source_excel VARCHAR(1000),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
)
|
||||
""")
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_product_variants_product_name "
|
||||
"ON product_variants (product_id, lower(name))"
|
||||
)
|
||||
op.execute("CREATE INDEX ON product_variants (product_id)")
|
||||
|
||||
# Add back variant_id to order_lines
|
||||
op.execute(
|
||||
"ALTER TABLE order_lines ADD COLUMN variant_id UUID "
|
||||
"REFERENCES product_variants(id) ON DELETE SET NULL"
|
||||
)
|
||||
|
||||
# Drop simple indexes and restore variant-aware indexes
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_tracking")
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_render")
|
||||
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_tracking "
|
||||
f"ON order_lines (order_id, product_id, COALESCE(variant_id, '{NIL_UUID}'::uuid)) "
|
||||
"WHERE output_type_id IS NULL"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_render "
|
||||
"ON order_lines (order_id, product_id, output_type_id, "
|
||||
f"COALESCE(variant_id, '{NIL_UUID}'::uuid)) "
|
||||
"WHERE output_type_id IS NOT NULL"
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Add product_render_positions table and render_position_id on order_lines.
|
||||
|
||||
Revision ID: 028
|
||||
Revises: 027
|
||||
Create Date: 2026-03-04
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "028"
|
||||
down_revision = "027"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
NIL_UUID = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ── New table: product_render_positions ──────────────────────────────────
|
||||
op.execute("""
|
||||
CREATE TABLE product_render_positions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
rotation_x DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
rotation_y DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
rotation_z DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
is_default BOOLEAN NOT NULL DEFAULT false,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
)
|
||||
""")
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_render_positions_product_name "
|
||||
"ON product_render_positions (product_id, lower(name))"
|
||||
)
|
||||
op.execute("CREATE INDEX ix_render_positions_product_id ON product_render_positions (product_id)")
|
||||
|
||||
# ── Add render_position_id to order_lines ────────────────────────────────
|
||||
op.execute(
|
||||
"ALTER TABLE order_lines ADD COLUMN render_position_id UUID "
|
||||
"REFERENCES product_render_positions(id) ON DELETE SET NULL"
|
||||
)
|
||||
|
||||
# ── Update unique indexes to include position ─────────────────────────────
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_tracking")
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_render")
|
||||
|
||||
op.execute(
|
||||
f"CREATE UNIQUE INDEX uq_order_lines_tracking "
|
||||
f"ON order_lines (order_id, product_id, COALESCE(render_position_id, '{NIL_UUID}'::uuid)) "
|
||||
f"WHERE output_type_id IS NULL"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE UNIQUE INDEX uq_order_lines_render "
|
||||
f"ON order_lines (order_id, product_id, output_type_id, "
|
||||
f"COALESCE(render_position_id, '{NIL_UUID}'::uuid)) "
|
||||
f"WHERE output_type_id IS NOT NULL"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Restore original unique indexes (without position)
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_tracking")
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_render")
|
||||
|
||||
op.execute("ALTER TABLE order_lines DROP COLUMN IF EXISTS render_position_id")
|
||||
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_tracking "
|
||||
"ON order_lines (order_id, product_id) "
|
||||
"WHERE output_type_id IS NULL"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_render "
|
||||
"ON order_lines (order_id, product_id, output_type_id) "
|
||||
"WHERE output_type_id IS NOT NULL"
|
||||
)
|
||||
|
||||
op.execute("DROP TABLE IF EXISTS product_render_positions CASCADE")
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Seed default render positions (3/4 Front + 3/4 Rear) for all existing products.
|
||||
|
||||
Revision ID: 029
|
||||
Revises: 028
|
||||
Create Date: 2026-03-04
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
revision = "029"
|
||||
down_revision = "028"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Insert two default positions for every product that currently has none.
|
||||
# The CTE guarantees both rows are inserted for the same set of products.
|
||||
op.execute("""
|
||||
WITH products_without_positions AS (
|
||||
SELECT p.id AS product_id
|
||||
FROM products p
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM product_render_positions rp WHERE rp.product_id = p.id
|
||||
)
|
||||
)
|
||||
INSERT INTO product_render_positions
|
||||
(id, product_id, name, rotation_x, rotation_y, rotation_z,
|
||||
is_default, sort_order, created_at, updated_at)
|
||||
SELECT gen_random_uuid(), product_id,
|
||||
'3/4 Front', -15.0, 45.0, 0.0, true, 0, NOW(), NOW()
|
||||
FROM products_without_positions
|
||||
UNION ALL
|
||||
SELECT gen_random_uuid(), product_id,
|
||||
'3/4 Rear', -15.0, -135.0, 0.0, false, 1, NOW(), NOW()
|
||||
FROM products_without_positions
|
||||
""")
|
||||
|
||||
|
||||
def downgrade():
|
||||
# Remove positions named exactly '3/4 Front' or '3/4 Rear'
|
||||
# where they are the only two positions on that product (i.e. seeded ones).
|
||||
op.execute("""
|
||||
DELETE FROM product_render_positions
|
||||
WHERE name IN ('3/4 Front', '3/4 Rear')
|
||||
AND product_id IN (
|
||||
SELECT product_id
|
||||
FROM product_render_positions
|
||||
GROUP BY product_id
|
||||
HAVING COUNT(*) = 2
|
||||
AND bool_or(name = '3/4 Front')
|
||||
AND bool_or(name = '3/4 Rear')
|
||||
)
|
||||
""")
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Seed 'Default' (unrotated) render position for all existing products.
|
||||
|
||||
Revision ID: 030
|
||||
Revises: 029
|
||||
Create Date: 2026-03-04
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
revision = "030"
|
||||
down_revision = "029"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Add 'Default' (0°/0°/0°) to every product that doesn't already have it.
|
||||
op.execute("""
|
||||
INSERT INTO product_render_positions
|
||||
(id, product_id, name, rotation_x, rotation_y, rotation_z,
|
||||
is_default, sort_order, created_at, updated_at)
|
||||
SELECT gen_random_uuid(), p.id,
|
||||
'Default', 0.0, 0.0, 0.0, false, 2, NOW(), NOW()
|
||||
FROM products p
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM product_render_positions rp
|
||||
WHERE rp.product_id = p.id
|
||||
AND lower(rp.name) = 'default'
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.execute("""
|
||||
DELETE FROM product_render_positions
|
||||
WHERE lower(name) = 'default'
|
||||
AND rotation_x = 0.0
|
||||
AND rotation_y = 0.0
|
||||
AND rotation_z = 0.0
|
||||
""")
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Add camera_orbit to render_templates
|
||||
|
||||
Revision ID: 031_camera_orbit
|
||||
Revises: 030_seed_default_position
|
||||
Create Date: 2026-03-04
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '031'
|
||||
down_revision = '030'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column(
|
||||
'render_templates',
|
||||
sa.Column('camera_orbit', sa.Boolean(), nullable=False,
|
||||
server_default='true'),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('render_templates', 'camera_orbit')
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user