From 966876dcedd33f6c1b4f99d33054722b4252e2a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Sun, 15 Mar 2026 01:58:45 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20reorganize=20Admin=20page=20?= =?UTF-8?q?=E2=80=94=208=20focused=20tabs,=20grouped=20system=20tools?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructured the overloaded 6-tab Admin page into 8 focused tabs: 1. Overview — pricing summary (unchanged) 2. Users — user CRUD (unchanged) 3. Render Settings — engine, samples, tessellation, viewer config only 4. Output Types — OutputTypeTable (extracted from old Pricing tab) 5. Templates & Positions — render templates + camera positions + material lib 6. Pricing — pricing tiers only (cleaned up) 7. Libraries — asset libraries + template editor (unchanged) 8. System Tools — ALL maintenance buttons organized into cards: - Reprocessing (stuck recovery, thumbnails, metadata, workflows) - USD/Canonical Scenes (missing masters, regenerate all) - Cleanup (orphaned media/STEP, purge renders) - GPU Status, SMTP, Dashboard Config Section header pattern (icon + title + description) for each section. Card pattern for grouped maintenance actions. Scrollable tab bar for smaller screens. No sub-components changed — only Admin.tsx reorganization. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/pages/Admin.tsx | 2368 +++++++++++++++++----------------- 1 file changed, 1218 insertions(+), 1150 deletions(-) diff --git a/frontend/src/pages/Admin.tsx b/frontend/src/pages/Admin.tsx index ed8f826..d741dbe 100644 --- a/frontend/src/pages/Admin.tsx +++ b/frontend/src/pages/Admin.tsx @@ -1,7 +1,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useState, useRef } from 'react' import { toast } from 'sonner' -import { UserPlus, Trash2, Pencil, ChevronDown, ChevronUp, ChevronRight, Settings, RefreshCw, CheckCircle2, XCircle, Clock, DollarSign, Layers, AlertTriangle, Upload, FileBox, Plus, X, LayoutDashboard, Cpu, Zap, AlertCircle } from 'lucide-react' +import { UserPlus, Trash2, Pencil, ChevronDown, ChevronUp, ChevronRight, Settings, RefreshCw, CheckCircle2, XCircle, Clock, DollarSign, Layers, AlertTriangle, Upload, FileBox, Plus, X, LayoutDashboard, Cpu, Zap, AlertCircle, Wrench, HardDrive, Mail, Monitor, Eye, Box } from 'lucide-react' import { Link } from 'react-router-dom' import api from '../api/client' import ConfirmModal from '../components/ConfirmModal' @@ -322,7 +322,7 @@ export default function AdminPage() { ) } - type AdminTab = 'overview' | 'users' | 'render' | 'pricing' | 'libraries' | 'config' + type AdminTab = 'overview' | 'users' | 'render-settings' | 'output-types' | 'templates' | 'pricing' | 'libraries' | 'system' const [activeTab, setActiveTab] = useState('overview') const hasUnsavedChanges = @@ -334,10 +334,12 @@ export default function AdminPage() { const TABS: { id: AdminTab; label: string }[] = [ { id: 'overview', label: 'Overview' }, { id: 'users', label: 'Users' }, - { id: 'render', label: 'Render' }, + { id: 'render-settings', label: 'Render Settings' }, + { id: 'output-types', label: 'Output Types' }, + { id: 'templates', label: 'Templates & Positions' }, { id: 'pricing', label: 'Pricing' }, { id: 'libraries', label: 'Libraries' }, - { id: 'config', label: 'Config' }, + { id: 'system', label: 'System Tools' }, ] return ( @@ -353,12 +355,12 @@ export default function AdminPage() { )} -
+
{TABS.map((tab) => (
@@ -533,480 +535,1096 @@ export default function AdminPage() { } - {/* ------------------------------------------------------------------ */} - {/* Blender Render Settings (admin only) */} - {/* ------------------------------------------------------------------ */} - {activeTab === 'render' && isAdmin &&
-
-
- -
-

Blender Render Settings

-

- Render quality, performance and thumbnail output options for Blender 5. -

-
+ {/* ================================================================== */} + {/* Render Settings */} + {/* ================================================================== */} + {activeTab === 'render-settings' && isAdmin && <> + + {/* ── Blender Engine & Quality ──────────────────────────────────── */} +
+
+ +

Blender Engine & Quality

- -
+

Render engine selection, sample counts, smooth angle, and performance tuning for Blender 5.

-
- {/* ── Render Quality ───────────────────────────────────────────── */} -
-

Render Quality

-
- - {/* Engine */} -
- Render engine - {(['cycles', 'eevee'] as const).map((eng) => ( - - ))} -
- - {/* Cycles device — only relevant for Cycles */} - {blender.blender_engine === 'cycles' && ( -
- Cycles device - {(['auto', 'gpu', 'cpu'] as const).map((dev) => ( - - ))} -

- {blender.cycles_device === 'auto' - ? 'Tries OptiX / CUDA / HIP, falls back to CPU if no GPU is available.' - : blender.cycles_device === 'gpu' - ? 'Always use GPU. Logs a warning if no compatible GPU is found.' - : 'Always use CPU — useful for debugging or when GPU is busy.'} -

-
- )} - - {/* Sample counts */} -
-
- - setBlenderDraft((d) => ({ ...d, blender_cycles_samples: Number(e.target.value) }))} - title="Number of Cycles path-tracing samples (1–4096). Higher values = better quality + longer render time. Default: 256" - className="w-full px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400" - /> -

Higher = better quality, slower

-
-
- - setBlenderDraft((d) => ({ ...d, blender_eevee_samples: Number(e.target.value) }))} - title="EEVEE anti-aliasing sample count (1–1024). Higher values = smoother edges + longer render time. Default: 64" - className="w-full px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400" - /> -

Higher = better AA, slower

-
-
- - {/* Smooth by angle */} -
- - Smooth angle - - - setBlenderDraft((d) => ({ ...d, blender_smooth_angle: Number(e.target.value) }))} - title="Auto-smooth angle in degrees (0–180°). Faces with dihedral angles below this threshold are shaded smooth; sharper edges stay hard. 30° works well for most mechanical parts." - className="w-24 px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400" - /> - ° -

- {(blender.blender_smooth_angle ?? 30) === 0 - ? '0° = flat shading on all faces.' - : `Faces with edges sharper than ${blender.blender_smooth_angle ?? 30}° stay hard; others smooth. 30° works well for most mechanical parts.`} -

-
-
-
- - {/* ── Performance ──────────────────────────────────────────────── */} -
-

Performance

-
-
- Max concurrent - setBlenderDraft((d) => ({ ...d, blender_max_concurrent_renders: Number(e.target.value) }))} - title="Maximum parallel Blender render jobs (1–16). Each job uses ~400 MB RAM. Applied live without restart. Default: 3" - className="w-24 px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400" - /> -

- Max parallel Blender render jobs (1–16). Higher values use more RAM (~400 MB each). Applied live without restart. -

-
-
- Stall timeout - setBlenderDraft((d) => ({ ...d, render_stall_timeout_minutes: Number(e.target.value) }))} - title="Minutes before a stuck render job is automatically restarted (10–10080). The watchdog checks every 5 minutes. Default: 120" - className="w-24 px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400" - /> -

- Minutes before a stuck render job is auto-restarted (10–10080). Checked every 5 min by the watchdog. -

-
-
-
- - {/* Save button — appears when draft has unsaved changes */} - {Object.keys(blenderDraft).length > 0 && ( - - )} - - {/* ── Output ───────────────────────────────────────────────────── */} -
-

Output

-
- - {(['jpg', 'png'] as const).map((fmt) => ( +
+
+ Service Status - ))} -

- {settings?.thumbnail_format === 'jpg' - ? 'JPEG — ~3–5× smaller files, minimal quality loss at 92% quality.' - : 'PNG — lossless, larger files.'} -

-
+
- {/* Product thumbnail priority chain */} - {(() => { - let priorityList: string[] = ['latest_render', 'cad_thumbnail'] - try { - const parsed = JSON.parse(settings?.product_thumbnail_priority ?? '["latest_render","cad_thumbnail"]') - if (Array.isArray(parsed)) priorityList = parsed - } catch {} +
+ {/* ── Render Quality ─────────────────────────────────────── */} +
+

Render Quality

+
- const savePriority = (list: string[]) => { - updateSettingsMut.mutate({ product_thumbnail_priority: JSON.stringify(list) } as any) - } - - const moveUp = (i: number) => { - if (i === 0) return - const next = [...priorityList] - ;[next[i - 1], next[i]] = [next[i], next[i - 1]] - savePriority(next) - } - const moveDown = (i: number) => { - if (i === priorityList.length - 1) return - const next = [...priorityList] - ;[next[i], next[i + 1]] = [next[i + 1], next[i]] - savePriority(next) - } - const remove = (i: number) => savePriority(priorityList.filter((_, j) => j !== i)) - const addEntry = () => { - if (!priorityNewEntry || priorityList.includes(priorityNewEntry)) return - savePriority([...priorityList, priorityNewEntry]) - setPriorityNewEntry('') - } - - const entryLabel = (e: string) => - e === 'cad_thumbnail' ? 'CAD Thumbnail' - : e === 'latest_render' ? 'Latest Render (any type)' - : outputTypes?.find((ot) => ot.id === e)?.name ?? `Output type …${e.slice(-8)}` - - const entryColor = (e: string) => - e === 'cad_thumbnail' ? 'bg-surface-alt border-border-default text-content-muted' - : e === 'latest_render' ? 'bg-status-info-bg border-border-default text-status-info-text' - : 'bg-status-success-bg border-border-default text-status-success-text' - - // Options not yet in the list - const addableOptions = [ - ...(['latest_render', 'cad_thumbnail'] as string[]).filter((v) => !priorityList.includes(v)), - ...(outputTypes ?? []).filter((ot) => !priorityList.includes(ot.id)).map((ot) => ot.id), - ] - - return ( -
- -
- {priorityList.map((entry, i) => ( -
- {i + 1} -
- {entryLabel(entry)} - {entry !== 'cad_thumbnail' && entry !== 'latest_render' && ( - newest completed render - )} -
+ {/* Engine */} +
+ Render engine + {(['cycles', 'eevee'] as const).map((eng) => ( - - -
- ))} + ))} +
- {addableOptions.length > 0 && ( -
- - + {/* Cycles device */} + {blender.blender_engine === 'cycles' && ( +
+ Cycles device + {(['auto', 'gpu', 'cpu'] as const).map((dev) => ( + + ))} +

+ {blender.cycles_device === 'auto' + ? 'Tries OptiX / CUDA / HIP, falls back to CPU if no GPU is available.' + : blender.cycles_device === 'gpu' + ? 'Always use GPU. Logs a warning if no compatible GPU is found.' + : 'Always use CPU -- useful for debugging or when GPU is busy.'} +

)} -

- Sources are tried top to bottom. For specific output types, the newest completed render of that type is used. "CAD Thumbnail" always matches and stops the search. -

-
-
- ) - })()} -
{/* end Output */} + {/* Sample counts */} +
+
+ + setBlenderDraft((d) => ({ ...d, blender_cycles_samples: Number(e.target.value) }))} + title="Number of Cycles path-tracing samples (1-4096). Higher values = better quality + longer render time. Default: 256" + className="w-full px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400" + /> +

Higher = better quality, slower

+
+
+ + setBlenderDraft((d) => ({ ...d, blender_eevee_samples: Number(e.target.value) }))} + title="EEVEE anti-aliasing sample count (1-1024). Higher values = smoother edges + longer render time. Default: 64" + className="w-full px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400" + /> +

