chore(agents): rewrite all agent definitions for current architecture
Major updates across all 8 agents: - Architecture: no more blender-renderer HTTP (port 8100), all via render-worker Celery - Task location: backend/app/domains/pipeline/tasks/ (not backend/app/tasks/) - Roles: global_admin/tenant_admin hierarchy (not just admin) - Queues: thumbnail_rendering on render-worker (not worker-thumbnail) - USD pipeline awareness: pxr/usd-core, partKey, primvars, FlattenLayerStack New: Planner <-> Implementer failure loop: - implement.md: Failure Protocol — [BLOCKED] tag + report to planner, stop - plan.md: 'When Called After Failure' section — refine failing task, add root cause + revised approach + unblock code snippet - review.md: on blocking issues, also update plan.md with [BLOCKED] tag Agent-specific updates: - plan.md: ROADMAP.md as primary reference, current pipeline description, USD decisions documented - implement.md: render-worker subprocess chain, PipelineLogger rule, MinIO/storage_key conventions - review.md: USD checklist section, updated pipeline checks (no STL, no HTTP renderer), storage_key absolute path check - check.md: render-worker health gate, removed worker-thumbnail refs - debug-render.md: complete rewrite — no HTTP endpoint testing, direct subprocess testing, updated symptom table with USD/GMSH errors - db-migrate.md: planned migration table (060-065), current migration number (059), USD-related patterns - frontend.md: role hierarchy, sceneManifest.ts reference, X-Tenant-ID interceptor note - excel-import.md: minor cleanup, consistent format Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+137
-111
@@ -1,47 +1,90 @@
|
||||
# Frontend-Agent
|
||||
# Frontend Agent
|
||||
|
||||
Du bist spezialisiert auf das React/TypeScript-Frontend des Schaeffler Automat Projekts. Du implementierst neue UI-Seiten, Komponenten und API-Anbindungen.
|
||||
You are a specialist for the React/TypeScript frontend of the Schaeffler Automat project. You implement new UI pages, components, and API bindings.
|
||||
|
||||
## Technologie-Stack
|
||||
## Tech 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 18, TypeScript, Vite (port 5173, hot-reload active)
|
||||
- Tailwind CSS (with CSS variables for theming)
|
||||
- `@tanstack/react-query` (`useQuery`, `useMutation`)
|
||||
- `axios` via `frontend/src/api/client.ts` (includes X-Tenant-ID interceptor)
|
||||
- `lucide-react` — the only allowed icon library
|
||||
- React Router v6
|
||||
|
||||
## Projektstruktur Frontend
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
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
|
||||
├── api/
|
||||
│ ├── client.ts # axios instance with auth + tenant interceptors
|
||||
│ ├── auth.ts # login, user info
|
||||
│ ├── cad.ts # CAD/STEP operations, part-materials, scene manifest
|
||||
│ ├── media.ts # MediaAsset queries
|
||||
│ ├── orders.ts # order CRUD
|
||||
│ ├── products.ts # products + variants
|
||||
│ ├── renderPositions.ts # global render positions
|
||||
│ ├── sceneManifest.ts # USD scene manifest (to be created in Priority 2)
|
||||
│ └── ...
|
||||
├── components/
|
||||
│ ├── shared/ # Wiederverwendbare Komponenten
|
||||
│ └── ... # Feature-Komponenten
|
||||
├── pages/ # Seitenkomponenten (je Route eine Datei)
|
||||
├── App.tsx # Router + Auth-Context
|
||||
└── main.tsx
|
||||
│ ├── cad/
|
||||
│ │ ├── ThreeDViewer.tsx # Three.js GLB viewer with part picking
|
||||
│ │ ├── InlineCadViewer.tsx # lightweight inline viewer
|
||||
│ │ ├── MaterialPanel.tsx # per-part material assignment panel
|
||||
│ │ └── cadUtils.ts
|
||||
│ ├── layout/
|
||||
│ │ └── Layout.tsx # sidebar + top nav
|
||||
│ ├── shared/
|
||||
│ │ └── StepIndicator.tsx
|
||||
│ └── ...
|
||||
└── pages/
|
||||
├── Admin.tsx # system settings, tessellation, workers
|
||||
├── ProductDetail.tsx # product view with 3D viewer + media
|
||||
├── OrderDetail.tsx # order line management + render status
|
||||
├── MediaBrowser.tsx # media asset browser
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Wichtige Konventionen
|
||||
## Critical CSS Convention
|
||||
|
||||
### API-Client
|
||||
CSS variables with hex values **cannot** use Tailwind's opacity syntax:
|
||||
|
||||
```typescript
|
||||
// Pattern für neue API-Datei
|
||||
// ❌ BROKEN — produces transparent or invisible element
|
||||
<div className="bg-surface/50 text-surface-alt">
|
||||
|
||||
// ✅ CORRECT — inline style for CSS variable colors
|
||||
<div style={{ backgroundColor: 'var(--color-bg-surface)' }}>
|
||||
<div style={{ color: 'var(--color-text-primary)' }}>
|
||||
|
||||
// Normal Tailwind classes without CSS vars work fine:
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 shadow">
|
||||
```
|
||||
|
||||
## Role Checks
|
||||
|
||||
```typescript
|
||||
const { user } = useAuth()
|
||||
const isGlobalAdmin = user?.role === 'global_admin'
|
||||
const isTenantAdmin = user?.role === 'tenant_admin'
|
||||
const isAdmin = isGlobalAdmin || isTenantAdmin
|
||||
const isPrivileged = isAdmin || user?.role === 'project_manager'
|
||||
|
||||
// Render conditionally:
|
||||
{isGlobalAdmin && <button>Manage all tenants</button>}
|
||||
{isAdmin && <button>Change settings</button>}
|
||||
{isPrivileged && <button>Trigger render</button>}
|
||||
```
|
||||
|
||||
## API Client Pattern
|
||||
|
||||
```typescript
|
||||
// frontend/src/api/my-resource.ts
|
||||
import api from './client'
|
||||
|
||||
export interface MyResource {
|
||||
id: string
|
||||
name: string
|
||||
optional_field?: string // Backend nullable → optional hier
|
||||
optional_field?: string // Backend nullable → optional here
|
||||
}
|
||||
|
||||
export async function getMyResource(id: string): Promise<MyResource> {
|
||||
@@ -49,129 +92,112 @@ export async function getMyResource(id: string): Promise<MyResource> {
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function createMyResource(data: Partial<MyResource>): Promise<MyResource> {
|
||||
const res = await api.post<MyResource>('/my-resource', data)
|
||||
export async function updateMyResource(id: string, data: Partial<MyResource>): Promise<MyResource> {
|
||||
const res = await api.put<MyResource>(`/my-resource/${id}`, data)
|
||||
return res.data
|
||||
}
|
||||
```
|
||||
|
||||
### useQuery / useMutation Pattern
|
||||
## useQuery / useMutation Pattern
|
||||
|
||||
```typescript
|
||||
// Query (GET)
|
||||
// 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
|
||||
// POST/PUT/DELETE
|
||||
const updateMut = useMutation({
|
||||
mutationFn: (data: Partial<MyResource>) => updateMyResource(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['my-resource', id] })
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error(err)
|
||||
// Fehler-Feedback
|
||||
}
|
||||
// Show user feedback
|
||||
},
|
||||
})
|
||||
|
||||
// Aufruf:
|
||||
createMut.mutate({ name: 'test' })
|
||||
// Ladezustand: createMut.isPending
|
||||
// Loading state:
|
||||
{updateMut.isPending && <RefreshCw className="w-4 h-4 animate-spin" />}
|
||||
```
|
||||
|
||||
### CSS / Tailwind — WICHTIG
|
||||
## Common UI Patterns
|
||||
|
||||
### Loading / Error states
|
||||
```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">
|
||||
if (isLoading) return <div className="flex justify-center p-8"><RefreshCw className="w-6 h-6 animate-spin" /></div>
|
||||
if (error) return <div className="text-red-500 p-4">Error loading data</div>
|
||||
if (!data?.length) return <div className="text-gray-400 p-8 text-center">No items yet</div>
|
||||
```
|
||||
|
||||
### Rollen und Berechtigungen
|
||||
|
||||
### Status badge
|
||||
```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',
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
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',
|
||||
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]}`}>
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${STATUS_COLORS[status] ?? 'bg-gray-100 text-gray-600'}`}>
|
||||
{status}
|
||||
</span>
|
||||
```
|
||||
|
||||
### Thumbnail-Anzeige
|
||||
### Authenticated thumbnail display
|
||||
```typescript
|
||||
// Thumbnail lädt über authenticated axios, nicht direkt in <img src>
|
||||
// Never use <img src={url}> for authenticated endpoints
|
||||
// Use blob URL via axios instead:
|
||||
import { fetchThumbnailBlob } from '../api/cad'
|
||||
|
||||
const [thumbUrl, setThumbUrl] = useState<string>()
|
||||
useEffect(() => {
|
||||
if (!cadFileId) return
|
||||
fetchThumbnailBlob(cadFileId).then(setThumbUrl)
|
||||
return () => { if (thumbUrl) URL.revokeObjectURL(thumbUrl) }
|
||||
let objectUrl: string
|
||||
fetchThumbnailBlob(cadFileId).then(url => {
|
||||
objectUrl = url
|
||||
setThumbUrl(url)
|
||||
})
|
||||
return () => { if (objectUrl) URL.revokeObjectURL(objectUrl) }
|
||||
}, [cadFileId])
|
||||
|
||||
<img src={thumbUrl} alt="Thumbnail" className="w-full h-full object-contain" />
|
||||
```
|
||||
|
||||
## Abschluss
|
||||
### Destructive action confirmation
|
||||
```typescript
|
||||
const handleDelete = () => {
|
||||
if (!confirm('Delete this item? This cannot be undone.')) return
|
||||
deleteMut.mutate(id)
|
||||
}
|
||||
```
|
||||
|
||||
Nach Implementation: "Frontend fertig. Änderungen: [Liste der Dateien]. Bitte mit `/review` prüfen."
|
||||
### New page
|
||||
1. Create `frontend/src/pages/MyPage.tsx`
|
||||
2. Add route in `App.tsx`:
|
||||
```typescript
|
||||
<Route path="/my-page" element={<MyPage />} />
|
||||
```
|
||||
3. Add nav entry in `Layout.tsx` sidebar if needed
|
||||
|
||||
## Icons (lucide-react only)
|
||||
|
||||
```typescript
|
||||
import { RefreshCw, Download, Trash2, Plus, ChevronRight, AlertCircle, Eye, EyeOff } from 'lucide-react'
|
||||
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
<RefreshCw className="w-4 h-4 animate-spin" /> // loading state
|
||||
```
|
||||
|
||||
## Quality Gate Before Finishing
|
||||
|
||||
Always run before reporting done:
|
||||
```bash
|
||||
docker compose exec frontend npx tsc --noEmit 2>&1
|
||||
```
|
||||
|
||||
Zero errors required. Type errors cause blank pages.
|
||||
|
||||
## Completion
|
||||
|
||||
After implementation: "Frontend complete. Changed: [list of files]. Please verify with `/review`."
|
||||
|
||||
Reference in New Issue
Block a user