cc89415a29
One "Queue Export" click now renders the EXR sequence, then rebuilds the shot headlessly with the studio slate/overlay template to produce the delivery MOV and review MP4. Implements RenderPipeline2 phases 1-2 plus the preview stage. Server: - New models Export, RenderJob, ExportEvent, Machine, WorkerHeartbeat, plus Project.deliveryConfig and per-submission slate fields (Export.vfxScope, Export.submissionNote, inherited from the shot's previous export). Both migrations are purely additive; no existing column is touched. - lib/render-pipeline: server-enforced state machine, transactional version increment with supersede, atomic FOR UPDATE SKIP LOCKED claim gated by machine availability windows, and a lease reaper run from instrumentation.ts. - /api/ext/* endpoints for the panel and workers; session-auth mirrors under /api/render and /api/machines for the web UI. - Pipeline pages: render queue, export detail, machine monitoring, plus an Exports tab on shot detail. RenderWorker (.NET 8 Windows service, new): - Registration, heartbeat as cancel channel, claim loop, aerender runner with progress parsing and stall watchdog, crash recovery and disk-spooled reporting that survives server downtime. - Preview stage: headless AE assembles the preview comp into a throwaway AEP with both output modules queued, then a single aerender pass renders them. Preview jobs are not claimed while an interactive AE session is open, so an artist's project is never taken over. AE panel: Queue Export with live status polling, urgent flag, retry, and the VFX Scope / Submission Note fields. Every existing panel action is unchanged. Preview chaining ships disabled behind SystemConfig preview.enabled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
142 lines
4.8 KiB
TypeScript
142 lines
4.8 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { usePathname } from 'next/navigation';
|
|
import { cn } from '@/lib/utils';
|
|
import Image from 'next/image';
|
|
import { Josefin_Sans } from 'next/font/google';
|
|
import { Montserrat } from 'next/font/google';
|
|
import {
|
|
LayoutDashboard,
|
|
FolderOpen,
|
|
Users,
|
|
UserCog,
|
|
Settings,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
ListTodo,
|
|
CalendarRange,
|
|
BarChart2,
|
|
ListVideo,
|
|
CloudUpload,
|
|
Clapperboard,
|
|
LayoutGrid,
|
|
Server,
|
|
} from 'lucide-react';
|
|
import { useState } from 'react';
|
|
import { useSession } from 'next-auth/react';
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
const navItems = [
|
|
{ href: '/dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
|
{ href: '/projects', label: 'Projects', icon: FolderOpen },
|
|
{ href: '/shot-status', label: 'Shot Status', icon: BarChart2, hideForClient: true },
|
|
{ href: '/playlist', label: 'Playlist', icon: ListVideo, hideForClient: true },
|
|
{ href: '/tasks', label: 'My Tasks', icon: ListTodo, hideForClient: true },
|
|
{ href: '/pipeline', label: 'Pipeline', icon: Server, hideForClient: true },
|
|
{ href: '/shoot-log', label: 'Shot Log', icon: Clapperboard, supervisorOnly: true },
|
|
{ href: '/storyboard', label: 'Storyboard', icon: LayoutGrid, supervisorOnly: true },
|
|
{ href: '/schedule', label: 'Schedule', icon: CalendarRange, adminOnly: true },
|
|
{ href: '/batch-upload', label: 'Batch Upload', icon: CloudUpload, adminOnly: true },
|
|
{ href: '/clients', label: 'Clients', icon: Users, adminOnly: true },
|
|
{ href: '/users', label: 'Users', icon: UserCog, adminOnly: true, adminStrictOnly: true },
|
|
{ href: '/settings', label: 'Settings', icon: Settings },
|
|
];
|
|
|
|
const josefin = Josefin_Sans({
|
|
subsets: ['latin'],
|
|
weight: ['300', '400'],
|
|
});
|
|
const montserrat = Montserrat({
|
|
subsets: ['latin'],
|
|
weight: ['200', '500', '600'],
|
|
});
|
|
|
|
export function Sidebar() {
|
|
const pathname = usePathname();
|
|
const { data: session } = useSession();
|
|
const [collapsed, setCollapsed] = useState(false);
|
|
const isAdmin = ['ADMIN', 'PRODUCER'].includes(session?.user?.role ?? '');
|
|
|
|
return (
|
|
<aside
|
|
className={cn(
|
|
'flex flex-col border-r border-zinc-800 bg-zinc-900 transition-all duration-200',
|
|
collapsed ? 'w-16' : 'w-60',
|
|
)}
|
|
>
|
|
{/* Logo */}
|
|
<div
|
|
className={cn(
|
|
'flex items-center gap-3 px-4 py-5 border-b border-zinc-800',
|
|
collapsed && 'justify-center px-0',
|
|
)}
|
|
>
|
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-black">
|
|
<Image src="/logo.svg" alt="Logo" width={32} height={32} />
|
|
</div>
|
|
|
|
{!collapsed && (
|
|
<div className={montserrat.className}>
|
|
<span className="block text-2xl font-light text-white leading-none">
|
|
TWO TALES
|
|
</span>
|
|
|
|
<span className="block text-[11px] tracking-[0.18em] italic text-zinc-400 leading-none -mt-0.25">
|
|
vfx review
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<nav className="flex-1 px-2 py-4 space-y-1">
|
|
{navItems.map((item) => {
|
|
if (item.adminOnly && !isAdmin) return null;
|
|
if ((item as any).adminStrictOnly && session?.user?.role !== 'ADMIN') return null;
|
|
if ((item as any).hideForClient && session?.user?.role === 'CLIENT') return null;
|
|
if ((item as any).supervisorOnly && !['ADMIN', 'PRODUCER', 'SUPERVISOR'].includes(session?.user?.role ?? '')) return null;
|
|
const Icon = item.icon;
|
|
const isActive =
|
|
pathname === item.href || pathname.startsWith(item.href + '/');
|
|
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className={cn(
|
|
'flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors',
|
|
collapsed && 'justify-center px-0 w-full',
|
|
isActive
|
|
? 'bg-amber-500/10 text-amber-400'
|
|
: 'text-zinc-400 hover:text-white hover:bg-zinc-800',
|
|
)}
|
|
title={collapsed ? item.label : undefined}
|
|
>
|
|
<Icon className="h-4 w-4 shrink-0" />
|
|
{!collapsed && <span>{item.label}</span>}
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
{/* Collapse toggle */}
|
|
<div className="border-t border-zinc-800 p-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
className="w-full"
|
|
onClick={() => setCollapsed(!collapsed)}
|
|
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
>
|
|
{collapsed ? (
|
|
<ChevronRight className="h-4 w-4" />
|
|
) : (
|
|
<ChevronLeft className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|