refactor(admin): remove Pillow as selectable renderer, restructure admin page
- Backend: VALID_RENDERERS = {"blender"} only; remove pillow from renderer-status response
- Frontend: remove renderer picker (pillow/blender buttons) — Blender is the only renderer
- Blender options always visible, grouped into Render Quality / Performance / Output sections
- Maintenance buttons in 2-column grid with descriptions
- Page reorder: Pricing Summary → Users → Blender Settings → Render Templates →
Asset Libraries → Output Types → Pricing Tiers → SMTP → Templates
- Pillow code kept internally as fallback (not exposed in UI)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ from app.utils.auth import require_admin, hash_password
|
|||||||
|
|
||||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||||
|
|
||||||
VALID_RENDERERS = {"pillow", "blender"}
|
VALID_RENDERERS = {"blender"}
|
||||||
VALID_ENGINES = {"cycles", "eevee"}
|
VALID_ENGINES = {"cycles", "eevee"}
|
||||||
VALID_FORMATS = {"jpg", "png"}
|
VALID_FORMATS = {"jpg", "png"}
|
||||||
VALID_STL_QUALITIES = {"low", "high"}
|
VALID_STL_QUALITIES = {"low", "high"}
|
||||||
@@ -459,7 +459,6 @@ async def renderer_status(
|
|||||||
blender_available = is_blender_available()
|
blender_available = is_blender_available()
|
||||||
blender_bin = find_blender()
|
blender_bin = find_blender()
|
||||||
return {
|
return {
|
||||||
"pillow": {"available": True, "note": "Built-in (always available)"},
|
|
||||||
"blender": {
|
"blender": {
|
||||||
"available": blender_available,
|
"available": blender_available,
|
||||||
"note": (
|
"note": (
|
||||||
|
|||||||
+170
-193
@@ -167,66 +167,6 @@ export default function AdminPage() {
|
|||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
<PricingSummaryCard />
|
<PricingSummaryCard />
|
||||||
|
|
||||||
{/* ------------------------------------------------------------------ */}
|
|
||||||
{/* Pricing Tiers */}
|
|
||||||
{/* ------------------------------------------------------------------ */}
|
|
||||||
<div className="card">
|
|
||||||
<div className="p-4 border-b border-border-default flex items-center gap-2">
|
|
||||||
<DollarSign size={16} className="text-content-muted" />
|
|
||||||
<div>
|
|
||||||
<h2 className="font-semibold text-content">Pricing Tiers</h2>
|
|
||||||
<p className="text-xs text-content-muted mt-0.5">
|
|
||||||
Configure price per rendering item by category and quality level.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<PricingTierTable />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ------------------------------------------------------------------ */}
|
|
||||||
{/* Output Types */}
|
|
||||||
{/* ------------------------------------------------------------------ */}
|
|
||||||
<div className="card">
|
|
||||||
<div className="p-4 border-b border-border-light flex items-center gap-2">
|
|
||||||
<Layers size={16} className="text-content-muted" />
|
|
||||||
<div>
|
|
||||||
<h2 className="font-semibold text-content">Output Types</h2>
|
|
||||||
<p className="text-xs text-content-muted mt-0.5">
|
|
||||||
Define what kinds of outputs orders can request (thumbnails, views, formats).
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<OutputTypeTable />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ------------------------------------------------------------------ */}
|
|
||||||
{/* Render Templates (admin/PM) */}
|
|
||||||
{/* ------------------------------------------------------------------ */}
|
|
||||||
<div className="card">
|
|
||||||
<div className="p-4 border-b border-border-light flex items-center gap-2">
|
|
||||||
<FileBox size={16} className="text-content-muted" />
|
|
||||||
<div>
|
|
||||||
<h2 className="font-semibold text-content">Render Templates</h2>
|
|
||||||
<p className="text-xs text-content-muted mt-0.5">
|
|
||||||
Upload .blend studio setups matched by Category + Output Type. Geometry is imported into the template at render time.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4">
|
|
||||||
<RenderTemplateTable />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Material Library sub-section */}
|
|
||||||
<div className="border-t border-border-light p-4">
|
|
||||||
<MaterialLibraryPanel />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ------------------------------------------------------------------ */}
|
|
||||||
{/* Asset Libraries */}
|
|
||||||
{/* ------------------------------------------------------------------ */}
|
|
||||||
<AssetLibraryPanel />
|
|
||||||
|
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
{/* Users (admin only) */}
|
{/* Users (admin only) */}
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
@@ -307,13 +247,18 @@ export default function AdminPage() {
|
|||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
{/* Renderer Settings (admin only) */}
|
{/* Blender Render Settings (admin only) */}
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
{isAdmin && <div className="card">
|
{isAdmin && <div className="card">
|
||||||
<div className="p-4 border-b border-border-default flex items-center justify-between">
|
<div className="p-4 border-b border-border-default flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Settings size={16} className="text-content-muted" />
|
<Settings size={16} className="text-content-muted" />
|
||||||
<h2 className="font-semibold text-content">Thumbnail Renderer</h2>
|
<div>
|
||||||
|
<h2 className="font-semibold text-content">Blender Render Settings</h2>
|
||||||
|
<p className="text-xs text-content-muted mt-0.5">
|
||||||
|
Render quality, performance and thumbnail output options for Blender 5.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => refetchStatus()}
|
onClick={() => refetchStatus()}
|
||||||
@@ -324,36 +269,11 @@ export default function AdminPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-6">
|
||||||
{/* Renderer picker */}
|
{/* ── Render Quality ───────────────────────────────────────────── */}
|
||||||
<div className="flex items-center gap-4 flex-wrap">
|
<div className="space-y-4">
|
||||||
<label className="text-sm font-medium text-content-secondary shrink-0">Active renderer:</label>
|
<p className="text-xs font-semibold text-content-secondary uppercase tracking-wide">Render Quality</p>
|
||||||
{(['blender', 'pillow'] as const).map((r) => (
|
|
||||||
<button
|
|
||||||
key={r}
|
|
||||||
onClick={() => updateSettingsMut.mutate({ thumbnail_renderer: r })}
|
|
||||||
disabled={updateSettingsMut.isPending}
|
|
||||||
title={
|
|
||||||
r === 'pillow'
|
|
||||||
? 'Python Pillow — generates a placeholder grey image (no 3D rendering)'
|
|
||||||
: 'Blender 5 — full ray-traced thumbnail via headless Blender (Cycles or EEVEE)'
|
|
||||||
}
|
|
||||||
className={`px-4 py-1.5 rounded-full border text-sm font-medium transition-colors ${
|
|
||||||
settings?.thumbnail_renderer === r
|
|
||||||
? 'text-white'
|
|
||||||
: 'bg-surface text-content-secondary border-border-default hover:border-accent hover:text-accent'
|
|
||||||
}`}
|
|
||||||
style={settings?.thumbnail_renderer === r ? { backgroundColor: 'var(--color-accent)', borderColor: 'var(--color-accent)' } : undefined}
|
|
||||||
>
|
|
||||||
{r === 'pillow' ? 'Pillow (placeholder)' : 'Blender 5'}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Blender options — shown only when blender is the active renderer */}
|
|
||||||
{settings?.thumbnail_renderer === 'blender' && (
|
|
||||||
<div className="rounded-lg border border-border-default bg-surface-alt p-4 space-y-4">
|
<div className="rounded-lg border border-border-default bg-surface-alt p-4 space-y-4">
|
||||||
<p className="text-xs font-semibold text-content-secondary uppercase tracking-wide">Blender 5 Options</p>
|
|
||||||
|
|
||||||
{/* Engine */}
|
{/* Engine */}
|
||||||
<div className="flex items-center gap-6 flex-wrap">
|
<div className="flex items-center gap-6 flex-wrap">
|
||||||
@@ -415,9 +335,7 @@ export default function AdminPage() {
|
|||||||
{/* Sample counts */}
|
{/* Sample counts */}
|
||||||
<div className="grid grid-cols-2 gap-4 max-w-sm">
|
<div className="grid grid-cols-2 gap-4 max-w-sm">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-content-secondary mb-1">
|
<label className="block text-xs font-medium text-content-secondary mb-1">Cycles samples</label>
|
||||||
Cycles samples
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={1} max={4096} step={32}
|
min={1} max={4096} step={32}
|
||||||
@@ -429,9 +347,7 @@ export default function AdminPage() {
|
|||||||
<p className="text-xs text-content-muted mt-0.5">Higher = better quality, slower</p>
|
<p className="text-xs text-content-muted mt-0.5">Higher = better quality, slower</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-content-secondary mb-1">
|
<label className="block text-xs font-medium text-content-secondary mb-1">EEVEE samples</label>
|
||||||
EEVEE samples
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={1} max={1024} step={16}
|
min={1} max={1024} step={16}
|
||||||
@@ -485,8 +401,13 @@ export default function AdminPage() {
|
|||||||
: `Faces with edges sharper than ${blender.blender_smooth_angle ?? 30}° stay hard; others smooth. 30° works well for most mechanical parts.`}
|
: `Faces with edges sharper than ${blender.blender_smooth_angle ?? 30}° stay hard; others smooth. 30° works well for most mechanical parts.`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Max concurrent renders */}
|
{/* ── Performance ──────────────────────────────────────────────── */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-xs font-semibold text-content-secondary uppercase tracking-wide">Performance</p>
|
||||||
|
<div className="rounded-lg border border-border-default bg-surface-alt p-4 space-y-4">
|
||||||
<div className="flex items-center gap-4 flex-wrap">
|
<div className="flex items-center gap-4 flex-wrap">
|
||||||
<span className="text-sm font-medium text-content-secondary w-28 shrink-0">Max concurrent</span>
|
<span className="text-sm font-medium text-content-secondary w-28 shrink-0">Max concurrent</span>
|
||||||
<input
|
<input
|
||||||
@@ -501,7 +422,6 @@ export default function AdminPage() {
|
|||||||
Max parallel Blender render jobs (1–16). Higher values use more RAM (~400 MB each). Applied live without restart.
|
Max parallel Blender render jobs (1–16). Higher values use more RAM (~400 MB each). Applied live without restart.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 flex-wrap">
|
<div className="flex items-center gap-4 flex-wrap">
|
||||||
<span className="text-sm font-medium text-content-secondary w-28 shrink-0">Stall timeout</span>
|
<span className="text-sm font-medium text-content-secondary w-28 shrink-0">Stall timeout</span>
|
||||||
<input
|
<input
|
||||||
@@ -516,23 +436,25 @@ export default function AdminPage() {
|
|||||||
Minutes before a stuck render job is auto-restarted (10–10080). Checked every 5 min by the watchdog.
|
Minutes before a stuck render job is auto-restarted (10–10080). Checked every 5 min by the watchdog.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Save blender options */}
|
|
||||||
{Object.keys(blenderDraft).length > 0 && (
|
|
||||||
<button
|
|
||||||
onClick={() => updateSettingsMut.mutate(blenderDraft)}
|
|
||||||
disabled={updateSettingsMut.isPending}
|
|
||||||
className="btn-primary text-sm"
|
|
||||||
>
|
|
||||||
{updateSettingsMut.isPending ? 'Saving…' : 'Save Blender Options'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Save button — appears when draft has unsaved changes */}
|
||||||
|
{Object.keys(blenderDraft).length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => updateSettingsMut.mutate(blenderDraft)}
|
||||||
|
disabled={updateSettingsMut.isPending}
|
||||||
|
className="btn-primary text-sm"
|
||||||
|
>
|
||||||
|
{updateSettingsMut.isPending ? 'Saving…' : 'Save Settings'}
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Output format — always visible, applies to all renderers */}
|
{/* ── Output ───────────────────────────────────────────────────── */}
|
||||||
<div className="flex items-center gap-4 flex-wrap pt-1">
|
<div className="space-y-4">
|
||||||
<label className="text-sm font-medium text-content-secondary shrink-0 w-28">Output format:</label>
|
<p className="text-xs font-semibold text-content-secondary uppercase tracking-wide">Output</p>
|
||||||
|
<div className="flex items-center gap-4 flex-wrap">
|
||||||
|
<label className="text-sm font-medium text-content-secondary shrink-0 w-28">Thumbnail format</label>
|
||||||
{(['jpg', 'png'] as const).map((fmt) => (
|
{(['jpg', 'png'] as const).map((fmt) => (
|
||||||
<button
|
<button
|
||||||
key={fmt}
|
key={fmt}
|
||||||
@@ -674,97 +596,152 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
</div>{/* end Output */}
|
||||||
|
|
||||||
{/* Service health */}
|
{/* ── Service Status ───────────────────────────────────────────── */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
<div className="space-y-3">
|
||||||
{rendererStatus && Object.entries(rendererStatus).map(([name, info]) => (
|
<p className="text-xs font-semibold text-content-secondary uppercase tracking-wide">Service Status</p>
|
||||||
<div
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
key={name}
|
{rendererStatus && Object.entries(rendererStatus).map(([name, info]) => (
|
||||||
className={`rounded-lg border p-3 flex items-start gap-2.5 ${
|
<div
|
||||||
info.available ? 'border-border-default bg-status-success-bg' : 'border-border-default bg-surface-alt'
|
key={name}
|
||||||
}`}
|
className={`rounded-lg border p-3 flex items-start gap-2.5 ${
|
||||||
>
|
info.available ? 'border-border-default bg-status-success-bg' : 'border-border-default bg-surface-alt'
|
||||||
{info.available
|
}`}
|
||||||
? <CheckCircle2 size={16} className="text-green-500 shrink-0 mt-0.5" />
|
>
|
||||||
: info.url === null
|
{info.available
|
||||||
? <CheckCircle2 size={16} className="text-green-500 shrink-0 mt-0.5" />
|
? <CheckCircle2 size={16} className="text-green-500 shrink-0 mt-0.5" />
|
||||||
: <XCircle size={16} className="text-content-muted shrink-0 mt-0.5" />
|
: <XCircle size={16} className="text-content-muted shrink-0 mt-0.5" />
|
||||||
}
|
}
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-sm font-semibold text-content capitalize">{name}</p>
|
<p className="text-sm font-semibold text-content capitalize">{name}</p>
|
||||||
<p className="text-xs text-content-muted truncate">{info.note || (info.available ? 'Online' : 'Offline')}</p>
|
<p className="text-xs text-content-muted truncate">{info.note || (info.available ? 'Online' : 'Offline')}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
{!rendererStatus && (
|
||||||
{!rendererStatus && (
|
<div className="flex items-center gap-2 text-xs text-content-muted p-2">
|
||||||
<div className="col-span-3 flex items-center gap-2 text-xs text-content-muted p-2">
|
<Clock size={13} /> Checking service status…
|
||||||
<Clock size={13} /> Checking service status…
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Process unprocessed / Regenerate thumbnails */}
|
{/* ── Maintenance ──────────────────────────────────────────────── */}
|
||||||
<div className="pt-2 border-t border-border-light space-y-2">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-3">
|
<p className="text-xs font-semibold text-content-secondary uppercase tracking-wide">Maintenance</p>
|
||||||
<button
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
onClick={() => processUnprocessedMut.mutate()}
|
<div className="flex flex-col gap-1">
|
||||||
disabled={processUnprocessedMut.isPending}
|
<button
|
||||||
className="btn-secondary text-sm"
|
onClick={() => processUnprocessedMut.mutate()}
|
||||||
title="Queue all pending and failed STEP files that have never been successfully processed"
|
disabled={processUnprocessedMut.isPending}
|
||||||
>
|
className="btn-secondary text-sm w-full justify-start"
|
||||||
<RefreshCw size={14} className={processUnprocessedMut.isPending ? 'animate-spin' : ''} />
|
title="Queue all pending and failed STEP files that have never been successfully processed"
|
||||||
{processUnprocessedMut.isPending ? 'Queueing…' : 'Process Unprocessed'}
|
>
|
||||||
</button>
|
<RefreshCw size={14} className={processUnprocessedMut.isPending ? 'animate-spin' : ''} />
|
||||||
<p className="text-xs text-content-muted">
|
{processUnprocessedMut.isPending ? 'Queueing…' : 'Process Unprocessed'}
|
||||||
Queues all pending and failed STEP files for initial processing.
|
</button>
|
||||||
</p>
|
<p className="text-xs text-content-muted">Queues all pending/failed STEP files for initial processing.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex flex-col gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => regenerateMut.mutate()}
|
onClick={() => regenerateMut.mutate()}
|
||||||
disabled={regenerateMut.isPending}
|
disabled={regenerateMut.isPending}
|
||||||
className="btn-secondary text-sm"
|
className="btn-secondary text-sm w-full justify-start"
|
||||||
title="Re-process all existing STEP files to regenerate thumbnails using the currently selected renderer"
|
title="Re-render thumbnails for all completed CAD files using the current Blender settings"
|
||||||
>
|
>
|
||||||
<RefreshCw size={14} className={regenerateMut.isPending ? 'animate-spin' : ''} />
|
<RefreshCw size={14} className={regenerateMut.isPending ? 'animate-spin' : ''} />
|
||||||
{regenerateMut.isPending ? 'Re-queuing…' : 'Regenerate All Thumbnails'}
|
{regenerateMut.isPending ? 'Re-queuing…' : 'Regenerate All Thumbnails'}
|
||||||
</button>
|
</button>
|
||||||
<p className="text-xs text-content-muted">
|
<p className="text-xs text-content-muted">Re-renders thumbnails for all completed CAD files.</p>
|
||||||
Re-processes all existing STEP files with the currently selected renderer.
|
</div>
|
||||||
</p>
|
<div className="flex flex-col gap-1">
|
||||||
</div>
|
<button
|
||||||
<div className="flex items-center gap-3">
|
onClick={() => generateMissingStlsMut.mutate()}
|
||||||
<button
|
disabled={generateMissingStlsMut.isPending}
|
||||||
onClick={() => generateMissingStlsMut.mutate()}
|
className="btn-secondary text-sm w-full justify-start"
|
||||||
disabled={generateMissingStlsMut.isPending}
|
title="Queue STL conversion for every low/high quality that is not yet cached on disk"
|
||||||
className="btn-secondary text-sm"
|
>
|
||||||
title="Queue STL conversion for every low/high quality that is not yet cached on disk"
|
<RefreshCw size={14} className={generateMissingStlsMut.isPending ? 'animate-spin' : ''} />
|
||||||
>
|
{generateMissingStlsMut.isPending ? 'Queueing…' : 'Generate Missing STLs'}
|
||||||
<RefreshCw size={14} className={generateMissingStlsMut.isPending ? 'animate-spin' : ''} />
|
</button>
|
||||||
{generateMissingStlsMut.isPending ? 'Queueing…' : 'Generate Missing STLs'}
|
<p className="text-xs text-content-muted">Generates low + high STL files for completed STEP files missing them.</p>
|
||||||
</button>
|
</div>
|
||||||
<p className="text-xs text-content-muted">
|
<div className="flex flex-col gap-1">
|
||||||
Generates low + high STL files for any completed STEP file that is missing them.
|
<button
|
||||||
</p>
|
onClick={() => seedWorkflowsMut.mutate()}
|
||||||
</div>
|
disabled={seedWorkflowsMut.isPending}
|
||||||
<div className="flex items-center gap-3">
|
className="btn-secondary text-sm w-full justify-start"
|
||||||
<button
|
title="Create standard workflow definitions (Still Cycles/EEVEE, Turntable, Multi-Angle) if not yet present"
|
||||||
onClick={() => seedWorkflowsMut.mutate()}
|
>
|
||||||
disabled={seedWorkflowsMut.isPending}
|
<RefreshCw size={14} className={seedWorkflowsMut.isPending ? 'animate-spin' : ''} />
|
||||||
className="btn-secondary text-sm"
|
{seedWorkflowsMut.isPending ? 'Seeding…' : 'Seed Standard Workflows'}
|
||||||
title="Create standard workflow definitions (Still Cycles/EEVEE, Turntable, Multi-Angle) if not yet present"
|
</button>
|
||||||
>
|
<p className="text-xs text-content-muted">Creates the 4 standard workflow definitions if they don't exist yet.</p>
|
||||||
<RefreshCw size={14} className={seedWorkflowsMut.isPending ? 'animate-spin' : ''} />
|
</div>
|
||||||
{seedWorkflowsMut.isPending ? 'Seeding…' : 'Seed Standard Workflows'}
|
|
||||||
</button>
|
|
||||||
<p className="text-xs text-content-muted">
|
|
||||||
Creates the 4 standard workflow definitions (still/turntable/multi-angle) if they don't exist.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
|
{/* ------------------------------------------------------------------ */}
|
||||||
|
{/* Render Templates (admin/PM) */}
|
||||||
|
{/* ------------------------------------------------------------------ */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="p-4 border-b border-border-light flex items-center gap-2">
|
||||||
|
<FileBox size={16} className="text-content-muted" />
|
||||||
|
<div>
|
||||||
|
<h2 className="font-semibold text-content">Render Templates</h2>
|
||||||
|
<p className="text-xs text-content-muted mt-0.5">
|
||||||
|
Upload .blend studio setups matched by Category + Output Type. Geometry is imported into the template at render time.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<RenderTemplateTable />
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-border-light p-4">
|
||||||
|
<MaterialLibraryPanel />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ------------------------------------------------------------------ */}
|
||||||
|
{/* Asset Libraries */}
|
||||||
|
{/* ------------------------------------------------------------------ */}
|
||||||
|
<AssetLibraryPanel />
|
||||||
|
|
||||||
|
{/* ------------------------------------------------------------------ */}
|
||||||
|
{/* Output Types */}
|
||||||
|
{/* ------------------------------------------------------------------ */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="p-4 border-b border-border-light flex items-center gap-2">
|
||||||
|
<Layers size={16} className="text-content-muted" />
|
||||||
|
<div>
|
||||||
|
<h2 className="font-semibold text-content">Output Types</h2>
|
||||||
|
<p className="text-xs text-content-muted mt-0.5">
|
||||||
|
Define what kinds of outputs orders can request (thumbnails, views, formats).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<OutputTypeTable />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ------------------------------------------------------------------ */}
|
||||||
|
{/* Pricing Tiers */}
|
||||||
|
{/* ------------------------------------------------------------------ */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="p-4 border-b border-border-default flex items-center gap-2">
|
||||||
|
<DollarSign size={16} className="text-content-muted" />
|
||||||
|
<div>
|
||||||
|
<h2 className="font-semibold text-content">Pricing Tiers</h2>
|
||||||
|
<p className="text-xs text-content-muted mt-0.5">
|
||||||
|
Configure price per rendering item by category and quality level.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PricingTierTable />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
{/* E-Mail / SMTP Settings */}
|
{/* E-Mail / SMTP Settings */}
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
|
|||||||
Reference in New Issue
Block a user