Files
twotalesanimation cc89415a29 feat(pipeline): render queue, worker service and automated preview generation
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>
2026-08-06 15:46:53 +02:00

994 lines
28 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?
@@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)
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[]
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")
}
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")
}