feb1c7b3e9
Completes RenderPipeline2 phases 3-5. Additive throughout: with validation.enabled=false the pipeline behaves exactly as before. Validation (§8) runs on the worker straight after a render, while it still holds the job and the files are local. Everything is read from the EXR header — one 64 KB read per sampled frame, no pixel decode — so the engine needs no oiiotool. The render job deliberately stays open through the stage, so a worker that dies there is reaped and retried instead of stranding the export in VALIDATING. Timecode is the primary check and is anchored on the plate, never the API: measured on the live show, Shot.timecodeStart is the editorial in-point and sits one handle-length (8f) ahead of the plate, so checking against it would pass the defective exports and fail the correct ones. Reference precedence is manifest timing plate -> plate found on disk -> API value, and the API rank is WARN-only. The panel now resolves plate provenance by walking the comp's own footage layers, so the worker never guesses which element carries the comp's timing. Verified against real studio renders via the new --validate mode: it catches both defect classes seen in shipped work — the +8 handle offset and frame-derived timecode (00:00:41:17 = frame 1001 at 24fps). QC (§10) is one service behind two front doors, API-key for the panel and session for the web. PASS means ready to upload and nothing else; FAIL sets the comp task to CHANGES. The panel gains a QC section that imports the real EXR sequence at full res rather than the H.264. Delivery (§11) rides the existing queue as a DELIVERY_BUILD job: hard links with a copy fallback, verification by re-hashing delivered bytes against the validation-time digest, manifest.json and the CSV report. fsutil hardlink list is unavailable over SMB, so link identity is never queried — reading the bytes back covers links and copies alike. Also fixes ApiClient.ProgressAsync throwing on transport failure, which would have aborted work in flight during a brief server outage; progress reporting is best-effort by contract. Removes the stale RenderWorker/ rule from the root .gitignore. The worker's source is tracked like the rest of the repo and its own .gitignore already covers bin/, obj/ and publish/; the rule silently excluded new files while the existing 20 stayed tracked. Not yet applied: migration 20260813000000 was hand-written because the dev database was unavailable — run prisma migrate deploy and verify before relying on it. None of this has run against a live server. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1093 lines
32 KiB
Plaintext
1093 lines
32 KiB
Plaintext
// This is your Prisma schema file,
|
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
binaryTargets = ["native", "linux-musl-arm64-openssl-3.0.x"]
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
// ─────────────────────────────────────────────
|
|
// ENUMS
|
|
// ─────────────────────────────────────────────
|
|
|
|
enum Role {
|
|
ADMIN
|
|
PRODUCER
|
|
SUPERVISOR
|
|
ARTIST
|
|
CLIENT
|
|
}
|
|
|
|
enum ProjectStatus {
|
|
ACTIVE
|
|
ON_HOLD
|
|
COMPLETED
|
|
ARCHIVED
|
|
}
|
|
|
|
enum ShotStatus {
|
|
WAITING
|
|
IN_PROGRESS
|
|
INTERNAL_REVIEW
|
|
READY_FOR_CLIENT
|
|
CLIENT_REVIEW
|
|
REVISIONS
|
|
COMPLETE
|
|
}
|
|
|
|
enum ShotApprovalStatus {
|
|
PENDING
|
|
INTERNALLY_APPROVED
|
|
CLIENT_APPROVED
|
|
}
|
|
|
|
enum ShotPriority {
|
|
LOW
|
|
NORMAL
|
|
HIGH
|
|
URGENT
|
|
}
|
|
|
|
enum ApprovalStatus {
|
|
PENDING_REVIEW
|
|
APPROVED
|
|
REJECTED
|
|
NEEDS_CHANGES
|
|
}
|
|
|
|
enum ReviewStatus {
|
|
PENDING
|
|
INTERNAL_APPROVED
|
|
CLIENT_APPROVED
|
|
NEEDS_CHANGES
|
|
FINAL_APPROVED
|
|
}
|
|
|
|
enum NotificationType {
|
|
VERSION_UPLOADED
|
|
FEEDBACK_ADDED
|
|
SHOT_APPROVED
|
|
SHOT_REJECTED
|
|
COMMENT_REPLY
|
|
MENTION
|
|
REVISION_REQUESTED
|
|
TASK_ASSIGNED
|
|
TASK_OVERDUE
|
|
TASK_APPROVED
|
|
TASK_CHANGES_REQUESTED
|
|
TASK_READY_FOR_REVIEW
|
|
}
|
|
|
|
enum TaskStatus {
|
|
TODO
|
|
IN_PROGRESS
|
|
INTERNAL_REVIEW
|
|
CLIENT_REVIEW
|
|
CHANGES
|
|
DONE
|
|
}
|
|
|
|
enum TaskType {
|
|
TRACK
|
|
ROTO
|
|
KEY
|
|
COMP
|
|
FX
|
|
LIGHTING
|
|
RENDER
|
|
ANIMATION
|
|
MODEL
|
|
TEXTURE
|
|
RIG
|
|
LOOKDEV
|
|
GENERAL
|
|
}
|
|
|
|
enum ProjectType {
|
|
STANDARD
|
|
EPISODIC
|
|
}
|
|
|
|
// ─────────────────────────────────────────────
|
|
// RENDER PIPELINE ENUMS (RenderPipeline2 §5.1)
|
|
// ─────────────────────────────────────────────
|
|
|
|
enum ExportStatus {
|
|
QUEUED
|
|
RENDERING
|
|
RENDER_FAILED
|
|
VALIDATING
|
|
VALIDATION_FAILED
|
|
GENERATING_PREVIEW
|
|
PREVIEW_FAILED
|
|
READY_FOR_QC
|
|
QC_FAILED
|
|
READY_FOR_DELIVERY
|
|
PACKAGED
|
|
DELIVERED
|
|
SUPERSEDED
|
|
ARCHIVED
|
|
CANCELLED
|
|
}
|
|
|
|
enum RenderJobStatus {
|
|
QUEUED
|
|
CLAIMED
|
|
RUNNING
|
|
COMPLETED
|
|
FAILED
|
|
CANCELLED
|
|
EXPIRED
|
|
}
|
|
|
|
enum RenderJobType {
|
|
AE_RENDER
|
|
PREVIEW_ONLY
|
|
DELIVERY_BUILD
|
|
}
|
|
|
|
enum ValidationStatus {
|
|
PASS
|
|
FAIL
|
|
WARN
|
|
SKIPPED
|
|
}
|
|
|
|
enum QCResult {
|
|
PASS
|
|
FAIL
|
|
}
|
|
|
|
enum MachineStatus {
|
|
ONLINE
|
|
OFFLINE
|
|
DISABLED
|
|
}
|
|
|
|
enum DeliveryStatus {
|
|
DRAFT
|
|
QUEUED
|
|
BUILDING
|
|
READY
|
|
DELIVERED
|
|
FAILED
|
|
CANCELLED
|
|
}
|
|
|
|
enum TakeQuality {
|
|
HERO
|
|
GOOD
|
|
PRINT
|
|
NO_GOOD
|
|
FALSE_START
|
|
}
|
|
|
|
enum AttachmentFileType {
|
|
IMAGE
|
|
VIDEO
|
|
HDRI
|
|
LIDAR
|
|
LENS_GRID
|
|
PDF
|
|
OTHER
|
|
}
|
|
|
|
enum AttachmentCategory {
|
|
SLATE
|
|
CAMERA_POSITION
|
|
WIDE_REFERENCE
|
|
LENS
|
|
LIGHTING
|
|
HDRI
|
|
TRACKING
|
|
TEXTURE
|
|
WITNESS
|
|
MISCELLANEOUS
|
|
}
|
|
|
|
// ─────────────────────────────────────────────
|
|
// AUTH MODELS (NextAuth v5 compatible)
|
|
// ─────────────────────────────────────────────
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
name String?
|
|
email String @unique
|
|
emailVerified DateTime?
|
|
image String?
|
|
passwordHash String?
|
|
role Role @default(ARTIST)
|
|
isActive Boolean @default(true)
|
|
mustChangePassword Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// NextAuth relations
|
|
accounts Account[]
|
|
sessions Session[]
|
|
|
|
// App relations
|
|
producedProjects Project[] @relation("ProducerProjects")
|
|
supervisedProjects Project[] @relation("SupervisorProjects")
|
|
assignedShots Shot[] @relation("ArtistShots")
|
|
uploadedVersions Version[]
|
|
comments Comment[]
|
|
commentReplies CommentReply[]
|
|
annotations Annotation[]
|
|
approvals Approval[]
|
|
notifications Notification[]
|
|
clientAccess ClientAccess[]
|
|
assignedTasks Task[] @relation("TaskArtist")
|
|
createdTasks Task[] @relation("TaskCreator")
|
|
sharedVersions Version[] @relation("VersionSharedBy")
|
|
ledAssets Asset[] @relation("AssetLead")
|
|
createdShootDays ShootDay[] @relation("ShootDayCreator")
|
|
createdTakes Take[] @relation("TakeCreator")
|
|
uploadedTakeAttachments TakeAttachment[] @relation("TakeAttachmentUploader")
|
|
|
|
@@map("users")
|
|
}
|
|
|
|
model Account {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
type String
|
|
provider String
|
|
providerAccountId String
|
|
refresh_token String? @db.Text
|
|
access_token String? @db.Text
|
|
expires_at Int?
|
|
token_type String?
|
|
scope String?
|
|
id_token String? @db.Text
|
|
session_state String?
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([provider, providerAccountId])
|
|
@@map("accounts")
|
|
}
|
|
|
|
model Session {
|
|
id String @id @default(cuid())
|
|
sessionToken String @unique
|
|
userId String
|
|
expires DateTime
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("sessions")
|
|
}
|
|
|
|
model VerificationToken {
|
|
identifier String
|
|
token String @unique
|
|
expires DateTime
|
|
|
|
@@unique([identifier, token])
|
|
@@map("verification_tokens")
|
|
}
|
|
|
|
// ─────────────────────────────────────────────
|
|
// CORE DOMAIN MODELS
|
|
// ─────────────────────────────────────────────
|
|
|
|
model Client {
|
|
id String @id @default(cuid())
|
|
company String
|
|
contactPerson String
|
|
email String @unique
|
|
phone String?
|
|
notes String? @db.Text
|
|
logoUrl String?
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
projects Project[]
|
|
clientAccess ClientAccess[]
|
|
|
|
@@map("clients")
|
|
}
|
|
|
|
/// Links a USER with CLIENT role to the client record they represent
|
|
model ClientAccess {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
clientId String
|
|
createdAt DateTime @default(now())
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, clientId])
|
|
@@map("client_access")
|
|
}
|
|
|
|
model Project {
|
|
id String @id @default(cuid())
|
|
name String
|
|
code String @unique
|
|
showId String @default("")
|
|
projectType ProjectType @default(STANDARD)
|
|
description String? @db.Text
|
|
status ProjectStatus @default(ACTIVE)
|
|
dueDate DateTime?
|
|
startDate DateTime?
|
|
clientId String?
|
|
producerId String?
|
|
supervisorId String?
|
|
slackWebhook String?
|
|
slackChannel String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
client Client? @relation(fields: [clientId], references: [id])
|
|
producer User? @relation("ProducerProjects", fields: [producerId], references: [id])
|
|
supervisor User? @relation("SupervisorProjects", fields: [supervisorId], references: [id])
|
|
shots Shot[]
|
|
assets Asset[]
|
|
tasks Task[]
|
|
shotGroups ShotGroup[]
|
|
reviewSessions ReviewSession[]
|
|
episodeDueDates EpisodeDueDate[]
|
|
shootDays ShootDay[]
|
|
|
|
/// Per-production delivery naming/layout templates (RenderPipeline2 §11.2);
|
|
/// falls back to SystemConfig defaults when null
|
|
deliveryConfig Json?
|
|
deliveryPackages DeliveryPackage[]
|
|
|
|
@@map("projects")
|
|
}
|
|
|
|
model EpisodeDueDate {
|
|
id String @id @default(cuid())
|
|
projectId String
|
|
episode String
|
|
dueDate DateTime
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([projectId, episode])
|
|
@@map("episode_due_dates")
|
|
}
|
|
|
|
model Shot {
|
|
id String @id @default(cuid())
|
|
shotCode String
|
|
scene String @default("")
|
|
episode String?
|
|
shotNumber Int @default(0)
|
|
sequence String?
|
|
description String? @db.Text
|
|
notes String? @db.Text
|
|
status ShotStatus @default(WAITING)
|
|
priority ShotPriority @default(NORMAL)
|
|
artistId String?
|
|
projectId String
|
|
frameStart Int?
|
|
frameEnd Int?
|
|
fps Float @default(24)
|
|
dueDate DateTime?
|
|
thumbnailUrl String?
|
|
originalFootageUrl String?
|
|
originalFootageKey String?
|
|
shotGroupId String?
|
|
shotApprovalStatus ShotApprovalStatus @default(PENDING)
|
|
sharedWithClient Boolean @default(false)
|
|
// EDL / pull CSV fields
|
|
sourceClip String?
|
|
timecodeStart String?
|
|
timecodeEnd String?
|
|
clipDuration String?
|
|
exrOutput String?
|
|
// Sequence / picture lock timecodes
|
|
seqTimecodeStart String?
|
|
seqTimecodeEnd String?
|
|
// High-res deliverable (stored in Hetzner Object Storage)
|
|
highResKey String?
|
|
highResFilename String?
|
|
// Shot-level version tracking (e.g. v001, v002)
|
|
shotVersion String @default("v001")
|
|
// Key shot flag — highlighted for prioritisation
|
|
isKeyShot Boolean @default(false)
|
|
// Netflix slate fields — managed via the Slate tab, used by the preview pipeline
|
|
slateType String?
|
|
slateDescription String? @db.Text
|
|
slateVfxScope String? @db.Text
|
|
slateSubmissionNote String? @db.Text
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
artist User? @relation("ArtistShots", fields: [artistId], references: [id])
|
|
shotGroup ShotGroup? @relation(fields: [shotGroupId], references: [id])
|
|
versions Version[]
|
|
tasks Task[]
|
|
footagePlates FootagePlate[]
|
|
references ShotReference[]
|
|
loggedTakes Take[] @relation("TakeToShot")
|
|
exports Export[]
|
|
|
|
@@unique([projectId, shotCode])
|
|
@@map("shots")
|
|
}
|
|
|
|
model FootagePlate {
|
|
id String @id @default(cuid())
|
|
shotId String
|
|
label String @default("")
|
|
fileUrl String
|
|
fileKey String @default("")
|
|
fileName String @default("")
|
|
fileSize BigInt?
|
|
sortOrder Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
|
|
shot Shot @relation(fields: [shotId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("footage_plates")
|
|
}
|
|
|
|
model ShotReference {
|
|
id String @id @default(cuid())
|
|
shotId String
|
|
label String?
|
|
fileUrl String
|
|
fileKey String @default("")
|
|
fileName String @default("")
|
|
fileSize BigInt?
|
|
sortOrder Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
|
|
shot Shot @relation(fields: [shotId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("shot_references")
|
|
}
|
|
|
|
model ShotGroup {
|
|
id String @id @default(cuid())
|
|
name String
|
|
projectId String
|
|
sortOrder Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
shots Shot[]
|
|
|
|
@@unique([projectId, name])
|
|
@@map("shot_groups")
|
|
}
|
|
|
|
model Version {
|
|
id String @id @default(cuid())
|
|
versionNumber Int
|
|
shotId String?
|
|
taskId String?
|
|
artistId String?
|
|
fileUrl String
|
|
fileName String
|
|
fileSize BigInt?
|
|
mimeType String?
|
|
thumbnailUrl String?
|
|
posterUrl String?
|
|
proxyUrl String?
|
|
fps Float @default(24)
|
|
duration Float?
|
|
frameCount Int?
|
|
width Int?
|
|
height Int?
|
|
notes String? @db.Text
|
|
approvalStatus ApprovalStatus @default(PENDING_REVIEW)
|
|
reviewStatus ReviewStatus @default(PENDING)
|
|
isLatest Boolean @default(true)
|
|
isClientVisible Boolean @default(false)
|
|
sharedAt DateTime?
|
|
sharedById String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
shot Shot? @relation(fields: [shotId], references: [id], onDelete: Cascade)
|
|
task Task? @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
|
artist User? @relation(fields: [artistId], references: [id])
|
|
sharedBy User? @relation("VersionSharedBy", fields: [sharedById], references: [id])
|
|
comments Comment[]
|
|
annotations Annotation[]
|
|
approvals Approval[]
|
|
|
|
@@map("versions")
|
|
}
|
|
|
|
model Comment {
|
|
id String @id @default(cuid())
|
|
versionId String
|
|
authorId String?
|
|
frameNumber Int
|
|
timestamp Float
|
|
text String @db.Text
|
|
isResolved Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
version Version @relation(fields: [versionId], references: [id], onDelete: Cascade)
|
|
author User? @relation(fields: [authorId], references: [id], onDelete: SetNull)
|
|
replies CommentReply[]
|
|
annotations Annotation[]
|
|
|
|
@@map("comments")
|
|
}
|
|
|
|
model CommentReply {
|
|
id String @id @default(cuid())
|
|
commentId String
|
|
authorId String?
|
|
text String @db.Text
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade)
|
|
author User? @relation(fields: [authorId], references: [id], onDelete: SetNull)
|
|
|
|
@@map("comment_replies")
|
|
}
|
|
|
|
/// Stores canvas drawing data as JSON (normalized 0-1 coordinates)
|
|
model Annotation {
|
|
id String @id @default(cuid())
|
|
versionId String
|
|
commentId String?
|
|
authorId String?
|
|
frameNumber Int
|
|
drawingData Json // Array of AnnotationShape objects
|
|
color String @default("#ef4444")
|
|
isVisible Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
|
|
version Version @relation(fields: [versionId], references: [id], onDelete: Cascade)
|
|
comment Comment? @relation(fields: [commentId], references: [id], onDelete: SetNull)
|
|
author User? @relation(fields: [authorId], references: [id], onDelete: SetNull)
|
|
|
|
@@map("annotations")
|
|
}
|
|
|
|
model Approval {
|
|
id String @id @default(cuid())
|
|
versionId String
|
|
userId String?
|
|
status ApprovalStatus
|
|
notes String? @db.Text
|
|
createdAt DateTime @default(now())
|
|
|
|
version Version @relation(fields: [versionId], references: [id], onDelete: Cascade)
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
|
|
@@map("approvals")
|
|
}
|
|
|
|
model Notification {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
type NotificationType
|
|
title String
|
|
message String @db.Text
|
|
data Json? // Extra context (versionId, shotCode, etc.)
|
|
isRead Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("notifications")
|
|
}
|
|
|
|
model Asset {
|
|
id String @id @default(cuid())
|
|
projectId String
|
|
assetCode String
|
|
name String
|
|
description String? @db.Text
|
|
status ShotStatus @default(WAITING)
|
|
priority ShotPriority @default(NORMAL)
|
|
leadId String?
|
|
dueDate DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
lead User? @relation("AssetLead", fields: [leadId], references: [id])
|
|
tasks Task[]
|
|
|
|
@@unique([projectId, assetCode])
|
|
@@map("assets")
|
|
}
|
|
|
|
model Task {
|
|
id String @id @default(cuid())
|
|
title String
|
|
description String? @db.Text
|
|
type TaskType @default(GENERAL)
|
|
status TaskStatus @default(TODO)
|
|
priority ShotPriority @default(NORMAL)
|
|
dueDate DateTime?
|
|
estimatedHours Float?
|
|
sortOrder Int @default(0)
|
|
shotId String?
|
|
assetId String?
|
|
assignedArtistId String?
|
|
createdById String?
|
|
projectId String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
scheduledStartDate DateTime?
|
|
scheduledEndDate DateTime?
|
|
scheduleNotes String? @db.Text
|
|
|
|
shot Shot? @relation(fields: [shotId], references: [id], onDelete: Cascade)
|
|
asset Asset? @relation(fields: [assetId], references: [id], onDelete: Cascade)
|
|
assignedArtist User? @relation("TaskArtist", fields: [assignedArtistId], references: [id])
|
|
createdBy User? @relation("TaskCreator", fields: [createdById], references: [id], onDelete: SetNull)
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
versions Version[]
|
|
|
|
@@index([scheduledStartDate])
|
|
@@index([scheduledEndDate])
|
|
@@map("tasks")
|
|
}
|
|
|
|
/// Secure tokenized review link for clients
|
|
model ReviewSession {
|
|
id String @id @default(cuid())
|
|
projectId String
|
|
token String @unique @default(cuid())
|
|
label String?
|
|
email String?
|
|
passwordHash String?
|
|
expiresAt DateTime
|
|
isActive Boolean @default(true)
|
|
accessCount Int @default(0)
|
|
allowedEpisodes String[] @default([])
|
|
createdAt DateTime @default(now())
|
|
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("review_sessions")
|
|
}
|
|
|
|
/// Key-value store for system-wide configuration (e.g. storage credentials)
|
|
model SystemConfig {
|
|
key String @id
|
|
value String @db.Text
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@map("system_config")
|
|
}
|
|
|
|
/// Physical display event log — polled by ESP32 dot-matrix display
|
|
model DisplayEvent {
|
|
id Int @id @default(autoincrement())
|
|
type String // APPROVED | CHANGES | COMMENT | REJECTED
|
|
shotCode String
|
|
by String // actor name (reviewer / commenter)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([createdAt])
|
|
@@map("display_events")
|
|
}
|
|
|
|
// ─────────────────────────────────────────────
|
|
// SHOOT LOG MODELS
|
|
// ─────────────────────────────────────────────
|
|
|
|
model ShootDay {
|
|
id String @id @default(cuid())
|
|
projectId String
|
|
date DateTime
|
|
unit String @default("A")
|
|
label String?
|
|
notes String? @db.Text
|
|
createdById String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
createdBy User? @relation("ShootDayCreator", fields: [createdById], references: [id], onDelete: SetNull)
|
|
setups Setup[]
|
|
|
|
@@map("shoot_days")
|
|
}
|
|
|
|
model Setup {
|
|
id String @id @default(cuid())
|
|
shootDayId String
|
|
name String
|
|
description String? @db.Text
|
|
sortOrder Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
shootDay ShootDay @relation(fields: [shootDayId], references: [id], onDelete: Cascade)
|
|
takes Take[]
|
|
|
|
@@map("setups")
|
|
}
|
|
|
|
model Take {
|
|
id String @id @default(cuid())
|
|
setupId String
|
|
takeNumber Int
|
|
|
|
// Optional link back to a pipeline shot
|
|
pipelineShotId String?
|
|
|
|
// General
|
|
scene String?
|
|
shotLabel String?
|
|
unitLabel String?
|
|
|
|
// Camera
|
|
cameraLetter String?
|
|
clipName String?
|
|
roll String?
|
|
cameraModel String?
|
|
resolution String?
|
|
codec String?
|
|
fps Float?
|
|
shutter String?
|
|
iso Int?
|
|
whiteBalance Int?
|
|
colourSpace String?
|
|
|
|
// Lens
|
|
lensSet String?
|
|
lens String?
|
|
tStop String?
|
|
filters String?
|
|
isAnamorphic Boolean @default(false)
|
|
|
|
// Tracking
|
|
hasHdri Boolean @default(false)
|
|
hasChromeBall Boolean @default(false)
|
|
hasGreyBall Boolean @default(false)
|
|
hasMacbeth Boolean @default(false)
|
|
hasCleanPlate Boolean @default(false)
|
|
hasSurvey Boolean @default(false)
|
|
hasLidar Boolean @default(false)
|
|
hasWitnessCamera Boolean @default(false)
|
|
hasLensGrid Boolean @default(false)
|
|
hasTexturePhotos Boolean @default(false)
|
|
hasPhotogrammetry Boolean @default(false)
|
|
|
|
// Environment
|
|
weather String?
|
|
sunDirection String?
|
|
artificialLights String? @db.Text
|
|
|
|
// Supervisor
|
|
supervisorNotes String? @db.Text
|
|
continuityNotes String? @db.Text
|
|
vfxRequirements String? @db.Text
|
|
quality TakeQuality @default(PRINT)
|
|
|
|
createdById String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
setup Setup @relation(fields: [setupId], references: [id], onDelete: Cascade)
|
|
pipelineShot Shot? @relation("TakeToShot", fields: [pipelineShotId], references: [id], onDelete: SetNull)
|
|
createdBy User? @relation("TakeCreator", fields: [createdById], references: [id], onDelete: SetNull)
|
|
attachments TakeAttachment[]
|
|
|
|
@@unique([setupId, takeNumber])
|
|
@@map("takes")
|
|
}
|
|
|
|
model TakeAttachment {
|
|
id String @id @default(cuid())
|
|
takeId String
|
|
fileUrl String
|
|
fileKey String @default("")
|
|
fileName String
|
|
fileSize BigInt?
|
|
fileType AttachmentFileType @default(IMAGE)
|
|
category AttachmentCategory @default(MISCELLANEOUS)
|
|
caption String?
|
|
sortOrder Int @default(0)
|
|
uploadedById String?
|
|
createdAt DateTime @default(now())
|
|
|
|
take Take @relation(fields: [takeId], references: [id], onDelete: Cascade)
|
|
uploadedBy User? @relation("TakeAttachmentUploader", fields: [uploadedById], references: [id], onDelete: SetNull)
|
|
|
|
@@map("take_attachments")
|
|
}
|
|
|
|
// ─────────────────────────────────────────────
|
|
// RENDER PIPELINE MODELS (RenderPipeline2 §5)
|
|
// Phases 1 + 2: Export, RenderJob, ExportEvent, Machine, WorkerHeartbeat.
|
|
// ValidationResult / QCReview / DeliveryPackage / DeliveryItem arrive in
|
|
// later phases as additive migrations.
|
|
// ─────────────────────────────────────────────
|
|
|
|
/// One row per "Queue Export" click — carries the §4 state machine.
|
|
model Export {
|
|
id String @id @default(cuid())
|
|
shotId String
|
|
shot Shot @relation(fields: [shotId], references: [id])
|
|
projectId String
|
|
taskId String? // comp task the preview Version attaches to
|
|
versionId String? // preview Version created at READY_FOR_QC (Phase 4)
|
|
versionNumber Int // 4
|
|
versionString String // "v004"
|
|
status ExportStatus @default(QUEUED)
|
|
statusChangedAt DateTime @default(now())
|
|
|
|
// Manifest (denormalised for querying; full manifest JSON on RenderJob)
|
|
aepPath String
|
|
compName String
|
|
rendererType String // "aerender"
|
|
outputDir String // render root for this export
|
|
outputPattern String // "UNG_106_010_020_cmp_TT_v004.[####].exr"
|
|
frameStart Int
|
|
frameEnd Int
|
|
fps Float
|
|
width Int
|
|
height Int
|
|
colorspace String? // expected, e.g. "ACES - ACEScg"
|
|
|
|
// Artifacts
|
|
deliveryMovPath String? // slate/burn-in delivery MOV on the SAN (§9)
|
|
previewMovKey String? // web H.264 transcode in object storage
|
|
thumbnailKey String?
|
|
metadataKey String? // metadata JSON in object storage
|
|
exrFileCount Int?
|
|
exrTotalBytes BigInt?
|
|
checksum String? // sequence-level digest (xxHash of per-file hashes)
|
|
|
|
// Slate fields authored per submission (not per shot): they change from one
|
|
// delivery to the next, so each Export carries its own and new exports
|
|
// default to the previous export's values.
|
|
vfxScope String? @db.Text
|
|
submissionNote String? @db.Text
|
|
|
|
submittedById String? // resolved User, else null
|
|
submittedByName String? // free text from panel config as fallback
|
|
supersededById String? // newer Export that replaced this one
|
|
|
|
renderJobs RenderJob[]
|
|
events ExportEvent[]
|
|
validations ValidationResult[]
|
|
qcReviews QCReview[]
|
|
deliveryItems DeliveryItem[]
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([shotId, versionNumber])
|
|
@@index([status])
|
|
@@index([projectId, status])
|
|
@@map("exports")
|
|
}
|
|
|
|
/// One execution attempt of work by a worker. Also used (type DELIVERY_BUILD)
|
|
/// for delivery package builds — one queue/claim/lease/heartbeat mechanism.
|
|
model RenderJob {
|
|
id String @id @default(cuid())
|
|
type RenderJobType @default(AE_RENDER)
|
|
exportId String? // set for AE_RENDER / PREVIEW_ONLY
|
|
export Export? @relation(fields: [exportId], references: [id])
|
|
deliveryId String? // set for DELIVERY_BUILD (Phase 5)
|
|
attempt Int @default(1)
|
|
maxAttempts Int @default(3)
|
|
status RenderJobStatus @default(QUEUED)
|
|
priority Int @default(50) // lower = sooner; <= 20 is urgent
|
|
manifest Json // full Render Manifest snapshot (§6.2)
|
|
|
|
machineId String?
|
|
machine Machine? @relation(fields: [machineId], references: [id])
|
|
claimedAt DateTime?
|
|
leaseExpiresAt DateTime? // claim + leaseSeconds; renewed by progress reports
|
|
startedAt DateTime?
|
|
finishedAt DateTime?
|
|
|
|
progress Float @default(0) // 0..1
|
|
currentFrame Int?
|
|
totalFrames Int?
|
|
etaSeconds Int?
|
|
exitCode Int?
|
|
errorMessage String?
|
|
logTail String? @db.Text // last ~200 lines of aerender output
|
|
logFileKey String? // full log uploaded to object storage on finish/fail
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([status, priority, createdAt])
|
|
@@index([machineId, status])
|
|
@@map("render_jobs")
|
|
}
|
|
|
|
/// Append-only audit log of Export state transitions (§5.9)
|
|
model ExportEvent {
|
|
id String @id @default(cuid())
|
|
exportId String
|
|
export Export @relation(fields: [exportId], references: [id])
|
|
fromStatus String?
|
|
toStatus String
|
|
actorType String // "WORKER" | "USER" | "SYSTEM"
|
|
actorId String? // machineId or userId
|
|
note String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([exportId, createdAt])
|
|
@@map("export_events")
|
|
}
|
|
|
|
/// A render-capable machine (artist workstation or future render node) (§5.6)
|
|
model Machine {
|
|
id String @id @default(cuid())
|
|
name String @unique // "RENDER-01"
|
|
hostname String
|
|
status MachineStatus @default(OFFLINE)
|
|
enabled Boolean @default(true) // admin kill-switch: disabled machines cannot claim
|
|
lastSeenAt DateTime?
|
|
workerVersion String?
|
|
aeVersion String? // "2026 (24.x)"
|
|
capabilities Json? // { maxConcurrentJobs: 1, tools: {...} }
|
|
availability Json? // §7.10: { mode, windows, allowUrgentAnytime } — null = ALWAYS
|
|
renderNowUntil DateTime? // manual "Render Now" override; claims allowed until this time
|
|
apiKeyHash String? // optional per-machine key (dormant; §15 — shared key for now)
|
|
|
|
renderJobs RenderJob[]
|
|
heartbeats WorkerHeartbeat[]
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@map("machines")
|
|
}
|
|
|
|
model WorkerHeartbeat {
|
|
id String @id @default(cuid())
|
|
machineId String
|
|
machine Machine @relation(fields: [machineId], references: [id])
|
|
createdAt DateTime @default(now())
|
|
cpuPercent Float?
|
|
memPercent Float?
|
|
diskFreeGb Float?
|
|
currentJobId String?
|
|
|
|
@@index([machineId, createdAt])
|
|
@@map("worker_heartbeats")
|
|
}
|
|
|
|
/// One row per check per validation run, so the UI shows exactly what failed
|
|
/// and history survives retries (§5.4).
|
|
model ValidationResult {
|
|
id String @id @default(cuid())
|
|
exportId String
|
|
export Export @relation(fields: [exportId], references: [id])
|
|
renderJobId String // which attempt produced this run
|
|
checkName String // "timecode_plate_match", "missing_frames", ...
|
|
status ValidationStatus
|
|
expected String?
|
|
actual String?
|
|
message String?
|
|
details Json? // e.g. list of missing frame numbers, or the timecode reference used
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([exportId, renderJobId])
|
|
@@map("validation_results")
|
|
}
|
|
|
|
/// Human technical QC on an Export (§5.5). Multiple rows allowed — the latest
|
|
/// is authoritative and drives T13/T14.
|
|
model QCReview {
|
|
id String @id @default(cuid())
|
|
exportId String
|
|
export Export @relation(fields: [exportId], references: [id])
|
|
result QCResult
|
|
notes String? @db.Text
|
|
reviewedById String? // resolved User where possible
|
|
reviewerName String? // fallback for API-key reviews from the panel
|
|
source String // "AE_PANEL" | "WEB"
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([exportId])
|
|
@@map("qc_reviews")
|
|
}
|
|
|
|
/// A dated delivery folder built from READY_FOR_DELIVERY exports (§5.7).
|
|
/// Never deleted — a mistaken package is CANCELLED or superseded.
|
|
model DeliveryPackage {
|
|
id String @id @default(cuid())
|
|
projectId String
|
|
project Project @relation(fields: [projectId], references: [id])
|
|
episode String // "101" — empty string on standard (non-episodic) shows
|
|
name String // "260805_Delivery"
|
|
packageNumber Int // sequential per project → "Package #15"
|
|
status DeliveryStatus @default(DRAFT)
|
|
rootPath String? // final path on the delivery volume
|
|
configSnapshot Json // naming template + options at build time (immutable record)
|
|
manifest Json? // built manifest: files, sizes, checksums
|
|
reportKey String? // delivery report in object storage; also written into the folder
|
|
errorMessage String?
|
|
createdById String
|
|
createdAt DateTime @default(now())
|
|
builtAt DateTime?
|
|
deliveredAt DateTime?
|
|
deliveredById String?
|
|
|
|
items DeliveryItem[]
|
|
|
|
@@unique([projectId, packageNumber])
|
|
@@index([projectId, episode])
|
|
@@index([status])
|
|
@@map("delivery_packages")
|
|
}
|
|
|
|
/// Version lock: the exact Export delivered. shotCode/versionString are
|
|
/// denormalised so a later shot rename or re-render never mutates history.
|
|
model DeliveryItem {
|
|
id String @id @default(cuid())
|
|
packageId String
|
|
package DeliveryPackage @relation(fields: [packageId], references: [id], onDelete: Cascade)
|
|
exportId String
|
|
export Export @relation(fields: [exportId], references: [id])
|
|
shotId String
|
|
shotCode String
|
|
versionString String
|
|
exrPath String? // path inside the package
|
|
movPath String?
|
|
fileCount Int?
|
|
totalBytes BigInt?
|
|
checksum String?
|
|
linkMode String? // "hardlink" | "copy" — what the builder actually did
|
|
status String @default("PENDING") // PENDING | COPIED | VERIFIED | FAILED
|
|
errorMessage String?
|
|
|
|
@@unique([packageId, exportId])
|
|
@@index([exportId])
|
|
@@map("delivery_items")
|
|
}
|
|
|
|
model SketchTemplate {
|
|
id String @id @default(cuid())
|
|
name String
|
|
fileUrl String
|
|
fileKey String @default("")
|
|
fileName String @default("")
|
|
fileSize BigInt?
|
|
sortOrder Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@map("sketch_templates")
|
|
}
|