Higher = better AA, slower

+
+
- {/* ── Service Status ───────────────────────────────────────────── */} -
-

Service Status

-
- {rendererStatus && Object.entries(rendererStatus).map(([name, info]) => ( -
- {info.available - ? - : - } -
-

{name}

-

{info.note || (info.available ? 'Online' : 'Offline')}

+ {/* Smooth by angle */} +
+ + Smooth angle + + + setBlenderDraft((d) => ({ ...d, blender_smooth_angle: Number(e.target.value) }))} + title="Auto-smooth angle in degrees (0-180). Faces with dihedral angles below this threshold are shaded smooth; sharper edges stay hard. 30 works well for most mechanical parts." + className="w-24 px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400" + /> + deg +

+ {(blender.blender_smooth_angle ?? 30) === 0 + ? '0 = flat shading on all faces.' + : `Faces with edges sharper than ${blender.blender_smooth_angle ?? 30} stay hard; others smooth. 30 works well for most mechanical parts.`} +

- ))} - {!rendererStatus && ( -
- Checking service status… +
+ + {/* ── Performance ────────────────────────────────────────── */} +
+

Performance

+
+
+ Max concurrent + setBlenderDraft((d) => ({ ...d, blender_max_concurrent_renders: Number(e.target.value) }))} + title="Maximum parallel Blender render jobs (1-16). Each job uses ~400 MB RAM. Applied live without restart. Default: 3" + className="w-24 px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400" + /> +

+ Max parallel Blender render jobs (1-16). Higher values use more RAM (~400 MB each). Applied live without restart. +

+
+
+ Stall timeout + setBlenderDraft((d) => ({ ...d, render_stall_timeout_minutes: Number(e.target.value) }))} + title="Minutes before a stuck render job is automatically restarted (10-10080). The watchdog checks every 5 minutes. Default: 120" + className="w-24 px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400" + /> +

