- Add \b word boundaries to SQL injection and command injection regex patterns
to prevent false positives on names like "Josh Smith" (sh\s+), "Norm " (rm\s+)
- Change default role in CreateUserModal from 'client' to 'admin'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MongoDB 7.0 rejects the invalid key with code 28811, causing 500 on
GET /organizations/{id}/members.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New FAQ guide (faq.md) covering: re-render accessible video, stuck jobs,
linguist/reviewer assignment, downloads, TTS voice, briefs, status colours
- extractSections() parses markdown body text per section; search now
matches against section body text, not just heading text
- FAQ tab added between Overview and Client in the sidebar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
u.get("key", "") returns None when key exists with null value in MongoDB,
causing Pydantic ValidationError on MemberDetail.email/full_name: str → 500.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolves TS2448/TS2454 — useMemo blocks referenced captionsVtt and
retimedCaptionsVtt before their useState declarations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add retimed captions overlay to accessible video player in QCDetail;
falls back to original captions if retimed VTT not yet generated
- Extend listUsers to accept comma-separated roles (e.g. linguist,admin)
so admin/production users appear in linguist/reviewer assignment dropdowns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Projects:
- PM now sees all active projects (same as admin/production) — was filtering
to empty when pm_client_ids and org memberships were both unset
Assign To:
- Replaced useOrganizations()+useOrgMembers() with a new GET /admin/brief-assignees
endpoint accessible to all authenticated users — returns active admin/PM/production
users sorted by name; shows role next to name in dropdown
Requested Outputs:
- Added SDH Captions (VTT), Descriptive Transcript, Accessible Video (MP4)
- Accessible Video shows Pause Insert / Voice Overlay radio selector
- Added descriptive_transcript field to RequestedOutputs model (backend + frontend)
Access:
- Brief routes now open to 'client' role in addition to admin/PM/production
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deleted route files, App.tsx routes, Sidebar nav item, and Dashboard
"Awaiting Upload" card. The feature wasn't ready (Project/Assign To dropdowns
were empty for non-admin users) and isn't needed at this stage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Platform admins query GET /organizations (not memberships) so currentOrgId
was always null — hiding the Settings nav link. Now falls back to the first
org from useOrganizations() for admins, gated with enabled:isPlatformAdmin
to avoid 403 for non-admin roles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The backend /organizations/{org_id}/members endpoint queries memberships
by organization_id (_id hex string), but the sidebar was building the URL
from organization_slug (e.g. "3m-test"), causing 403 on every Settings page
load ("Failed to load members.").
- Sidebar: derive currentOrgId from organization_id; option values = org ID
- OrgSettingsLayout: alias orgSlug param as orgId for clarity
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cloud Run-generated freeze segments caused FFprobe to return code 1 with
empty stderr when dispatched to the Celery ffmpeg queue, crashing the
render for every language. The freeze segments are created to an exact
pre-computed duration (ad_duration + silence_before + silence_after),
so probing is unnecessary — assign that value directly instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove `default` from <track> element so browser doesn't render native
captions on top of the custom overlay (was causing double display)
- Remove positionTop logic — always render overlay at bottom-14 (above controls)
regardless of VTT line hints; applies to both VideoWithCaptions and VideoReviewPlayer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Navbar: add WebSocket connection dot (green/yellow/red/gray) from GlobalWebSocketContext
- Profile page: /profile route shows email, full_name, role, auth_provider, languages
- JobResponse: expose failure and error fields (were stored in MongoDB but not returned)
so frontend now shows actual render error message instead of generic fallback
- render_accessible_video: write JOB_TASK_FAILED audit log entry on render failure
with language, error detail, step=render
- rerender_accessible_video: same audit log on re-render failure, step=rerender
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend (Phase A):
- A1: Adaptive silence buffer — natural_gap_ms persisted per cue; renderer computes
per-cue silence_before/silence_after instead of fixed 500ms; per-cue silence files
- A2: Forward-preferred snap — snap_pause_point prefers boundaries up to 4s ahead
over boundaries within 1.5s behind, reducing mid-scene cuts
- A3: Min-gap validation — pause points with < 200ms gap trigger forward search
to the next acceptable gap
- natural_gap_ms added to PausePointData model and api.ts type
- New config fields: whisper_snap_forward_window, whisper_snap_backward_window,
ad_silence_buffer_default, ad_silence_buffer_min_after, ad_min_acceptable_gap
- Tests: test_whisper_snap.py (13 tests), test_video_renderer_buffers.py
Frontend (Phase B):
- B1: Drag pause-point markers — pointer state machine with 3px move threshold,
clamp to min/max bounds, click-without-move still opens PausePointEditor
- B2: Drag freeze blocks — orange blocks translate with linked pause point
- B3: Time tooltip visible during drag, hidden on release
- Tests: TimelinePreview.drag.test.tsx (10 tests)
Fixes:
- Share link pointed to ai-sandbox.oliver.solutions — added app_url to Settings
with correct optical-dev.oliver.solutions default; share_url now configurable
via APP_URL env var
- Removed all ai-sandbox.oliver.solutions references from docker-compose,
apache config, docs, and scripts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sidebar My QC Queue badge was showing org-wide pending_qc job count
instead of the current user personal assigned tasks. Now uses
useMyQCQueueCount which sums the linguist and reviewer queue totals
from the same me/language-qc-queue API the queue page uses.
Refresh button now shows a spinner and Refreshing label while the
refetch is in progress so users can see the action took effect.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
list_org_members and _membership_from_doc used bracket access on role_in_org
which raises KeyError if the field is absent (old docs or direct DB inserts).
Also handles ValueError if the stored value doesn't match a valid OrgRole.
Falls back to OrgRole.MEMBER in both cases.
Fixes 500 on GET /organizations/{org_id}/members.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
assign, assign-reviewer, reassign-reviewer, and bulk-assign endpoints
were gated to project_manager/production/admin only, but the Reviewer
QC Detail page exposes Assign buttons to reviewer users.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Captures admin, client, linguist, reviewer, production, project-manager,
and global help screenshots from optical-dev using Playwright MCP.
All markdown-referenced filenames now have corresponding PNG files.
Placeholders used where live data or role permissions prevent full capture.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Markdown guides use /help-screenshots/... (root-relative). With Vite
base=/video-accessibility/, images were requested at the wrong URL.
Custom img renderer now prepends import.meta.env.BASE_URL so paths
resolve correctly on both /video-accessibility/ and local dev.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Screenshots captured via Playwright against optical-dev. Covers:
global (login + interface), client, linguist, reviewer, production,
project-manager, admin — all 25 PNGs under frontend/public/help-screenshots/.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Frontend sends audio_description_vtt: "" for CC-only jobs.
Pydantic validator converts "" to None before validation,
so the backend skips VTT format validation and returns 200
instead of 400.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug 1: Editing any AD cue never updated descriptive_transcript.txt in GCS.
Bug 2A: Uploading replacement CC or AD .vtt had the same root cause.
After saving captions or AD VTT, read the other stream from GCS if not
provided in the request, merge both via generate_descriptive_transcript(),
upload the result to {job_id}/{lang}/descriptive_transcript.txt, and
update lang_output["descriptive_transcript_gcs"] before the DB write.
Bug 2B (CC-only job → 400 on empty audio_description_vtt): already fixed
by the existing `if request.audio_description_vtt:` guard (empty string
and None are both falsy) and frontend `adVtt || undefined` sending no
field rather than an empty string.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hardcoded --concurrency=8 with 512MB memory limit caused 1162+ OOM restarts.
Default is 2; set TTS_WORKER_CONCURRENCY in .env.production to override.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NewBrief: use useAllProjects() (was useProjects('') which never fired)
- NewBrief: expand languages from 12 to 52 options with region variants
- NewBrief: add Assign To dropdown from org members
- Backend: add GET /clients/all-projects endpoint for cross-client project listing
- Backend: add assignee_id to JobBriefCreate/JobBriefResponse models + routes
- notify.py: send completion email to PMs (pm_client_ids) not client user — fixes email never arriving (was looking up users._id by client entity ID)
- Downloads: add Download All button that fetches all files sequentially
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- glossary_service: add step-by-step debug/warning logs at each early-return point so
the exact failure reason is visible in worker logs (project not found, no active version, etc.)
- glossary_service: guard against source_term_lower=None in ahocorasick automaton build
- glossary_service: guard against target_locale=None in _get_translation
- glossary_service: add full traceback to the outer exception catch for easier debugging
- JobsList: fix statusFilter stale state — useEffect now always syncs with URL params,
clearing the filter when no ?status= param is present (previously the filter was never
cleared, so navigating from /jobs?status=X to /jobs kept the old filter)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous migrations used async-for on a dict (Atlas returns firstBatch, not
async cursor) — silently failed. New migration reads firstBatch correctly and
sets the complete status list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
lameenc.encode() returns bytearray, but google-cloud-storage's
_to_bytes() only accepts bytes/str — causing TypeError on every
upload_from_string() call. Cast to bytes() before returning.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ffmpeg was missing from the base image, causing all pydub operations
(AudioSegment.from_file, export) to fail in worker and tts-worker containers.
Moved ffmpeg install from whisper-worker stage to the shared base stage so
all container variants (api, worker, tts-worker, whisper-worker) have it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gemini TTS _pcm_to_mp3 used pydub.AudioSegment.export(format='mp3') which
requires a system ffmpeg binary. Worker containers don't have ffmpeg installed
(video ops run on Cloud Run). Switch to lameenc which is pure Python and
encodes PCM→MP3 without any system binary.
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>
google.cloud.run_v2 is not installed; optical-dev dispatches pipeline tasks
via local Celery workers, not Cloud Run Jobs.
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>