Commit graph

412 commits

Author SHA1 Message Date
Vadym Samoilenko
fb99a5e8c7 feat(vtt): add note field to VttUpdateRequest and wire through create_version calls
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
2026-05-14 11:44:07 +01:00
Vadym Samoilenko
07d2112e53 fix(cost): use new_event_loop pattern for Whisper cost tracking (matches ingest_and_ai.py) 2026-05-14 11:43:20 +01:00
Vadym Samoilenko
922cb9318e feat(cost): add Whisper transcription cost tracking
Records audio_duration (as chars) + latency_ms to cost tracker after each
successful transcription; wrapped in try/except so it never fails the task.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:42:17 +01:00
Vadym Samoilenko
cff62c51ff fix(audit): add details to submit_brief and approve_brief audit calls 2026-05-14 11:41:22 +01:00
Vadym Samoilenko
b24f7a9a0f feat(audit): add audit logging to brief and share routes
Adds BRIEF_CREATE/UPDATE/SUBMIT/APPROVE audit calls to routes_briefs.py
and SHARE_TOKEN_CREATE/REVOKE/SHARE_CLIENT_DECISION to routes_share.py;
public client_decision endpoint passes user=None per convention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:40:19 +01:00
Vadym Samoilenko
11bf08a29d feat(audit): add audit logging to org and invitation routes
Adds audit log entries for all write endpoints in routes_organizations.py
(ORG_CREATE, ORG_UPDATE, ORG_MEMBER_ADD, ORG_MEMBER_UPDATE, ORG_MEMBER_REMOVE)
and routes_invitations.py (INVITATION_CREATE, INVITATION_REVOKE, INVITATION_ACCEPT).
The public accept endpoint passes user=None per the no-auth contract.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:37:43 +01:00
Vadym Samoilenko
42a0c8acb1 fix(audit): deactivate_client details + non-raising audit insert in service 2026-05-14 11:35:40 +01:00
Vadym Samoilenko
bd1dd69467 feat(audit): add audit logging to client management routes
All 13 write endpoints in routes_clients.py now emit audit log entries
(CLIENT_CREATE, CLIENT_UPDATE, CLIENT_DEACTIVATE, CLIENT_PM_ASSIGN/REMOVE,
CLIENT_TEAM_CREATE/UPDATE/DELETE, CLIENT_TEAM_MEMBER_ADD/REMOVE,
CLIENT_PROJECT_CREATE/UPDATE/ARCHIVE). request: Request added to each
endpoint signature; resource_name and relevant details included in every call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:33:58 +01:00
Vadym Samoilenko
82d438df7c fix(audit): remove per-cue audit noise from mark_cue_reviewed endpoint 2026-05-14 11:31:37 +01:00
Vadym Samoilenko
7bba8256ce feat(audit): add audit logging to language QC routes
Adds audit_logger.log_action calls to all 13 write endpoints in
routes_language_qc.py using existing AuditAction enum values. Also
adds missing http_request: Request parameter to mark_cue_reviewed.
2026-05-14 11:30:28 +01:00
Vadym Samoilenko
000e99c2d0 feat(audit): add missing AuditAction enum values for clients, orgs, invitations, QC, briefs, share 2026-05-14 11:28:30 +01:00
Vadym Samoilenko
700347857a chore: ignore .worktrees directory 2026-05-14 11:27:13 +01:00
Vadym Samoilenko
3b31012901 fix(vtt): strip cue settings from end timestamp in parse_ad_cues
tts_synthesis.parse_ad_cues() was passing "00:00:02.500 line:0%" to
_parse_timestamp() — cue settings were not stripped from the end-time part
of the timing line. Split on whitespace and take first token only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 19:18:02 +01:00
Vadym Samoilenko
f22d568fc5 fix(security): fix false-positive injection blocks on French/multilingual VTT content
- Remove ';' from command-injection pattern — semicolons are common in French
  and other European languages, not a shell injection risk in JSON context
