Files
vfxreview/TECHNICAL_ARCHITECTURE_REPORT_CURRENT_STATE.md
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

754 lines
22 KiB
Markdown

# 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.