b151e8cc52
Deploy / deploy (push) Successful in 7m25s
Clients sometimes ship footage under a naming convention that doesn't match our own shot codes. Adds an optional Shot.clientCode, editable on the Slate tab: when set, it replaces shotCode only in what the client actually sees — slate identity, overlay burn-in, and delivered EXR/MOV file and folder names in Netflix packages — with our own version string flatly appended (no "_cmp_TT_" studio jargon mixed in). Internal render paths, EXR filenames on the SAN, comp names, and DB lookups are untouched. Shots without a client code render identically to before. Delivery packages denormalize clientCode at build time, same as the existing shotCode/versionString snapshot, so a later shot edit can't retroactively change already-built delivery history. The worker now renames delivered EXR frames and the delivery MOV using the existing frame-number parser (SequencePattern) so only the base name changes — frame numbers and padding survive exactly. This is the one piece of the change that could plausibly ship the wrong bytes, so it's covered by new unit tests (renaming, padding, MOV rename, checksum-survives-rename, and a graceful fallback for an unparseable frame number) rather than resting on manual verification alone. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1155 lines
35 KiB
Plaintext
1155 lines
35 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
|
|
/// A standalone Blender render: a queue entry and nothing more. It has no
|
|
/// Export, takes no shot version, and is not validated, QC'd or delivered —
|
|
/// it is "Ctrl+F12, but on the farm".
|
|
BLENDER_RENDER
|
|
}
|
|
|
|
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?
|
|
/// Per-production AE workspace: the show's slate/overlay template AEP, its
|
|
/// renders root, and any comp names that depart from the {showId}_* naming
|
|
/// convention. Read by both the panel and the headless preview build, which
|
|
/// is why it lives here rather than in per-machine panel settings.
|
|
pipelineConfig 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
|
|
/// Client-supplied naming alias, used only for slates, overlay burn-ins,
|
|
/// and delivered file/folder names — never for internal paths, comp names
|
|
/// or DB lookups, which always use shotCode. Null = use shotCode as today.
|
|
clientCode 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)
|
|
|
|
/// Full-duration (handles included), ungraded MP4 of just the picture-lock
|
|
/// plate — a diagnostic reference to check back against, not a reviewable
|
|
/// deliverable, so it lives here rather than as a Version.
|
|
piclockRefKey String?
|
|
|
|
// 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
|
|
|
|
/// Render the EXRs but build no preview, inheriting the previous export's
|
|
/// review media instead. For an export whose framing differs from the review
|
|
/// framing — unscaled or un-punched-in plates — the earlier MOV is the
|
|
/// correct preview, and rebuilding it from these EXRs would be wrong.
|
|
skipPreview Boolean @default(false)
|
|
|
|
/// Skip picture-lock trimming and the picture-lock reference render for this
|
|
/// export — set when picture lock is missing or wrong for the shot, so the
|
|
/// preview build falls back to the untrimmed full render range instead of
|
|
/// trimming against bad data.
|
|
noPictureLock Boolean @default(false)
|
|
|
|
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)
|
|
/// Set for jobs that belong to a project but not to a shot — a standalone
|
|
/// Blender render. Lets the queue be filtered by production without
|
|
/// inventing an Export for work that has no shot.
|
|
projectId String?
|
|
/// Human label for such a job, since there is no shot code to show.
|
|
label String?
|
|
submittedBy String? // email/name of whoever queued it
|
|
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])
|
|
@@index([projectId, 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")
|
|
}
|
|
|
|
/// What version of each studio tool is running where. Written by the tool
|
|
/// itself on startup, so it reflects what is *deployed* rather than what
|
|
/// someone remembered to write down. One row per (tool, machine).
|
|
model ToolVersion {
|
|
id String @id @default(cuid())
|
|
tool String // "AE_PANEL" | "RENDER_WORKER"
|
|
machineName String
|
|
version String // "2.1.0"
|
|
buildRef String? // git sha / build stamp, when the tool can report one
|
|
hostUser String? // artist email, or the worker's service account
|
|
hostApp String? // "After Effects 24.6" / OS build
|
|
details Json?
|
|
firstSeenAt DateTime @default(now())
|
|
lastSeenAt DateTime @default(now())
|
|
|
|
@@unique([tool, machineName])
|
|
@@index([tool])
|
|
@@map("tool_versions")
|
|
}
|
|
|
|
/// 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
|
|
/// Snapshot of Shot.clientCode at package build time — same immutability
|
|
/// rule as shotCode/versionString: a later shot edit must not retroactively
|
|
/// change already-built delivery history.
|
|
clientCode 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")
|
|
}
|