- Skip security pattern scanning for free-text fields (captions_vtt,
  audio_description_vtt, notes, etc.) — natural language always generates
  false positives against injection regexes
- Add GET/HEAD to GCS CORS config so browsers can load signed VTT URLs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 19:11:01 +01:00
Vadym Samoilenko
4645e67611 fix(glossary-list): show real embedding progress in glossary list view
- Add current_version_embedding_status/embedded_count/term_count to GlossaryResponse
- Batch-fetch current versions in list endpoint (single extra query, not N queries)
- Add get_versions_by_ids() helper to glossary_service
- Fix GlossaryList.tsx: embeddingBadge('') → embeddingBadge(g) with real status + pct

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 19:00:56 +01:00
Vadym Samoilenko
e70a67718e fix(glossary): hard-delete glossary with cascade on archive
archive_glossary() now deletes terms, versions, and the glossary document
instead of soft-deleting. Prevents orphaned 34k-term datasets from consuming
embedding quota and storage after a glossary is removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 18:44:51 +01:00
Vadym Samoilenko
6bf88474ee feat(embed): switch embeddings to Vertex AI text-multilingual-embedding-002
Replace AI Studio gemini-embedding-001 with Vertex AI text-multilingual-embedding-002
via google-genai SDK (vertexai=True). Vertex AI uses ADC (already configured) and
has significantly higher per-project quotas than AI Studio per-user limits.
Same 768-dim output; multilingual model better suited for 50+ language glossaries.
Add gcp_location config field (default us-central1).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 18:41:32 +01:00
Vadym Samoilenko
7a7b6c1c12 fix(embed): respect Gemini 429 retryDelay and reduce concurrency
- Parse retryDelay from 429 error body and sleep for server_delay+1s
  instead of our own 2s/4s backoff (which was shorter than API requires)
- Reduce embed concurrency 5→2 to halve burst when multiple glossary
  versions embed simultaneously
- Increase max_retries 3→5 and initial backoff 2s→8s for headroom

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 18:07:22 +01:00
Vadym Samoilenko
ca312d48fa chore(lint): fix all ruff errors — 0 warnings remaining
- B904 (55): add `from err` / `from None` to raise-in-except across 13 files
- F821 (1): add missing HTTPException import in routes_language_qc.py
- F841 (7): remove unused variable assignments (current_user, job_title, tts_provider, etc.)
- W293 (13): strip trailing whitespace from blank lines
- C416 (4): rewrite unnecessary dict comprehensions as dict()
- C401 (1): rewrite unnecessary generator as set comprehension
- E701 (4): split multi-statement lines in cost_tracker.py
- E741 (1): rename ambiguous `l` to `lang` in cloud_run_dispatch.py
- B007 (4): prefix unused loop variables with _ in tts.py, video_renderer.py
- I001 (1): sort imports in tasks/__init__.py (move stdlib to top)
- E402 (3): move threading/time/signals imports to top of tasks/__init__.py
- UP042 (9): replace (str, Enum) with StrEnum in all model/schema enums

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 17:13:08 +01:00
Vadym Samoilenko
16000a8bd9 fix(glossary,vtt): 4 bugs — locale fallback, ingestion source, cue settings, overlap on save
- glossary_service: _get_translation now handles bare→specific fallback (fr→fr-FR);
  previously only specific→bare worked, causing zero term matches when job uses
  bare locale codes ("fr") but XLSX has region columns ("fr_fr" → "fr-FR")
- ingest_and_ai: use title + brand_context as glossary source text; previously
  empty brand_context caused glossary to be skipped entirely during AI ingestion
- routes_jobs.py: apply fix_overlapping_cues before validating PATCH /vtt;
  mirrors what AI generation already does — prevents save errors for minor overlaps
- frontend/vtt.ts: preserve raw cue settings (line:0%, align:end, etc.) through
  parse→build round-trip; previously settings were parsed into positionTop flag
  only and dropped on serialization, losing caption positioning after edit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 16:58:13 +01:00
