PyMongo Collection raises NotImplementedError on bool(), so 'if not self.collection'
crashes on every audit log write. Changed to 'if self.collection is None'.
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>
default_factory=PyObjectId produced "" (empty string) since
Annotated[str, ...] is a type annotation, not a callable factory.
Replace with lambda: str(ObjectId()) to generate a real unique ID.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
poetry.lock was out of sync with pyproject.toml (cost-tracker and
glossary deps added since last lock). Regenerated with Poetry 2.1.4.
Also updated Dockerfile.whisper-service from poetry==1.8.2 to 2.1.4
to match the main Dockerfile and avoid format incompatibility.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
poetry.lock was generated with 2.1.4 — using 1.8.2 caused
incompatible lock file error and failed Docker builds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The function was copy-pasted identically in ingest_and_ai.py and
translate_and_synthesize.py. Extracted to tasks/_websocket_bridge.py
as the single definition; all four task modules now import from there.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- login and microsoft_login routes now use Depends(get_database) instead
of creating a per-request MongoClient — removes connection-pool churn
under load
- MicrosoftAuthService._get_openid_config/_get_jwks/validate_token are
now async, using httpx.AsyncClient instead of blocking requests.get —
removes ~400ms event-loop block per Microsoft login
- Removed unused AsyncIOMotorClient import from routes_auth.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
seed_default_admin now skips creation and logs a warning when
DEFAULT_ADMIN_PASSWORD is unset instead of falling back to the
hardcoded ChangeMe123! value. Existing-admin promotion path is
unaffected. Added DEFAULT_ADMIN_PASSWORD to .env.prod.example.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced the bare except that leaked str(e) (JWT library internals,
claim validation messages) with a generic "Invalid refresh token" detail.
Full traceback is now logged server-side via the structured logger.
Re-raises HTTPException before the generic handler so valid 401s from
inner checks are not double-wrapped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
get_current_user and get_current_user_optional now reject any token
whose payload carries type="refresh". Access tokens carry no type field
so the check is asymmetric and safe. Prevents a refresh-cookie value
from being replayed as a Bearer access token.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removed /api/v1/auth/login from the rate-limit bypass list in both
rate_limiting.py and main.py. The existing 5-req/5-min limit for the
login endpoint was already configured but never applied.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
user.role stored as plain string in MongoDB — calling .value on it
caused AttributeError on every login, blocking all auth.
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>
authz.py (new):
- MembershipContext — per-request membership dict for the current user
- get_membership_context FastAPI dependency
- require_org_role(min_role) — dependency factory keyed off org_id path param
- require_platform_admin()
- OrgScopedQuery — adds organization_id filter; platform admin passes through
- bump_user_membership_cache — invalidates Redis key on membership writes
dependencies.py:
- get_accessible_project_ids now queries memberships collection first;
legacy pm_client_ids / team.member_user_ids fallback retained until migration runs
(four job-route access checks at lines 608/1054/1181/1538 are fixed via this function)
routes_clients.py:
- _assert_pm_or_admin and _assert_client_access are now async and query memberships
- All 10 call sites updated with await + db arg
emailer.py:
- Switched from SendGrid to Mailgun REST API via httpx (already in requirements)
- _send() is now fully async; same public method signatures preserved
- send_completion_email uses _send()
config.py:
- Added mailgun_api_key, mailgun_domain, mailgun_from settings
- sendgrid_api_key kept with empty default for backward compat
migration_2026-04-28-000003:
- Backfills job.organization_id from project.client_id
- Creates (organization_id, status, created_at) sparse index on jobs
routes_organizations.py / routes_invitations.py:
- Call bump_user_membership_cache after every membership write
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>
- New migration updates MongoDB users collection validator to accept
project_manager role and pm_client_ids field
- full-deploy.sh was missing the run_migrations step entirely; added it
after rebuild_containers so new role/field validators apply on every deploy
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>
- New services/cost_tracker.py: sync httpx preflight()/record() + async wrappers;
BudgetExceeded exception; no-op when COST_TRACKER_BASE_URL is empty
- Preflight budget check added before ingestion (Gemini), per-language translation
(video-native + traditional), and per-language TTS dispatch
- _record_gemini_usage and _record_tts_cost now call cost_tracker directly;
removes broken asyncio.get_event_loop() hack from sync Celery worker
- Fix: _cost_ctx now threaded into extract_accessibility_targeted (video-native path)
- Fix: user_id/cost_project_id now propagated through dispatch_language_tts →
synthesize_cue_task.s() and the rerender_accessible_video.py re-render path
- Remove oliver-cost-tracker SDK dependency (was commented-out/never installed)
- Drop cost_tracker_outbox_path setting and get_cost_tracker() factory
- Update COST_TRACKER_BASE_URL default to optical-dev.oliver.solutions in
.env.prod.example, docker-compose.yml, and all Cloud Run service yamls
- Cloud Run yamls use Secret Manager ref (cost-tracker-api-key) for the API key
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>
Was checking backend/.git and frontend/.git (submodule pattern) which
never exists — git pull silently skipped, deploying stale code.
Now pulls root repo first, falls back to submodule pattern.
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>
Without charset specification, browsers/tools interpret text/vtt as
Latin-1, causing UTF-8 multi-byte characters like ♪ (U+266A) to render
as garbled text (♪).
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>
Microsoft can return different email casings for the same user (e.g.
VadymSamoilenko@... vs vadymsamoilenko@...). The previous case-sensitive
find_one would miss the existing user, then fail on insert_one with a
duplicate key error on the _id field (ms-{sub[:20]}).
Fix: look up by _id first (deterministic from Microsoft sub), then fall
back to case-insensitive email regex for local-to-Microsoft migrations.
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>