+ Minutes before a stuck render job is auto-restarted (10-10080). Checked every 5 min by the watchdog. +

+
+
+ + {/* Save button */} + {Object.keys(blenderDraft).length > 0 && ( + )} + + {/* ── Output ────────────────────────────────────────────── */} +
+

Output

+
+ + {(['jpg', 'png'] as const).map((fmt) => ( + + ))} +

+ {settings?.thumbnail_format === 'jpg' + ? 'JPEG -- ~3-5x smaller files, minimal quality loss at 92% quality.' + : 'PNG -- lossless, larger files.'} +

+
+ + {/* Product thumbnail priority chain */} + {(() => { + let priorityList: string[] = ['latest_render', 'cad_thumbnail'] + try { + const parsed = JSON.parse(settings?.product_thumbnail_priority ?? '["latest_render","cad_thumbnail"]') + if (Array.isArray(parsed)) priorityList = parsed + } catch {} + + const savePriority = (list: string[]) => { + updateSettingsMut.mutate({ product_thumbnail_priority: JSON.stringify(list) } as any) + } + + const moveUp = (i: number) => { + if (i === 0) return + const next = [...priorityList] + ;[next[i - 1], next[i]] = [next[i], next[i - 1]] + savePriority(next) + } + const moveDown = (i: number) => { + if (i === priorityList.length - 1) return + const next = [...priorityList] + ;[next[i], next[i + 1]] = [next[i + 1], next[i]] + savePriority(next) + } + const remove = (i: number) => savePriority(priorityList.filter((_, j) => j !== i)) + const addEntry = () => { + if (!priorityNewEntry || priorityList.includes(priorityNewEntry)) return + savePriority([...priorityList, priorityNewEntry]) + setPriorityNewEntry('') + } + + const entryLabel = (e: string) => + e === 'cad_thumbnail' ? 'CAD Thumbnail' + : e === 'latest_render' ? 'Latest Render (any type)' + : outputTypes?.find((ot) => ot.id === e)?.name ?? `Output type ...${e.slice(-8)}` + + const entryColor = (e: string) => + e === 'cad_thumbnail' ? 'bg-surface-alt border-border-default text-content-muted' + : e === 'latest_render' ? 'bg-status-info-bg border-border-default text-status-info-text' + : 'bg-status-success-bg border-border-default text-status-success-text' + + // Options not yet in the list + const addableOptions = [ + ...(['latest_render', 'cad_thumbnail'] as string[]).filter((v) => !priorityList.includes(v)), + ...(outputTypes ?? []).filter((ot) => !priorityList.includes(ot.id)).map((ot) => ot.id), + ] + + return ( +
+ +
+ {priorityList.map((entry, i) => ( +
+ {i + 1} +
+ {entryLabel(entry)} + {entry !== 'cad_thumbnail' && entry !== 'latest_render' && ( + newest completed render + )} +
+ + + +
+ ))} + + {addableOptions.length > 0 && ( +
+ + +
+ )} + +

+ Sources are tried top to bottom. For specific output types, the newest completed render of that type is used. "CAD Thumbnail" always matches and stops the search. +

+
+
+ ) + })()} +
{/* end Output */} + + {/* ── Service Status ─────────────────────────────────────── */} +
+

Service Status

+
+ {rendererStatus && Object.entries(rendererStatus).map(([name, info]) => ( +
+ {info.available + ? + : + } +
+

{name}

+

{info.note || (info.available ? 'Online' : 'Offline')}

+
+
+ ))} + {!rendererStatus && ( +
+ Checking service status... +
+ )} +
+
+
- {/* ── Maintenance ──────────────────────────────────────────────── */} -
-

Maintenance

-
-
+ {/* ── Tessellation Quality ──────────────────────────────────────── */} +
+
+ +

Tessellation Quality

+
+

Controls how STEP geometry is converted to triangle meshes. Affects both the 3D viewer and Blender renders.