Vadym Samoilenko
69eff9ca9d chore(deps): regenerate poetry.lock after google-cloud-texttospeech upgrade
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 18:38:36 +01:00
Vadym Samoilenko
76bee82119 fix(pipeline): fix 5 QA tickets — caption alignment, glossary, source_has_ad render, filler words, NL error surfacing
- caption_aligner: lower match ratio 0.5→0.35, widen search window 60→150, add time-based cursor fallback on miss
- gemini.py: explicit 'MUST use glossary terms' requirement in translate_vtt prompt; source_has_ad prompt now instructs not to include AD narration in captions
- ingest_and_ai: load glossary for source language and pass to extract_accessibility
- render_accessible_video: handle source_has_ad=True via caption-embed path (ffmpeg subtitle inject, no AD pipeline)
- translate_and_synthesize: track failed languages, write translation_errors to DB, add exc_info to error log
- vtt.py: expand _FILLER_PATTERNS to nl/pt/pl/uk/ru, widen EN/ES/FR/DE/IT lists
- gemini_ingestion.md: strengthen line:0% placement rule, expand disfluency examples per language

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 18:36:59 +01:00
Vadym Samoilenko
f7708f0214 chore(deps): upgrade google-cloud-texttospeech to ^2.36.0
2.27.0 (previously locked) lacks VoiceSelectionParams.model_name field
required for Gemini TTS model selection via Cloud TTS API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 17:26:30 +01:00
Vadym Samoilenko
c380a96c72 refactor(tts): switch Gemini TTS from AI Studio API to Cloud TTS API
The AI Studio API (generativelanguage.googleapis.com) enforces a hard 10 RPM
quota on preview models regardless of billing tier. Switching to Cloud TTS API
(texttospeech.googleapis.com) with the same Gemini models uses a separate,
production-grade quota that scales on paid plans.

