API base URL and MSAL redirect URI were pointing to old ai-sandbox host,
causing Microsoft auth popup to redirect back to the wrong domain.
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>
Backend:
- New UserRole.PROJECT_MANAGER with pm_client_ids[] on User model
- New models: Client (slug-based), Team (member_user_ids[]), Project (client-scoped)
- Job model gains project_id field
- New GET/POST/PATCH/DELETE /clients, /clients/{id}/teams, /clients/{id}/projects,
/clients/{id}/pm routes (admin-only client CRUD; PM or admin for teams/projects)
- get_accessible_project_ids() helper: staff→all, PM→their clients' projects,
CLIENT→projects from teams they belong to (with legacy owner fallback)
- list_jobs, get_job, bulk_download, get_vtt_content, delete_job all use new isolation
Frontend:
- UserRole type gains 'project_manager'
- Job, JobCreateRequest gain project_id field
- Client, Team, Project, PMUser types added
- ApiClient: full client/team/project/PM CRUD methods
- useClients hook with all query/mutation hooks
- Admin pages: ClientList + ClientDetail (teams, members, projects, PM assignment)
- NewJob form: client + project picker (shown when clients exist)
- Sidebar: Clients nav item for admin and project_manager roles
- Routes: /admin/clients and /admin/clients/:clientId behind RoleGate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
useJobDownloads was firing for any non-early status (pending_qc, translating,
etc.), hitting the backend /downloads endpoint which returns 400 when no outputs
exist yet. React Query then retried 3+ times.
Downloads page only renders content for completed jobs anyway, so disable the
query for any other status by passing 'created' (which is in EARLY_STATUSES).
JobDetail is unaffected — it uses the hook separately with its own status guard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PATCH /jobs/{job_id} endpoint for updating title and cost_tracker_project_id
- cost_tracker_project_id exposed on JobResponse (GET /jobs/{id})
- Inline project ID field in QCDetail and FinalDetail — saved via PATCH
- "AI Cost Dashboard" link in UserList header
- cost_tracker_project_id added to Job type and JobUpdateRequest schema
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add LINGUIST role to UserRole enum (backend + frontend)
- Grant linguists access to QC Review, Final Review, review notes, and VTT editing
- Add MongoDB migration to update schema validator with linguist role
- Add admin seed: vadymsamoilenko@oliver.agency is promoted to admin on startup
- Add User Management sidebar link for admin users
- Fix Login.tsx role type cast to use UserRole instead of hardcoded union
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SDH (sdh_captions_vtt) was missing from the Downloads page type labels
and filename extensions map.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Set staleTime=0 on useAccessibleVideoEditState so edit-state is
immediately refetched when invalidated after render completes
- Force prevJobStatusRef='rendering_qc' at render start so the
pending_qc completion effect always fires, even when fast renders
complete before the 10s polling interval catches rendering_qc status
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rewrote VTT translation to two-step (text-only → Gemini → apply to original timestamps) preventing caption timing desync
- Added polling fallback for all processing states and Safari visibilitychange WebSocket reconnect
- Added 11 new TTS languages (cs, da, fi, hu, no, sk, sv, es-419, pt-BR, fr-CA)
- Updated caption/AD prompts to DCMP Captioning Key & Description Key standards (line splitting, ♪ music notation, italic tags, caption positioning, ethics guidelines)
- Added descriptive transcript generation (WCAG 2.1 §1.2.1) combining captions + AD into plain text
- Fixed amix normalize=0 to prevent audio loss in rendered videos
- Fixed AD re-timing double-count when source_ms is None
- Fixed cue block numbering to be 1-based in VttEditor and Timeline Preview
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
**BUG-1 & BUG-2 — Wrong audio plays after re-render / MP3 doesn't match text**
Root cause: audio files were named by index (cue_0.mp3, cue_1.mp3). When a cue
was inserted or deleted, all following indices shifted but old MP3 files kept
their original names, so re-render would play the wrong audio for the wrong cue.
Fix: renamed files to cue_N_CONTENTHASH.mp3 and introduced an ad_cue_manifest
stored in the job document that maps each cue index to its correct GCS URI.
Re-render now reads from the manifest instead of guessing by filename.
Also: editing AD cue text in the VTT editor now automatically queues TTS
regeneration for changed cues — no more silent mismatches.
**BUG-3 — App crash / state desync when uploading VTT or clearing TTS queue**
Fixed handleVttFileUpload to only update local editor state after the server
confirms the save — previously local state was updated first, so a network
error left the UI showing content that wasn't actually saved.
Fixed handleClearRegenerationQueue to only remove items from local state if
the server removal succeeded — previously all items were cleared regardless.
**BUG-4 — AI generates different audio descriptions every time**
Added GenerateContentConfig(temperature=0.2, top_p=0.8, top_k=40) to the
Gemini API call so output is more consistent across runs.
**BUG-5 — On-screen text inconsistently described**
Strengthened the AI prompt rule from a vague suggestion to a mandatory
requirement with an explicit format: "Text on screen reads: [exact text]".
Applied to both gemini_ingestion.md and gemini_ingestion_targeted.md.
**BUG-6 — No notification when re-render finishes**
Added rendering_qc toast notification and a dismissible green banner that
appears in QCDetail when re-render transitions to pending_qc. The banner
auto-dismisses after 10 seconds. Also increased WebSocket reconnect attempts
from 5 to 15 and capped backoff at 60s to prevent falling back to manual refresh.
**BUG-7 — Timeline preview looks accurate but isn't after edits**
Added isStale prop to TimelinePreview. The timeline now shows an amber tint
and "Preview may be outdated" label whenever there are unsaved pause point
changes, pending TTS regenerations, or a new VTT has been uploaded.
**BUG-8 — ElevenLabs API errors break TTS with no fallback**
Added try/except fallback chain in _synthesize_single_cue: if the configured
provider fails, it automatically retries with google, then gemini.
**BUG-9 — Concurrent re-render requests cause race conditions**
Made the PENDING_QC → RENDERING_QC status transition conditional (only
succeeds if the job is still in PENDING_QC). Returns HTTP 409 if a re-render
is already in progress. The completion transition back to PENDING_QC is also
conditional so a cancelled/overridden render doesn't corrupt job state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SDH captions extend standard VTT with speaker identification labels,
sound effects [PHONE RINGS], music notation ♪, and off-screen indicators.
- Add sdh_vtt flag to RequestedOutputs model and frontend form
- Add sdh_captions_vtt_gcs field to LangOutput model
- Inject SDH generation instructions into both Gemini prompts via
{SDH_FIELD} and {SDH_GUIDELINES} placeholders when requested
- Upload sdh_captions.vtt to GCS in ingest task
- Pass SDH through video_native translation (Gemini generates it directly)
and traditional translation (translate source SDH VTT via Gemini)
- Expose sdh_captions_vtt in downloads endpoint and bulk zip export
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add brand_context field (job model, API, frontend form) so clients can
list brand names present in their video; Gemini uses these names instead
of generic descriptors (e.g. "Sellotape" not "sticky tape")
- Add ethical guidelines section to both Gemini prompts covering
person-first language, consistent race/gender description only when
plot-relevant, no guessing at unconfirmed identity
- Revamp audio description rules: priority ordering (essential →
high-priority → time-permitting), pre-teaching placement, no cinematic
jargon, succinct style replacing the former "20% longer" instruction
- Thread brand_context through full stack: routes → job doc → ingest
task → translate task → both Gemini prompt templates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes race condition where timeline preview never updated after AD VTT
re-render: useJob now polls every 5s during rendering_qc, and QCDetail
invalidates the edit-state query when the job transitions back to pending_qc.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>