+ +
+
+ {/* Presets */} + {(() => { + const PRESETS = [ + { + label: 'Draft', + icon: '***', + description: 'Fast preview -- visible faceting on curved surfaces', + useCase: 'Quick checks, large assemblies', + color: 'border-amber-400', + activeColor: 'border-amber-500 ring-2 ring-amber-200', + values: { scene_linear_deflection: 0.2, scene_angular_deflection: 0.3, render_linear_deflection: 0.05, render_angular_deflection: 0.1 }, + }, + { + label: 'Standard', + icon: '---', + description: 'Smooth curves, good quality-to-size ratio', + useCase: 'Recommended for most parts', + color: 'border-blue-400', + activeColor: 'border-blue-500 ring-2 ring-blue-200', + values: { scene_linear_deflection: 0.1, scene_angular_deflection: 0.1, render_linear_deflection: 0.03, render_angular_deflection: 0.05 }, + }, + { + label: 'Fine', + icon: '+++', + description: 'Near-perfect surfaces, 3-5x larger files', + useCase: 'Close-up renders, small precision parts', + color: 'border-emerald-400', + activeColor: 'border-emerald-500 ring-2 ring-emerald-200', + values: { scene_linear_deflection: 0.05, scene_angular_deflection: 0.05, render_linear_deflection: 0.01, render_angular_deflection: 0.02 }, + }, + { + label: 'Ultra', + icon: '***', + description: 'Maximum fidelity, very slow export', + useCase: 'Marketing renders, extreme close-ups', + color: 'border-purple-400', + activeColor: 'border-purple-500 ring-2 ring-purple-200', + values: { scene_linear_deflection: 0.02, scene_angular_deflection: 0.02, render_linear_deflection: 0.005, render_angular_deflection: 0.01 }, + }, + ] + const isActive = (preset: typeof PRESETS[0]) => + tess.scene_linear_deflection === preset.values.scene_linear_deflection && + tess.scene_angular_deflection === preset.values.scene_angular_deflection && + tess.render_linear_deflection === preset.values.render_linear_deflection && + tess.render_angular_deflection === preset.values.render_angular_deflection + const isCustom = !PRESETS.some(isActive) + return ( +
+

Quality Presets

+
+ {PRESETS.map(preset => ( + + ))} +
+ {isCustom && ( +

Current values don't match any preset (custom configuration)

+ )} +
+ ) + })()} + + {/* Tessellation engine selector */} +
+

Tessellation Engine

+
+ {[ + { value: 'occ', label: 'OCC BRepMesh', description: 'Default engine. Fast, but produces fan-shaped triangles at cylinder seam lines.' }, + { value: 'gmsh', label: 'GMSH Frontal-Delaunay', description: 'Uniform mesh -- eliminates fan artifacts on cylindrical parts. 10-30% slower. Recommended for bearings.' }, + ].map(opt => ( + + ))} +
+
+ + {/* Explanation of deflection parameters */} +
+

How deflection values work

+
+
+

Linear deflection (mm)

+

Maximum allowed distance between the original curved surface and the generated triangles. A value of 0.1 mm means no triangle edge can deviate more than 0.1 mm from the true surface. Lower values produce smoother curves but more triangles.

+
+
+

Angular deflection (rad)

+

Maximum angle between adjacent triangle normals. Controls how finely curved regions are subdivided. A value of 0.1 rad (~6 deg) means neighboring triangles can differ by at most ~6 deg. Primarily affects small fillets and tight curvatures.

+
+
+
+ + + + {/* Manual inputs */} + {showAdvancedTess && (<> +
+
+
+

3D Viewer + USD Master

+

Used for the interactive 3D viewer GLB and the canonical USD scene file. Optimized for real-time display.

+
+
+ + setTessellationDraft(d => ({ ...d, scene_linear_deflection: parseFloat(e.target.value) }))} + className="w-24 px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400" + /> + mm +
+
+ + setTessellationDraft(d => ({ ...d, scene_angular_deflection: parseFloat(e.target.value) }))} + className="w-24 px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400" + /> + rad +
+
+
+
+

Blender Render Output

+

Used for final Blender renders (stills, turntables). Higher quality since render time matters more than file size.

+
+
+ + setTessellationDraft(d => ({ ...d, render_linear_deflection: parseFloat(e.target.value) }))} + className="w-24 px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400" + /> + mm +
+
+ + setTessellationDraft(d => ({ ...d, render_angular_deflection: parseFloat(e.target.value) }))} + className="w-24 px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400" + /> + rad +
+
+
+ )} +
+ + {Object.keys(tessellationDraft).length > 0 && ( + + )} +
+
+
+
+ + {/* ── 3D Viewer & GLB Export ────────────────────────────────────── */} +
+
+ +

3D Viewer & GLB Export

+
+

Settings for the interactive 3D viewer and GLB geometry export pipeline.

+ +
+
+ {/* Scale Factor */} +
+
+ + setViewerDraft(d => ({ ...d, gltf_scale_factor: parseFloat(e.target.value) }))} + className="input w-full" + /> +

Default 0.001 converts mm to meters

+
+
+ + +

Smooths surface normals during GLB export for a less faceted look in the 3D viewer.

+
+
+ + {/* Camera / Zoom Limits */} +
+
+ + setViewerDraft(d => ({ ...d, viewer_max_distance: parseFloat(e.target.value) }))} + title="Maximum camera distance from the model in the 3D viewer (in metres after mm to m conversion). Default: 50" + className="input w-full" + /> +

Maximum camera pull-back distance in the 3D viewer (metres).

+
+
+ + setViewerDraft(d => ({ ...d, viewer_min_distance: parseFloat(e.target.value) }))} + title="Minimum camera distance from the model in the 3D viewer (in metres). Default: 0.001. Prevents clipping into the geometry." + className="input w-full" + /> +

Closest the camera can zoom in (metres). Prevents clipping through geometry.

+
+
+ + {/* PBR Material Quality */} +
+
+ + +

Material data embedded in exported GLB files.

+
+
+ + setViewerDraft(d => ({ ...d, gltf_pbr_roughness: parseFloat(e.target.value) }))} + title="Surface roughness for GLB PBR materials (0 = mirror-smooth, 1 = fully matte). Default: 0.4 -- appropriate for brushed metal." + className="input w-full" + /> +

