- translate_and_synthesize.py: fetch job_doc from DB right before the combined
MP3 upload so gcs_path() has the gcs_prefix needed for newer jobs; removes the
duplicate fetch that existed later in the same function
- migration_2026-04-30-000001: add 'cancelled' to MongoDB $jsonSchema validator
enum so cancel_job writes no longer fail Document validation
- Dashboard.tsx: include all active processing statuses in the Processing counter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds POST /jobs/{id}/cancel endpoint that revokes the Celery task and
sets status to 'cancelled'. Shows a confirmation widget in the job
detail sidebar for admin/production roles when the job is in an active
processing state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Click the pencil icon next to the job title to rename it inline.
Enter saves, Escape or blur cancels. Available for admin/production/PM.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- docker-compose.yml: add USE_CELERY_FALLBACK env var to api and worker
services so cloud_run_dispatch uses Celery on optical-dev
- JobDetail.tsx: show actual error message instead of generic
"Processing failed at ." when failure step is unknown; also show
job.error string when no structured failure object exists
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a reviewer saves the source language VTT during QC and confirms
the re-translate dialog, all target languages are re-translated via
Celery. Job transitions to `translating` and returns to `pending_qc`
when done. Existing polling in useJob covers progress display.
- schemas/job.py: add `retranslate_languages: bool` to VttUpdateRequest
- audit_log.py: add VTT_RETRANSLATE audit action
- translate_and_synthesize_task: accept languages/retranslate params,
filter to specified languages, skip video render, return to PENDING_QC
- routes_jobs.py: add _trigger_retranslation helper, call after VTT save
- types/api.ts: add retranslate_languages to VttUpdateRequest
- useJob.ts: invalidate all lang VTTs on retranslate
- QCDetail.tsx: confirmation dialog when saving source VTT with targets
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Guard useJobDownloads with !!jobStatus so it never fires when job is
still loading (status undefined on first render)
- Expand EARLY_STATUSES to cover translating/tts_generating/rendering_*
which also have no outputs yet
- Remove Downloads.tsx hack that locked downloads to completed-only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
T-2: Extract getJobStatusColor() into utils/jobStatusMessages.ts; StatusBadge now uses the
shared helper (single source of truth for badge colors).
PR-7: GET /admin/production/queue-stats — returns Celery queue depths via Redis LLEN.
Production dashboard shows a live panel (10s refresh) with per-queue task counts.
PR-8: POST /admin/production/jobs/{id}/upload-final-vtt — Production/Admin can upload a
hand-crafted VTT to bypass AI, writing to GCS and advancing the job to PENDING_QC.
Upload modal added to FailuresList with language + type (captions/ad) selectors.
docker-compose.optical-dev.yml: enable USE_CELERY_FALLBACK=true, set worker replicas=1
for all pipeline workers (ffmpeg/tts/whisper) with WORKER_CONCURRENCY=2 so the full
pipeline runs on the 2-CPU optical-dev server until Cloud Run VPC Connector is ready.
Fix: remove unused effectiveMs variable in TimelinePreview (TS6133).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
R-8 — Linguist language competence:
- Add User.languages[] BCP-47 field to backend model + UserResponse schema
- Frontend: show amber warning in assign modal when selected linguist has no
competence listed for the target language
PM VTT editing (FinalDetail):
- PM and ADMIN can now edit captions/AD in the final review stage
- VttEditor becomes read-write with onCueSave wired to updateVttMutation
- Other roles remain read-only
Timeline right-click + add pause:
- Right-click anywhere on the timeline opens a context menu showing the timestamp
- If near a pause point marker: "Edit timing" + "Regenerate TTS" options
- If on empty space: "Add AD cue at Xs" → inserts a new AD cue in the editor
- Pause point markers widened from 1px → 2px (3px on hover) for easier clicking
- Right-click on a pause point marker directly opens the editor
VttEditor insertAtTimeMs prop:
- New prop triggers programmatic insert at a specific video timestamp
- Used by the timeline right-click "Add AD cue here" action
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove hover gate on insert/delete action buttons — all 3 buttons now permanently
visible when !readOnly so the insert affordance is clear on touch and small screens
- Add GapInsertRow: a clickable dashed bar shown before the first cue (when gap > 0.5s)
and between any two cues with a gap > 0.5s — directly addresses the case where music
or silence precedes the first caption (e.g. 0:00–24.5s gap in the Command Strip video)
- Fix: insertCue now calls saveCue immediately so the placeholder cue persists even if
the user navigates away before typing text
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GET /admin/production/failures: list failed jobs filtered by step/org
- POST /admin/production/bulk-retry: dispatch retry for up to 50 jobs
with "auto" (from failure.step) or "from_scratch" strategies
- FailuresList.tsx: accordion-grouped by error type, multi-select,
bulk retry action, step label, retry count (red >3), updated date
- Sidebar: "Failures" item with live badge for production/admin roles
(polls useJobs with processing_failed,tts_failed,render_failed)
- New useFailures / useBulkRetry hooks
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
W-4: team assignment (linguist/reviewer) stored on job at creation,
auto-assigned to all language QC states on first GET /language-qc
(lazy init via auto_assign_defaults)
L-3 WS: broadcast_to_job when reviewer opens VTT for editing;
QCDetail shows "User X is editing [lang]" banner (auto-clears 5s)
R-5: comment broadcast via broadcast_to_job on add_comment();
QCDetail invalidates comments query on language_qc_comment WS event
L-15: QCDetail subscribes to language_qc_assigned WS event →
refetches lang-qc data and shows toast
R-7: VttEditor gets onCuePlay prop; AD editor in QCDetail wires
handleAdCuePlay → switches to accessible video mode, seeks & plays
T-15: beforeunload warning in NewJob while upload is in progress
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
T-6: Add Blob URL native <track> in VideoWithCaptions so browser CC button works in fullscreen.
T-7: Sync hidden <audio> AD playback with video play/pause/seeked events.
T-11: Double Submit Cookie CSRF — _set_auth_cookies issues httponly refresh_token + readable csrf_token; /refresh validates X-CSRF-Token header; frontend reads csrf_token cookie and sends header on all refresh calls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
VttDiffView component (frontend/src/components/VttEditor/VttDiffView.tsx):
- Lazy-loads VTT version list (newest-first) and diffs version 1 (AI baseline)
against the latest version
- Renders unified diff: green lines = added, red lines = removed (unchanged hidden)
- Collapsed by default; expand with "↔ Diff vs AI baseline" button
- Shows +N/-N change summary in header
QCDetail integration:
- VttDiffView added below both Captions and Audio Description VttEditors
(only appears for the selected language)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- VttContentResponse gets etag field (SHA1 of captions+AD content)
- VttUpdateRequest gets if_match field (optional)
- GET /jobs/{id}/vtt: computes and returns etag
- PATCH /jobs/{id}/vtt: if if_match present, fetches current content, recomputes
hash, returns 409 Conflict if mismatch
Frontend:
- VttContentResponse type + VttUpdateRequest type updated
- QCDetail stores vttEtag from GET response
- All updateVttMutation calls pass if_match: vttEtag
- 409 responses show specific "Conflict: another user has modified" message
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
VttEditor:
- New props: glossaryTerms and language
- Glossary: source_term occurrences underlined (amber) with preferred translation
tooltip on hover; only terms that have a translation for the current language
- CPS badge: ⚡ N CPS shown in amber when characters-per-second > 20
QCDetail:
- Fetches active glossary for job's client (getGlossaries → find one with
current_version_id → getGlossaryTerms up to 500 terms)
- Passes glossaryTerms + language to both Captions and AD VttEditor instances
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- ShareToken model (share_tokens collection)
- POST /jobs/{id}/share — create token (PM/PROD/ADMIN)
- GET /jobs/{id}/share — list active tokens
- DELETE /jobs/{id}/share/{token_id} — revoke token
- GET /public/share/{token} — unauthenticated preview with signed GCS URLs (6h TTL)
Returns video, captions, AD for all languages
Frontend:
- ShareView.tsx — public page at /share/:token with language switcher, video player, download tiles
- App.tsx — /share/:token route (no auth wrapper)
- QCDetail.tsx — "↗ Share link" button in header → modal to generate + copy link
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- POST /jobs/{job_id}/languages/bulk-assign — assigns linguist (required) and
reviewer (optional) across all or selected languages; supports only_unassigned
flag and optional deadline
- bulkAssignLanguages() added to API client
- QCDetail: "Assign all languages" button in Languages header; opens modal with
linguist/reviewer dropdowns, deadline, and skip-already-assigned checkbox
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- JobsList: switch from size:10000 to server-side pagination (PAGE_SIZE=50)
with page state and numbered pagination controls
- JobsList: move status filter server-side; search/user/date remain client-side
- JobsList: add PM quick-filter presets (Final Review / In QC / Failed)
shown for project_manager and admin roles
- JobsList: extend canManageJobs, New Job button, and Final Review action
link to include project_manager role
- NewJob (W-5): autofill job languages from project.default_languages
when selecting an existing project from the dropdown
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Changes:
- Dashboard: add project_manager case (final review / QC counts / new job widgets)
and production case (AI pipeline / failures widgets)
- Sidebar: add project_manager to Final Review and Audit Log nav items;
live badge counts for QC Queue (pending_qc) and Final Review (pending_final_review)
- App.tsx: add project_manager to Final Review and Audit Log RoleGates (W-10, PM-18)
- Login: role-based redirect after login — linguist/reviewer → /qc/queue, others → /
- language_qc._assert_can_approve: enforce two-stage QC; remove linguist self-approve
fallback; require reviewer assignment + submitted_for_review_at (W-6)
- routes_jobs.complete_job: allow project_manager to complete jobs (W-9)
- notify.py: re-enable email notifications (W-7)
- Fix 400 on cue save: treat empty-string audio_description_vtt/captions_vtt as absent
both in backend (truthy check) and frontend (|| undefined) — root cause was adVtt
initialising to '' when job has no AD track
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Two-stage QC workflow: linguist edits + submits → reviewer approves/rejects per language.
New statuses: in_progress, pending_review, in_review. New service functions: submit_for_review,
open_review, assign_reviewer, reassign_reviewer, add_comment. Linguist and reviewer deadlines.
- Reject now resets language to in_progress so linguist can iterate without full re-assignment.
- QC comment threads per language (append-only), visible to all assignees.
- Email notifications via Mailgun on: assignment, submit-for-review, comment, approve, reject.
Best-effort (failures do not roll back QC actions). asyncio.gather for parallel fan-out.
- New audit actions: LANGUAGE_QC_REVIEWER_ASSIGN/REASSIGN, LANGUAGE_QC_SUBMIT,
LANGUAGE_QC_OPEN_REVIEW, LANGUAGE_QC_COMMENT.
- Inline project picker in NewJob: "+ Create new project…" option with name, default
languages, default linguist, default reviewer. Pre-fills languages on the new job.
- Project model extended with default_languages, default_linguist_id, default_reviewer_id.
- RBAC: CLIENT org-members can now create projects (backend guard relaxed).
- LinguistQueue: role toggle "As linguist / As reviewer" + new status tabs.
- QCDetail: two-slot assignment cards (linguist + reviewer), deadline display, role-aware
action buttons, comments panel with optimistic insert and 15s refetch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Both fields now show a validation error on submit if not selected
- Labels updated to show required asterisk
- Section always visible regardless of client list length
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Querying only role=linguist left the dropdown empty since no active linguist
users exist. Now fetches all active users and filters out clients on the
frontend, so any staff member (PM, reviewer, admin, linguist) can be assigned.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- routes_admin.py: size query param max raised from 100 → 500 so
ClientDetail.tsx (size=200) no longer returns 422
- GlossaryDetail.tsx: three .toLocaleString() calls guarded with ?? 0
to prevent TypeError when term_count is undefined on first render
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Job.language_qc dict tracks per-language status (pending/in_review/approved/rejected)
with full event history; qc_assignments denormalized array enables efficient queue queries
- language_qc service handles assign/reassign/approve/reject/reopen with atomic DB updates,
audit logging, and auto-advancement to pending_final_review when all languages approved
- Linguists can only edit VTT and trigger re-renders for their assigned language (403 guard)
- return_to_qc resets all language statuses while preserving assignments
- routes_language_qc.py: 7 new endpoints; /me/language-qc-queue for linguist queue
- Startup migration idempotently seeds language_qc for all existing jobs
- Frontend: LanguageQCState types, API methods, LinguistQueue page, QCDetail redesigned
with per-language status badges, assignment dropdown, inline approve/reject buttons,
progress bar, and reject modal; My QC Queue sidebar link
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace inline anchor links with plain text — the correct dashboard URL
is only on the UserList button.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Only the main AI Cost Dashboard button in UserList should use the new URL.
The inline helper links inside the Cost Tracker Project ID inputs stay on cost.oliver.agency.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add /no-access empty-state page for zero-membership users
- Add /org/:orgSlug/settings/* routes (members, teams, invitations, general)
- OrgSettingsLayout with tab nav (NavLink-based)
- OrgMembersPage: member table with search, role editor, remove action
- OrgInvitationsPage: list with status badges + revoke
- OrgTeamsPage: read-only teams list
- OrgGeneralPage: org info display
- InviteMemberModal: email + role form → POST /organizations/:id/invitations
- Sidebar: org-switcher (single label / multi-org dropdown), currentOrgSlug
derived from route params or first membership, Settings gear link at bottom
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- models/invitation.py — Invitation model + create/accept/preview schemas
- routes_invitations.py — org-scoped POST/GET/DELETE + public preview/accept endpoints
Single-use token via find_one_and_update; sha256(token) stored in DB, plaintext in email URL
- emailer.py — _send() helper; send_invitation_email, send_welcome_email, send_password_reset_email
send_completion_email refactored to use _send()
- migration_2026-04-28-000002 — creates invitations collection with TTL index (30d audit trail)
- routes_auth.py — new MS SSO users provisioned with zero memberships instead of role=PRODUCTION;
they land on "no access" page until an admin invites them
- main.py — registers invitations_org_router and invitations_router
Frontend:
- routes/AcceptInvite.tsx — public page at /accept-invite?token=...
Four states: new user (name+password), existing user (confirm), MS user, already-member
- App.tsx — /accept-invite route outside RequireAuth
- types/api.ts — Invitation, InvitationCreate, InvitationPreview, InvitationAcceptRequest/Response
- lib/api.ts — listInvitations, createInvitation, revokeInvitation, previewInvitation, acceptInvitation
- hooks/useClients.ts — useInvitations, useCreateInvitation, useRevokeInvitation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pydantic v2 + FastAPI serializes Field(alias="_id") as _id in JSON,
so client.id was always undefined on the frontend — causing option
values to fall back to text content ("3M") and firing /clients/3M/teams 404s.
- Remove Field(alias="_id") from Client/Team/Project models; id is now a
plain string field populated explicitly in _client_from_doc etc.
- API now returns id not _id, matching the TypeScript Client interface
- Add clientId !== "undefined" guard to useTeams, usePMs, useProjects
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add project_manager to all role dropdowns (UserList filter, create modal, UserDetail edit form)
- Add indigo badge color for project_manager in user list table
- Expose pm_client_ids in UserResponse schema and all admin user endpoints
- Add pm_client_ids to frontend User type
- Add UserAssignmentsPanel to UserDetail sidebar: PM users see client toggle list; other roles see client → team membership picker
- Add flexible hooks (useTeamsForClient, useAssignPMAny, useRemovePMAny, useAddTeamMemberAny, useRemoveTeamMemberAny)
- Fix useClient guard against literal "undefined" string causing 404 requests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>