chore: snapshot workflow migration progress

This commit is contained in:
2026-04-12 11:49:04 +02:00
parent 0cd02513d5
commit 3e810c74a3
163 changed files with 31774 additions and 2753 deletions
File diff suppressed because it is too large Load Diff
@@ -34,6 +34,32 @@ const EMPTY_FORM = {
lighting_only: false,
shadow_catcher_enabled: false,
camera_orbit: true,
workflow_input_schema_text: '[]',
}
function stringifyWorkflowInputSchema(value: unknown): string {
try {
return JSON.stringify(Array.isArray(value) ? value : [], null, 2)
} catch {
return '[]'
}
}
function parseWorkflowInputSchemaText(rawValue: unknown): unknown[] {
const text = typeof rawValue === 'string' ? rawValue.trim() : ''
if (!text) return []
let parsed: unknown
try {
parsed = JSON.parse(text)
} catch {
throw new Error('Workflow input schema must be valid JSON')
}
if (!Array.isArray(parsed)) {
throw new Error('Workflow input schema must be a JSON array')
}
return parsed
}
export default function RenderTemplateTable() {
@@ -43,7 +69,7 @@ export default function RenderTemplateTable() {
const [addFile, setAddFile] = useState<File | null>(null)
const [cloneBlendFrom, setCloneBlendFrom] = useState<string>('')
const [editingId, setEditingId] = useState<string | null>(null)
const [editDraft, setEditDraft] = useState<Partial<RenderTemplate>>({})
const [editDraft, setEditDraft] = useState<(Partial<RenderTemplate> & { workflow_input_schema_text?: string })>({})
const fileInputRef = useRef<HTMLInputElement>(null)
const reuploadRef = useRef<HTMLInputElement>(null)
const [reuploadId, setReuploadId] = useState<string | null>(null)
@@ -75,6 +101,7 @@ export default function RenderTemplateTable() {
fd.append('lighting_only', String(form.lighting_only))
fd.append('shadow_catcher_enabled', String(form.shadow_catcher_enabled))
fd.append('camera_orbit', String(form.camera_orbit))
fd.append('workflow_input_schema', JSON.stringify(parseWorkflowInputSchemaText(form.workflow_input_schema_text)))
return createRenderTemplate(fd)
},
onSuccess: () => {
@@ -85,7 +112,7 @@ export default function RenderTemplateTable() {
setCloneBlendFrom('')
setShowAdd(false)
},
onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to create template'),
onError: (e: any) => toast.error(e.response?.data?.detail || e.message || 'Failed to create template'),
})
const updateMut = useMutation({
@@ -96,7 +123,7 @@ export default function RenderTemplateTable() {
qc.invalidateQueries({ queryKey: ['render-templates'] })
setEditingId(null)
},
onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to update'),
onError: (e: any) => toast.error(e.response?.data?.detail || e.message || 'Failed to update'),
})
const deleteMut = useMutation({
@@ -128,6 +155,7 @@ export default function RenderTemplateTable() {
shadow_catcher_enabled: t.shadow_catcher_enabled,
camera_orbit: t.camera_orbit,
output_type_ids: t.output_type_ids ?? [],
workflow_input_schema: t.workflow_input_schema ?? [],
}),
onSuccess: () => {
toast.success('Template duplicated')
@@ -147,13 +175,19 @@ export default function RenderTemplateTable() {
lighting_only: t.lighting_only,
shadow_catcher_enabled: t.shadow_catcher_enabled,
camera_orbit: t.camera_orbit,
workflow_input_schema_text: stringifyWorkflowInputSchema(t.workflow_input_schema),
is_active: t.is_active,
})
}
function saveEdit() {
if (!editingId) return
updateMut.mutate({ id: editingId, data: editDraft as Record<string, unknown> })
const data: Record<string, unknown> = { ...editDraft }
if (Object.prototype.hasOwnProperty.call(editDraft, 'workflow_input_schema_text')) {
data.workflow_input_schema = parseWorkflowInputSchemaText(editDraft.workflow_input_schema_text)
delete data.workflow_input_schema_text
}
updateMut.mutate({ id: editingId, data })
}
// Render the edit form grid (shared between edit-row and add-row)
@@ -174,6 +208,9 @@ export default function RenderTemplateTable() {
if (field === 'lighting_only') return editDraft.lighting_only ?? t!.lighting_only
if (field === 'shadow_catcher_enabled') return editDraft.shadow_catcher_enabled ?? t!.shadow_catcher_enabled
if (field === 'camera_orbit') return editDraft.camera_orbit ?? t!.camera_orbit
if (field === 'workflow_input_schema_text') {
return editDraft.workflow_input_schema_text ?? stringifyWorkflowInputSchema(t!.workflow_input_schema)
}
if (field === 'is_active') return editDraft.is_active ?? t!.is_active
return (editDraft as any)[field] ?? (t as any)[field]
}
@@ -381,7 +418,27 @@ export default function RenderTemplateTable() {
)}
</div>
{/* Row 4: Active + Save/Cancel */}
<div className="mt-4">
<label className="block text-xs font-medium text-content-muted mb-1">
Workflow Input Schema (JSON)
</label>
<textarea
className="input-sm w-full min-h-36 font-mono text-xs"
value={String(val('workflow_input_schema_text') ?? '[]')}
onChange={(e) => set('workflow_input_schema_text', e.target.value)}
placeholder='[{"key":"studio_variant","label":"Studio Variant","type":"select","options":[{"value":"default","label":"Default"}]}]'
/>
<p className="mt-1 text-xs text-content-muted">
Defines additional `resolve_template` node inputs for this .blend template.
</p>
<p className="mt-1 text-xs text-content-muted">
Matching variants can be bound inside the template via markers like
`template-input:studio_variant=warm` or a `template_input=studio_variant=warm`
custom property on collections, objects, or worlds.
</p>
</div>
{/* Row 5: Active + Save/Cancel */}
<div className="flex items-center justify-between mt-4 pt-3 border-t border-border-light">
{isEdit ? (
<label className="flex items-center gap-2">
@@ -0,0 +1,80 @@
import type { OutputTypeWorkflowRolloutMode } from '../../api/outputTypes'
export interface OutputTypeRolloutPresentation {
badgeLabel: string
badgeClassName: string
statusLabel: string
statusClassName: string
operatorHint: string
rowSummary: string
}
interface OutputTypeRolloutPresentationOptions {
hasWorkflowLink: boolean
workflowRolloutMode: OutputTypeWorkflowRolloutMode
hasBlockingIssues?: boolean
}
export function getOutputTypeRolloutPresentation({
hasWorkflowLink,
workflowRolloutMode,
hasBlockingIssues = false,
}: OutputTypeRolloutPresentationOptions): OutputTypeRolloutPresentation {
if (!hasWorkflowLink) {
return {
badgeLabel: 'Legacy Only',
badgeClassName: 'bg-surface-muted text-content-muted',
statusLabel: 'Production: Legacy',
statusClassName: 'bg-slate-100 text-slate-700',
operatorHint:
'No workflow is linked. Production stays entirely on the legacy dispatcher until a compatible graph workflow is attached.',
rowSummary: 'No linked graph workflow.',
}
}
if (hasBlockingIssues) {
return {
badgeLabel: 'Contract Blocked',
badgeClassName: 'bg-red-100 text-red-700',
statusLabel: 'Do Not Promote',
statusClassName: 'bg-red-100 text-red-700',
operatorHint:
'The current workflow binding is contract-invalid. Keep legacy authoritative until family, artifact, and rollout settings are fixed.',
rowSummary: 'Linked workflow needs contract fixes before rollout.',
}
}
switch (workflowRolloutMode) {
case 'graph':
return {
badgeLabel: 'Graph Authoritative',
badgeClassName: 'bg-status-success-bg text-status-success-text',
statusLabel: 'Production: Graph',
statusClassName: 'bg-emerald-100 text-emerald-700',
operatorHint:
'Graph dispatch is authoritative for production. Legacy remains the operational fallback if graph dispatch fails.',
rowSummary: 'Graph drives production with legacy fallback armed.',
}
case 'shadow':
return {
badgeLabel: 'Shadow',
badgeClassName: 'bg-status-info-bg text-status-info-text',
statusLabel: 'Production: Legacy',
statusClassName: 'bg-sky-100 text-sky-700',
operatorHint:
'Legacy stays authoritative while the graph runs as an observer for parity and rollout-gate checks.',
rowSummary: 'Graph observes only; legacy remains authoritative.',
}
case 'legacy_only':
default:
return {
badgeLabel: 'Legacy Only',
badgeClassName: 'bg-surface-muted text-content-muted',
statusLabel: 'Production: Legacy',
statusClassName: 'bg-slate-100 text-slate-700',
operatorHint:
'A workflow is linked for authoring and future rollout, but production dispatch remains on the legacy path.',
rowSummary: 'Linked graph is not active in production.',
}
}
}