0 = mirror-smooth, 1 = fully matte. Default 0.4 suits brushed metal.

+
+
+ + setViewerDraft(d => ({ ...d, gltf_pbr_metallic: parseFloat(e.target.value) }))} + title="Metallic factor for GLB PBR materials (0 = dielectric/plastic, 1 = fully metallic). Default: 0.6 -- suitable for steel parts." + className="input w-full" + /> +

0 = plastic/dielectric, 1 = fully metallic. Default 0.6 suits steel parts.

+
+
+ +
+ + {Object.keys(viewerDraft).length > 0 && ( + + )} +
+
+
+
+ + } + + {/* ================================================================== */} + {/* Output Types */} + {/* ================================================================== */} + {activeTab === 'output-types' && <> +
+
+ +

Output Types

+
+

Define what kinds of outputs orders can request (thumbnails, views, formats).

+
+
+ +
+ } + + {/* ================================================================== */} + {/* Templates & Positions */} + {/* ================================================================== */} + {activeTab === 'templates' && <> + + {/* ── Render Templates ──────────────────────────────────────────── */} +
+
+ +

Render Templates

+
+

Upload .blend studio setups matched by Category + Output Type. Geometry is imported into the template at render time.

+ +
+
+ +
+
+ +
+
+
+ + {/* ── Global Render Positions ───────────────────────────────────── */} + {isAdmin && ( +
+
+ +

Global Render Positions

+
+

Camera rotation presets available to all products. Per-product positions override these.

+ +
+
+ +
+
+
+ )} + + {/* ── Material Library link ─────────────────────────────────────── */} +
+
+

Material Library

+

+ Manage shared materials for CAD part assignments. +

+
+ + Open Material Library + +
+ + } + + {/* ================================================================== */} + {/* Pricing */} + {/* ================================================================== */} + {activeTab === 'pricing' && <> +
+
+ +

Pricing Tiers

+
+

Configure price per rendering item by category and quality level.

+
+
+ +
+ } + + {/* ================================================================== */} + {/* Libraries */} + {/* ================================================================== */} + {activeTab === 'libraries' && <> + + + {/* ── Templates (legacy editor) ─────────────────────────────────── */} +
+
+

Templates

+

+ Click Edit to configure standard fields and component schema for each template. +

+
+
+ {templates?.map((t) => { + const isEditing = editingTemplateId === t.id + return ( +
+ {/* Row */} +
+ +
+

{t.name}

+

{t.category_key}

+
+ + {t.is_active ? 'active' : 'inactive'} + + +
+ + {/* Inline editor panel */} + {isEditing && ( +
+ setEditingTemplateId(null)} + /> +
+ )} +
+ ) + })} +
+
+ } + + {/* ================================================================== */} + {/* System Tools */} + {/* ================================================================== */} + {activeTab === 'system' && isAdmin && <> + + {/* ── Reprocessing ──────────────────────────────────────────────── */} +
+
+ +

Reprocessing

+
+

Queue STEP files for reprocessing, re-render thumbnails, or re-extract metadata.

+ +
+
+

Stuck File Recovery

+

Resets files stuck in 'processing' for more than 10 minutes to 'failed'. Runs automatically every 5 min.

+
-

Resets files stuck in 'processing' to 'failed'. Runs automatically every 5 min.

-
+
+ +
+

Process Unprocessed

+

Queues all pending/failed STEP files for initial processing.

+
-

Queues all pending/failed STEP files for initial processing.

-
+
+ +
+

Regenerate Thumbnails

+

Re-renders thumbnails for all completed CAD files using current Blender settings.

+
-

Re-renders thumbnails for all completed CAD files.

-
+
+ +
+

Re-extract CAD Metadata

+

Updates dimensions and edge data for existing files (no re-render).

+
+ +
+
+ +
+

Seed Standard Workflows

+

Creates the 4 standard workflow definitions if they don't exist yet.

+
+ +
+
+
+
+ + {/* ── USD / Canonical Scenes ────────────────────────────────────── */} +
+
+ +

USD / Canonical Scenes

+
+

Manage USD master exports and canonical scene generation for CAD files.

+ +
+
+

Generate Missing USD Masters

+

Exports USD canonical scene for all completed CAD files missing one.

+
-

Exports USD canonical scene for all completed CAD files missing one.

-
+
+ +
+

Generate Missing Canonical Scenes

+

Queues geometry GLB + USD master for all completed CAD files missing a canonical scene.

+
-

Queues geometry GLB + USD master for all completed CAD files missing a canonical scene.

-
+
+ +
+

Regenerate All GLB + USD

+

Re-exports GLB and USD for all completed CAD files, replacing existing assets.

+
-

Re-exports GLB and USD for all completed CAD files, replacing existing assets.

-
+
+
+
+ + {/* ── Cleanup ───────────────────────────────────────────────────── */} +
+
+ +

Cleanup

+
+

Import, clean up, or purge media assets and orphaned records.

+ +
+
+

Import Existing Media

+

Registers existing renders and CAD thumbnails in the Media Browser.

+
-

Registers existing renders & CAD thumbnails in the Media Browser.

-
+
+ +
+

Clean Up Orphaned Media

+

Removes DB records for renders whose files no longer exist on disk.

+
-

Removes DB records for renders whose files no longer exist on disk.

-
+
+ +
+

Clean Up Orphaned STEP Files

+

Removes STEP files, thumbnails, and DB records not linked to any product.

+
-

Removes STEP files, thumbnails, and DB records not linked to any product.

-
+
+ +
+

Purge All Stills & Turntables

+

Deletes all rendered images and animations. Thumbnails, GLBs, and USD files are preserved.

+
-

Deletes all rendered images and animations. Thumbnails, GLBs, and USD files are preserved.

-
-
- -

Updates dimensions and edge data for existing files (no re-render).

-
-
- -

Creates the 4 standard workflow definitions if they don't exist yet.

-
} - {/* ------------------------------------------------------------------ */} - {/* Global Render Positions (admin only) */} - {/* ------------------------------------------------------------------ */} - {activeTab === 'render' && isAdmin &&
-
- -
-

Global Render Positions

-

- Camera rotation presets available to all products. Per-product positions override these. -

+ {/* ── GPU Status ────────────────────────────────────────────────── */} +
+
+ +

GPU Status

-
-
- -
-
} +