Changes:
- Replace genai.Client + generate_content(AUDIO) with texttospeech.TextToSpeechClient
- Style prompt now goes to SynthesisInput.prompt (dedicated field, not prepended text)
- Speed goes to AudioConfig.speaking_rate (no longer encoded in prompt text)
- Cloud TTS returns MP3 directly — remove PCM→MP3 lameenc conversion
- config: update pro model from gemini-2.5-pro-preview-tts → gemini-2.5-pro-tts (GA)
- Service account already has roles/aiplatform.user (granted today)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 17:16:32 +01:00
Vadym Samoilenko
95dbed03bd fix(tts): respect API retryDelay on 429 instead of short exponential backoff
Gemini TTS allows 10 RPM; with concurrency=8 the rate limit is hit quickly.
The previous backoff (1-3s) was far too short — the API returns retryDelay ~37s.
Both synthesize_cue_task (Celery retry countdown) and GeminiTTSService
(_synthesize_cue_with_retry sleep) now parse the retryDelay from the 429
error message and use it (+ 5s buffer) instead of the exponential guess.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 17:04:45 +01:00
Vadym Samoilenko
39a9d62b06 fix(qc): dispatch TTS+render for source-only jobs when accessible_video_mp4 is requested
When EN is approved on a source-only job (no target languages), the translation
branch was skipped entirely, leaving the accessible video render pipeline never
dispatched. Added elif branch: if accessible_video_mp4 is requested and there
are no target languages to translate, dispatch translate_and_synthesize_task
(which will skip translation, run TTS for source language, and dispatch the
render task).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 16:58:39 +01:00
Vadym Samoilenko
36b3b3e47c fix(ui): correct sdh field name to sdh_vtt in job detail outputs
Used wrong field name sdh_captions instead of sdh_vtt, causing
TypeScript build failure on optical-dev.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 14:16:53 +01:00
Vadym Samoilenko
8598852da1 fix(ui): show all 5 requested output types in job detail
accessible_video_mp4 and sdh_captions were missing from the
Outputs section render — only 3 of 5 fields were displayed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 14:14:50 +01:00
Vadym Samoilenko
77a4eb10e0 fix(auth): await get_redis() coroutine in membership cache
get_redis() is an async function but was called without await in
_cached_memberships(), causing RuntimeWarning and silently bypassing
the Redis membership cache on every request — all membership lookups
were hitting MongoDB instead of cache.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 13:57:57 +01:00
Vadym Samoilenko
5a93bdc1b6 fix(tts): run TTS pipeline when accessible_video_mp4=True even if audio_description_mp3=False
Per-cue MP3s (ad_cue_manifest) are required by render_accessible_video_task regardless
of whether the assembled ad.mp3 is requested as a client download. Previously, jobs with
accessible_video_mp4=True but audio_description_mp3=False would silently skip TTS, leaving
render tasks never dispatched and jobs stuck in tts_generating indefinitely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 13:53:49 +01:00
Vadym Samoilenko
c8a610b3f7 fix(vtt): auto-fix overlapping cues from AI-generated output
Gemini occasionally produces captions where a cue's start_time is
earlier than the previous cue's end_time. Add VTTEditor.fix_overlapping_cues()
that trims each cue's end_time to 1ms before the next cue's start, applied
to both captions and AD VTT immediately after AI generation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 13:23:08 +01:00
Vadym Samoilenko
3371466e10 fix(ui): hide error banner for non-failed job statuses
job.error persists after retry succeeds — only show it when status is
tts_failed / render_failed / processing_failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 13:12:31 +01:00
Vadym Samoilenko
cff1b35aa0 fix(gemini): fallback on empty response (response.text is None)
Gemini occasionally returns response.text=None under load or safety filters.
Treat it as a retriable error so the fallback chain is used.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 12:54:10 +01:00
Vadym Samoilenko
796cd85a1d fix(gemini): include 503 UNAVAILABLE in fallback retry condition
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 12:38:26 +01:00
Vadym Samoilenko
e2391e2603 fix(gemini): correct fallback model ID + graceful downloads for failed jobs
- gemini-3.1-flash-preview doesn't exist; replace with gemini-3-flash-preview
- GET /jobs/{id}/downloads: return empty {} instead of 400 when job has no
  outputs (e.g. processing_failed before AI stage completes)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 12:32:39 +01:00
Vadym Samoilenko
56a3a62368 feat(gemini): add model fallback chain on 429 quota errors
Routes all generate_content calls through _generate() which retries
gemini-3.1-flash-preview then gemini-2.5-pro when primary model hits
RESOURCE_EXHAUSTED. Cost tracker records actual model used.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 12:02:59 +01:00
Vadym Samoilenko
f38325b461 fix(tts): scope retranslation TTS to target language only
When retranslate=True, _generate_tts_for_languages was receiving
the full outputs dict (all 9 languages) and regenerating TTS + render
for every language on every single-language retranslation task.
That multiplied API calls by 8x and triggered unnecessary renders.

Now passes only the target language outputs when retranslate=True.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 16:57:20 +01:00
Vadym Samoilenko
b873f0af6d fix(translation): use per-language dot-notation to prevent race condition
concurrent retranslation tasks (concurrency:2) were each replacing the
entire outputs doc, so the last writer silently overwrote the others.
Now each task only writes outputs.<lang> for the languages it processed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 16:45:28 +01:00
Vadym Samoilenko
865473937f feat(qc): bulk retranslate broken languages button
Adds "↺ Retranslate broken (N)" button in the Languages panel header.
Visible to production/admin when EN is approved and there are languages
with video_native origin or missing captions_vtt_gcs.
Confirm modal shows each broken language with its failure reason,
then queues individual retranslation tasks sequentially.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 16:41:55 +01:00
Vadym Samoilenko
290d5e32e6 fix: 7 caption/AD quality bugs + retranslation error handling
Bug fixes:
- Bug 1a: source_has_ad flag prevents AI generating AD over existing professional AD;
  JobBrief/Job models, gemini service prompt conditional, NewBrief UI checkbox
