Commit graph

331 commits

Author SHA1 Message Date
Vadym Samoilenko
3fb8dce3ee feat(ai): upgrade Gemini models to 3.1-pro-preview and 3.1-pro-tts-preview
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 21:00:32 +01:00
Vadym Samoilenko
12fe4ebcbb feat(tts): upgrade Gemini TTS model to gemini-3.1-flash-tts-preview
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 20:57:37 +01:00
Vadym Samoilenko
43ef3a6cd8 fix(migrations): correct listCollections cursor parsing, add processing_failed+cancelled to status enum
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>
2026-04-30 20:47:21 +01:00
Vadym Samoilenko
8a1440201e fix(migrations): connect to mongo before running migrations in run.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 20:43:48 +01:00
Vadym Samoilenko
99554173e6 feat(migrations): add run.py entry point for python -m app.migrations.run
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 20:41:52 +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
08a8a0d636 fix(tts): convert lameenc bytearray to bytes before GCS upload
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>
2026-04-30 19:35:28 +01:00
Vadym Samoilenko
77a9d3b255 fix(docker): add ffmpeg to base image — fixes pydub AudioSegment in worker
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>
2026-04-30 19:12:57 +01:00
Vadym Samoilenko
7c15acc18a chore: update poetry.lock after adding lameenc dependency
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 18:34:04 +01:00
Vadym Samoilenko
a53cf960ae fix(tts): replace pydub MP3 export with lameenc (pure Python, no system ffmpeg)
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>
2026-04-30 18:24:15 +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
37873c433d fix(deploy): set USE_CELERY_FALLBACK=true on optical-dev — no Cloud Run Jobs here
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>
2026-04-30 17:14:45 +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
46477b7b32 fix(deploy): target sites-enabled instead of sites-available for Apache Include injection
On optical-dev the Apache vhost is a standalone file in sites-enabled (not
a symlink to sites-available), so injecting the Include into sites-available
had no effect and the ProxyPassMatch rules were never loaded by Apache.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 16:32:23 +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
24d93277de fix(deploy): restore original memory limits on ffmpeg/whisper workers
faster_whisper loads its model into RAM at startup regardless of whether
tasks are routed to Cloud Run — reducing the limit to 512M caused OOM kill
on container start. Restored original limits (ffmpeg: 1G, whisper: 2G).

Cloud Run URLs (FFMPEG_SERVICE_URL / WHISPER_SERVICE_URL) remain set so CPU
offload is still active.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 14:32:24 +01:00
Vadym Samoilenko
ec1ce5c13a feat(deploy): offload ffmpeg+whisper to Cloud Run HTTP services on optical-dev
Sets FFMPEG_SERVICE_URL and WHISPER_SERVICE_URL so video_renderer.py and
whisper_transcribe.py route CPU-heavy work to Cloud Run instead of running
ffmpeg/Whisper locally. Both Cloud Run services and IAM (roles/run.invoker
for accessible-video-worker@ and video-accessibility@ SAs) are already
provisioned — only the env vars were missing.

ffmpeg-worker container: 1G/0.5CPU → 256M/0.25CPU (HTTP dispatcher only)
whisper-worker container: 2G/0.5CPU → 512M/0.25CPU (HTTP dispatcher only)

Expected outcome: ffmpeg-worker drops from 51% CPU / 97% RAM to < 5% CPU.
Server load avg should fall from ~2.2 to ~1.0-1.3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 14:28:58 +01:00
Vadym Samoilenko
5fd370c093 test: fix all unit tests — 168 passing, 0 failures
- conftest.py: set required env vars before app import to prevent Settings() crash
- gcs.py: lazy bucket init checks _bucket instead of _client; add @bucket.setter
- vtt.py: fix float precision in _format_timestamp; include empty-text cues in parser
- security.py: guard verify_password against empty hash (passlib UnknownHashError)
- tts.py: _parse_timestamp raises ValueError("Invalid timestamp format: …")
- emailer.py: HTML-escape job_title in _render_completion_template (XSS fix)
- test_emailer.py: rewrite for Mailgun-based service (replaced SendGrid)
- test_gcs.py: fix UploadFile constructor, MIME type, remove executor.submit mock
- test_gemini.py: patch module-level client instead of non-existent genai.upload_file;
  translate_vtt tests use numbered-list mock responses matching new implementation