Verify that the render-worker is using the GPU (not CPU fallback).

- {/* ------------------------------------------------------------------ */} - {/* Render Templates (admin/PM) */} - {/* ------------------------------------------------------------------ */} - {activeTab === 'render' &&
-
- -
-

Render Templates

-

- Upload .blend studio setups matched by Category + Output Type. Geometry is imported into the template at render time. -

-
-
-
- -
-
- -
-
} +
+ - {/* ------------------------------------------------------------------ */} - {/* Asset Libraries */} - {/* ------------------------------------------------------------------ */} - {activeTab === 'libraries' && } - - {/* ------------------------------------------------------------------ */} - {/* Output Types */} - {/* ------------------------------------------------------------------ */} - {activeTab === 'pricing' &&
-
- -
-

Output Types

-

- Define what kinds of outputs orders can request (thumbnails, views, formats). -

-
-
- -
} - - {/* ------------------------------------------------------------------ */} - {/* Pricing Tiers */} - {/* ------------------------------------------------------------------ */} - {activeTab === 'pricing' &&
-
- -
-

Pricing Tiers

-

- Configure price per rendering item by category and quality level. -

-
-
- -
} - - {/* ------------------------------------------------------------------ */} - {/* E-Mail / SMTP Settings */} - {/* ------------------------------------------------------------------ */} - {activeTab === 'config' && isAdmin && ( -
-
-

E-Mail Notifications (SMTP)

-

- Configure outbound SMTP for email notifications. Enable only when credentials are set. -

-
-
-
- - {smtp.smtp_enabled && ( - Active - )} -
-
-
- - setSmtpDraft(d => ({ ...d, smtp_host: e.target.value }))} - placeholder="smtp.example.com" - className="w-full px-3 py-1.5 rounded-md border border-border-default text-sm bg-surface text-content focus:outline-none focus:ring-2 focus:ring-accent" - /> -
-
- - setSmtpDraft(d => ({ ...d, smtp_port: parseInt(e.target.value) || 587 }))} - className="w-full px-3 py-1.5 rounded-md border border-border-default text-sm bg-surface text-content focus:outline-none focus:ring-2 focus:ring-accent" - /> -
-
- - setSmtpDraft(d => ({ ...d, smtp_user: e.target.value }))} - placeholder="user@example.com" - className="w-full px-3 py-1.5 rounded-md border border-border-default text-sm bg-surface text-content focus:outline-none focus:ring-2 focus:ring-accent" - /> -
-
- - setSmtpDraft(d => ({ ...d, smtp_password: e.target.value }))} - placeholder="••••••••" - className="w-full px-3 py-1.5 rounded-md border border-border-default text-sm bg-surface text-content focus:outline-none focus:ring-2 focus:ring-accent" - /> -
-
- - setSmtpDraft(d => ({ ...d, smtp_from_address: e.target.value }))} - placeholder="noreply@schaeffler.com" - className="w-full px-3 py-1.5 rounded-md border border-border-default text-sm bg-surface text-content focus:outline-none focus:ring-2 focus:ring-accent" - /> -
-
- -
-
- )} - - {/* ------------------------------------------------------------------ */} - {/* Templates */} - {/* ------------------------------------------------------------------ */} - {activeTab === 'libraries' &&
-
-

Templates

-

- Click Edit to configure standard fields and component schema for each template. -

-
-
- {templates?.map((t) => { - const isEditing = editingTemplateId === t.id - return ( -
- {/* Row */} -
+ {gpuProbeExpanded && ( +
+
-
-

{t.name}

-

{t.category_key}

-
- - {t.is_active ? 'active' : 'inactive'} - - + {gpuProbeResult && ( + + Last checked: {gpuProbeResult.timestamp ? new Date(gpuProbeResult.timestamp).toLocaleString() : '--'} + + )}
- {/* Inline editor panel */} - {isEditing && ( -
- setEditingTemplateId(null)} - /> + {gpuProbeResult && ( +
+
+ Status + {gpuStatusBadge()} +
+ {gpuProbeResult.device_type && ( +
+ Device type + {gpuProbeResult.device_type} +
+ )} + {gpuProbeResult.devices && gpuProbeResult.devices.length > 0 && ( +
+ Devices +
+ {gpuProbeResult.devices.map((d: string, i: number) => ( + {d} + ))} +
+
+ )} + {gpuProbeResult.render_time_s != null && ( +
+ Render time + {gpuProbeResult.render_time_s.toFixed(2)}s +
+ )} + {gpuProbeResult.error && ( +
+ Error + {gpuProbeResult.error} +
+ )}
)} -
- ) - })} -
-
} - {/* ------------------------------------------------------------------ */} - {/* Dashboard Widget Configuration (admin only) */} - {/* ------------------------------------------------------------------ */} - {activeTab === 'config' && isAdmin && ( -
-
- -
-

Dashboard Widget-Konfiguration

-

- Sets the default widget layout for all users of this tenant. Users can customize their own layout individually. -

-
-
-
-
-

- Tenant default:{' '} - - {tenantDefaultWidgets && tenantDefaultWidgets.length > 0 - ? `${tenantDefaultWidgets.length} Widget${tenantDefaultWidgets.length !== 1 ? 's' : ''} configured` - : 'No default set yet (system default active)'} - -

-
- + {!gpuProbeResult && !gpuProbing && ( +

No probe result yet. Click "Run GPU Check" to trigger a test render.

+ )} +
+ )}
- )} + + {/* ── SMTP Settings ─────────────────────────────────────────────── */} +
+
+ +

E-Mail Notifications (SMTP)

+
+

Configure outbound SMTP for email notifications. Enable only when credentials are set.

+ +
+
+
+ + {smtp.smtp_enabled && ( + Active + )} +
+
+
+ + setSmtpDraft(d => ({ ...d, smtp_host: e.target.value }))} + placeholder="smtp.example.com" + className="w-full px-3 py-1.5 rounded-md border border-border-default text-sm bg-surface text-content focus:outline-none focus:ring-2 focus:ring-accent" + /> +
+
+ + setSmtpDraft(d => ({ ...d, smtp_port: parseInt(e.target.value) || 587 }))} + className="w-full px-3 py-1.5 rounded-md border border-border-default text-sm bg-surface text-content focus:outline-none focus:ring-2 focus:ring-accent" + /> +
+
+ + setSmtpDraft(d => ({ ...d, smtp_user: e.target.value }))} + placeholder="user@example.com" + className="w-full px-3 py-1.5 rounded-md border border-border-default text-sm bg-surface text-content focus:outline-none focus:ring-2 focus:ring-accent" + /> +
+
+ + setSmtpDraft(d => ({ ...d, smtp_password: e.target.value }))} + placeholder="--------" + className="w-full px-3 py-1.5 rounded-md border border-border-default text-sm bg-surface text-content focus:outline-none focus:ring-2 focus:ring-accent" + /> +
+
+ + setSmtpDraft(d => ({ ...d, smtp_from_address: e.target.value }))} + placeholder="noreply@schaeffler.com" + className="w-full px-3 py-1.5 rounded-md border border-border-default text-sm bg-surface text-content focus:outline-none focus:ring-2 focus:ring-accent" + /> +
+
+ +
+
+
+ + {/* ── Dashboard Config ──────────────────────────────────────────── */} +
+
+ +