- Bug 1b: disable native textTracks on video element to prevent double captions
- Bug 2: caption ALL audible speech including off-screen narrators (prompt fix)
- Bug 3: DCMP §6.01 disfluency removal for EN/ES/FR/DE/IT (prompt + post-pass)
- Bug 4: VTT cue settings (line:0%, position:) preserved through parser round-trip
- Bug 5: Whisper word-level timestamp alignment via new caption_aligner service
- Bug 6: assert_cue_alignment used .start/.end; renamed to .start_time/.end_time
- New migration: backfill source_has_ad=False on existing jobs and job_briefs

Also fix retranslation error handling to preserve existing GCS URIs on failure
so video_native captions remain accessible if retranslation fails.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 15:38:20 +01:00
Vadym Samoilenko
00dd1643f5 docs(help): add screenshot to PM EN-first pipeline section
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
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 12:47:50 +01:00
Vadym Samoilenko
c3835843db docs(help): document EN-first translation pipeline for all roles
Add §6 EN-First Translation Pipeline to Production, Linguist, Admin, and
Project Manager guides explaining the new flow: translations are generated
only after English QC is approved, preserving 1:1 cue structure.

Documents origin badges (⚠ video-native), the amber TranslationGateBanner
on target-language cards, the ↺ Re-translate from EN button, and the
blue info note on the New Job form. Adds 5 new screenshots captured from
the deployed optical-dev environment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 12:42:58 +01:00
Vadym Samoilenko
4ba489eaaa docs(help): document EN-first translation pipeline for all roles
Add §6 EN-First Translation Pipeline to Production, Linguist, Admin, and
Project Manager guides explaining the new flow: translations are generated
only after English QC is approved, preserving 1:1 cue structure.

Documents origin badges (⚠ video-native), the amber TranslationGateBanner
on target-language cards, the ↺ Re-translate from EN button, and the
blue info note on the New Job form. Adds 5 new screenshots captured from
the deployed optical-dev environment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 12:42:03 +01:00
Vadym Samoilenko
f99be62256 fix(briefs): replace video_native with traditional in NewBrief default
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 12:21:29 +01:00
Vadym Samoilenko
c74fde4f40 fix(translation): approve_source triggers translation for any role
approve_source is callable by any qualified role (reviewer, linguist,
production, admin) — not just linguists. Now correctly dispatches the
translation pipeline when target languages are untranslated, regardless
of who approves the source. Without this fix, only language_qc.approve_language
(EN path) would trigger translation, leaving other roles stuck.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 12:13:39 +01:00
Vadym Samoilenko
fddf803b74 feat(translation): enforce EN-first pipeline with cue-preserving translations
All translations now derive strictly from the approved English master VTT,
eliminating the cue-count and timestamp drift reported by linguists
(e.g. PL AD = 11 cues vs EN AD = 17 cues).

Key changes:
- Remove video_native translation mode entirely; all languages go through
  translate_vtt() which guarantees 1:1 cue alignment with EN master
- Transcreation languages now use translate_vtt(style="transcreate") —
  same cue-preserving contract, culturally-adapted instructions
- Post-translation cue alignment validator added (VTTEditor.assert_cue_alignment)
- After ingestion, job moves to PENDING_QC (EN-only) instead of TRANSLATING;
  translation pipeline dispatches automatically when EN QC is approved
- New POST /jobs/{id}/retranslate-language endpoint for PM/admin to fix
  legacy video_native jobs on demand
- Frontend: origin badge (EN-aligned / transcreated / video-native warning),
  EN-first gate banner on target-language cards, Re-translate from EN button
  with confirm modal, removed translation mode selector from NewJob

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 12:11:35 +01:00
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
3a2bbc9ca0 fix(membership): correct \$unwind option preserveNullAndEmpty → preserveNullAndEmptyArrays
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>
2026-05-01 18:58:07 +01:00