- test_tts.py: fix aiohttp async CM mock pattern; fix error message match
- test_models.py: update JobCreate to use source_is_english instead of language
- test_security.py: set jwt_access_ttl_min in token test
- test_cross_tenant_isolation.py: add patch to imports

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 14:02:04 +01:00
Vadym Samoilenko
90cbf23f0d chore: remove obsolete deploy scripts (ai-sandbox era)
Some checks failed
Deploy Backend / Deploy API to Cloud Run (push) Has been cancelled
Deploy Frontend / Build and Deploy Frontend (push) Has been cancelled
CI / Backend Lint & Test (push) Has been cancelled
CI / Frontend Lint & Test (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / Dependency Check (push) Has been cancelled
Deploy Backend / Deploy Worker to Cloud Run (push) Has been cancelled
Deploy Backend / Run Smoke Tests (push) Has been cancelled
Deploy Backend / Notify Deployment Status (push) Has been cancelled
Deploy Frontend / Notify Deployment Status (push) Has been cancelled
CI / Integration Tests (push) Has been cancelled
CI / Build Backend Docker Image (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
deploy.sh and full-deploy.sh predate the optical-dev setup and reference
old URLs/compose files. deploy-dev.sh is the single source of truth.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 12:10:57 +01:00
Vadym Samoilenko
5e55d9f27a fix(deploy): add reservations to workers in optical-dev — prevent limit < reservation OOM error
whisper-worker base has reservation 4G, optical-dev limit 2G causes Docker error.
Added explicit reservations to all three pipeline workers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 12:07:52 +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
4edd4da0b2 fix(deploy): optical-dev deploy script and Apache config ready for production
deploy-dev.sh:
- BUILD_SERVICES now includes tts-worker, ffmpeg-worker, whisper-worker (enabled
  in docker-compose.optical-dev.yml via USE_CELERY_FALLBACK=true)
- ensure_apache_modules(): enables proxy, proxy_http, proxy_wstunnel, rewrite
- Apache fragment: WS proxy (ws://) placed BEFORE HTTP /api/ proxy (required
  for correct longest-match precedence in Apache)
- Added ProxyTimeout 600 (10 min) and LimitRequestBody 2147483648 (2 GB) for
  large video uploads; disablereuse=on for WS pool correctness
- Fragment always regenerated on deploy (picks up PORT/WEBROOT changes)
- Logs command uses full $COMPOSE variable instead of hardcoded partial flags

deploy/apache-video-accessibility.conf:
- Static reference copy of the Apache fragment with inline comments explaining
  each directive

.env.production:
- Updated remaining ai-sandbox.oliver.solutions URLs to optical-dev.oliver.solutions
  (API_BASE_URL, COOKIE_DOMAIN, CLIENT_BASE_URL, AZURE_REDIRECT_URI, CORS_ORIGINS)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 11:24:40 +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
3f557724d3 feat(api): L-18 blocked-on-source, PR-10 promote-to-qc, R-12 reviewed_cues reset
- POST /{job_id}/actions/blocked_on_source (L-18): linguist/reviewer flags a source
  video issue; moves job to QC_FEEDBACK and records blocked_on_source_reason/at/by
- POST /{job_id}/actions/promote_to_qc (PR-10): production/admin manually bypasses
  AI processing for edge-case failures; adds audit history entry
- Reset reviewed_cues to 0 on submit_for_review (R-12) so reviewer must re-acknowledge
  all cues after each linguist resubmit
- Add assert_job_in_user_org + get_user_org_ids to core/dependencies.py (used by
  the new endpoints and the cross-tenant isolation test suite)
- Remove unused ingest_and_ai_task / translate_and_synthesize_task imports

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 10:38:39 +01:00
Vadym Samoilenko
ff372c7322 fix(security): close MT-17/18/19, restore cross-tenant tests, quick wins
Blocks 1–5 of stabilization plan:

SECURITY
- validation.py: restore settings.upload_max_video_bytes (T-14 regression fix)
  and JSON object key validation that was incorrectly removed
- MT-18: add accessible_org_ids filter to list_for_reviewer/list_for_linguist
  so reviewers/linguists only see jobs from their own org in QC queue
- MT-17: add Membership.team_ids[], write to it on invitation acceptance and
  direct team add/remove; migration backfills from Team.member_user_ids
- MT-19: validate all target_team_ids belong to invitation's org_id at creation

TESTS
- Restore test_cross_tenant_isolation.py (was deleted, only .pyc remained)
- Extend with MT-18 reviewer org isolation tests

QUICK WINS
- W-8: remove time.sleep(1) + dead debug block from POST /jobs (task was
  undefined — would have caused NameError → HTTP 500 on every job creation)
- T-13: warn at startup when REDIS_URL configured but connection failed
- T-16: skip language_qc lifespan migration when count=0 (no DB scan on startup)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 10:32:23 +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
ea30425a63 fix(migrations): version/description as class vars, not instance vars in Migration base
__init__ was setting self.version = "0000-00-00-000000" on every instantiation,
overriding the subclass class variable. All migrations were recorded in DB
with the default version instead of their own, causing duplicate key errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 22:16:12 +01:00
Vadym Samoilenko
89fa87ba8a refactor(docker): remove ffmpeg from api/worker images — runs on Cloud Run Jobs
Heavy pipeline tasks (ingest, translate, render, tts) now dispatch to
va-worker Cloud Run Job which has its own Dockerfile.cloudrun with ffmpeg.
API and lightweight Celery worker (notify/embed) don't need it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 22:08:25 +01:00
Vadym Samoilenko
f4a82dcf76 fix(migrations): replace relative imports with absolute in PR-7 migrations
Migration runner executes scripts outside package context — relative
imports fail. Pattern matches all other migration files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 22:05:32 +01:00
Vadym Samoilenko
1e5a07b06e fix(deploy): change API host port to 8012 (8010 also occupied)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 22:02:44 +01:00
Vadym Samoilenko
582f8ad2e8 fix(deploy): change API host port 8003→8010, move image to video-accessibility repo
Port 8003 is occupied by infra-api-1 on optical-dev server.
Artifact Registry repo renamed from nexus to video-accessibility.
cloudbuild.yaml defaults _TAG to 'latest' for manual runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 22:02:14 +01:00
Vadym Samoilenko
b3ace22009 feat(infra): move heavy workers to Cloud Run Jobs
Heavy pipeline tasks (ingest, translate, render, rerender) now dispatch
to a Cloud Run Job (va-worker) instead of local Celery workers. optical-dev
runs only api + lightweight worker (notify/embed) within its 2-CPU budget.

- backend/app/tasks/runner.py — Cloud Run Job entrypoint
- backend/app/services/cloud_run_dispatch.py — replaces .delay() for heavy tasks
- backend/Dockerfile.cloudrun — Cloud Run worker image (ffmpeg included)
- docker-compose.optical-dev.yml — 2-CPU safe overrides, disables heavy workers
- cloudbuild.yaml — builds va-worker image and updates Cloud Run Job
- deploy-dev.sh — uses 3-file compose, builds only api+worker locally
- routes_jobs, routes_admin_production, ingest_and_ai, translate_and_synthesize
  — all dispatch sites updated to use cloud_run_dispatch.dispatch()

USE_CELERY_FALLBACK=true in .env.local to use Celery locally during dev.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 21:47:10 +01:00
Vadym Samoilenko
f723e3f0bc chore(deploy): add whisper-worker, --redeploy flag, usage hints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 21:36:45 +01:00
Vadym Samoilenko
c7eaa7a952 chore: add deploy-dev.sh for optical-dev deployment
Sequential image builds (one at a time to avoid OOM), auto Apache
fragment, migrations, frontend rsync, smoke test. Flags:
  --skip-build / --skip-frontend / --skip-migrations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 21:35:19 +01:00
Vadym Samoilenko
49835f9b0c feat(pr7): final hardening — MT-11..MT-16, W-12..W-14, GCS org-prefix
Closes all remaining multi-tenant security gaps and adds production UX:

Security (MT-11/12/13/15/16):
- Cross-org assignment guard in language_qc for linguist/reviewer slots
- Remove PM/CLIENT bypass from _assert_client_access
- Bind all 8 glossary handlers to MembershipContext + OrgRole check
- Consolidate authz: get_job_or_403, assert_user_in_org, OrgScopedQuery in list_jobs
- JWT access tokens now carry org_ids hint claim (transient, not authoritative)

GCS org-prefix (MT-14):
- gcs_prefix field on Job: orgs/{org_id}/jobs/{job_id} for new jobs
- gcs_path() helper — falls back to legacy {job_id}/ for old jobs
- Rewrote 30+ hardcoded GCS path sites across tasks and routes
- Operator script tools/migrate_gcs_org_prefix.py (copy-verify-delete, resumable)

Failure recovery (W-13/14):
- Unified JobFailure schema: step/type/message/retriable/occurred_at/retry_count
- PROCESSING_FAILED status; legacy TTS_FAILED/RENDER_FAILED kept for back-compat
- Fix: translation-phase exceptions now record step="translation" not "tts"
- Generic POST /jobs/{id}/retry dispatches by failure.step
- GET /admin/production/failures + POST /admin/production/bulk-retry (cap 50)
- FailureBanner in JobDetail, failures badge in Sidebar

Job Brief workflow (W-12):
- JobBrief model + 6 CRUD endpoints (list/create/get/patch/submit/approve)
- create_job accepts brief_id Form param; copies org/deadline/project; marks FULFILLED
- BriefsList, NewBrief, BriefDetail UI; NewJob pre-fills from ?brief_id=
- Briefs badge in Sidebar for submitted briefs

Migrations: 2026-04-29-000000 (failure indexes) + 2026-04-29-000001 (job_briefs)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 20:55:50 +01:00
Vadym Samoilenko
4623b89aeb feat(mt-16): JWT org_ids claim + transient user.org_ids in deps
- create_access_token gains optional org_ids: list[str] param; encodes
  {exp, sub, org_ids, v:2} — org_ids is a prefilter hint only, never
  used as authorization source of truth (Redis cache is authoritative)
- Login, MS login, refresh endpoints: fetch memberships and include
  org_ids in issued access tokens via _get_user_org_ids() helper
- routes_invitations.py accept flow: same org_ids population on token
- get_current_user: reads org_ids from payload, attaches as transient
  user.__dict__["org_ids"] — available to OrgScopedQuery for prefilter
- Force logout: rotate JWT_SECRET env var at deployment time (no code
  change needed; all existing tokens immediately invalidated)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 20:46:39 +01:00
Vadym Samoilenko
54fcf47887 feat(mt-14): gcs_prefix on Job, gcs_path helper, rewrite path sites
- gcs_path(job, *parts) helper in gcs.py: uses job.gcs_prefix if set,
  falls back to job._id (legacy) — backward-compatible for all old jobs
- create_job: sets gcs_prefix=orgs/{org_id}/jobs/{job_id} when
  organization_id is known; legacy jobs without org get null prefix
- Rewrote hardcoded f"{job_id}/{lang}/..." paths in:
  - ingest_and_ai.py (4 upload sites)
  - translate_and_synthesize.py (9 sites via bulk regex)
  - render_accessible_video.py (3 sites: segments, video, captions)
  - rerender_accessible_video.py (3 sites)
- tools/migrate_gcs_org_prefix.py: idempotent operator script —
  preflight checks, copy→verify(count+md5)→mongo update→delete,
  ThreadPoolExecutor(4), resume file, dry-run + rollback modes

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 20:45:12 +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
595897e61a feat(w-12): JobBrief model, endpoints, migration + brief→job linkage
- JobBrief model (DRAFT→SUBMITTED→APPROVED→FULFILLED) with 6 CRUD
  endpoints: list, create, get, patch (DRAFT only), submit, approve
- All endpoints use MembershipContext; read=VIEWER, mutate=MANAGER,
  approve=ADMIN for org-scoped access
- create_job accepts brief_id Form field; validates APPROVED brief,
  copies organization_id/project_id/deadline from brief, marks brief
  FULFILLED after job insert
- organization_id now populated from project client_id on job create
  (fixes missing multi-tenant field on new jobs)
- migration_2026-04-29-000001: job_briefs collection + 4 indexes
- Wired briefs router into main.py

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 20:38:08 +01:00