Dashboard Configuration

+
+

Sets the default widget layout for all users of this tenant. Users can customize their own layout individually.

+ +
+
+
+

+ Tenant default:{' '} + + {tenantDefaultWidgets && tenantDefaultWidgets.length > 0 + ? `${tenantDefaultWidgets.length} Widget${tenantDefaultWidgets.length !== 1 ? 's' : ''} configured` + : 'No default set yet (system default active)'} + +

+
+ +
+
+
+ + } {showTenantDashboardModal && ( )} - {/* ------------------------------------------------------------------ */} - {/* 3D Viewer & GLB Export Settings */} - {/* ------------------------------------------------------------------ */} - {activeTab === 'render' &&
-
-

3D Viewer & GLB Export

-

- Settings for the 3D viewer and GLB geometry export -

-
-
- {/* Scale Factor */} -
-
- - setViewerDraft(d => ({ ...d, gltf_scale_factor: parseFloat(e.target.value) }))} - className="input w-full" - /> -

Default 0.001 converts mm to meters

-
-
- - -

Smooths surface normals during GLB export for a less faceted look in the 3D viewer.

-
-
- - {/* Camera / Zoom Limits */} -
-
- - setViewerDraft(d => ({ ...d, viewer_max_distance: parseFloat(e.target.value) }))} - title="Maximum camera distance from the model in the 3D viewer (in metres after mm→m conversion). Default: 50" - className="input w-full" - /> -

Maximum camera pull-back distance in the 3D viewer (metres).

-
-
- - setViewerDraft(d => ({ ...d, viewer_min_distance: parseFloat(e.target.value) }))} - title="Minimum camera distance from the model in the 3D viewer (in metres). Default: 0.001. Prevents clipping into the geometry." - className="input w-full" - /> -

Closest the camera can zoom in (metres). Prevents clipping through geometry.

-
-
- - {/* PBR Material Quality */} -
-
- - -

Material data embedded in exported GLB files.

-
-
- - setViewerDraft(d => ({ ...d, gltf_pbr_roughness: parseFloat(e.target.value) }))} - title="Surface roughness for GLB PBR materials (0 = mirror-smooth, 1 = fully matte). Default: 0.4 — appropriate for brushed metal." - className="input w-full" - /> -

0 = mirror-smooth, 1 = fully matte. Default 0.4 suits brushed metal.

-
-
- - setViewerDraft(d => ({ ...d, gltf_pbr_metallic: parseFloat(e.target.value) }))} - title="Metallic factor for GLB PBR materials (0 = dielectric/plastic, 1 = fully metallic). Default: 0.6 — suitable for steel parts." - className="input w-full" - /> -

0 = plastic/dielectric, 1 = fully metallic. Default 0.6 suits steel parts.

