A /board page that replaces the shared doc used as the day-to-day tracker.
One card per active/on-hold project with a status line, dated milestones
and to-do lines typed or pasted in the doc's own format:
DH2_VFX_027_090 finalise (CHRIS)
DH2_VFX_005_010 gfx update @michelle wait: client GFX ADH_GFX_005_020
Shot codes link to the shot (live status + latest version), (NAME)/@name
assigns, wait: marks a blocker, headings group lines. Codes that match no
shot are flagged, with a relink when there is an obvious near miss.
Board lines are their own model, not Tasks: tasks drive shot status and
notifications, and a note like "finalise" must not touch either.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recipient moves to the tooltip, the version and date no longer wrap, and the
column is narrower so Notes keeps its room.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shows what last reached the client with evidence (shared EXR package or
manual delivery), anything newer still on its way, and the last review MOV,
so it's clear on Shot Status what has actually been sent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A built package used to become DELIVERED on one click with nothing behind
it, and nothing recorded what a client had asked for.
- Marking delivered is now two recorded steps: Record upload (the link on
the client's portal, contents confirmed) then Share link (who it was sent
to). The second is refused until the first is on record.
- Delivery requests log a client's ask by episode, pasted shot codes or a
Shot Status selection. A line closes only when its shot goes out with
evidence after the request was logged; a request can otherwise only be
cancelled with a reason.
- Delivered Outside Pipeline takes an upload link and recipient, required
for shots on an open request.
- The dashboard gains an Awaiting delivery section listing open requests
and built packages the client does not have yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Check shots on the Playlist page and "Play selection" plays them back to
back without slate or handles. The cut comes from the version's handle and
slate metadata (new cutSpan in lib/handles); the media is never changed.
SequencePlayer double-buffers two videos: the next clip is parked a few
frames before its cut-in and pre-rolled hidden, the outgoing clip pauses on
its last cut frame, and the cut fires on the incoming clip's cut-in frame
(requestVideoFrameCallback), with the start time calibrated at each cut.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A search box above the exports table matches any part of the shot code or
client code, case-insensitively, so "UNG_114" lists a whole episode. It is
applied server-side (the table is paged there), debounced, and scopes the
summary counts too; combines with the project and status filters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shots delivered by hand before the pipeline have no Export, so they could
never appear in a DeliveryPackage and the dashboard countdown showed them
as not delivered. Add Shot.manualDeliveredAt/ById/Note, a "Delivered
Outside Pipeline" bulk action on Shot Status (mark with the real date, or
clear), a badge on marked shots, and count them in the countdown alongside
DELIVERED EXR packages. The real date keeps the weekly rate honest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Worker 1.7.0 (preview.highQuality, default on) now, after the AE render:
- encodes a High quality MP4 from the delivery MOV (full-res H.264 High,
CRF 14 capped at 100 Mbps) -> Version.hqFileUrl
- uploads the MOV itself -> Version.highResKey/Filename/Size
Both best-effort (the preview never fails over them), with a lease
keep-alive during the long encode/uploads; the export timeline records
what was attached.
Player: HQ switch when a High quality source exists; keeps frame, play
state, speed and zoom, remembered per browser, falls back to Standard if
the file won't load. ProRes itself stays download-only (browser support).
Downloads: the client portal's High Res button serves the version's
ProRes (client-code filename), falling back to the shot's manual file;
staff get a ProRes button on the review page.
Housekeeping (lib/render-pipeline/high-res.ts): HQ MP4 + ProRes are kept
while a version is latest, client-shared, approved or younger than
preview.highResRetentionDays (14); otherwise deleted from storage. Runs
per task after finalize and hourly from the reaper; deleting a version or
a reslate removes its files. The Standard MP4 is never touched.
Also: ProRes/Handles top-bar buttons go icon-only below lg so they don't
overlap the version selector.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
The project picker (fixed 288px) and the Group by buttons shared one
unwrapping row, so on a phone the buttons ran off the right edge. Below md
the picker fills the row and Group by wraps onto its own line; from md it
is one row as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
At phone width the sidebar stayed 240px wide, leaving pages a ~135px strip.
Below md it is now hidden behind a menu button in the header and opens as a
drawer (closes on navigation, backdrop, X or Escape); from md up the sidebar
is unchanged. Page wrappers use p-4 on phones and keep p-6/p-8 from md.
Shot detail: on phones the thumbnail takes the full width with the details
below it instead of pushing them off screen, the action buttons wrap, and
the tab bar scrolls sideways. Desktop sizing is kept as it was.
Every change is behind a md: breakpoint, so the desktop layout is the same.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Coming back from a shot left the shot status page at the top with its
sections reset. The scroll restore saved window.scrollY, but dashboard pages
scroll the layout's <main>, so it always saved 0; the Projects page had the
same bug. Only episode expansion was remembered — scene and group sections
came back fully collapsed.
A shared useScrollMemory hook saves the real scroll container's position as
the user scrolls (so any way of leaving keeps it) and restores it once the
content that sets the page height is back. It ignores the scroll the shared
<main> fires when the next page replaces this one, which otherwise
overwrote the saved position.
Shot status now remembers episode, scene and group expansion per project.
The client portal already kept its filters and collapsed episodes but
restored scroll before they were applied; it now uses the hook too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The queue page loaded up to 200 exports in one table with the Blender
renders appended underneath, so a Blender job sat below the whole export
history. Exports and Blender renders are now separate tabs; the Blender tab
label shows how many are active or failed so a render stays visible from
the exports tab.
Exports page 25 at a time. The summary cards now come from a server-side
count per status (listExports.statusCounts) instead of counting the loaded
rows, which was already wrong past 200, and clicking a card filters to it.
The project filter moves to the header since it applies to both tabs, and
the duplicated Deliveries button is gone.
Adds .claude/launch.json for running the dev server from the preview pane.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A grid of twenty projects was twenty near-identical blocks of text. The cards
now lead with a still, with the code, client and title set over it.
There is no poster column on Project and adding one would mean somebody
maintaining it, so the image is chosen from the shots the project already has:
a key shot with a still first, then the most recently touched shot with one.
Within each, thumbnailUrl beats generatedThumbnailUrl - the same precedence
the rest of the app displays shots by, since the reference frame from the
brief is a deliberate choice and a promoted render frame is whatever the
pipeline landed on. The pick is deterministic rather than random: a card whose
image changes between two loads reads as a glitch, and the eye stops using the
image to find the project.
One DISTINCT ON query covers every project on the page, instead of a query
each or dragging back every shot that has a still.
Equal heights come from three things: auto-rows-fr on the grid, h-full on the
card, and fixed room for the description, which was the only part that varied.
A project with no stills gets a placeholder of the same footprint rather than
a hole in the row. The whole card is now the link, so the Open button is gone.
Also fixes a latent bug: the card read project.deadline, but the column is
dueDate, so the due date never rendered. Both are accepted now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Completes the operations dashboard.
QC queue: what is waiting on a person to watch the frames, oldest first.
Every stage before QC is worked by the farm, so this is the only one that
cannot drain itself - depth here is the best early warning that a delivery
date is going to move, and it had no home outside a filter on the queue page.
Deliveries: the last few packages, flagging FAILED and builds stuck in
BUILDING. A failed build is otherwise silent - the exports stay
READY_FOR_DELIVERY, nothing reaches the delivery volume, and nobody finds out
until the client asks where the shots are.
Whose court: versions sent to the client and unanswered, against shots with
unresolved notes. Two numbers rather than one because they call for opposite
responses - a fortnight-old review is a phone call, unactioned notes are a
scheduling problem.
The artist view now scopes to the artist throughout: the blocked list, and
the counters above it, which otherwise quoted a studio-wide figure next to a
personal list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Turns "23 shots to go" into "you are not going to make it". One card per
episode from EpisodeDueDate (or the project's own date on a standard show):
days left, the episode's shots by where they actually are, and the only
comparison that matters - shots per day still needed against shots per day
the studio managed over the last week.
A shot counts as delivered when it appears in a DeliveryItem inside a
DELIVERED EXR_DELIVERY package. Shot.status is deliberately not the measure:
it says where the work is, not whether the client has the frames, and a
MOV_REVIEW package ships nothing.
The pace arithmetic is a pure function so the sentence a producer acts on is
testable: a rate that only just lands on the due date reports as "no slack"
rather than fine, and a week with no deliveries reports as stalled instead of
projecting infinity.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dashboard was a generic task-manager view of a studio that does not work
that way. Two of its five headline numbers counted Task.dueDate, which no
pipeline code writes - the only writer is a manual field on the task detail
page, so those cards read zero by construction. The rest were unscoped and
cumulative ("approved" across every project ever finished), the activity feed
was the notification table, and the shot queue sampled 15 rows by updatedAt,
a column the farm touches constantly. Nothing about failed exports, QC depth
or the render farm appeared at all, so a RENDER_FAILED was only visible if
someone thought to open the queue and filter for it.
Replaced with pipeline state, which the system maintains on its own:
- Blocked queue: the four failed statuses plus exports that stopped
progressing, each with the reason read off the actual cause - the failing
validation check, the QC note, the aerender error, or the machine that
claimed it and went quiet.
- Farm strip: machines, queue depth by job type, oldest unclaimed job, and
worker-version drift, since a worker's version decides which fixes it has.
- Five counters that can go down, scoped to live productions, each linking to
the rows it counted.
The queue page ignored ?status=, so the counters had nowhere to link; it now
takes an initial filter from the URL. Stat cards are hidden from CLIENT users
- the old ones were generic version counts, these are internal pipeline state.
Thresholds live in SystemConfig (dashboard.qcStaleHours, .stalledJobHours) so
they can be tuned without a redeploy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds an "Assign Artist" entry to the bulk actions menu, backed by a
bulkAssignArtist server action. The action re-checks the target user
server-side — a deactivated account or a CLIENT is rejected rather than
trusted from the picker — and passing null clears the assignment, so
reassignment and hand-back are the same flow.
Like single-shot assignment in updateShot this writes the column and
sends no notification, so reshuffling a batch during planning stays quiet.
Also renders the Artist column the page was already querying but never
showing; without it there was no way to see the result of an assignment.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two small board additions.
The shot version now shows next to the shot code. It is Shot.shotVersion —
the value the AE panel burns into overlays and queueExport renders under,
and the one the shot detail page edits — not a count of uploaded review
Versions, so the board agrees with what the artist has in the panel.
Shot.isScaled records that a shot is scaled in the edit: the plate is not
used at its native size, which changes how the shot is comped and
reviewed. That makes it a standing property of the shot rather than
something buried in the notes, so it is flagged the way isKeyShot is — a
cyan pill next to the shot name, and a Scaled toggle column beside Key.
toggleScaledShot() (actions/shots.ts) mirrors toggleKeyShot, same roles.
KeyShotToggle's switch markup would have been duplicated verbatim for the
second flag, so both now sit on a shared FlagToggle taking a colour tone
and a setter. The key-shot switch behaves exactly as before, optimistic
update and rollback included.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A formal review is shared as a flat, dated folder of the preview MOVs —
{deliveryRoot}/PRODUCER_REVIEWS/20260828_Producer_review, then _02, _03
for further reviews the same day. It reuses the delivery package's
machinery end to end: DeliveryPackage/DeliveryItem, the DELIVERY_BUILD
job, the same claim path and the same hardlink-or-copy builder, with
DeliveryPackage.kind telling the two apart.
Three things separate a review from a delivery:
A review never advances an Export. Reviews happen throughout a shot's
life, long before QC passes it, and the same version is routinely shown
twice — so nothing is marked PACKAGED, and markDelivered() refuses a
review package outright (the button is hidden too).
Selection is by shot, not by export. Shot Status → Bulk Actions → Build
MOV Review Package; the server resolves each shot to its newest export
that actually has a deliveryMovPath, which is not always its newest
export — a shot whose latest version is still rendering belongs in the
review as the version that can be watched. Shots with no MOV at all are
listed in the confirm dialog with the reason rather than silently
shrinking the folder.
The folder is exactly the MOVs. No CSV, no manifest.json written beside
them; the manifest still returns to the server, so the record of what was
shared is kept without cluttering the share. Names follow the same
client-naming rule as a delivery, so a shot reads identically in a review
folder and in the delivery that follows it.
Verification (DeliveryStage.BuildReviewItem) is proportional as it is for
EXRs, adapted to a single file with no validation-time digest: a hard
link is the source and needs nothing, a copy is checked against its
source by length always and by full hash under "always-hash". A missing
MOV fails the item here — in an EXR delivery the MOV is a viewing copy
beside the real deliverable, but in a review it is the deliverable, and a
review folder quietly one shot short is worse than one that says so.
Worker bumped to 1.6.0 for the new manifest field (DeliveryManifest.Kind).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
Blender work is not a shot submission: no Export, no version, no
validation, QC or delivery. It is "Ctrl+F12, but on the farm", so it is
modelled as what it is — a queue entry and nothing more.
That turned out to need very little. Claim, progress, fail and
complete-render already tolerated a job with no Export behind them, so a
Blender job reuses the whole lifecycle; RenderJob gains projectId, label
and submittedBy so a job with no shot code still has something to show.
Capability gating is new and was already missing: the claim now filters
job types by the machine's reported tools. Without it a workstation
without Blender would claim Blender work and fail it repeatedly, burning
the attempts while a machine that could have rendered it sat idle. The
worker asks for Blender jobs unconditionally and the server refuses them
— trusting the worker to self-select would have left the same hole.
The runner treats a clean exit with no saved frames as a failure:
Blender exits 0 when a frame range is empty or an output path is
unwritable, and a job that silently rendered nothing is the worst
outcome available.
Standalone jobs auto-requeue on a transient failure like any other work;
delivery builds are deliberately excluded, since a half-written package
wants a human first.
Adds the Blender add-on (BlenderPanel/) and its docs. The add-on refuses
to submit an unsaved file — the farm opens the .blend from disk, so
unsaved changes would render the previous state without warning. Its
payload is pinned by a contract test, since add-on and server only ever
meet over JSON.
Worker 1.5.0. Adds migration 20260815000000_blender_render_jobs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
Checks fail on renders that are genuinely fine — a plate the engine
cannot reach, an expectation that does not apply to this shot — and a
VALIDATION_FAILED export was a dead end, blocking delivery on a shot a
human can see is correct.
Override validation on the Export detail page resumes the pipeline where
a pass would have handed off: preview, then READY_FOR_QC. It never skips
QC and never delivers directly, so the frames are still looked at — the
override waives the automated check, not the human one.
The failed ValidationResult rows are left untouched rather than rewritten,
so the QC queue keeps listing them as warnings and whoever QCs the shot
sees what was waved through. The reason is required and recorded with the
checks it covered on the export's timeline, and the validation panel
keeps an "overridden" badge long after the event scrolls away.
Restricted to admin/producer/supervisor: waving a failed check through is
a supervisor's call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"What is running where" was answerable only by asking someone. Each tool
now reports its own version on startup into a ToolVersion registry, shown
on Pipeline → Machines.
The worker's version moves out of config.json and into the assembly:
a hand-maintained string on each machine is exactly the thing that goes
stale, and a machine reporting a version it is not running is worse than
no version at all. publish.ps1 stamps the git SHA via SourceRevisionId,
so a deployed exe traces back to a commit (1.1.0+feb1c7b).
The panel gains PANEL_VERSION, shown in its title and every diagnostics
report, and pings on load.
Staleness is derived rather than declared: the newest version seen for a
tool is the benchmark, so updating one machine is what flags the rest.
There is no expected-version list to keep in sync — which would have been
one more thing to forget.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Completes RenderPipeline2 phases 3-5. Additive throughout: with
validation.enabled=false the pipeline behaves exactly as before.
Validation (§8) runs on the worker straight after a render, while it
still holds the job and the files are local. Everything is read from the
EXR header — one 64 KB read per sampled frame, no pixel decode — so the
engine needs no oiiotool. The render job deliberately stays open through
the stage, so a worker that dies there is reaped and retried instead of
stranding the export in VALIDATING.
Timecode is the primary check and is anchored on the plate, never the
API: measured on the live show, Shot.timecodeStart is the editorial
in-point and sits one handle-length (8f) ahead of the plate, so checking
against it would pass the defective exports and fail the correct ones.
Reference precedence is manifest timing plate -> plate found on disk ->
API value, and the API rank is WARN-only. The panel now resolves plate
provenance by walking the comp's own footage layers, so the worker never
guesses which element carries the comp's timing.
Verified against real studio renders via the new --validate mode: it
catches both defect classes seen in shipped work — the +8 handle offset
and frame-derived timecode (00:00:41:17 = frame 1001 at 24fps).
QC (§10) is one service behind two front doors, API-key for the panel
and session for the web. PASS means ready to upload and nothing else;
FAIL sets the comp task to CHANGES. The panel gains a QC section that
imports the real EXR sequence at full res rather than the H.264.
Delivery (§11) rides the existing queue as a DELIVERY_BUILD job: hard
links with a copy fallback, verification by re-hashing delivered bytes
against the validation-time digest, manifest.json and the CSV report.
fsutil hardlink list is unavailable over SMB, so link identity is never
queried — reading the bytes back covers links and copies alike.
Also fixes ApiClient.ProgressAsync throwing on transport failure, which
would have aborted work in flight during a brief server outage; progress
reporting is best-effort by contract.
Removes the stale RenderWorker/ rule from the root .gitignore. The
worker's source is tracked like the rest of the repo and its own
.gitignore already covers bin/, obj/ and publish/; the rule silently
excluded new files while the existing 20 stayed tracked.
Not yet applied: migration 20260813000000 was hand-written because the
dev database was unavailable — run prisma migrate deploy and verify
before relying on it. None of this has run against a live server.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>