Commit graph

178 commits

Author SHA1 Message Date
Vadym Samoilenko
e2af5c0f2f fix(share): correct decisionState type narrowing in ShareView
Wrap the render condition in parentheses and include 'submitting' so
TypeScript does not narrow decisionState to 'idle'|'error' inside the
review form block, eliminating four TS2367 comparison errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 09:57:35 +01:00
Vadym Samoilenko
d70b5acaf9 feat(share): client review form on share link; hide client role from UI
- POST /public/share/{token}/decision — unauthenticated approve/reject via share token
  - approve: validates assets, sets status completed, triggers notification
  - reject: sets status qc_feedback, stores client name + notes in review history
- ShareView: review form (name, comments, Approve / Return for Corrections)
  - shows only when job is pending_final_review
  - confirmation screen after decision
- api.ts: submitShareDecision()
- Hide 'client' role from UserList/UserDetail dropdowns
- Hide 'Client' guide tab from Help

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 09:51:58 +01:00
Vadym Samoilenko
f91cb16005 fix(middleware): add word boundaries to injection patterns; default role to admin
- 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>
2026-05-06 09:45:28 +01:00
Vadym Samoilenko
5f084f359f feat(help): add FAQ tab + full-content search
- 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>
2026-05-01 18:46:06 +01:00
Vadym Samoilenko
e52abca74b fix(qc): move accessibleCaptionCues memos after state declarations
Resolves TS2448/TS2454 — useMemo blocks referenced captionsVtt and
retimedCaptionsVtt before their useState declarations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 18:31:58 +01:00
Vadym Samoilenko
90867e9824 fix(qc): show captions on accessible video + allow admin/PM as linguist/reviewer
- 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>
2026-05-01 18:07:55 +01:00
Vadym Samoilenko
68ac65ac05 fix(briefs): fix Project/Assign-To dropdowns and expand Requested Outputs
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>
2026-05-01 17:54:21 +01:00
Vadym Samoilenko
a14444d61c Revert "feat(briefs): remove Briefs feature from frontend"
This reverts commit 98ece9faac.
2026-05-01 17:49:12 +01:00
Vadym Samoilenko
98ece9faac feat(briefs): remove Briefs feature from frontend
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>
2026-05-01 17:48:02 +01:00
Vadym Samoilenko
27286e23db fix(sidebar): show org settings link for platform admins without memberships
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>
2026-05-01 17:45:49 +01:00
Vadym Samoilenko
e60e7c96e7 fix(settings): use organization_id (not slug) in /org/:orgSlug/settings URL
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>
2026-05-01 17:34:20 +01:00
Vadym Samoilenko
9733700874 fix(captions): remove duplicate native track display, fix position to bottom
- 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>
2026-05-01 16:33:33 +01:00
Vadym Samoilenko
df7fec701d fix(ui): connection dot in navbar, profile page, render error visibility + audit log
- 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>
2026-05-01 16:19:12 +01:00
Vadym Samoilenko
2f4925353a feat(pause-insert): adaptive buffer, forward-snap, timeline drag + share link fix
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>
2026-05-01 16:09:09 +01:00
Vadym Samoilenko
31d631f70d fix(qc-queue): correct sidebar badge and Refresh button loading state
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>
2026-05-01 14:41:37 +01:00
Vadym Samoilenko
d4cb31e5d9 feat(help): add real screenshots for all 7 role guides (77 images)
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>
2026-05-01 14:02:54 +01:00
Vadym Samoilenko
2c816a5e69 docs(help): add Timeline Preview & Rendering section to linguist/reviewer/production/admin guides
All 4 roles that access QCDetail now have section explaining:
- Timeline bar colour legend (Video/AD Audio/Queued/Pause Point/Adjusted)
- Render Accessible Video Changes panel triggers and behaviour
- Whisper pause refinement checkbox guidance
- Step-by-step render workflow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 13:20:48 +01:00
Vadym Samoilenko
ce048a2196 fix(help): resolve screenshot paths under Vite subpath deploy
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>
2026-05-01 13:14:47 +01:00
Vadym Samoilenko
67219797b6 feat(help): add captured screenshots for all 7 role guides (25 images)
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>
2026-05-01 13:11:40 +01:00
Vadym Samoilenko
6559ccc1f9 feat(help): in-app role-based help guides + screenshot capture pipeline
- Help.tsx: role tabs, TOC scroll-spy, search, lightbox, react-markdown renderer
- 7 markdown guides (global, client, linguist, reviewer, production, PM, admin)
  with explicit click/drag/keyboard annotations throughout