-
-
- -
- - {Object.keys(viewerDraft).length > 0 && ( - - )} -
-
-
} - - {/* ------------------------------------------------------------------ */} - {/* Tessellation Quality */} - {/* ------------------------------------------------------------------ */} - {activeTab === 'render' &&
-
-

Tessellation Quality

-

- Controls how STEP geometry is converted to triangle meshes. These settings affect both the 3D viewer and Blender renders. -

-
-
- {/* Presets */} - {(() => { - const PRESETS = [ - { - label: 'Draft', - icon: '⚡', - description: 'Fast preview — visible faceting on curved surfaces', - useCase: 'Quick checks, large assemblies', - color: 'border-amber-400', - activeColor: 'border-amber-500 ring-2 ring-amber-200', - values: { scene_linear_deflection: 0.2, scene_angular_deflection: 0.3, render_linear_deflection: 0.05, render_angular_deflection: 0.1 }, - }, - { - label: 'Standard', - icon: '●', - description: 'Smooth curves, good quality-to-size ratio', - useCase: 'Recommended for most parts', - color: 'border-blue-400', - activeColor: 'border-blue-500 ring-2 ring-blue-200', - values: { scene_linear_deflection: 0.1, scene_angular_deflection: 0.1, render_linear_deflection: 0.03, render_angular_deflection: 0.05 }, - }, - { - label: 'Fine', - icon: '◆', - description: 'Near-perfect surfaces, 3-5x larger files', - useCase: 'Close-up renders, small precision parts', - color: 'border-emerald-400', - activeColor: 'border-emerald-500 ring-2 ring-emerald-200', - values: { scene_linear_deflection: 0.05, scene_angular_deflection: 0.05, render_linear_deflection: 0.01, render_angular_deflection: 0.02 }, - }, - { - label: 'Ultra', - icon: '★', - description: 'Maximum fidelity, very slow export', - useCase: 'Marketing renders, extreme close-ups', - color: 'border-purple-400', - activeColor: 'border-purple-500 ring-2 ring-purple-200', - values: { scene_linear_deflection: 0.02, scene_angular_deflection: 0.02, render_linear_deflection: 0.005, render_angular_deflection: 0.01 }, - }, - ] - const isActive = (preset: typeof PRESETS[0]) => - tess.scene_linear_deflection === preset.values.scene_linear_deflection && - tess.scene_angular_deflection === preset.values.scene_angular_deflection && - tess.render_linear_deflection === preset.values.render_linear_deflection && - tess.render_angular_deflection === preset.values.render_angular_deflection - const isCustom = !PRESETS.some(isActive) - return ( -
-

Quality Presets

-
- {PRESETS.map(preset => ( - - ))} -
- {isCustom && ( -

Current values don't match any preset (custom configuration)

- )} -
- ) - })()} - - {/* Tessellation engine selector */} -
-

Tessellation Engine

-
- {[ - { value: 'occ', label: 'OCC BRepMesh', description: 'Default engine. Fast, but produces fan-shaped triangles at cylinder seam lines.' }, - { value: 'gmsh', label: 'GMSH Frontal-Delaunay', description: 'Uniform mesh — eliminates fan artifacts on cylindrical parts. 10-30% slower. Recommended for bearings.' }, - ].map(opt => ( - - ))} -
-
- - {/* Explanation of deflection parameters */} -
-

How deflection values work

-
-
-

Linear deflection (mm)

-

Maximum allowed distance between the original curved surface and the generated triangles. A value of 0.1 mm means no triangle edge can deviate more than 0.1 mm from the true surface. Lower values produce smoother curves but more triangles.

-
-
-

Angular deflection (rad)

-

Maximum angle between adjacent triangle normals. Controls how finely curved regions are subdivided. A value of 0.1 rad (~6°) means neighboring triangles can differ by at most ~6°. Primarily affects small fillets and tight curvatures.

-
-
-
- - - - {/* Manual inputs */} - {showAdvancedTess && (<> -
-
-
-

3D Viewer + USD Master

-

Used for the interactive 3D viewer GLB and the canonical USD scene file. Optimized for real-time display.

-
-
- - setTessellationDraft(d => ({ ...d, scene_linear_deflection: parseFloat(e.target.value) }))} - className="w-24 px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400" - /> - mm -
-
- - setTessellationDraft(d => ({ ...d, scene_angular_deflection: parseFloat(e.target.value) }))} - className="w-24 px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400" - /> - rad -
-
-
-
-

Blender Render Output

-

Used for final Blender renders (stills, turntables). Higher quality since render time matters more than file size.

-
-
- - setTessellationDraft(d => ({ ...d, render_linear_deflection: parseFloat(e.target.value) }))} - className="w-24 px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400" - /> - mm -
-
- - setTessellationDraft(d => ({ ...d, render_angular_deflection: parseFloat(e.target.value) }))} - className="w-24 px-3 py-1.5 border border-border-default rounded-md text-sm focus:outline-none focus:border-blue-400" - /> - rad -
-
-
- )} -
- - {Object.keys(tessellationDraft).length > 0 && ( - - )} -
-
-
} - - {/* ------------------------------------------------------------------ */} - {/* Material Library link */} - {/* ------------------------------------------------------------------ */} - {activeTab === 'render' &&
-
-

Material Library

-

- Manage shared materials for CAD part assignments. -

-
- - Open Material Library → - -
} - - {/* ------------------------------------------------------------------ */} - {/* GPU Status */} - {/* ------------------------------------------------------------------ */} - {activeTab === 'render' && isAdmin && ( -
- - - {gpuProbeExpanded && ( -
-
- - {gpuProbeResult && ( - - Last checked: {gpuProbeResult.timestamp ? new Date(gpuProbeResult.timestamp).toLocaleString() : '—'} - - )} -
- - {gpuProbeResult && ( -
-
- Status - {gpuStatusBadge()} -
- {gpuProbeResult.device_type && ( -
- Device type - {gpuProbeResult.device_type} -
- )} - {gpuProbeResult.devices && gpuProbeResult.devices.length > 0 && ( -
- Devices -
- {gpuProbeResult.devices.map((d: string, i: number) => ( - {d} - ))} -
-
- )} - {gpuProbeResult.render_time_s != null && ( -
- Render time - {gpuProbeResult.render_time_s.toFixed(2)}s -
- )} - {gpuProbeResult.error && ( -
- Error - {gpuProbeResult.error} -
- )} -
- )} - - {!gpuProbeResult && !gpuProbing && ( -

No probe result yet. Click "Run GPU Check" to trigger a test render.

- )} -
- )} -
- )} -

- {defaultTier ? `${Number(defaultTier.price_per_item).toFixed(2)}` : '—'} + {defaultTier ? `${Number(defaultTier.price_per_item).toFixed(2)}` : '--'}

Global default price

{!defaultTier && ( @@ -1893,7 +1961,7 @@ function PricingSummaryCard() { } -// ── Asset Library Panel ─────────────────────────────────────────────────────── +// -- Asset Library Panel ------------------------------------------------------------------- function AssetLibraryPanel() { const qc = useQueryClient() @@ -1992,7 +2060,7 @@ function AssetLibraryPanel() { disabled={!newName || !newFile || createMut.isPending} onClick={() => createMut.mutate()} > - {createMut.isPending ? 'Creating…' : 'Create'} + {createMut.isPending ? 'Creating...' : 'Create'}
- {lib.original_filename ?? '—'} + {lib.original_filename ?? '--'} {matCount} materials {ngCount} node groups