fix: render pipeline + multi-tenancy bugs (B-Fix-1 through B-Fix-9)

- Remove worker-thumbnail (no Blender, was competing on thumbnail_rendering)
- Move render_order_line_task to thumbnail_rendering queue (render-worker)
- Restore template_service.py real implementation (fix circular import shim)
- Thread tenant_id through STEP upload, Excel import, product create
- Make system tables (output_types, materials, etc.) tenant_id nullable
- Fix tenants frontend 307-redirect: use trailing slash /tenants/
- Remove Flamenco + Three.js from Admin UI (unsupported)
- Set all output_types render_backend to celery (was flamenco)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 19:34:20 +01:00
parent 381f44bc8b
commit ab3f9c734a
14 changed files with 149 additions and 369 deletions
@@ -9,9 +9,8 @@ import type { OutputType } from '../../api/outputTypes'
import { listPricingTiers } from '../../api/pricing'
import type { PricingTier } from '../../api/pricing'
const RENDERERS = ['threejs', 'blender', 'pillow']
const RENDERERS = ['blender', 'pillow']
const FORMATS = ['png', 'jpg', 'gltf', 'stl', 'mp4', 'webm']
const BACKENDS = ['auto', 'celery', 'flamenco']
const ALL_CATEGORIES = [
{ key: 'TRB', label: 'TRB' },
{ key: 'Kugellager', label: 'Kugellager' },
@@ -173,10 +172,9 @@ export default function OutputTypeTable() {
<thead>
<tr className="border-b border-border-light text-left">
<th className="px-4 py-2 font-medium text-content-secondary">Name</th>
<th className="px-4 py-2 font-medium text-content-secondary" title="Renderer used to produce this output (pillow / blender / threejs / flamenco)">Renderer</th>
<th className="px-4 py-2 font-medium text-content-secondary" title="Renderer used to produce this output (blender / pillow)">Renderer</th>
<th className="px-4 py-2 font-medium text-content-secondary" title="Output file format (jpg, png, mp4, webm)">Format</th>
<th className="px-4 py-2 font-medium text-content-secondary" title="Render backend override: auto = system default, celery = local worker, flamenco = render farm">Backend</th>
<th className="px-4 py-2 font-medium text-content-secondary" title="Animation — output is a video (mp4/webm turntable); uses Flamenco by default">Anim</th>
<th className="px-4 py-2 font-medium text-content-secondary" title="Animation — output is a video (mp4/webm turntable)">Anim</th>
<th className="px-4 py-2 font-medium text-content-secondary" title="Turntable animation settings: frame count, FPS, and rotation axis (animation output types only)">Turntable</th>
<th className="px-4 py-2 font-medium text-content-secondary" title="Transparent Background — render with alpha channel (PNG only; Blender film_transparent)">BG</th>
<th className="px-4 py-2 font-medium text-content-secondary" title="Cycles compute device override — auto, gpu, or cpu (Blender/Cycles only)">Device</th>
@@ -194,7 +192,7 @@ export default function OutputTypeTable() {
<tbody>
{isLoading && (
<tr>
<td colSpan={17} className="px-4 py-4 text-center text-content-muted">Loading</td>
<td colSpan={16} className="px-4 py-4 text-center text-content-muted">Loading</td>
</tr>
)}
{types?.map((ot) => (
@@ -226,15 +224,6 @@ export default function OutputTypeTable() {
{FORMATS.map((f) => <option key={f}>{f}</option>)}
</select>
</td>
<td className="px-4 py-2">
<select
className="input-sm"
value={editDraft.render_backend ?? ot.render_backend}
onChange={(e) => setEditDraft({ ...editDraft, render_backend: e.target.value })}
>
{BACKENDS.map((b) => <option key={b}>{b}</option>)}
</select>
</td>
<td className="px-4 py-2">
<input
type="checkbox"
@@ -523,15 +512,6 @@ export default function OutputTypeTable() {
<td className="px-4 py-2 font-medium">{ot.name}</td>
<td className="px-4 py-2 text-content-muted">{ot.renderer}</td>
<td className="px-4 py-2 text-content-muted">{ot.output_format}</td>
<td className="px-4 py-2">
<span className={`text-xs px-1.5 py-0.5 rounded ${
ot.render_backend === 'flamenco' ? 'bg-status-warning-bg text-status-warning-text' :
ot.render_backend === 'celery' ? 'bg-status-info-bg text-status-info-text' :
'bg-surface-muted text-content-muted'
}`}>
{ot.render_backend}
</span>
</td>
<td className="px-4 py-2">
{ot.is_animation && (
<span className="text-xs px-1.5 py-0.5 rounded bg-purple-50 text-purple-700">video</span>
@@ -557,13 +537,13 @@ export default function OutputTypeTable() {
{ot.transparent_bg && (
<span className="text-xs px-1.5 py-0.5 rounded bg-sky-50 text-sky-700" title="Transparent background">alpha</span>
)}
{ot.render_settings?.bg_color && (
{!!ot.render_settings?.bg_color && (
<div className="flex items-center gap-1" title={`BG color: ${ot.render_settings.bg_color}`}>
<span
className="inline-block w-4 h-4 rounded border border-border-default"
style={{ backgroundColor: ot.render_settings.bg_color as string }}
/>
<span className="text-xs text-content-muted font-mono">{ot.render_settings.bg_color}</span>
<span className="text-xs text-content-muted font-mono">{ot.render_settings.bg_color as string}</span>
</div>
)}
</div>
@@ -604,7 +584,7 @@ export default function OutputTypeTable() {
<td className="px-4 py-2">
{showBlenderSettings(ot.renderer) ? (
ot.render_settings?.samples ? (
<span className="text-xs font-medium text-content-secondary">{ot.render_settings.samples}</span>
<span className="text-xs font-medium text-content-secondary">{ot.render_settings.samples as number}</span>
) : (
<span className="text-xs text-content-muted">default</span>
)
@@ -621,7 +601,7 @@ export default function OutputTypeTable() {
</span>
) : null}
{ot.render_settings?.noise_threshold ? (
<span className="text-xs text-content-muted">t={ot.render_settings.noise_threshold}</span>
<span className="text-xs text-content-muted">t={ot.render_settings.noise_threshold as string}</span>
) : null}
{ot.render_settings?.denoising_prefilter ? (
<span className="text-xs text-content-muted">{ot.render_settings.denoising_prefilter as string}</span>
@@ -728,15 +708,6 @@ export default function OutputTypeTable() {
{FORMATS.map((f) => <option key={f}>{f}</option>)}
</select>
</td>
<td className="px-4 py-2">
<select
className="input-sm"
value={form.render_backend}
onChange={(e) => setForm({ ...form, render_backend: e.target.value })}
>
{BACKENDS.map((b) => <option key={b}>{b}</option>)}
</select>
</td>
<td className="px-4 py-2">
<input
type="checkbox"