- Sidebar: Help button added at bottom of nav (all roles)
- App.tsx: /help route, no RoleGate
- frontend/public/help-screenshots/{role}/: directories ready for screenshots
- tools/capture-help-screenshots.ts: Playwright screenshot script
  - Clicks "Local login" toggle before filling credentials
  - Uses test-admin local account (not SSO)
- backend/scripts/seed_test_users.py: idempotent MongoDB seed script
  creates 6 local-auth users (admin + 5 roles) for capture + local dev
- .env.screenshots.example: template with test-admin credentials
- Removes docs/video_accessibility_user_guide_v3.md (superseded by in-app guides)
- Deps: react-markdown, remark-gfm, rehype-raw added to frontend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 13:08:13 +01:00
Vadym Samoilenko
d2adfbc3b4 fix(dashboard): briefsData is array, not {briefs:[]} — remove stale .briefs accessor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 12:13:58 +01:00
Vadym Samoilenko
32b12ff0a6 feat(ux): P2 role UX — reviewer queue, dashboard widgets, org filter, WS toast
Phase 2.3: VttEditor sticky banner + Re-translate wired into QCDetail
Phase 3.1: RoleGate on /briefs/* (PM/admin/production only)
Phase 3.2: LinguistQueue — sortable Assigned column, defaultRole prop
Phase 3.3: ReviewerQueue component + /qc/reviewer-queue route + sidebar entry
Phase 3.4: PM dashboard — Overdue and Stuck >24h widgets
Phase 3.5: Production dashboard — Awaiting Upload and Pending QC Handoff widgets
Phase 3.6: Admin UserList — org_id filter dropdown (uses listOrganizations)
WebSocket: onTerminalClose callback + error toast in GlobalWebSocketContext
Runbook: Apache ProxyTimeout ≥60s recommendation for WebSocket keepalives
Backend: fix F841 unused variable in test_cross_tenant_isolation.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 11:58:29 +01:00
Vadym Samoilenko
b427ee9f49 fix(authz): MT-3/6/7/8 org isolation + P1 English-first QC enforcement
Multi-tenancy isolation (P0):
- MT-3: Add get_job_or_403 (org membership check) to all 19+ job action endpoints
- MT-6: Same gate added to all review_notes (5) and vtt_versions (4) handlers
- MT-7: WebSocket /ws/jobs/{job_id} closes with 4403 on org mismatch;
  /ws/jobs passes accessible_org_ids to ConnectionManager; server-side
  keepalive at 20 s (asyncio.wait_for timeout) prevents proxy idle drops
- MT-8: list_users scoped to org memberships for non-platform-admins

WebSocket fixes (Mod Comms 2026-03-18 incident):
- Frontend heartbeat lowered 30 000 → 20 000 ms (was at Apache timeout edge)
- Terminal close codes 4001/4003/4004/4403 no longer trigger reconnect loop
- Silently discard server "keepalive" frames alongside existing "pong"

English-first QC (P1):
- _assert_can_approve blocks target language approval until source is APPROVED
- PRODUCTION/ADMIN roles bypass the gate
- Source VTT edits reset stale APPROVED/PENDING_REVIEW/IN_REVIEW target states

Tests (all passing):
- backend/tests/unit/test_language_qc_english_first.py (15 cases)
- backend/tests/unit/test_routes_jobs_org_isolation.py (12 cases)
- backend/tests/unit/test_review_notes_org_isolation.py (16 parametrized cases)
- backend/tests/unit/test_vtt_versions_org_isolation.py (16 parametrized cases)
- backend/tests/unit/test_websocket_org_isolation.py (11 cases)
- backend/tests/unit/test_admin_users_org_filter.py (7 cases)
- frontend: useJobStatusWebSocket.terminal.test.ts (9 cases)
- frontend: useJobStatusWebSocket.heartbeat.test.ts (9 cases)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 11:43:10 +01:00
Vadym Samoilenko
5d8d992e5a feat(briefs+notify+downloads): fix projects dropdown, add assignee, expand languages, fix PM email, add Download All
- 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>
2026-04-30 21:47:28 +01:00
Vadym Samoilenko
3bed598025 fix(glossary+jobs): add debug logging for glossary failures and fix AllJobs filter stale state
- 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>
2026-04-30 21:25:41 +01:00
Vadym Samoilenko
2e8cf8269e fix(tts): fetch job_doc before gcs_path call in _generate_language_tts; add cancelled migration
- 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>
2026-04-30 20:36:03 +01:00
Vadym Samoilenko
f681bd4f53 feat: add Stop Process button to cancel in-progress jobs
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>
2026-04-30 19:50:39 +01:00
Vadym Samoilenko
b0a90777ed fix(ts): cast job.error to string before rendering in failure banner
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 18:03:04 +01:00
Vadym Samoilenko
efa2395527 feat: inline title rename in JobDetail and QCDetail
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>
2026-04-30 17:52:43 +01:00
Vadym Samoilenko
0badae9e5d feat(jobs-list): add per-row Edit (rename) and Delete buttons
- Edit button opens inline rename modal with Enter/Escape support
- Delete button shows confirmation modal with clear warning about
  permanent removal from storage and database
- Both actions available for admin/production/project_manager roles
- Delete uses existing single-job DELETE endpoint (GCS + MongoDB)
- Rename uses existing PATCH endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 17:49:51 +01:00
Vadym Samoilenko
5db01248b6 fix: pass USE_CELERY_FALLBACK to containers and show real error in failure UI
- 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>
2026-04-30 17:48:02 +01:00
Vadym Samoilenko
105895dd14 feat: apply EN source VTT changes to all target languages
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>
2026-04-30 17:13:06 +01:00
Vadym Samoilenko
ce4b3b0d95 fix(frontend): prevent premature downloads fetch before job has outputs
- 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>
2026-04-30 16:54:01 +01:00
Vadym Samoilenko
31199f8705 chore: push all session changes — backend hardening, tests, apache config, deploy scripts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 15:52:14 +01:00
Vadym Samoilenko
d5e63129dd feat(upload): PR-3 GCS resumable chunked upload for large videos
Files >100 MB bypass the load balancer via browser→GCS direct upload:
- POST /jobs/upload/init — creates GCS resumable session, returns job_id + session URI
- POST /jobs/upload/complete — verifies GCS object, creates job, dispatches ingestion
- Frontend sends 8 MB chunks with Content-Range directly to GCS session URI
- infra/gcs-cors.json + deploy-dev.sh ensure_gcs_cors() enable browser CORS on bucket

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 11:35:13 +01:00
Vadym Samoilenko
c1948ea198 feat(ux): T-2/PR-7/PR-8 — status color helper, queue stats widget, upload-final-VTT override
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>
2026-04-30 11:12:36 +01:00
Vadym Samoilenko
e4b350cd7d feat(ux): R-8 linguist language warn, PM CC editing, timeline right-click + CC insert
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>
2026-04-30 10:51:31 +01:00
Vadym Samoilenko
518796c852 fix(vtt-editor): always-visible insert buttons + gap insert rows for silent sections
- 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>
2026-04-30 10:43:24 +01:00
Vadym Samoilenko
812a2bffce fix(frontend): remove /api suffix from VITE_API_BASE_URL (api.ts appends /api/v1 itself)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 22:32:15 +01:00
Vadym Samoilenko
9413200681 fix(login): replace placeholder support email with actual contact
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 22:29:26 +01:00
Vadym Samoilenko
8e33b413a3 fix(frontend): update .env.production URLs to optical-dev.oliver.solutions
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>
2026-04-29 22:28:57 +01:00
Vadym Samoilenko
2ab5a6f681 fix(frontend): remove unused useRetryTts; npm audit fix — 0 vulnerabilities
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 22:25:18 +01:00
Vadym Samoilenko
5679a38f1e fix(ts): resolve 5 TypeScript errors blocking frontend build
- QCDetail: remove unused commentsQuery variable
- BriefDetail: remove unused navigate import and assignment
- JobDetail: import type JobFailure, remove unused handleRetryTts
- NewJob: sdh_vtt fallback to false (boolean | undefined → boolean)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 22:22:55 +01:00
Vadym Samoilenko
fe608401be feat(w-12): brief workflow UI — list, create, detail, NewJob pre-fill
- BriefsList.tsx: table with status badge, submitted badge count
- NewBrief.tsx: form with title, description, outputs, language picker,
  deadline, project selector; calls POST /briefs
- BriefDetail.tsx: status actions — Submit (DRAFT), Approve (SUBMITTED,
  admin/PM), Create Job link (?brief_id=) for APPROVED briefs
- NewJob.tsx: reads ?brief_id, fetches brief via useBrief, pre-fills
  languages/outputs/deadline/project_id; sends brief_id in FormData
- Sidebar: Briefs link (client/production/admin/PM) with submitted-count
  badge from useBriefs()
- JobCreateRequest type: brief_id optional field
- briefs API methods: listBriefs, createBrief, getBrief, submitBrief,
  approveBrief; hooks: useBriefs, useBrief, useCreateBrief,
  useSubmitBrief, useApproveBrief

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 20:41:49 +01:00
Vadym Samoilenko
a945653e73 feat(w-14): bulk failures dashboard + sidebar badge
- 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>
2026-04-29 20:36:30 +01:00
Vadym Samoilenko
264561895e feat(w-13): generic /jobs/{id}/retry endpoint + unified failure UI
- POST /jobs/{job_id}/retry dispatches correct pipeline task based on
  failure.step: ingestion/ai_processing → ingest_and_ai_task,
  translation/tts → translate_and_synthesize_task, render → rerender
- Increments retry_count, writes JOB_RETRY audit log entry
- Adds processing_failed to JobStatus type; JobFailure interface on Job
- Replaces TTS-only retry block with FailureBanner showing step/message/
  retry_count for all failed statuses (processing_failed, tts_failed,
  render_failed); Escalate mailto link for high-retry-count cases
- useRetryJob hook + apiClient.retryJob() call new endpoint

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 20:33:50 +01:00
Vadym Samoilenko
08fcb4daa4 feat(pr6): WS real-time updates, per-cue AD playback, upload guard
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>
2026-04-29 19:42:57 +01:00
Vadym Samoilenko
bdfa0f82ab fix(lint): restore baseline lint count — no new errors introduced
QCDetail.tsx: 4 new `any` types replaced with `unknown` + type casts.
backend: ruff auto-fix sorted imports, removed unused imports, updated Optional[X] → X | None in routes_share + share_token model.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 19:16:35 +01:00
Vadym Samoilenko
1317ee7ca4 feat(t6+t7+t11): native captions track, AD audio sync, CSRF protection
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>
2026-04-29 19:08:27 +01:00
Vadym Samoilenko
aba43a67d7 feat(l7): diff AI baseline vs current VTT in QCDetail
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>
2026-04-29 19:03:25 +01:00