"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>
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>
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>
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>
Points were normalised to the whole player box, but the video is drawn
object-contain inside it, so any change in the player's shape (window or
panel resize, fullscreen, phone rotation, the annotation toolbar opening)
moved the picture under the marks.
The canvas now sits exactly over the displayed picture (same letterbox
measurement as AspectOverlay), so points are fractions of the image and
new saves are tagged version "2.0". Legacy "1.0" annotations are mapped
onto the picture at load time from their stored canvasWidth/canvasHeight
and the video's intrinsic size; no data migration. The save action now
accepts both versions.
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>
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>
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>
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>
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>