+1380
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,753 @@
|
||||
# Technical Architecture Report (Current State)
|
||||
|
||||
Date: 2026-07-31
|
||||
Scope: Current implementation only (no redesign suggestions)
|
||||
|
||||
---
|
||||
|
||||
## 1. Database
|
||||
|
||||
Authoritative schema source: [prisma/schema.prisma](prisma/schema.prisma)
|
||||
|
||||
### Full Prisma Schema
|
||||
The full schema is defined in [prisma/schema.prisma](prisma/schema.prisma).
|
||||
|
||||
### Models related to requested domains
|
||||
|
||||
- Users/Auth
|
||||
- User
|
||||
- Account
|
||||
- Session
|
||||
- VerificationToken
|
||||
- ClientAccess
|
||||
- Projects/Episodes
|
||||
- Project
|
||||
- EpisodeDueDate
|
||||
- Client
|
||||
- Shots/Tasks/Versions/Reviews
|
||||
- Shot
|
||||
- ShotGroup
|
||||
- Task
|
||||
- Version
|
||||
- Comment
|
||||
- CommentReply
|
||||
- Annotation
|
||||
- Approval
|
||||
- ReviewSession
|
||||
- Files/Storage
|
||||
- FootagePlate
|
||||
- ShotReference
|
||||
- SystemConfig
|
||||
- Delivery/Export adjacent
|
||||
- Shot fields: highResKey, highResFilename, exrOutput, shotVersion
|
||||
- Version fields: fileUrl, fileName, proxyUrl, thumbnailUrl, posterUrl
|
||||
|
||||
### Relationship summary
|
||||
|
||||
- Client 1:N Project
|
||||
- Project 1:N Shot
|
||||
- Project 1:N Task
|
||||
- Project 1:N ReviewSession
|
||||
- Project 1:N EpisodeDueDate
|
||||
- Shot 1:N Task
|
||||
- Shot 1:N Version
|
||||
- Shot 1:N FootagePlate
|
||||
- Shot 1:N ShotReference
|
||||
- Task 1:N Version
|
||||
- Version 1:N Comment
|
||||
- Version 1:N Annotation
|
||||
- Version 1:N Approval
|
||||
- Comment 1:N CommentReply
|
||||
- User has many assigned/created entities across shots, tasks, versions, comments, approvals
|
||||
|
||||
### Existing status enums
|
||||
|
||||
From [prisma/schema.prisma](prisma/schema.prisma):
|
||||
|
||||
- ProjectStatus: ACTIVE, ON_HOLD, COMPLETED, ARCHIVED
|
||||
- ShotStatus: WAITING, IN_PROGRESS, INTERNAL_REVIEW, READY_FOR_CLIENT, CLIENT_REVIEW, REVISIONS, COMPLETE
|
||||
- ShotApprovalStatus: PENDING, INTERNALLY_APPROVED, CLIENT_APPROVED
|
||||
- TaskStatus: TODO, IN_PROGRESS, INTERNAL_REVIEW, CLIENT_REVIEW, CHANGES, DONE
|
||||
- ApprovalStatus: PENDING_REVIEW, APPROVED, REJECTED, NEEDS_CHANGES
|
||||
- ReviewStatus: PENDING, INTERNAL_APPROVED, CLIENT_APPROVED, NEEDS_CHANGES, FINAL_APPROVED
|
||||
|
||||
Notes:
|
||||
- There is no dedicated Delivery model.
|
||||
- There is no dedicated Export model.
|
||||
- Delivery/export state is represented by file pointers and shot/version metadata fields.
|
||||
|
||||
---
|
||||
|
||||
## 2. API
|
||||
|
||||
### 2.1 Shots
|
||||
|
||||
#### External shot APIs
|
||||
|
||||
- GET /api/ext/projects
|
||||
- URL: /api/ext/projects
|
||||
- Method: GET
|
||||
- Purpose: List projects for pipeline tools
|
||||
- Request body: None
|
||||
- Response: projects[] with id, name, code, showId, projectType, status, dates, _count
|
||||
- Source: [app/api/ext/projects/route.ts](app/api/ext/projects/route.ts)
|
||||
|
||||
- GET /api/ext/projects/{projectCode}/episodes
|
||||
- URL: /api/ext/projects/{projectCode}/episodes
|
||||
- Method: GET
|
||||
- Purpose: List distinct episodes and optionally shot payloads per episode
|
||||
- Request body: None
|
||||
- Response: project + episodes[]; optional shots[] includes exrOutput/timecodes
|
||||
- Source: [app/api/ext/projects/[projectCode]/episodes/route.ts](app/api/ext/projects/%5BprojectCode%5D/episodes/route.ts)
|
||||
|
||||
- GET /api/ext/projects/{projectCode}/shots
|
||||
- URL: /api/ext/projects/{projectCode}/shots
|
||||
- Method: GET
|
||||
- Purpose: List shots by project code with filters/pagination
|
||||
- Request body: None
|
||||
- Response: project + pagination + shots[]
|
||||
- Source: [app/api/ext/projects/[projectCode]/shots/route.ts](app/api/ext/projects/%5BprojectCode%5D/shots/route.ts)
|
||||
|
||||
- GET /api/ext/shots
|
||||
- URL: /api/ext/shots
|
||||
- Method: GET
|
||||
- Purpose: Legacy listing by projectId
|
||||
- Request body: None
|
||||
- Response: shots[] + total
|
||||
- Source: [app/api/ext/shots/route.ts](app/api/ext/shots/route.ts)
|
||||
|
||||
- POST /api/ext/shots
|
||||
- URL: /api/ext/shots
|
||||
- Method: POST
|
||||
- Purpose: Create shot from external tool (JSON or multipart thumbnail)
|
||||
- Request body:
|
||||
- projectId, scene
|
||||
- optional episode, description, artistId, priority, fps, frameStart, frameEnd, dueDate, thumbnailUrl, shotGroupName, shotCode
|
||||
- optional thumbnail file (multipart)
|
||||
- Response: created shot object
|
||||
- Source: [app/api/ext/shots/route.ts](app/api/ext/shots/route.ts)
|
||||
|
||||
- GET /api/ext/shots/lookup
|
||||
- URL: /api/ext/shots/lookup
|
||||
- Method: GET
|
||||
- Purpose: Canonical shot lookup by shotCode (+ optional projectCode)
|
||||
- Request body: None
|
||||
- Response: shot object with project, artist, tasks, latest version, exrOutput, shotVersion, source/seq timecodes
|
||||
- Source: [app/api/ext/shots/lookup/route.ts](app/api/ext/shots/lookup/route.ts)
|
||||
|
||||
- GET /api/ext/shots/{shotId}
|
||||
- URL: /api/ext/shots/{shotId}
|
||||
- Method: GET
|
||||
- Purpose: Shot detail by DB id, or byCode mode
|
||||
- Request body: None
|
||||
- Response: full shot detail with tasks/latest version/counts
|
||||
- Source: [app/api/ext/shots/[shotId]/route.ts](app/api/ext/shots/%5BshotId%5D/route.ts)
|
||||
|
||||
- PATCH /api/ext/shots/{shotId}
|
||||
- URL: /api/ext/shots/{shotId}
|
||||
- Method: PATCH
|
||||
- Purpose: Update mutable shot field(s) from pipeline tools
|
||||
- Request body: shotVersion (v###) currently supported
|
||||
- Response: success + updated shot id/shotVersion
|
||||
- Source: [app/api/ext/shots/[shotId]/route.ts](app/api/ext/shots/%5BshotId%5D/route.ts)
|
||||
|
||||
#### Internal shot APIs
|
||||
|
||||
- GET /api/shots/{shotId}
|
||||
- URL: /api/shots/{shotId}
|
||||
- Method: GET
|
||||
- Purpose: Internal dashboard shot detail payload
|
||||
- Request body: None
|
||||
- Response: shot + tasks + artists + permissions flags
|
||||
- Source: [app/api/shots/[shotId]/route.ts](app/api/shots/%5BshotId%5D/route.ts)
|
||||
|
||||
- GET /api/projects/{projectId}/episodes
|
||||
- URL: /api/projects/{projectId}/episodes
|
||||
- Method: GET
|
||||
- Purpose: Internal distinct episode list for project
|
||||
- Request body: None
|
||||
- Response: episodes[]
|
||||
- Source: [app/api/projects/[projectId]/episodes/route.ts](app/api/projects/%5BprojectId%5D/episodes/route.ts)
|
||||
|
||||
### 2.2 Reviews
|
||||
|
||||
- GET /api/review-sessions
|
||||
- Purpose: list review sessions
|
||||
- POST /api/review-sessions
|
||||
- Purpose: create review session token and portal link
|
||||
- DELETE /api/review-sessions
|
||||
- Purpose: deactivate review session
|
||||
- Source: [app/api/review-sessions/route.ts](app/api/review-sessions/route.ts)
|
||||
|
||||
- POST /api/client/{token}/auth
|
||||
- Purpose: review password check + unlock cookie
|
||||
- Source: [app/api/client/[token]/auth/route.ts](app/api/client/%5Btoken%5D/auth/route.ts)
|
||||
|
||||
- GET /api/client/{token}/project
|
||||
- Purpose: client portal project payload (shared shots/versions)
|
||||
- Source: [app/api/client/[token]/project/route.ts](app/api/client/%5Btoken%5D/project/route.ts)
|
||||
|
||||
- GET /api/client/{token}/versions/{versionId}
|
||||
- Purpose: client review detail payload
|
||||
- Source: [app/api/client/[token]/versions/[versionId]/route.ts](app/api/client/%5Btoken%5D/versions/%5BversionId%5D/route.ts)
|
||||
|
||||
- POST /api/client/{token}/comment
|
||||
- Purpose: client frame comment
|
||||
- Request body: versionId, frameNumber, timestamp, text
|
||||
- Response: created comment
|
||||
- Source: [app/api/client/[token]/comment/route.ts](app/api/client/%5Btoken%5D/comment/route.ts)
|
||||
|
||||
- POST /api/client/{token}/annotation
|
||||
- Purpose: client annotation write
|
||||
- Request body: versionId, frameNumber, drawingData, optional color
|
||||
- Response: annotation
|
||||
- Source: [app/api/client/[token]/annotation/route.ts](app/api/client/%5Btoken%5D/annotation/route.ts)
|
||||
|
||||
- POST /api/client/{token}/approve
|
||||
- Purpose: shot-level approve/changes and legacy version-level approval
|
||||
- Request body:
|
||||
- shot mode: shotId + action
|
||||
- version mode: versionId + status + notes
|
||||
- Response: success
|
||||
- Source: [app/api/client/[token]/approve/route.ts](app/api/client/%5Btoken%5D/approve/route.ts)
|
||||
|
||||
- GET /api/versions/{versionId}/comments
|
||||
- Purpose: internal comments read
|
||||
- Source: [app/api/versions/[versionId]/comments/route.ts](app/api/versions/%5BversionId%5D/comments/route.ts)
|
||||
|
||||
- GET /api/versions/{versionId}/annotations
|
||||
- Purpose: internal annotations read
|
||||
- Source: [app/api/versions/[versionId]/annotations/route.ts](app/api/versions/%5BversionId%5D/annotations/route.ts)
|
||||
|
||||
- GET /api/playlist
|
||||
- Purpose: latest version per shot playlist
|
||||
- Source: [app/api/playlist/route.ts](app/api/playlist/route.ts)
|
||||
|
||||
### 2.3 File uploads
|
||||
|
||||
- POST /api/upload
|
||||
- Purpose: authenticated upload to Hetzner via app server
|
||||
- Body: multipart file (+ type)
|
||||
- Response: url, key
|
||||
- Source: [app/api/upload/route.ts](app/api/upload/route.ts)
|
||||
|
||||
- POST /api/upload/local
|
||||
- Purpose: authenticated video upload path
|
||||
- Body: multipart file
|
||||
- Response: url, key
|
||||
- Source: [app/api/upload/local/route.ts](app/api/upload/local/route.ts)
|
||||
|
||||
- POST /api/upload/presign
|
||||
- Purpose: direct browser->object-storage upload URL
|
||||
- Body: fileName, contentType, optional folder
|
||||
- Response: presignedUrl, key, url
|
||||
- Source: [app/api/upload/presign/route.ts](app/api/upload/presign/route.ts)
|
||||
|
||||
- GET/POST /api/uploadthing
|
||||
- Purpose: UploadThing route handler passthrough (if configured)
|
||||
- Source: [app/api/uploadthing/route.ts](app/api/uploadthing/route.ts)
|
||||
|
||||
- POST /api/batch-upload/presign
|
||||
- Purpose: presign high-res upload key
|
||||
- Body: fileName
|
||||
- Response: presignedUrl, key
|
||||
- Source: [app/api/batch-upload/presign/route.ts](app/api/batch-upload/presign/route.ts)
|
||||
|
||||
- POST /api/batch-upload/preview
|
||||
- Purpose: classify upload actions before upload
|
||||
- Body: projectId, fileNames[]
|
||||
- Response: items[] with statuses (new-version, rename-and-upload, create-task, update-highres, etc)
|
||||
- Source: [app/api/batch-upload/preview/route.ts](app/api/batch-upload/preview/route.ts)
|
||||
|
||||
- POST /api/batch-upload/upload
|
||||
- Purpose: commit highres key or create version records
|
||||
- Body:
|
||||
- update-highres: action, shotId, projectId, key, fileName
|
||||
- version upload: action, shotId, projectId, file, task routing fields
|
||||
- Response: success + action result
|
||||
- Source: [app/api/batch-upload/upload/route.ts](app/api/batch-upload/upload/route.ts)
|
||||
|
||||
### 2.4 EXRs / Rendering / Delivery / Storage / Metadata
|
||||
|
||||
- GET /api/files/{...key}
|
||||
- Purpose: file serving, range requests, local/Hetzner routing
|
||||
- Source: [app/api/files/[...key]/route.ts](app/api/files/%5B...key%5D/route.ts)
|
||||
|
||||
- GET/POST /api/admin/migration
|
||||
- Purpose: local uploads migration status and per-key migration to Hetzner
|
||||
- Source: [app/api/admin/migration/route.ts](app/api/admin/migration/route.ts)
|
||||
|
||||
- POST/DELETE /api/storage-test
|
||||
- Purpose: upload test object / delete test object
|
||||
- Source: [app/api/storage-test/route.ts](app/api/storage-test/route.ts)
|
||||
|
||||
- POST/DELETE /api/shots/{shotId}/highres
|
||||
- Purpose: upload/remove high-res deliverable on shot
|
||||
- Source: [app/api/shots/[shotId]/highres/route.ts](app/api/shots/%5BshotId%5D/highres/route.ts)
|
||||
|
||||
- GET /api/shots/{shotId}/highres/download
|
||||
- Purpose: internal presigned download URL for high-res
|
||||
- Source: [app/api/shots/[shotId]/highres/download/route.ts](app/api/shots/%5BshotId%5D/highres/download/route.ts)
|
||||
|
||||
- GET /api/client/{token}/shots/{shotId}/highres/download
|
||||
- Purpose: client token-gated presigned high-res download URL
|
||||
- Source: [app/api/client/[token]/shots/[shotId]/highres/download/route.ts](app/api/client/%5Btoken%5D/shots/%5BshotId%5D/highres/download/route.ts)
|
||||
|
||||
- GET/POST/DELETE /api/shots/{shotId}/references
|
||||
- Purpose: shot reference image management
|
||||
- Source: [app/api/shots/[shotId]/references/route.ts](app/api/shots/%5BshotId%5D/references/route.ts)
|
||||
|
||||
- GET /api/storyboard/pdf
|
||||
- Purpose: server-rendered printable storyboard HTML with metadata options
|
||||
- Source: [app/api/storyboard/pdf/route.ts](app/api/storyboard/pdf/route.ts)
|
||||
|
||||
Notes:
|
||||
- No dedicated REST endpoint that runs ffmpeg in this repository.
|
||||
- No dedicated REST endpoint that performs EXR rendering on the server.
|
||||
- No dedicated deliveries API namespace.
|
||||
|
||||
---
|
||||
|
||||
## 3. Storage
|
||||
|
||||
Primary storage abstraction: [lib/storage.ts](lib/storage.ts)
|
||||
|
||||
### Provider modes
|
||||
|
||||
- local
|
||||
- uploadthing
|
||||
- s3
|
||||
- r2
|
||||
- b2
|
||||
- minio
|
||||
|
||||
### Dedicated high-res object storage path
|
||||
|
||||
- Hetzner object storage helper methods are used for high-res and presigned direct uploads.
|
||||
- Config source precedence:
|
||||
- SystemConfig table keys
|
||||
- env fallback
|
||||
|
||||
### Folder/key layout in object storage (current code paths)
|
||||
|
||||
- videos/
|
||||
- image/
|
||||
- highres/
|
||||
- storage-test/
|
||||
|
||||
### Local storage
|
||||
|
||||
- LOCAL_UPLOAD_DIR (default ./uploads)
|
||||
- Served via /api/files catch-all route
|
||||
|
||||
### File naming conventions
|
||||
|
||||
- Key format: {folder}/{uuid}-{sanitized-file-name}
|
||||
- Sanitization done by sanitizeFileName to avoid URL/signature problems in object keys
|
||||
|
||||
### EXR locations
|
||||
|
||||
Within web app/runtime:
|
||||
- EXR references are naming metadata (Shot.exrOutput) and file-serving pathing.
|
||||
|
||||
Within DCC tooling docs/scripts:
|
||||
- AE/Nuke workflows target shared export roots and per-shot folders.
|
||||
|
||||
### Preview locations
|
||||
|
||||
- Version media URLs stored in Version.fileUrl
|
||||
- Accessed through app player routes and /api/files routing where applicable
|
||||
|
||||
### Thumbnail locations
|
||||
|
||||
- Shot.thumbnailUrl
|
||||
- Version.thumbnailUrl
|
||||
- Can point to /api/files/{key} or external provider URL
|
||||
|
||||
Relevant sources:
|
||||
- [lib/storage.ts](lib/storage.ts)
|
||||
- [app/api/files/[...key]/route.ts](app/api/files/%5B...key%5D/route.ts)
|
||||
- [actions/settings.ts](actions/settings.ts)
|
||||
|
||||
---
|
||||
|
||||
## 4. Export Pipeline (Current)
|
||||
|
||||
### AE panel
|
||||
|
||||
Documented in repo:
|
||||
- [VFXReviewConnector.md](VFXReviewConnector.md)
|
||||
- [EXT_API_REFERENCE.md](EXT_API_REFERENCE.md)
|
||||
|
||||
Implemented panel script also exists on the host AE installation (outside workspace), and behavior aligns with docs:
|
||||
- Episodes/shots discovery via ext APIs
|
||||
- Shot lookup API usage for metadata
|
||||
- Overlay/slate essential property updates (burn-in style overlays)
|
||||
- Queue EXR / Queue MP4 / Queue MOV render queue actions
|
||||
- Queue EXR (review convention)
|
||||
- Import exported EXR and import renders
|
||||
- Pull picture lock using seq timecodes
|
||||
- Increment shot version by PATCH call
|
||||
- Delivery prep that copies/renames EXR files to delivery folder convention
|
||||
|
||||
### External API endpoints used by DCC tools
|
||||
|
||||
- GET /api/ext/projects/{projectCode}/episodes
|
||||
- GET /api/ext/projects/{projectCode}/shots
|
||||
- GET /api/ext/shots/lookup
|
||||
- PATCH /api/ext/shots/{shotId}
|
||||
|
||||
### Existing render scripts and pipelines
|
||||
|
||||
- AE panel render queue automation (external script)
|
||||
- Nuke connector script creating write-node outputs and shot scripts:
|
||||
- [VFXReviewConnector.py](VFXReviewConnector.py)
|
||||
|
||||
### ffmpeg scripts
|
||||
|
||||
- None found in this repository.
|
||||
|
||||
### Proxy generation
|
||||
|
||||
- Version.proxyUrl field exists in schema.
|
||||
- No implemented proxy-generation worker/function found.
|
||||
|
||||
### Thumbnail generation
|
||||
|
||||
- Upload/assignment flows exist.
|
||||
- No server-side frame-extract thumbnail generator found.
|
||||
|
||||
### Metadata extraction
|
||||
|
||||
- EDL / picture-tracker CSV metadata parsing implemented:
|
||||
- [lib/edl-utils.ts](lib/edl-utils.ts)
|
||||
- [actions/shots.ts](actions/shots.ts)
|
||||
|
||||
### Burn-in generation
|
||||
|
||||
- Achieved via DCC overlay/slate layers and essential properties in AE/Nuke tool workflows.
|
||||
- No ffmpeg burn-in path found.
|
||||
|
||||
---
|
||||
|
||||
## 5. Review System
|
||||
|
||||
### Current review workflow
|
||||
|
||||
Core status derivation:
|
||||
- [lib/shot-status.ts](lib/shot-status.ts)
|
||||
|
||||
Server action orchestration:
|
||||
- [actions/versions.ts](actions/versions.ts)
|
||||
- [actions/approvals.ts](actions/approvals.ts)
|
||||
- [actions/tasks.ts](actions/tasks.ts)
|
||||
- [actions/shots.ts](actions/shots.ts)
|
||||
- [actions/comments.ts](actions/comments.ts)
|
||||
|
||||
### Internal review
|
||||
|
||||
- Version upload:
|
||||
- marks previous versions non-latest
|
||||
- creates new latest version
|
||||
- moves Task to INTERNAL_REVIEW
|
||||
- recalculates Shot.status
|
||||
|
||||
### Client review
|
||||
|
||||
- Tokenized ReviewSession links
|
||||
- Optional password gate with signed cookie unlock
|
||||
- Client comment/annotation/approval endpoints
|
||||
- Share/unshare semantics via shot and version visibility fields
|
||||
|
||||
### Task status flow
|
||||
|
||||
Observed statuses:
|
||||
- TODO
|
||||
- IN_PROGRESS
|
||||
- INTERNAL_REVIEW
|
||||
- CLIENT_REVIEW
|
||||
- CHANGES
|
||||
- DONE
|
||||
|
||||
### Shot status flow
|
||||
|
||||
Derived in priority order:
|
||||
- REVISIONS (any task CHANGES)
|
||||
- COMPLETE (shotApprovalStatus CLIENT_APPROVED)
|
||||
- CLIENT_REVIEW / READY_FOR_CLIENT (internally approved + share flag)
|
||||
- IN_PROGRESS (task TODO/IN_PROGRESS)
|
||||
- INTERNAL_REVIEW (tasks exist)
|
||||
- WAITING (no tasks)
|
||||
|
||||
### Approval process
|
||||
|
||||
- Version-level approvals create Approval rows and update Version.approvalStatus
|
||||
- Shot-level client actions supported via client approve endpoint and shot actions
|
||||
|
||||
Reference doc in repo:
|
||||
- [Shot task workflow.md](Shot%20task%20workflow.md)
|
||||
|
||||
---
|
||||
|
||||
## 6. Authentication
|
||||
|
||||
### External tools
|
||||
|
||||
- /api/ext/* routes authenticate using API_SECRET_KEY via:
|
||||
- Authorization: Bearer <key>
|
||||
- x-api-key header
|
||||
|
||||
### Client review links
|
||||
|
||||
- tokenized route with ReviewSession lookup
|
||||
- optional password hash validation and signed unlock cookie
|
||||
|
||||
### App users
|
||||
|
||||
- NextAuth credentials provider
|
||||
- bcrypt password hash compare
|
||||
- JWT session strategy
|
||||
|
||||
### Middleware behavior
|
||||
|
||||
- Route classes allowed through middleware auth gate:
|
||||
- /api/ext/
|
||||
- /api/client/
|
||||
- /api/display/
|
||||
- /api/files/
|
||||
- /api/uploadthing
|
||||
- Per-route auth still enforced in handlers
|
||||
|
||||
Sources:
|
||||
- [auth.ts](auth.ts)
|
||||
- [auth.config.ts](auth.config.ts)
|
||||
- [middleware.ts](middleware.ts)
|
||||
- [lib/review-auth.ts](lib/review-auth.ts)
|
||||
|
||||
---
|
||||
|
||||
## 7. Existing Background Jobs
|
||||
|
||||
### Cron jobs
|
||||
- None found.
|
||||
|
||||
### Queues
|
||||
- None found.
|
||||
|
||||
### Workers
|
||||
- None found.
|
||||
|
||||
### Polling services
|
||||
- Display devices poll:
|
||||
- /api/display/events
|
||||
- /api/dashboard/stats
|
||||
|
||||
### Docker containers
|
||||
- vfxreview app container
|
||||
- postgres container
|
||||
- Source: [docker-compose.yml](docker-compose.yml)
|
||||
|
||||
### Scheduled tasks
|
||||
- Startup migration commands in container entrypoint:
|
||||
- [entrypoint.sh](entrypoint.sh)
|
||||
- AE panel Prep Delivery launches background PowerShell copy scripts on workstation (outside web app runtime)
|
||||
|
||||
---
|
||||
|
||||
## 8. After Effects Integration (Implemented Features)
|
||||
|
||||
Repo-level references:
|
||||
- [VFXReviewConnector.md](VFXReviewConnector.md)
|
||||
- [EXT_API_REFERENCE.md](EXT_API_REFERENCE.md)
|
||||
|
||||
Implemented capabilities observed/documented:
|
||||
|
||||
- API calls
|
||||
- episodes/shots listing and shot lookup
|
||||
- shot version PATCH
|
||||
- Authentication
|
||||
- bearer API token in script
|
||||
- Comp discovery
|
||||
- shot code extraction from comp names and dropdown selection
|
||||
- Render queue integration
|
||||
- EXR Sequence
|
||||
- REVIEW_PREVIEW (MP4)
|
||||
- 4444 Tri (MOV)
|
||||
- EXR review sequence queue variant
|
||||
- Output path generation
|
||||
- shot/version naming conventions + export root paths
|
||||
- Shot lookup
|
||||
- uses /api/ext/shots/lookup for metadata and naming decisions
|
||||
- Upload features
|
||||
- no direct upload-to-web API flow in panel docs/script (render/output handled in DCC/filesystem)
|
||||
- Burn-in generation
|
||||
- overlay and slate essential properties
|
||||
- Review integration
|
||||
- preview comp build and version increment sync
|
||||
|
||||
---
|
||||
|
||||
## 9. Configuration
|
||||
|
||||
### Environment variables
|
||||
|
||||
Primary reference: [.env.example](.env.example)
|
||||
|
||||
Observed vars in code/docs include:
|
||||
|
||||
- DATABASE_URL
|
||||
- NEXTAUTH_SECRET
|
||||
- NEXTAUTH_URL
|
||||
- NEXT_PUBLIC_APP_URL
|
||||
- NEXT_PUBLIC_APP_NAME
|
||||
- API_SECRET_KEY
|
||||
- AUTH_SECRET
|
||||
- STORAGE_PROVIDER
|
||||
- LOCAL_UPLOAD_DIR
|
||||
- AWS_ACCESS_KEY_ID
|
||||
- AWS_SECRET_ACCESS_KEY
|
||||
- AWS_REGION
|
||||
- AWS_BUCKET_NAME
|
||||
- R2_ACCESS_KEY_ID
|
||||
- R2_SECRET_ACCESS_KEY
|
||||
- R2_ACCOUNT_ID
|
||||
- R2_BUCKET_NAME
|
||||
- R2_PUBLIC_URL
|
||||
- B2_APPLICATION_KEY_ID
|
||||
- B2_APPLICATION_KEY
|
||||
- B2_BUCKET_NAME
|
||||
- B2_ENDPOINT
|
||||
- MINIO_ENDPOINT
|
||||
- MINIO_ACCESS_KEY
|
||||
- MINIO_SECRET_KEY
|
||||
- MINIO_BUCKET_NAME
|
||||
- HETZNER_ENDPOINT
|
||||
- HETZNER_ACCESS_KEY
|
||||
- HETZNER_SECRET_KEY
|
||||
- HETZNER_BUCKET_NAME
|
||||
- UPLOADTHING_SECRET
|
||||
- UPLOADTHING_APP_ID
|
||||
- EMAIL_FROM
|
||||
- EMAIL_SERVER_HOST
|
||||
- EMAIL_SERVER_PORT
|
||||
- EMAIL_SERVER_USER
|
||||
- EMAIL_SERVER_PASSWORD
|
||||
- SLACK_DEFAULT_WEBHOOK
|
||||
|
||||
### Storage configuration
|
||||
|
||||
- Provider abstraction in [lib/storage.ts](lib/storage.ts)
|
||||
- Hetzner overrides in [actions/settings.ts](actions/settings.ts) and SystemConfig table
|
||||
|
||||
### Render configuration
|
||||
|
||||
- No server-side renderer configuration module found.
|
||||
- DCC render templates are documented in [VFXReviewConnector.md](VFXReviewConnector.md).
|
||||
|
||||
### ffmpeg configuration
|
||||
|
||||
- None found in repository.
|
||||
|
||||
### Object storage configuration
|
||||
|
||||
- Implemented for AWS S3/R2/B2/MinIO + dedicated Hetzner helper path
|
||||
|
||||
---
|
||||
|
||||
## 10. Existing Utility Functions (Reusable)
|
||||
|
||||
### Metadata extraction
|
||||
|
||||
- parseEdlCsv
|
||||
- parsePictureTrackerCsv
|
||||
- Source: [lib/edl-utils.ts](lib/edl-utils.ts)
|
||||
|
||||
### File scanning
|
||||
|
||||
- Local upload tree walker in migration route
|
||||
- Nuke connector plate/render directory scanners
|
||||
- Sources:
|
||||
- [app/api/admin/migration/route.ts](app/api/admin/migration/route.ts)
|
||||
- [VFXReviewConnector.py](VFXReviewConnector.py)
|
||||
|
||||
### EXR sequence detection
|
||||
|
||||
- Nuke connector helper sequence detectors and pattern conversion
|
||||
- Source: [VFXReviewConnector.py](VFXReviewConnector.py)
|
||||
|
||||
### MOV generation
|
||||
|
||||
- No server-side MOV generation utility found.
|
||||
- MOV outputs are DCC render queue outputs in external scripts/docs.
|
||||
|
||||
### Checksums
|
||||
|
||||
- No checksum utility found.
|
||||
|
||||
### Frame counting
|
||||
|
||||
- durationToFrameCount and frame math helpers
|
||||
- Source: [lib/frame-utils.ts](lib/frame-utils.ts)
|
||||
|
||||
### Timecode extraction/conversion
|
||||
|
||||
- frameToTimecode / formatTimecode
|
||||
- CSV timecode validation/parsing
|
||||
- Sources:
|
||||
- [lib/frame-utils.ts](lib/frame-utils.ts)
|
||||
- [lib/utils.ts](lib/utils.ts)
|
||||
- [lib/edl-utils.ts](lib/edl-utils.ts)
|
||||
|
||||
---
|
||||
|
||||
## 11. High-Level Architecture Diagram
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[AE Panel / Nuke Connector\nExternal DCC tools] -->|API key auth| B[Next.js App Router APIs]
|
||||
B --> C[Prisma ORM]
|
||||
C --> D[(PostgreSQL)]
|
||||
|
||||
B --> E[Storage Abstraction]
|
||||
E --> F[(Hetzner Object Storage)]
|
||||
E --> G[(S3/R2/B2/MinIO)]
|
||||
B --> H[(Local uploads dir)]
|
||||
|
||||
I[Internal reviewers\nNextAuth session] --> B
|
||||
J[Client reviewers\nToken review sessions] --> B
|
||||
|
||||
K[ESP32 display client] -->|x-display-key polling| B
|
||||
|
||||
A --> L[Shared filesystem render roots\nEXR/MP4/MOV outputs]
|
||||
L -->|served/linked via app metadata| B
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Files Inspected (Primary)
|
||||
|
||||
- [prisma/schema.prisma](prisma/schema.prisma)
|
||||
- [lib/storage.ts](lib/storage.ts)
|
||||
- [lib/shot-status.ts](lib/shot-status.ts)
|
||||
- [lib/review-auth.ts](lib/review-auth.ts)
|
||||
- [lib/edl-utils.ts](lib/edl-utils.ts)
|
||||
- [lib/frame-utils.ts](lib/frame-utils.ts)
|
||||
- [lib/utils.ts](lib/utils.ts)
|
||||
- [auth.ts](auth.ts)
|
||||
- [auth.config.ts](auth.config.ts)
|
||||
- [middleware.ts](middleware.ts)
|
||||
- [next.config.ts](next.config.ts)
|
||||
- [.env.example](.env.example)
|
||||
- [docker-compose.yml](docker-compose.yml)
|
||||
- [Dockerfile](Dockerfile)
|
||||
- [entrypoint.sh](entrypoint.sh)
|
||||
- [EXT_API_REFERENCE.md](EXT_API_REFERENCE.md)
|
||||
- [VFXReviewConnector.md](VFXReviewConnector.md)
|
||||
- [VFXReviewConnector.py](VFXReviewConnector.py)
|
||||
- [# VFXReview Connector for Nuke.md](#%20VFXReview%20Connector%20for%20Nuke.md)
|
||||
- API handlers under [app/api](app/api)
|
||||
- Server actions under [actions](actions)
|
||||
|
||||
---
|
||||
|
||||
End of current-state report.
|
||||
@@ -89,7 +89,7 @@ export default function ShotDetailPage() {
|
||||
const [isDuplicating, setIsDuplicating] = useState(false);
|
||||
const [isActioning, setIsActioning] = useState(false);
|
||||
const [highResDialogOpen, setHighResDialogOpen] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<"tasks" | "reviews" | "footage" | "settings">("tasks");
|
||||
const [activeTab, setActiveTab] = useState<"tasks" | "reviews" | "footage" | "settings" | "exports">("tasks");
|
||||
const [editingVersion, setEditingVersion] = useState(false);
|
||||
const [versionInput, setVersionInput] = useState("");
|
||||
const [savingVersion, setSavingVersion] = useState(false);
|
||||
@@ -531,6 +531,18 @@ export default function ShotDetailPage() {
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
Reviews
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("exports")}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors -mb-px",
|
||||
activeTab === "exports"
|
||||
? "border-amber-500 text-amber-400"
|
||||
: "border-transparent text-zinc-500 hover:text-zinc-300"
|
||||
)}
|
||||
>
|
||||
<ListTodo className="h-4 w-4" />
|
||||
Exports
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("footage")}
|
||||
className={cn(
|
||||
@@ -658,6 +670,16 @@ export default function ShotDetailPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === "exports" && (
|
||||
<div className="rounded-lg border border-border p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium">Export history</h3>
|
||||
<span className="text-xs text-muted-foreground">Queue API-backed entries</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Exports submitted for this shot will appear here once they are queued through the new pipeline API.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "settings" && canManage && (
|
||||
<ShotSettingsTab shot={shot} artists={artists} onSaved={fetchShot} />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { auth } from "@/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { listExportsForQueue } from "@/lib/render-pipeline/exports";
|
||||
|
||||
export const metadata = { title: "Render Queue — VFX Review" };
|
||||
|
||||
export default async function RenderQueuePage() {
|
||||
const session = await auth();
|
||||
if (!session?.user) redirect("/login");
|
||||
|
||||
const exports = await listExportsForQueue();
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">Render Queue</h1>
|
||||
<p className="text-sm text-muted-foreground">Queued and active exports from the pipeline.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-zinc-900/80 text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left">Shot</th>
|
||||
<th className="px-4 py-3 text-left">Version</th>
|
||||
<th className="px-4 py-3 text-left">Status</th>
|
||||
<th className="px-4 py-3 text-left">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{exports.map((item) => (
|
||||
<tr key={item.id} className="border-t border-border/60">
|
||||
<td className="px-4 py-3 font-mono">{item.shot?.shotCode ?? item.shotId}</td>
|
||||
<td className="px-4 py-3">{item.versionString}</td>
|
||||
<td className="px-4 py-3">{item.status}</td>
|
||||
<td className="px-4 py-3">{new Date(item.createdAt).toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
{exports.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={4} className="px-4 py-8 text-center text-muted-foreground">
|
||||
No exports queued yet.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getLatestExportForShot } from "@/lib/render-pipeline/exports";
|
||||
|
||||
function isAuthorized(req: NextRequest): boolean {
|
||||
const apiKey = process.env.API_SECRET_KEY;
|
||||
if (!apiKey) return false;
|
||||
const authHeader = req.headers.get("authorization") ?? "";
|
||||
if (authHeader.startsWith("Bearer ")) return authHeader.slice(7) === apiKey;
|
||||
return (req.headers.get("x-api-key") ?? "") === apiKey;
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
if (!isAuthorized(req)) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const shotId = searchParams.get("shotId");
|
||||
if (!shotId) {
|
||||
return NextResponse.json({ error: "shotId query param is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const exportRecord = await getLatestExportForShot(shotId);
|
||||
return NextResponse.json({ export: exportRecord });
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { createRenderExport, listExportsForQueue } from "@/lib/render-pipeline/exports";
|
||||
|
||||
function isAuthorized(req: NextRequest): boolean {
|
||||
const apiKey = process.env.API_SECRET_KEY;
|
||||
if (!apiKey) return false;
|
||||
const authHeader = req.headers.get("authorization") ?? "";
|
||||
if (authHeader.startsWith("Bearer ")) return authHeader.slice(7) === apiKey;
|
||||
return (req.headers.get("x-api-key") ?? "") === apiKey;
|
||||
}
|
||||
|
||||
const createExportSchema = z.object({
|
||||
shotId: z.string().min(1),
|
||||
projectId: z.string().min(1),
|
||||
manifest: z.unknown(),
|
||||
submittedById: z.string().optional().nullable(),
|
||||
submittedByName: z.string().optional().nullable(),
|
||||
taskId: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
if (!isAuthorized(req)) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const exports = await listExportsForQueue();
|
||||
return NextResponse.json({ exports });
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
if (!isAuthorized(req)) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await req.json();
|
||||
const parsed = createExportSchema.parse(body);
|
||||
const result = await createRenderExport(parsed);
|
||||
return NextResponse.json(result, { status: 201 });
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({ error: "Validation error", details: error.errors }, { status: 422 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: error instanceof Error ? error.message : "Failed to queue export" }, { status: 400 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/auth";
|
||||
import { listExportsForQueue } from "@/lib/render-pipeline/exports";
|
||||
|
||||
export async function GET() {
|
||||
const session = await auth();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const exports = await listExportsForQueue();
|
||||
return NextResponse.json({ exports });
|
||||
}
|
||||
@@ -31,6 +31,7 @@ const navItems = [
|
||||
{ href: '/projects', label: 'Projects', icon: FolderOpen },
|
||||
{ href: '/shot-status', label: 'Shot Status', icon: BarChart2, hideForClient: true },
|
||||
{ href: '/playlist', label: 'Playlist', icon: ListVideo, hideForClient: true },
|
||||
{ href: '/render-queue', label: 'Render Queue', icon: ListTodo, hideForClient: true },
|
||||
{ href: '/tasks', label: 'My Tasks', icon: ListTodo, hideForClient: true },
|
||||
{ href: '/shoot-log', label: 'Shot Log', icon: Clapperboard, supervisorOnly: true },
|
||||
{ href: '/storyboard', label: 'Storyboard', icon: LayoutGrid, supervisorOnly: true },
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildVersionString, deriveOutputBaseName, validateExportManifest } from "./exports";
|
||||
|
||||
describe("render export helpers", () => {
|
||||
it("formats version strings with zero-padding", () => {
|
||||
expect(buildVersionString(4)).toBe("v004");
|
||||
expect(buildVersionString(42)).toBe("v042");
|
||||
});
|
||||
|
||||
it("derives the output base name from a frame pattern", () => {
|
||||
expect(deriveOutputBaseName("UNG_106_010_020_cmp_TT_v004.[####].exr")).toBe(
|
||||
"UNG_106_010_020_cmp_TT_v004"
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects invalid manifests", () => {
|
||||
expect(() =>
|
||||
validateExportManifest({
|
||||
shotCode: "UNG_106_010_020",
|
||||
projectCode: "UNG_S1",
|
||||
aepPath: "",
|
||||
compName: "",
|
||||
frameStart: 1001,
|
||||
frameEnd: 1000,
|
||||
fps: 24,
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
import { z } from "zod";
|
||||
import { db } from "@/lib/db";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
const exportManifestSchema = z.object({
|
||||
shotCode: z.string().min(1),
|
||||
projectCode: z.string().min(1),
|
||||
aepPath: z.string().min(1),
|
||||
compName: z.string().min(1),
|
||||
rendererType: z.string().default("aerender"),
|
||||
outputDir: z.string().optional(),
|
||||
outputPattern: z.string().optional(),
|
||||
frameStart: z.number().int().positive(),
|
||||
frameEnd: z.number().int().positive(),
|
||||
fps: z.number().positive(),
|
||||
width: z.number().int().positive().optional(),
|
||||
height: z.number().int().positive().optional(),
|
||||
colorspace: z.string().optional(),
|
||||
});
|
||||
|
||||
export function validateExportManifest(input: unknown) {
|
||||
return exportManifestSchema.parse(input);
|
||||
}
|
||||
|
||||
export function buildVersionString(versionNumber: number) {
|
||||
return `v${versionNumber.toString().padStart(3, "0")}`;
|
||||
}
|
||||
|
||||
export function deriveOutputBaseName(outputPattern: string) {
|
||||
const match = outputPattern.match(/^(.*)\.(?:\[#+\]|\{#+\})\.([A-Za-z0-9]+)$/);
|
||||
return match ? match[1] : outputPattern;
|
||||
}
|
||||
|
||||
export async function createRenderExport(input: {
|
||||
shotId: string;
|
||||
projectId: string;
|
||||
manifest: unknown;
|
||||
submittedById?: string | null;
|
||||
submittedByName?: string | null;
|
||||
taskId?: string | null;
|
||||
}) {
|
||||
const manifest = validateExportManifest(input.manifest);
|
||||
const shot = await db.shot.findUnique({
|
||||
where: { id: input.shotId },
|
||||
select: { id: true, shotVersion: true, exrOutput: true, projectId: true },
|
||||
});
|
||||
|
||||
if (!shot) {
|
||||
throw new Error("Shot not found");
|
||||
}
|
||||
|
||||
if (shot.projectId !== input.projectId) {
|
||||
throw new Error("Shot does not belong to the supplied project");
|
||||
}
|
||||
|
||||
const versionNumber = parseInt((shot.shotVersion ?? "v001").replace(/^v/i, ""), 10) + 1;
|
||||
const versionString = buildVersionString(versionNumber);
|
||||
|
||||
const outputPattern = manifest.outputPattern ?? `${manifest.compName}_TT_${versionString}.[####].exr`;
|
||||
const outputDir = manifest.outputDir ?? `${shot.exrOutput ?? manifest.compName}_${versionString}`;
|
||||
|
||||
const result = await db.$transaction(async (tx) => {
|
||||
const activeExports = await tx.export.findMany({
|
||||
where: {
|
||||
shotId: input.shotId,
|
||||
status: { in: ["QUEUED", "RENDERING", "VALIDATING", "GENERATING_PREVIEW", "READY_FOR_QC", "READY_FOR_DELIVERY"] },
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
const exportRecord = await tx.export.create({
|
||||
data: {
|
||||
shotId: input.shotId,
|
||||
projectId: input.projectId,
|
||||
taskId: input.taskId ?? undefined,
|
||||
versionNumber,
|
||||
versionString,
|
||||
status: "QUEUED",
|
||||
aepPath: manifest.aepPath,
|
||||
compName: manifest.compName,
|
||||
rendererType: manifest.rendererType,
|
||||
outputDir,
|
||||
outputPattern,
|
||||
frameStart: manifest.frameStart,
|
||||
frameEnd: manifest.frameEnd,
|
||||
fps: manifest.fps,
|
||||
width: manifest.width ?? 1920,
|
||||
height: manifest.height ?? 1080,
|
||||
colorspace: manifest.colorspace,
|
||||
submittedById: input.submittedById ?? undefined,
|
||||
submittedByName: input.submittedByName ?? undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.exportEvent.createMany({
|
||||
data: [
|
||||
{
|
||||
exportId: exportRecord.id,
|
||||
toStatus: "QUEUED",
|
||||
actorType: "USER",
|
||||
actorId: input.submittedById ?? undefined,
|
||||
note: "Queued from panel",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await tx.renderJob.create({
|
||||
data: {
|
||||
type: "AE_RENDER",
|
||||
exportId: exportRecord.id,
|
||||
attempt: 1,
|
||||
maxAttempts: 3,
|
||||
status: "QUEUED",
|
||||
manifest: manifest as Prisma.JsonObject,
|
||||
priority: 50,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.shot.update({
|
||||
where: { id: input.shotId },
|
||||
data: {
|
||||
shotVersion: versionString,
|
||||
exrOutput: deriveOutputBaseName(outputPattern),
|
||||
},
|
||||
});
|
||||
|
||||
if (activeExports.length > 0) {
|
||||
await tx.export.updateMany({
|
||||
where: { id: { in: activeExports.map((item) => item.id) } },
|
||||
data: {
|
||||
status: "SUPERSEDED",
|
||||
supersededById: exportRecord.id,
|
||||
statusChangedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await tx.exportEvent.createMany({
|
||||
data: activeExports.map((item) => ({
|
||||
exportId: item.id,
|
||||
fromStatus: "QUEUED",
|
||||
toStatus: "SUPERSEDED",
|
||||
actorType: "SYSTEM",
|
||||
note: "Superseded by newer export",
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return { exportRecord, versionString };
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function listExportsForQueue() {
|
||||
return db.export.findMany({
|
||||
where: { status: { notIn: ["SUPERSEDED", "ARCHIVED", "CANCELLED", "DELIVERED"] } },
|
||||
orderBy: [{ createdAt: "desc" }],
|
||||
include: {
|
||||
shot: { select: { shotCode: true, shotVersion: true, project: { select: { code: true } } } },
|
||||
renderJobs: { orderBy: [{ createdAt: "desc" }], take: 1 },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getLatestExportForShot(shotId: string) {
|
||||
return db.export.findFirst({
|
||||
where: { shotId },
|
||||
orderBy: [{ createdAt: "desc" }],
|
||||
include: {
|
||||
shot: { select: { shotCode: true, project: { select: { code: true } } } },
|
||||
renderJobs: { orderBy: [{ createdAt: "desc" }], take: 1 },
|
||||
validations: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -144,6 +144,40 @@ enum AttachmentCategory {
|
||||
MISCELLANEOUS
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// AUTH MODELS (NextAuth v5 compatible)
|
||||
// ─────────────────────────────────────────────
|
||||
@@ -271,6 +305,7 @@ model Project {
|
||||
projectType ProjectType @default(STANDARD)
|
||||
description String? @db.Text
|
||||
status ProjectStatus @default(ACTIVE)
|
||||
deliveryConfig Json?
|
||||
dueDate DateTime?
|
||||
startDate DateTime?
|
||||
clientId String?
|
||||
@@ -356,6 +391,7 @@ model Shot {
|
||||
shotGroup ShotGroup? @relation(fields: [shotGroupId], references: [id])
|
||||
versions Version[]
|
||||
tasks Task[]
|
||||
exports Export[]
|
||||
footagePlates FootagePlate[]
|
||||
references ShotReference[]
|
||||
loggedTakes Take[] @relation("TakeToShot")
|
||||
@@ -410,6 +446,96 @@ model ShotGroup {
|
||||
@@map("shot_groups")
|
||||
}
|
||||
|
||||
model Export {
|
||||
id String @id @default(cuid())
|
||||
shotId String
|
||||
shot Shot @relation(fields: [shotId], references: [id], onDelete: Cascade)
|
||||
projectId String
|
||||
taskId String?
|
||||
versionId String?
|
||||
versionNumber Int
|
||||
versionString String
|
||||
status ExportStatus @default(QUEUED)
|
||||
statusChangedAt DateTime @default(now())
|
||||
aepPath String
|
||||
compName String
|
||||
rendererType String
|
||||
outputDir String
|
||||
outputPattern String
|
||||
frameStart Int
|
||||
frameEnd Int
|
||||
fps Float
|
||||
width Int
|
||||
height Int
|
||||
colorspace String?
|
||||
deliveryMovPath String?
|
||||
previewMovKey String?
|
||||
thumbnailKey String?
|
||||
metadataKey String?
|
||||
exrFileCount Int?
|
||||
exrTotalBytes BigInt?
|
||||
checksum String?
|
||||
submittedById String?
|
||||
submittedByName String?
|
||||
supersededById String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
renderJobs RenderJob[]
|
||||
events ExportEvent[]
|
||||
|
||||
@@unique([shotId, versionNumber])
|
||||
@@index([status])
|
||||
@@index([projectId, status])
|
||||
@@map("exports")
|
||||
}
|
||||
|
||||
model RenderJob {
|
||||
id String @id @default(cuid())
|
||||
type RenderJobType @default(AE_RENDER)
|
||||
exportId String?
|
||||
export Export? @relation(fields: [exportId], references: [id], onDelete: Cascade)
|
||||
deliveryId String?
|
||||
attempt Int @default(1)
|
||||
maxAttempts Int @default(3)
|
||||
status RenderJobStatus @default(QUEUED)
|
||||
priority Int @default(50)
|
||||
manifest Json
|
||||
machineId String?
|
||||
claimedAt DateTime?
|
||||
leaseExpiresAt DateTime?
|
||||
startedAt DateTime?
|
||||
finishedAt DateTime?
|
||||
progress Float @default(0)
|
||||
currentFrame Int?
|
||||
totalFrames Int?
|
||||
etaSeconds Int?
|
||||
exitCode Int?
|
||||
errorMessage String?
|
||||
logTail String?
|
||||
logFileKey String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([status, priority, createdAt])
|
||||
@@map("render_jobs")
|
||||
}
|
||||
|
||||
model ExportEvent {
|
||||
id String @id @default(cuid())
|
||||
exportId String
|
||||
export Export @relation(fields: [exportId], references: [id], onDelete: Cascade)
|
||||
fromStatus String?
|
||||
toStatus String
|
||||
actorType String
|
||||
actorId String?
|
||||
note String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([exportId, createdAt])
|
||||
@@map("export_events")
|
||||
}
|
||||
|
||||
model Version {
|
||||
id String @id @default(cuid())
|
||||
versionNumber Int
|
||||
|
||||
Reference in New Issue
Block a user