Commit Graph

19 Commits

Author SHA1 Message Date
twotalesanimation 2e9fa45580 feat(review): flag handles in the review player (metadata only)
Deploy / deploy (push) Successful in 3m31s
Renders run 8 frames past the cut at each end. Directors were flagging
glitches in unfixed handles as shot problems, so the player now marks
them — without trimming or burning anything into the EXRs/MOV/MP4.

- Version.handleHead/handleTail/pictureStartFrame, Export.handleHead/
  handleTail; migration backfills pipeline exports and their preview
  versions from the manifest's handleFrames (else 8)
- AE panel: "Includes handles [8] frames each end" -> manifest.review;
  older manifests fall back to source.handleFrames
- finalize copies handles onto the Version; worker 1.6.4 reports the
  head slate length (pictureStartSeconds), else the player infers it
  as MP4 frames - EXR frameCount
- Player: hatched HANDLE / SLATE regions and cut marks on the timeline,
  "HEAD/TAIL HANDLE · NOT IN EDIT" overlay on handle frames, HANDLE badge
  on notes, "Edit range only" toggle (default on in the client portal)
- Staff Handles editor on the review page for manual uploads/corrections

Also fixes two player bugs found while testing: timeToFrame floored
float error (every seek landed a frame early), and J/reverse never ran
(the keyboard effect's cleanup stopped it on the next render).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:07:29 +02:00
twotalesanimation 94792279c5 fix(status): reset review state on a new version, and derive shot status everywhere
Requesting changes on a shot and then re-rendering it through the render queue
left every layer of the app still showing the old verdict. Three independent
causes, all on the pipeline path.

finalizePreview inherited the superseded version's approvalStatus
unconditionally. The carry-over exists so a re-export of a signed-off shot does
not reappear as awaiting review, but it applied equally to NEEDS_CHANGES and
REJECTED, so the render queued to address a note arrived already wearing that
note and nothing registered that the work had been redone. carryApproval now
carries only APPROVED.

The pipeline never moved the task. One left on CHANGES by a review or a QC fail
stayed there through every re-render, with no way out but a manual kanban drag.
A genuinely new version now moves it to INTERNAL_REVIEW, as actions/versions.ts
and batch-upload already did. A replaced version is excluded: regeneratePreview
re-delivers the same version after a slate fix and is not a new submission.

The pipeline never called recalcShotStatus at all, so shot boards went stale the
moment a render changed a task. finalizePreview and the QC fail path both
recalculate now; the latter already documented the REVISIONS derivation it was
relying on and never triggering.

Shot.status is derived from tasks and approval flags, but six call sites wrote
it directly with the status they expected: internallyApproveShot,
shareWithClient, unshareFromClient, clientApproveShot, unapproveShot and the
client approve route. Rule 1 of the derivation is that any CHANGES task outranks
an approval, so a shot could be pushed past open rework and then snap back to
REVISIONS on the next unrelated recalc, which is close to unreportable as a bug.
They now set the approval inputs and let recalcShotStatus derive.
unshareFromClient additionally hardcoded READY_FOR_CLIENT even for a
client-approved shot. updateShotStatus keeps its direct write as the deliberate
manual override, now documented as transient for the same reason.

markShotForRevisions replaces the duplicated change-request logic. Both copies
marked all non-DONE tasks CHANGES, which matches nothing on the shot this is
most often called for: one out for client review precisely because all its tasks
are finished. The request then derived to INTERNAL_REVIEW and had no visible
effect anywhere. When nothing is picked up it now reopens the task behind the
shot's latest version, which is the deliverable the reviewer was looking at.

A client's shot-level change request also marks the versions and writes Approval
rows now. The dashboard counters, the version badges and the portal's own
filters all read Version.approvalStatus, so a shot-level verdict was invisible
everywhere those look.

clearStaleShotApproval covers the reverse case: uploading a new version onto an
approved shot moved the task to INTERNAL_REVIEW and changed nothing visible,
because the derivation short-circuits on shotApprovalStatus. The shot kept
reading COMPLETE while carrying work nobody had looked at. This is the one
user-visible behaviour change here, so watch the boards on a live show: an
approved shot now drops back to INTERNAL_REVIEW on a new upload. sharedWithClient
is deliberately left alone, since revoking client access stays a separate act.

submitApproval and unapproveVersion are transactional now. The verdict, the
version, the task and the derived shot status are one decision, and a failure
partway through left the three layers disagreeing with no way to tell which one
was right.

KanbanCard compared approvalStatus against "CHANGES_REQUESTED", which is not a
member of the enum, so the changes-requested icon never rendered at all.

Thirteen tests cover carryApproval, markShotForRevisions including the all-DONE
regression, and clearStaleShotApproval. What is not covered is the pipeline
transaction itself, which needs a database: queue a render against a shot in
REVISIONS and confirm the task and shot both move before trusting this on a
show.

Version.reviewStatus is still written nowhere while being published through
ext/shots/lookup and EXT_API_REFERENCE.md as a permanent "PENDING". Left alone
deliberately: removing a field from that contract could break the connector.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 18:17:04 +02:00
twotalesanimation 6678721fb1 fix(pipeline): create the comp task an export needs when a shot has none
Deploy / deploy (push) Successful in 6m48s
A shot with no COMP task rendered and previewed fine and then showed
nothing in the app. createExport set Export.taskId from the shot's first
COMP task or null, and finalizePreview registered the review Version with
that same null taskId — so the MP4 was uploaded and stored, but the
Version hung off no task and none of the review flows had anywhere to
draw it. From the artist's side the export completed and produced
nothing.

E1 now resolves the shot's comp task inside the version-allocation
transaction and creates one when there isn't any, titled after the
preview file the export will produce: {shotCode}_cmp_TT_{version}, the
same convention the manual batch upload's create-task path already uses.

renderOutputBase() is exported and buildPreviewManifest takes its `base`
from it, so the task title and the MOV/MP4 name come from one function
rather than two copies of the same template. The title therefore already
carries the version, which makes finalizePreview's rename-to-the-
delivered-version a no-op on that first export rather than an immediate
rename of a task created moments earlier.

The lookup and the create sit inside the transaction so the P2002
version-collision retry rolls a losing attempt's task back with
everything else, instead of leaving a duplicate behind.

recalcShotStatus runs only when a task was actually created: a shot with
no tasks derives WAITING and now has one. Approval-driven statuses still
take precedence, so an already-approved shot stays COMPLETE.

Both the queue-time ExportEvent note and the E1 response record what
happened, so a task appearing under a shot on its own is traceable to the
export that made it.

Exports already in flight with a null taskId keep their orphaned
versions; backfilling those is a separate migration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 14:09:01 +02:00
twotalesanimation ed9f9b13c1 feat(client-portal): show shots and task titles in the client's own naming
Deploy / deploy (push) Successful in 6m8s
Shot.clientCode already drove slates, overlay burn-ins and delivered file
names, but the client portal — the one place the client actually reads — was
still showing our internal codes.

Task titles were the larger leak. They follow the house convention
"UNG_106_010_020_cmp_TT_v004", so the internal code sat embedded in the
string the client reads and copies, in three places on the shot list and
again in the player header. Swapping only the heading would have left it on
display directly underneath.

lib/shot-code.ts resolves both: clientShotCode picks the alias when set, and
clientTaskTitle swaps the code out of a title by substring, leaving a
hand-edited title that does not carry the code untouched rather than guessing
at it.

Applied at the API boundary rather than at each render site, so the shot
list, search, playlist labels, alt text, player header and every
copy-to-clipboard button inherit it and nothing downstream has to remember
which code it is holding. The internal code is no longer sent to the browser
on these routes at all.

Slack notifications deliberately keep the internal code: those go to our
artists, who need the code that matches the comp and the SAN path.

The two existing "clientCode || shotCode" expressions in preview.ts and
deliveries.ts now use the same helper so slates, deliveries and the portal
cannot drift apart. Shots with no clientCode are unchanged throughout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 23:16:53 +02:00
twotalesanimation 1fad6a739d fix(worker): drain ffmpeg's pipes so thumbnail generation stops deadlocking
Deploy / deploy (push) Successful in 9m16s
ffmpeg was started with stdout and stderr redirected and neither pipe ever
read. Windows gives an anonymous pipe a ~4 KB buffer, which ffmpeg's banner
and stream summary exceed on their own, so it blocked forever on the write,
never exited, and the two-minute cap fired:

    TaskCanceledException at Process.WaitForExitAsync
    Preview complete for job ... in 221s (mov + mp4)

The exception was swallowed as "non-fatal", so every preview silently
produced no thumbnail while paying 120s for the privilege. Nothing
downstream was wrong — there was simply never a thumbnail to promote.

Both pipes are now drained concurrently before the wait. A timeout kills the
process tree rather than leaking an ffmpeg per job, and attempts report a
three-way outcome so a hung or unstartable ffmpeg is not retried — without
that, the slate fallback would have doubled the wasted time to four minutes.

Failures are no longer silent: an unset or missing ffmpegPath, the seek that
was chosen, and ffmpeg's own exit code, command and stderr are all logged.

finalizePreview likewise records every promotion outcome on the export
timeline, not just success. Silence there was indistinguishable from a broken
worker, a project whose flag never saved, and the feature working as
configured — which is what made this take so long to find.

Adds scripts/diagnose-thumbnails.mjs, which walks the chain (ffmpeg →
Export.thumbnailKey → project flag → Shot.generatedThumbnailUrl → what the UI
resolves) and names the first broken link.

Worker 1.5.1 -> 1.5.2; republish required for the fix to take effect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 22:48:29 +02:00
twotalesanimation caaa333f02 feat(pipeline): render-derived shot thumbnails, and bulk shot delete
Deploy / deploy (push) Failing after 19m11s
Two independent features landing together:

Generated shot thumbnails — a shot can now carry a thumbnail promoted
from a render export (Shot.generatedThumbnailUrl/generatedThumbnailFrom)
alongside the existing brief-supplied thumbnailUrl, which always takes
display precedence (lib/shot-thumbnail.ts) so a promoted frame can only
ever fill a gap, never overwrite the brief. Shows with
pipelineConfig.autoShotThumbnail promote automatically on every
finished export (lib/render-pipeline/preview.ts); promoteExportThumbnail/
clearGeneratedThumbnail (actions/shots.ts) are the manual equivalent for
everyone else. Worker-side seek logic for picking the promoted frame is
covered by RenderWorker/VFXReviewWorker.Tests/ThumbnailSeekTests.cs.

Bulk shot delete — the shot-status page's selection bar gets a "Bulk
Actions" dropdown (alongside the existing Change Due Date) with Delete
Shots. bulkDeleteShots() (actions/shots.ts) deletes one at a time rather
than in a single statement: exports_shotId_fkey is ON DELETE RESTRICT
by design, so a shot with renders or deliveries on record is protected
and gets skipped and reported by name rather than aborting the whole
selection or being silently destroyed along with its history.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 17:06:55 +02:00
twotalesanimation b151e8cc52 feat(pipeline): per-shot client naming alias for slates, overlays, and delivery
Deploy / deploy (push) Successful in 7m25s
Clients sometimes ship footage under a naming convention that doesn't
match our own shot codes. Adds an optional Shot.clientCode, editable on
the Slate tab: when set, it replaces shotCode only in what the client
actually sees — slate identity, overlay burn-in, and delivered EXR/MOV
file and folder names in Netflix packages — with our own version string
flatly appended (no "_cmp_TT_" studio jargon mixed in). Internal render
paths, EXR filenames on the SAN, comp names, and DB lookups are
untouched. Shots without a client code render identically to before.

Delivery packages denormalize clientCode at build time, same as the
existing shotCode/versionString snapshot, so a later shot edit can't
retroactively change already-built delivery history. The worker now
renames delivered EXR frames and the delivery MOV using the existing
frame-number parser (SequencePattern) so only the base name changes —
frame numbers and padding survive exactly. This is the one piece of the
change that could plausibly ship the wrong bytes, so it's covered by
new unit tests (renaming, padding, MOV rename, checksum-survives-rename,
and a graceful fallback for an unparseable frame number) rather than
resting on manual verification alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 00:38:12 +02:00
twotalesanimation a18dfc6203 revert(pipeline): back out picture-lock trimming from the preview build
Deploy / deploy (push) Successful in 6m19s
Production fallout after enabling it: the trimmed EXR footage started
after the head handle as intended, but the overlay's burn-in timecode
(hardcoded to start at frame 1001) did not shift with it, so the
delivered burn-in no longer matched the footage. The picture-lock
reference render also never worked correctly in practice. Reverts the
AE panel checkbox, the preview-build script's trim/reference logic, and
the server/worker plumbing around it back to their pre-feature state
(82dfaee) — untrimmed previews, no picture-lock reference, overlay
timecode correct again.

The noPictureLock/piclockRefKey schema columns and their migration are
deliberately left in place: they're already applied in production, and
dropping an applied migration's file risks the next `prisma migrate
deploy`. They sit unused and harmless until this is revisited.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 21:44:45 +02:00
twotalesanimation 67d8639861 fix(pipeline): trim picture-lock handles at the footage comp, not the template's SHOT layer
Deploy / deploy (push) Successful in 6m17s
Manipulating startTime/outPoint on previewComp's SHOT layer touched a
comp duplicated from the artist-authored template, whose slate/shot
timing relationship the build script doesn't own -- and it dropped the
slate frame in production. The trim now happens one level down, on the
private single-layer footage precomp this script fully controls;
previewComp's own sizing logic is untouched from before this feature
existed, since it already generalizes correctly to any shotComp
duration.

Also threads the picture-lock skip reason through to the export's
timeline, so a shot missing sequence timecodes or a matching picture
lock file is visible without digging into worker-machine logs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 14:22:49 +02:00
twotalesanimation 86962eb4bd feat(pipeline): automate picture-lock trimming in the preview build
Deploy / deploy (push) Successful in 10m37s
Previews used to ship the full padded (handle-included) render to
clients, with an artist manually pulling picture lock and hand-trimming
the comp to match. The automated preview stage now trims the client
comp to the picture-lock edit itself (a fixed handle count off each
end, per the studio's existing convention) and separately builds an
ungraded, full-duration picture-lock reference MP4 as a diagnostic
sidecar to check back against. A new "No Picture Lock" panel checkbox
is the single override for shots where picture lock is missing or
wrong, falling back to today's untrimmed behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 12:59:05 +02:00
twotalesanimation 53fc8dc65b fix(pipeline): re-exports keep a shot's approval, and carry no client-facing text
Deploy / deploy (push) Successful in 6m1s
A shot is routinely re-exported for delivery after it has already been
client-approved — most often an unscaled/un-punched-in pass. Every
pipeline-created Version defaulted to approvalStatus PENDING_REVIEW,
which is exactly what the dashboard counts, the shot page badge and the
playlist read as "awaiting review" — so a re-export made an approved
shot look like new unreviewed work to the whole team, with nothing
actually different about its approval.

approvalStatus is now inherited from the task's previous latest version
rather than left at the create default. A task with no prior version
(its first export) still gets PENDING_REVIEW, which is correct.

Separately, every pipeline version was stamped with notes: "Pipeline
render vXXX (internal only)" — and notes is the artist's message to
reviewers, rendered verbatim in the client portal and the review player.
Nothing enforces that a pipeline version stays unshared once created (see
the gap noted in RenderPipeline2.md), so that internal-sounding string
could reach a client if the shot were re-shared after a re-export. notes
is left unset; what a version is and where it came from belongs on the
Export and its timeline, which is where a human would actually go
looking.

RenderPipeline2.md §9 documents both, plus the share-flow gap that let
notes reach a client in the first place: shareWithClient promotes
whichever Version is isLatest with no awareness that a pipeline version
is meant to stay internal permanently. Not fixed here — closing it needs
a way to identify a pipeline-origin version at the share call sites, and
Export.versionId is currently a one-way scalar with no inverse relation
on Version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 15:52:49 +02:00
twotalesanimation bada122f42 feat(pipeline): per-project AE templates, and a workspace that fits any show
Deploy / deploy (push) Successful in 7m4s
Initialise Workspace built UNG_101..UNG_117 from a hardcoded range and
imported one hardcoded AEP, and fourteen findComp calls named UNG's comps
literally. None of it worked for a second show.

Comp names now follow a convention derived from the show id —
{showId}_VFX_OVERLAY, {showId}_EXPORT_TEMPLATE, _SHOW LUT — which
reproduces UNG's existing names exactly, so nothing changes for the show
it was written for. A project whose template departs from the pattern can
override any of the three.

Paths cannot be derived: UNG's show id is UNG while its folder is
UNGO_VFX. Those live on Project.pipelineConfig and are edited under
Project → Settings → Pipeline. They are resolved server-side and handed
to the panel with the project list, and read by buildPreviewManifest for
the headless build, so the panel and the farm cannot disagree about which
template — or which show LUT — a shot was built with. That mattered more
than the folder names: a preview rendered through another show's LUT is
quietly the wrong colour rather than obviously broken.

Episode folders come from the project's episode list, falling back to the
folders under its footage root for a show whose episodes are not entered
yet.

Panel 2.5.0. Adds migration 20260814010000_project_pipeline_config.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 08:24:40 +02:00
twotalesanimation 41ee826700 fix(pipeline): keep an urgent export urgent through a validation override
Deploy / deploy (push) Successful in 6m11s
Overriding a failed validation queued the preview at priority 50
regardless. It took its rank from the export's last PREVIEW_ONLY job —
but validation runs before the preview stage, so a VALIDATION_FAILED
export has never had one, and the missing row fell through to a
hardcoded normal priority. An urgent shot lost its urgency at exactly
the moment someone was standing over it waiting.

Every preview now derives its rank from the export's AE_RENDER priority
rather than from a previous preview. That fixes the missing-row case and
also removes a slow drift: a preview's priority is already boosted, so
re-deriving from it moved an export a step up the queue on every retry.

createPreviewJob takes the render priority and the attempt number, so the
first build, a retry, a regenerate and an override all rank identically.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:19:19 +02:00
twotalesanimation c9b0f25bb2 fix(pipeline): let a finished render's preview outrank pending renders
Deploy / deploy (push) Successful in 6m11s
Urgent already propagated — a preview inherits its render's priority, so
an urgent export stays ahead all the way to QC. The complaint was the
other half: at equal priority previews always ran last.

The queue orders by priority then age, and a preview job is only created
when its render finishes, so it is always newer than the renders queued
beside it. Ten shots ran as ten renders and then ten previews, and
nothing was reviewable until the end of the batch.

render.previewPriorityBoost (default 1) subtracts from the render's
priority when the preview is created, so a shot finishes end to end
before the next render starts. The boost never promotes a job across the
urgent threshold: a normal preview must not inherit permission to run
outside the render window that its render never had. Set it to 0 for the
old behaviour.

Worth knowing separately: a worker will not claim preview jobs at all
while an artist has AE open on that machine, which during the day is the
larger reason previews appear to wait.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:11:49 +02:00
twotalesanimation 4642ecb11f feat(pipeline): skip the preview build and inherit the previous one
Deploy / deploy (push) Successful in 7m8s
Some exports are deliberately framed differently from their review —
unscaled or un-punched-in EXRs — and a preview rebuilt from those frames
would be the wrong picture. The previous export's MOV is the correct one.

skipPreview renders the EXRs and goes straight to READY_FOR_QC. Available
at Queue Export (a "No preview" checkbox in the panel, E1 field) and as an
option when overriding a validation failure.

The inheritance is as important as the skip: deliveryMovPath, the review
MP4, the thumbnail and the Version are carried over from the newest
earlier export of that shot which has them. Without it the delivery
builder — which takes its MOV from deliveryMovPath — would package EXRs
with no MOV at all. Where there is nothing to inherit, the export still
reaches QC and the timeline says it has no review media.

QC is untouched by any of this: a skipped preview still needs a human
pass before the export can be delivered.

Panel 2.3.0. Adds migration 20260814000000_export_skip_preview.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:28:14 +02:00
twotalesanimation 1163bd6455 feat(preview): burn the production name into the slate and the overlay
Deploy / deploy (push) Successful in 5m57s
The preview manifest now carries the show name, taken from the project's
name rather than its code — a slate is read by a human, so "Ungovernable"
beats "UNG_S1".

Both the worker's headless build and the panel's own preview set it, so a
farm-generated preview and an artist-built one still match. The panel
writes it whenever it touches an overlay, not only when building a
preview, so existing comps pick it up on the next Add Overlay.

The slate's other properties are title case ("Date", "Scene") while the
overlay's are upper ("DATE", "SHOT NAME"), so both spellings of SHOW are
attempted; a template exposing neither reports its actual property names
as a build warning rather than failing silently, matching how the VFX
Scope and Submission Note fields already behave.

Panel 2.2.0, worker 1.2.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 01:42:37 +02:00
twotalesanimation 0ce41123c5 feat(render-pipeline): regenerate previews without re-rendering EXRs
Deploy / deploy (push) Successful in 5m58s
A slate or burn-in mistake is usually caught after a preview has already
succeeded, and there was no way back: retryPreview required PREVIEW_FAILED, and
READY_FOR_QC only led on to QC_FAILED/READY_FOR_DELIVERY/SUPERSEDED. The only
recourse was a whole new export, re-rendering every EXR and burning a version
number to fix a typo.

- transitions: READY_FOR_QC and QC_FAILED can now go back to GENERATING_PREVIEW.
- regeneratePreview() rebuilds the manifest from scratch rather than patching
  anything, so shot code, episode/scene, frame count, slate date and burn-ins
  are all re-derived exactly as on a first-time preview. Validated EXRs are
  reused and the version number is untouched -- this re-delivers the same
  version, it does not make a new one.
- vfxScope and submissionNote are frozen onto the Export at submission time, so
  regenerating refreshes that snapshot from the shot's slate fields (explicit
  override wins, same precedence as createExport). Slate text edited on the web
  app's Slate tab is therefore picked up.
- finalizePreview replaces the media on the export's existing Version instead of
  stacking a second row, so the version number stays put and reviewer notes and
  annotations survive the reslate. First-time previews are unaffected.
- POST /api/ext/exports/{id}/regenerate-preview exposes it to the panel.
- Panel: Regenerate Previews button, gated on the same statuses as the server,
  with a confirm covering what is and is not reused.

Also fixes the slate description source: the Slate tab authors
shot.slateDescription (falling back to shot.description) but the preview
manifest only ever read shot.description, so edits made there never reached the
rendered slate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 11:02:23 +02:00
twotalesanimation 783e82eaf5 feat(render-pipeline): sync task title to preview version, fill slate ShotName
finalizePreview now retitles the export's task so its version suffix matches the
preview that was just uploaded (UNG_102_019_010_cmp_TT_v004 -> _v005), which is
the rename the manual batch upload already does on rename-and-upload. The new
nextTaskTitle helper mirrors nextExrOutputBase so the task title, EXR base and
preview filename all derive the suffix the same way. It uses the export's
versionString rather than versionNumber, since the DB number is bumped to stay
monotonic per task while the filename follows the shot's shotVersion.

The rename only fires when a Version was actually created, runs in the same
transaction, and is recorded in the ExportEvent note; the finalize response now
returns the task id, title and whether it changed. Task status is deliberately
untouched (pipeline renders must not resurface in the review flow, S10.0).

The headless preview build was addressing the slate's identity fields by index
only, so ShotName was never filled on the farm. setSlateIdentity writes both the
named property and the index -- the name-with-index-fallback pattern the panel
already uses for VersionName -- and warns with the template's actual property
names when neither resolves, instead of failing silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 10:30:47 +02:00
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