a3859aa61bccceef169bc79e68493b2036579464
69 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a3859aa61b |
feat(connector): build a QC review session for a whole project
Build QC Review Session (QC section) makes a QC_{shot}_{version} comp
for every shot in the project's QC queue. The export sits over its
earlier versions and plates, and only the export and the main plate are
switched on. Each scene also gets a continuous edit, cut to the edit
with the handles trimmed, and a contact sheet. QC Pass/Fail now act on
the shot comp in the viewer, the selected shot in a scene comp, or the
shot under the playhead in a scene edit, and label the result
green/red. The QC queue now also returns seqTimecodeStart, handleHead
and handleTail.
Compare Versions now lines versions up by EXR timecode, then by file
frame number. It used to use Export.frameStart, which is
timecode-anchored, not 1001-based.
Also includes Keep AEP (panel 2.9.0): Queue Export saves a reduced
.aep copy in each export's version folder.
Panel 2.10.0. Not yet run in After Effects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
842a8fe036 |
feat(board): paste client feedback from WhatsApp
Deploy / deploy (push) Successful in 3m34s
"Paste feedback" on each board card takes a client's message as it arrived
and splits it into rounds ("FEEDBACK 113 DUE Oct 7 12:00 PM") and per-shot
notes, tolerating WhatsApp copy prefixes, bold markers, wrapped lines,
several codes per note and codes without the show prefix.
A review step shows each note's shot, the version the client was sent,
the player frame and a "move task to Changes" tick (on by default).
Saving creates a FeedbackRound, a player comment per note tagged
source=WHATSAPP (shown as "Client feedback") and a board line under the
round's due time; ticking the line off resolves the comment.
Quoted frames are read as comp frames: shot frameStart or the house 1001,
plus the slate (recorded, else 1 for pipeline previews). Version.duration
is never stored on live, so the length-based slate inference can't be used.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
34af26dfbf |
feat(board): daily board linked to shots
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> |
||
|
|
88112112d4 |
feat(shot-status): last sent column per shot
Deploy / deploy (push) Successful in 3m27s
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> |
||
|
|
91e69f0bcb |
feat(worker): parallel multipart uploads for large artifacts
Deploy / deploy (push) Successful in 3m18s
From the studio a single connection to Hetzner nbg1 (188 ms away) is capped near 0.1 MB/s, although a browser speed test shows 50 Mbps. Measured: 1 connection 0.13 MB/s, 8 -> 0.64, 24 -> 1.69 MB/s. At the old speed a ProRes MOV could not finish inside the 30 min request timeout. - Worker 1.7.2 uploads files >= 16 MB as S3 multipart: 8 MB parts on uploadConcurrency connections (config.json, default 16), each part retried up to 4 times; aborts the upload if a part keeps failing. - artifact-presign returns uploadId + per-part presigned URLs when asked for multipart; new E20b artifact-complete stitches or aborts. - Old workers and old servers fall back to the single PUT. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
477aef7b27 |
feat(worker): upload status in the job log, ProRes uploaded in the background
Deploy / deploy (push) Successful in 3m25s
Preview jobs looked stuck after aerender finished: the worker was silently
uploading the review MP4s and the ProRes MOV over a slow uplink, and a
restart mid-upload threw the preview away.
- Worker 1.7.1 replaces the job's log tail with live upload status and logs
each upload's size, duration and speed.
- The ProRes MOV now uploads after finalize via a persistent queue
(HighResUploader, resumes after restart, 5 attempts) and attaches through
the new E13b endpoint POST /api/ext/render/jobs/{id}/attach-highres.
A MOV from a superseded preview is discarded.
Deploy the server before the 1.7.1 worker.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
f30acef101 |
feat(deliveries): delivery requests and upload/share evidence
Deploy / deploy (push) Successful in 5m17s
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> |
||
|
|
b596c97faf |
feat(playlist): seamless playback of selected shots, cut only
Deploy / deploy (push) Successful in 3m12s
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> |
||
|
|
5e29c3b88d |
feat(pipeline): search the render queue by shot or client code
Deploy / deploy (push) Successful in 3m16s
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> |
||
|
|
c2509512ff |
feat(delivery): record shots delivered outside the render pipeline
Deploy / deploy (push) Successful in 3m34s
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> |
||
|
|
27f9f7ac64 |
fix(api): shot API 500 once a version has a ProRes size
Deploy / deploy (push) Successful in 3m16s
Version.highResSize (added in
|
||
|
|
9660cdc21c |
feat(review): High quality playback switch and per-version ProRes download
Deploy / deploy (push) Successful in 3m23s
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> |
||
|
|
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> |
||
|
|
532ebd82d7 |
feat(pipeline): tabbed render queue with paged exports
Deploy / deploy (push) Successful in 3m32s
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> |
||
|
|
2dee1bf6fc |
feat(batch-upload): bulk footage plate upload matched by filename
Deploy / deploy (push) Successful in 6m39s
Adds a third section to the Batch Upload page that attaches plate movies to shots derived from the filename: DH2_VFX_058_120_REF.mov becomes the plate "REF" on shot DH2_VFX_058_120. Shot matching reuses findMatchingShotCode, so it behaves like the rest of the page (case-insensitive, longest code wins, underscore-boundary only) and the filename remainder becomes the label. Bytes go straight to Hetzner via the existing presign endpoint since plates are multi-GB, with XHR for per-file progress. Files whose shot already has a plate of the same name are skipped so re-running a batch does not stack duplicates. Also resets the thumbnail and plate previews when the project changes — they are resolved against a specific project, but only the version-upload flow was being cleared. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
469d782b4f |
feat(projects): poster art and equal-height project cards
Deploy / deploy (push) Successful in 6m27s
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> |
||
|
|
6afbe82a29 |
feat(dashboard): QC queue, deliveries and whose-court panels
Deploy / deploy (push) Successful in 6m51s
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> |
||
|
|
6bfc013e35 |
feat(dashboard): episode delivery countdown
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> |
||
|
|
f6a7f0ef5a |
feat(dashboard): show what is blocked and how the farm is doing
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>
|
||
|
|
a073f92da3 |
fix(delivery): number same-day packages per episode, not per project
Deploy / deploy (push) Successful in 7m31s
Numbering was scoped to the project, so the day's first delivery of
episode 102 was named _02 purely because episode 101 had already gone out
that morning. The count is meant to distinguish repeat deliveries of the
same content, and packageDir already separates episodes — three episodes
delivering today each land in their own folder and each are that folder's
first delivery. Adding `episode` to the lookup makes the suffix mean what
the folder layout already implies: only a second delivery of the *same*
episode becomes _02. The [projectId, episode] index already exists, so
the extra filter costs nothing.
packageNameTemplate also moves from {yymmdd} to {yyyymmdd}. The AE panel
has always built its local delivery folders with a four-digit year, so
this is the server matching the connector rather than a new convention.
The {yymmdd} token stays in TokenValues: a project carrying its own
template in deliveryConfig is unaffected either way.
Existing packages are untouched — buildDeliveryJobManifest reads rootPath
off the record. On the day of the changeover an episode that already
delivered as 260903_Delivery starts again at the bare 20260903_Delivery,
which is a different name and a different folder, so nothing merges.
Producer reviews keep their per-project numbering: a review can span
several episodes and stores episode as "", so scoping it the same way
would put every such review in one bucket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
75f9258b69 |
feat(player): aspect-ratio blanking overlay in the review player
Deploy / deploy (push) Successful in 5m39s
A shot is reviewed at the framing it was rendered at but usually delivered cropped, so add a dropdown beside the playback-speed control that lays delivery blanking over the video: 2.39 through 9:16, widest to tallest, with "No blanking" as the default. Purely a viewing aid — nothing is written to the review, and the video element is untouched. The geometry is the awkward part. The video is object-contain, so the picture is letterboxed inside the element and bars drawn against the element's edges would be wrong on any panel whose shape does not match the source. letterboxRect works out where the picture is actually painted and the bars are positioned against that, kept current by a ResizeObserver and a loadedmetadata listener so resizes and fullscreen stay correct. Ratios within half a pixel of the source draw nothing rather than a hairline of rounding error. Two modes, since blanking and crop bars are different jobs: solid black hides the crop the way the master will, guide dims it so the excluded area is still readable. The overlay sits under the annotation canvas — deliberately no z-index, since the canvas sets none either and DOM order alone gives marks drawn over a blanked area the right precedence. Verified against real browser layout, not just unit tests: the computed picture rect lands exactly on the image edges across three panel shapes, and the visible area measures the requested ratio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
269a5cbfda |
fix(delivery): separate packages by show, and number same-day deliveries
Deploy / deploy (push) Successful in 6m27s
Two faults in how a package's folder is named, both of which put unrelated
files in one directory on the delivery volume.
The default layout had no {showId}, so every show's episode 101 resolved to
{deliveryRoot}/101/{packageName}. The token existed and was populated — it
just never appeared in the path. With hardlinked packages that means two
shows delivering the same episode number on the same day write into the same
folder, and identically named shots overwrite each other. reviewDir had the
same hole, and is worse there: reviewNameFor scopes its lookup by projectId,
so a second show computes the bare base name and lands on the first show's
folder instead of suffixing past it.
createDeliveryPackage also never checked for an existing package of the same
name. packageNameTemplate is date-only and packageNumber is a per-project
counter that is not in the template, so a second delivery on the same day got
an identical name and rootPath and built on top of the first. MOV reviews
already did this correctly, so nextReviewName moves to deliveries.ts as
nextPackageName and both paths share it; the review-side name is kept as an
alias because it reads better at that call site.
Putting {showId} ahead of {episode} exposes an empty segment on non-episodic
shows, so paths go through tidyPath, which collapses it and leaves the UNC
root alone.
Historic packages are unaffected: buildDeliveryJobManifest reads rootPath and
configSnapshot off the record, so only new packages use the new layout. A
project with its own deliveryConfig.layout in the database still overrides
these defaults.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
28e8c47907 |
feat(client-portal): let a reviewer edit and delete their own notes
Notes left through a review link were write-once. A client who mistyped a timecode or changed their mind had no way to correct it and had to add a second note contradicting the first, which the studio then had to reconcile by hand. PATCH and DELETE on comment/[commentId] cover both. Only the reviewer's own notes are touchable: the studio's comments and those of every other review session are read-only through a link, whatever the holder sends. Ownership is re-checked server-side on every write, so the canEdit flag now returned by the versions route only decides whether to offer the controls and grants nothing. Refusals are 404 rather than 403 throughout, since a link holder has no business learning which comment ids exist outside their own notes. Only the text is editable. The frame a note is pinned to is what the studio works from, so moving it would silently repoint feedback someone may already have actioned; a note about a different frame is a new note. Deleting a drawing note also removes its strokes, mirroring deleteComment in actions/comments.ts, and the page reloads the player afterwards so the frame does not keep showing marks that no longer exist. Delete goes through a confirmation that shows the note and says plainly that it disappears for the studio too. Worth knowing before this reaches a show: everyone holding the same review link is the same author. A ReviewSession with no email resolves to a token-derived guest user, so one link sent to three people gives all three one identity and any of them can now edit or delete the others' notes. That model is not new, which is why their comments already showed as one name, but edit and delete is the first feature where it grants real power rather than muddling attribution. Per-reviewer links with email set are the fix if that is not wanted, and that is a change to how links are issued rather than to this code. The scope is documented on reviewClientEmail so the next reader meets it before it surprises them. getOrCreateClientUser was copy-pasted into the comment and approve routes with the fallback-email expression spelled out in three places. It is one set of helpers in review-auth.ts now, including findReviewUser, a read-only variant so that merely opening a review link does not conjure a User row just to answer "is this theirs?", and ownsComment, the single rule shared by the route that offers the controls and the route that enforces them. Nine tests cover that rule directly: internal users' notes stay read-only, a session that has never posted owns nothing, and an authorless comment — one whose User was deleted, since Comment.authorId is SetNull — cannot be claimed by anyone. The routes themselves are not covered, so exercise edit and delete against a real review link before handing one to a client. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
93137cdcd2 |
fix(connector): anchor built comps on the plate's own EXR timecode
Build Shot produced comps starting at 00:00:00:00 because the panel never actually read the EXR timecode. getFootageStartFrames ranked a fragile "New Comp from Selection" probe first: it toggles the Project panel via the Window menu to get focus, and where that does not land the menu item is greyed out, executeCommand silently does nothing, and the probe returns null. A shot with no timecodeStart then fell through the API rank to zero. The worker already reads EXR headers properly in ExrHeader.cs, and its validation ranks the plate's own timeCode above the API value, which stores the editorial in-point and sits a handle-length ahead of the plate by design. Shots were therefore being built from rank 3 and validated against rank 1. That is a timecode desync waiting to reach a delivery, not just a cosmetic start frame. readExrTimecode ports the worker's reader to ExtendScript and becomes rank 1: one short header read, walking `name\0 type\0 int32 size data` for timeCode and falling back to timeCodeString where a writer omitted the packed attribute. decodePackedTimecode mirrors Timecode.FromPacked, decoding the SMPTE 12M BCD a byte at a time so the arithmetic never touches the sign bit. The menu probe is kept as rank 2 and now runs only when the plate carries no timecode at all, so the Project-panel toggle is off the normal path. setCompStartFrame was the second failure. Assigning displayStartFrame on a version of AE that does not have the property is ignored silently rather than throwing, so the early return skipped the displayStartTime fallback entirely. It now reads the value back, falls through when it did not take, and returns whether anything stuck; buildShotFromCode logs when nothing does. A start frame that cannot be set is now visible in the diagnostics report instead of a comp quietly sitting at zero. A plate whose timecode is present but zero still falls through to the API rank, keeping the existing frames > 0 convention so the safety net survives. If a genuine 00:00:00:00 plate should win outright that is a deliberate follow-up. The parser is covered against synthetic headers, including the 0x18585712 example documented in ExrHeader.cs, drop-frame, truncated and non-EXR input. What is not yet exercised in a live After Effects session is mainSource.file resolving to the sequence's first frame and displayStartFrame taking on a given AE build, so build one shot and check its start against the plate before relying on this. Picture-lock automation was reverted for a timecode desync of the same shape. PANEL_VERSION goes to 2.6.0 per the documented rule, so Pipeline → Machines does not show two workstations on one version behaving differently. The startSource comment in the manifest schema enumerated only the two older values; the queue-time provenance check keys on /\bAPI\b/ and so already reads "EXR timecode …" as trustworthy rather than suspect. The same silent-failure setCompStartFrame still exists in the farm-side vfxr_build_preview.jsx, which is fed the right value from the manifest but can drop it the same way. Left for its own change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
7f066c1372 |
feat(pipeline): MOV review delivery packages from selected shots
Deploy / deploy (push) Successful in 7m28s
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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
(
|
||
|
|
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> |
||
|
|
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> |
||
|
|
82dfaee56f |
fix(delivery): stop the 422 retry loop on failed delivery items
Deploy / deploy (push) Successful in 6m37s
A FAILED delivery item never sets fileCount/totalBytes, and the worker's Dictionary-based JSON payload serializes those as explicit nulls (the WhenWritingNull option only suppresses nulls on POCO properties, not dictionary values). The server's schema allowed the fields to be omitted but not null, so the report was permanently rejected with 422 - and the worker only ever treated 409/404 as terminal, so it retried the same malformed payload forever across restarts. |
||
|
|
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> |
||
|
|
70cf45ead4 |
feat(pipeline): standalone Blender renders on the same queue
Deploy / deploy (push) Successful in 7m2s
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
e66e4868ea |
feat(validation): manual override for a failed run
Deploy / deploy (push) Successful in 6m13s
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> |
||
|
|
af810258e6 |
perf(delivery): stop re-reading hard links to verify them
Deploy / deploy (push) Successful in 6m25s
A 25-shot episode took about an hour to package when the linking itself is instant. All of it was verification: every delivered file was read back and hashed, and at ~100 MB/s over SMB one 2.8 GB shot costs 30 s. Re-hashing a hard link cannot disagree with validation. The entry is a second name for the same data, so it re-reads the exact bytes that were already hashed — half an hour per episode to prove something that cannot be false. Verification is now proportional to what was actually done: copies are read back in full, links carry over the validation-time digest. Linking has one gap hashing covered incidentally: a source changed after it passed QC, which a link would carry faithfully. Source timestamps are now checked against the export's validation time, which costs a stat per file rather than a full read. "verifyMode": "always-hash" restores unconditional read-back for a production that requires it. Worker 1.3.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
37f73a2bd7 |
fix(validation): separate output file numbering from comp frame numbers
Deploy / deploy (push) Successful in 6m13s
A render carries frame numbers in two unrelated systems. The comp is anchored to its plate's timecode, so its timeline runs 1247792-1247896; the output module names files from 1001. Validation compared the file numbers against the comp range and failed every correctly-rendered shot by ~1.2 million frames — first_frame, last_frame and missing_frames all reporting a defect that was not there. The manifest now carries outputFrameStart (the output module's starting number, house convention 1001) alongside the comp range. Count and contiguity are measured on the output numbering; first/last compare against outputFrameStart and are SKIPPED on older manifests, where the count and gap checks still hold on their own. The same conflation broke the API timecode fallback: it anchored on the comp's frameStart while the render's first frame is 1001, producing a +1246791 frame "offset". Shot.timecodeStart is the edit in-point and the plate carries handleFrames of head handle, so the reference is the API value less handles, anchored on the render's own first frame. Verified on the shot that reported it: UNG_107_051_010 v004 now passes count, contiguity and a plate-anchored timecode match. Regression tests cover both coordinate systems, including that a genuine +8 handle offset still fails. Panel 2.1.2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a4f8226f3f |
feat(pipeline): report deployed tool versions from the tools themselves
"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> |
||
|
|
feb1c7b3e9 |
feat(render-pipeline): validation, QC and delivery builder
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
7dcb9303e5 |
updated export paths
Deploy / deploy (push) Successful in 6m32s
|