obsidian/01 Projects/video-accessibility/Full-Implementation-Plan.md
2026-04-30 11:36:34 +01:00

55 KiB
Raw Blame History

title date tags aliases status
Video Accessibility — Full Audit & Implementation Plan 2026-04-29
video-accessibility
saas
plan
snoopy-watching-fountain
in-progress

Staff Workflow & Multi-Tenancy Audit — Video Accessibility Platform

Branch state (на момент аудита)

  • Текущая ветка: main (HEAD a168af1, 2026-04-29)
  • origin/dev — строгий предок main (HEAD 02c4240). Ничего уникального на dev нет — git merge-base --is-ancestor origin/dev main → true.
  • Все недавние фичи (two-stage QC, project picker, comments, email notifications, deadlines) уже на main.
  • Working tree: modified .DS_Store, .env.local, .env.production; untracked .claude/. Ничего из этого коммитить не надо.

Решение по веткам перед началом работы:

  • Все PR из плана делать feature-branches от main (e.g. feat/multi-tenant-isolation).
  • dev либо привести к main (git push origin main:dev после согласования), либо игнорировать — на сегодня бесполезен.

Context

Платформа позиционируется как SaaS для нескольких компаний (organizations) и команд (teams). Реальные пользователи — сотрудники: PM, переводчик (linguist), ревьюер, production. CLIENT — это только viewer, получающий read-only доступ к расшаренному проекту/таске. Только ADMIN видит всё.

Полный список ролей: ADMIN, PROJECT_MANAGER, PRODUCTION, REVIEWER, LINGUIST, CLIENT.

Разделение ответственности:

  • ADMIN — платформенный оператор (все организации, биллинг, импертонатор)
  • PROJECT_MANAGER (PM) — владелец отношений с клиентом: создаёт проекты, назначает команду, держит дедлайны, делает финальный ревью, доставляет клиенту
  • PRODUCTION — операционная роль: загружает видео, настраивает outputs/TTS/method, мониторит AI-пайплайн, ловит фейлы (Retry TTS / Retry Render), делает bulk-операции (delete/reprocess)
  • REVIEWER — вторая стадия QC после linguist'а
  • LINGUIST — первая стадия QC: правит per-language VTT, использует глоссарий
  • CLIENT — read-only viewer расшаренного контента (вне основного UI)

Аудит покрыл:

  1. Multi-tenant isolation — есть ли утечки данных между организациями/командами
  2. PM workflow — создание клиента/проекта, назначение команд, мониторинг, финальный ревью, доставка
  3. Production workflow — upload, конфигурация джобы, мониторинг пайплайна, восстановление после фейлов
  4. Linguist workflow — назначение, очередь, VTT-редактор, глоссарий, версионирование
  5. Reviewer workflow — двухстадийный QC, апрув/реджект, диффы

Найдено 100+ дефектов. Главный архитектурный вывод: изоляция по тенантам в коде сегодня дырявая — staff-роли (LINGUIST/REVIEWER/PRODUCTION) видят джобы, файлы, аудит, ноты ВСЕХ компаний из-за того, что get_accessible_project_ids для них возвращает None (что значит "без ограничений"). До запуска SaaS это надо чинить.

Сквозной критерий редизайна: программа, интерфейс и последовательность действий должны быть логичными, последовательными и удобными для каждой роли при ежедневной работе. Каждая правка ниже мерится тремя вопросами:

  1. Логично ли? — интуитивно ли понятен следующий шаг? нет ли скрытых блокеров?
  2. Последовательно ли? — один и тот же объект выглядит/называется/ведёт себя одинаково везде?
  3. Удобно ли? — для частых действий минимум кликов, минимум перепрыгивания между страницами, минимум контекст-свитчинга?

Идеальный flow по ролям (сквозная карта)

Описывает, как должна выглядеть последовательность действий после фикса. Это контракт UX.

PM (Project Manager) — день в жизни

Login → /dashboard/pm
  ├─ Виджеты: "Pending my final review (3)", "Overdue (1)", "Stuck (0)", "New comments (5)"
  ├─ Каждая карточка кликается → переход прямо к пулу нужных джоб
  └─ Sidebar badges: "Final (3)", "Comments (5)", "Awaiting upload (2)"

Создать новый проект для клиента
  Clients → My clients (только своя org) → клиент X → "+ New Project"
  Заполнить: name, default_languages, default_linguist, default_reviewer,
             default_deadline_days, default_outputs, brand_context, glossary attach
  → Project создан → "Brief Production" (опц. note + due date) или "Upload now myself"

Назначить команду (per-language)
  Project page → languages list → для каждого языка: linguist + reviewer + deadline
  Авто-fill из project defaults; bulk apply кнопкой
  Team matrix может быть заполнена ДО того как Production загрузил видео

Мониторинг
  /jobs со фильтрами: "Pending my action" / "Overdue" / "In Production" / "Failed"
  Колонки: Title | Project | Status | Assigned (avatars) | Deadline | Updated
  Sort: by deadline asc по умолчанию
  Действия: reassign / extend deadline / return to QC / message Production

Финальный ревью
  Sidebar "Final Review (3)" → /admin/final
  Открыть джобу → preview всех языков (player с language switcher) → notes →
    "Approve & Notify Client" (preview email) | "Send back to <linguist|reviewer>"

Доставка
  После approve → Notify Client UI: 
    edit subject/body, CC, выбрать assets, "Send" / "Generate share link instead"

Главные принципы:

  • PM не загружает видео в типичном кейсе — это делает Production. PM может, но это не основной путь.
  • Назначения и дедлайны можно настроить ДО того как Production обработал AI.
  • Один клик из дашборда в actionable список.
  • Нет скрытых модалок 2-уровня вложенности (текущий QCDetail-модалки assign — нарушение).

Production — день в жизни

Login → /dashboard/production
  ├─ Виджеты: "Awaiting upload (briefs from PM) (4)", "AI processing (2)",
  │           "Failed — needs my action (1)", "Pending QC handoff (3)"
  ├─ Каждый виджет → actionable пул
  └─ Sidebar badges: "Briefs (4)", "Failures (1)"

Получение брифа от PM
  Inbox: "Brief from PM Vadym for Client Acme / Project Q4 — 5 videos, due Friday"
  Click → /jobs/new pre-populated:
    client+project locked, languages/outputs/TTS из project defaults, deadline скопирован
    Если PM уже назначил team — видны avatars (informational, нельзя менять без PM)

Загрузка
  /jobs/new (или /projects/{id}/upload):
  ├─ Multi drop-zone — все 5 файлов сразу
  ├─ Per-file: title (auto от filename), brand context override
  ├─ Outputs матрица — общие на всю партию (или override per-file)
  ├─ Languages — pre-filled, чекбоксы
  ├─ TTS prefs — collapsible, дефолт из project
  ├─ Accessible Video method (pause_insert / overlay) — collapsible
  └─ Submit → 5 джоб созданы и поставлены в очередь AI

Мониторинг пайплайна
  /jobs?my_org → group by status, real-time updates через WS
  Виджет "AI processing" / "Translating" / "Rendering" с прогрессом
  Failure появляется → toast + email + sidebar badge

Восстановление после фейла
  Click failed job → "Retry TTS" / "Retry Render" с подсказкой о причине
  Если фейл системный (Gemini quota / TTS API down) — модалка "Retry all failed"
  Audit log entry с reason

Передача в QC
  AI завершён → автоматический переход в pending_qc, linguists получают email
  Production видит "Pending QC handoff" → может ничего не делать (auto)
  Если PM не назначил team — Production может назначить (PROD имеет это право)

Главные принципы:

  • Production — операционная роль, главная метрика — пропускная способность (jobs/day, failure rate).
  • Bulk-операции в первую очередь для Production (загрузить 50 файлов разом, retry 10 failed разом).
  • Real-time видимость пайплайна — без ручного refresh.
  • Granular failure visibility — точное место и причина (не "что-то сломалось").

Linguist (translator) — рабочий день

Login → /qc/queue (роль-aware redirect)
  Очередь: rows по {Job, Language, Status, Deadline}
  Sort: by deadline asc, статусные группы (Pending → Rejected → In Progress → Submitted)
  Badge в sidebar = pending count

Открыть задачу
  Click row → /qc/{job_id}?lang=es&pane=editor
  Layout: video player (left) | VTT editor (right) | comments+glossary (bottom drawer)
  Глоссарий: preferred term подсвечен в cue text, hover = "preferred translation"

Редактирование
  Click cue → inline edit text + start/end time
  Hotkeys: Cmd+Enter save, ]/[ next/prev cue, Cmd+G open glossary, Cmd+P play cue
  CPS warning > 20 — ненавязчивый бейдж рядом с cue
  Save = autosave (debounced 30s)
  "Save as Version" с note — milestone

Submit
  Когда все cues просмотрены / done → "Submit for Review"
  Поле "Comments for reviewer" (optional)
  → перевод уходит reviewer'у, в моей очереди статус "In Review"

Что-то пошло не так с источником
  "Block on source issue" → выбрать reason → ноту PM'у
  Джоба этого языка уходит в "Blocked", в очереди отдельный таб

Следующая задача
  В редакторе кнопка "Save & Next" — переходит к следующему pending в моей очереди
  Без возврата на queue вручную

Главные принципы:

  • Linguist не должен видеть других тенантов / других чужих языков — фильтруем строго.
  • Глоссарий всегда под рукой (drawer / sidebar).
  • "Next" — главный навигатор, не back-button.

Reviewer — рабочий день

Login → /qc/review-queue
  Очередь: только то что linguist уже submit (status=pending_review)
  Бэйдж в sidebar = pending review count

Открыть задачу  
  Layout: video | VTT editor | "Diff vs AI baseline" toggle | comments
  Slider "Mark cue as reviewed" на каждой cue (или auto on focus)
  Прогресс "47/120 cues reviewed" сверху

Действия
  - Approve (требует reviewed >= 80% cues + confirmation modal)
  - Reject → выбрать категорию (Timing / Mistranslation / Terminology / Profanity / Length) + textarea + cues to flag
  - Comment to linguist на конкретную cue
  - Edit cue inline (минорные правки) — отдельная история, не клобирует linguist save

После approve
  Авто next-in-queue

Главные принципы:

  • Approve кнопка появляется только когда читал.
  • Reject — структурный (категории), чтобы можно было вычислить топ-проблемы.
  • "Diff vs AI baseline" — главный инструмент, должен быть на видном месте.

Client (read-only viewer) — единственный сценарий

Получает email/Slack ссылку: https://.../share/{token}
  → Public read-only страница (БЕЗ логина или с lightweight email-verify)
  → Видит preview видео с captions, language switcher, download buttons
  → "Request revision" → форма (без правок в системе, создаёт ноту PM)

CLIENT-роль внутри основного приложения должна перестать существовать как навигация. Только share-страница.

ADMIN — суперпользователь

/admin → cross-org дашборд
  - Все организации, метрики использования, биллинг (если есть)
  - Audit log по всем org (с фильтром per-org)
  - User management cross-org
  - Импертонатор: "Login as user X" с warning баннером

P0 — Multi-tenancy: критичные утечки данных между организациями

Любой залогиненный staff (LINGUIST / REVIEWER / PRODUCTION) сегодня видит данные всех тенантов. Это главный блокер SaaS-запуска.

MT-1. get_accessible_project_ids для staff-ролей возвращает None (= "без фильтра")

  • Файл: backend/app/core/dependencies.py:118-119, 160-161
  • Эффект: все listing-эндпоинты (GET /jobs, GET /clients/{id}/projects, bulk action handlers) теряют org-фильтр для linguist/reviewer/production.
  • Фикс: возвращать [] (= "ничего") если у пользователя нет explicit membership; админ — отдельная ветка return None (всё). Staff обязан иметь membership в org/team.

MT-2. GET /jobs без organization_id фильтра для staff

  • Файл: backend/app/api/v1/routes_jobs.py:509-548
  • Эффект: linguist в org A видит весь список джоб org B.
  • Фикс: во всех list-запросах добавить {"organization_id": {"$in": user.org_ids}}.

MT-3. GET /jobs/{id} и detail-эндпоинты не проверяют owning org

  • Файлы: routes_jobs.py:603-623, 1075-1096, 1210-1221, 1602-1615
  • Эффект: угадан/слит job_id → staff качает source.mp4, VTT, AD из другого тенанта.
  • Фикс: ввести assert_job_in_user_org(job, user) helper и вызывать в КАЖДОМ detail-эндпоинте. Добавить тест test_cross_tenant_access.py.

MT-4. Bulk endpoints без membership-чека

  • Файлы: routes_jobs.py:196-256 (delete), 258-338 (approve), 341-402 (return-to-qc), 413-506 (bulk download)
  • Эффект: PRODUCTION/REVIEWER может массово удалить/апрувнуть джобы любого тенанта.
  • Фикс: в цикле перед мутацией — проверять job.organization_id in user.org_ids иначе skip + log warning.

MT-5. Модели без organization_id

  • Файлы: models/job.py:227, models/review_note.py:9-25, models/vtt_version.py:15-32, models/audit_log.py:91-133, models/glossary.py:26-38
  • Эффект: даже если поправить authz, фильтровать нечем — у документов нет тенант-маркера.
  • Фикс: миграция: добавить organization_id (denormalized из clients/project), индекс {organization_id: 1, created_at: -1} на jobs/audit_logs/review_notes. Переписать запросы через OrgScopedQuery из authz.py:_ (уже частично существует).

MT-6. Review notes / VTT versions без org-фильтра

  • Файлы: routes_review_notes.py:25-53, routes_vtt_versions.py:21-32, 35-61, 64-81, 89-108
  • Эффект: комментарии и история VTT любого тенанта читаются по job_id.
  • Фикс: в каждом handler — await assert_job_in_user_org(job_id, user) первым шагом.

MT-7. WebSocket subscribe без membership check

  • Файл: backend/app/services/websocket.py:93-117, routes_websockets.py
  • Эффект: любой залогиненный юзер может подписаться на ws://…/job/{any_job_id} и слушать события другого тенанта.
  • Фикс: в connect_job_status после auth проверить, что job принадлежит org юзера.

MT-8. GET /admin/users возвращает всех платформенных пользователей

  • Файл: backend/app/api/v1/routes_admin.py:30-74
  • Эффект: PM в org A в селекторе "Assign linguist" видит linguists из org B.
  • Фикс: GET /organizations/{id}/users?role=linguist (новый endpoint), фронт useUsers({orgId}). Старый admin endpoint оставить только для ADMIN.

MT-9. Audit log глобальный

  • Файлы: routes_admin.py:604-707, services/audit_logger.py:104-184
  • Эффект: PRODUCTION (или PM, если когда-то откроют доступ) видит actions всех орг.
  • Фикс: добавить organization_id в AuditLog, фильтровать по org юзера.

MT-10. /admin/stats агрегирует по всем org

  • Файл: routes_admin.py:296-351, 422-494
  • Эффект: утечка volume-метрик (количество джоб у каждого тенанта).
  • Фикс: $match: {organization_id: {$in: user.org_ids}} в первом этапе пайплайна. ADMIN — отдельная ветка.

MT-11. assign_linguist / assign_reviewer не проверяют org assignee

  • Файл: backend/app/services/language_qc.py:133-280
  • Эффект: PM org A назначает linguist org B → linguist видит чужой джоб в очереди.
  • Фикс: перед сохранением — assert linguist.org_id == job.organization_id.

MT-12. _assert_client_access имеет legacy-байпас для PM/CLIENT

  • Файл: backend/app/api/v1/routes_clients.py:487-488 (if user.role in (CLIENT, PROJECT_MANAGER): return)
  • Эффект: любой PM получает доступ к любому client независимо от membership.
  • Фикс: удалить fallback, требовать explicit Membership запись.

MT-13. Glossary listing без membership-чека на client_id

  • Файл: backend/app/api/v1/routes_glossaries.py:47-56
  • Эффект: linguist org A читает глоссарий org B по /clients/{any_client_id}/glossaries.
  • Фикс: добавить assert_client_in_user_org.

MT-14. GCS bucket без org-префикса в путях

  • Файлы: backend/app/services/gcs.py:18, 71-100, routes_jobs.py:103, 1656-1676, routes_files.py:34
  • Эффект: утечка path → прямой download через signed URL без проверки org.
  • Фикс: перейти на путь {org_id}/{job_id}/source.mp4. get_signed_url обязан принимать user и валидировать ownership пути. Migrate существующие файлы lazy-rewrite или org_id=legacy.

MT-15. Два параллельных authz-стека

  • Файлы: core/authz.py (новый, MembershipContext/OrgScopedQuery) vs core/dependencies.py:require_roles + get_accessible_project_ids (legacy)
  • Эффект: новый стек используется только в routes_organizations.py. Все остальные роуты — на легаси, который течёт.
  • Фикс: консолидировать на authz.py, удалить get_accessible_project_ids. Это масштабная миграция, но без неё дальнейшая разработка только увеличит долг.

MT-16. JWT без org_id/memberships claims

  • Файл: backend/app/core/security.py:21,34
  • Эффект: каждый запрос грузит memberships из Mongo (60s cache → окно стейл-роли).
  • Фикс: включить org_ids: [...] в access token. Force re-login при изменении membership.

MT-17. Membership без team_id

  • Файл: backend/app/models/membership.py:9-15 vs models/client.py:39-46 (teams.member_user_ids[])
  • Эффект: team-isolation размазан между двумя источниками истины.
  • Фикс: один источник — Membership.team_ids[]. Удалить member_user_ids из Team.

MT-18. list_for_reviewer сканирует ВСЕ джобы

  • Файл: backend/app/services/language_qc.py:867-896 (query {})
  • Эффект: курсор limit*5 тянет джобы любого тенанта, фильтрация в Python — leak в логи / OOM-риск.
  • Фикс: {"organization_id": {"$in": user.org_ids}, "language_qc.{lang}.assigned_reviewer_id": user.id} индекс.

MT-19. Invitation создаёт target_team_ids без валидации org

  • Файл: routes_invitations.py:99-157, 321-325
  • Фикс: валидировать что все team_ids принадлежат org_id приглашения.

P0 — Workflow blockers (ломают core ежедневный сценарий)

W-1. PM не имеет PM-specific дашборда

  • Файлы: frontend/src/routes/Dashboard.tsx:25-138, 135-137
  • Проблема: нет ветки case 'project_manager' — PM падает в default. Stats считаются по всем орг (см. MT-10).
  • Фикс: PM-дашборд: "needs my final review", "deadlines this week", "stuck on TTS_FAILED/RENDER_FAILED", "comments awaiting my reply", scoped к его org.

W-2. Linguist/Reviewer тоже не имеют корректной landing-страницы

  • Файлы: Sidebar.tsx:46-49, нет редиректа на /qc/queue
  • Фикс: на login redirect по role: PM → /dashboard/pm, Linguist → /qc/queue, Reviewer → /qc/review-queue.

W-3. Sidebar не имеет badge с количеством заданий

  • Файл: frontend/src/components/Layout/Sidebar.tsx
  • Фикс: badge на "My QC Queue" / "Final Review" с количеством pending для текущей роли. Данные уже фетчатся каждые 30 сек (LinguistQueue.tsx:92).

W-4. PM не может назначить команду при создании джобы

  • Файлы: frontend/src/routes/jobs/NewJob.tsx, frontend/src/routes/admin/QCDetail.tsx:1013-1064
  • Проблема: ассайнмент linguist/reviewer доступен только после pending_qc (через QCDetail). PM создаёт джобу → ждёт AI → возвращается → ассайнит каждый язык по 2 модалки (linguist + reviewer).
  • Фикс: на форме создания джобы — секция "Team" с матрицей language × {linguist, reviewer, deadline}. Auto-fill из Project.default_linguist_id/default_reviewer_id. Можно ассайнить ДО pending_qc.

W-5. Project defaults применяются только при создании проекта

  • Файл: NewJob.tsx:309-311
  • Проблема: при выборе существующего проекта default_languages/default_linguist_id/default_reviewer_id НЕ подставляются.
  • Фикс: на change selectedProjectIdsetValue('languages', project.default_languages) и т.д.

W-6. Two-stage QC order не enforced

  • Файлы: services/language_qc.py:965-981, 973-977, QCDetail.tsx:798-799
  • Проблема: _assert_can_approve пропускает linguist'а если reviewer не назначен. PROD/ADMIN апрувит в любом state. Frontend canApproveAll && qcStatus !== 'approved' шорткатит весь pipeline.
  • Фикс: жёстко требовать submitted_for_review_at перед reviewer-approve. Для admin — explicit override flag с audit-логом.

W-7. Email о completion жёстко выключен

  • Файл: backend/app/tasks/notify.py:111 (email_enabled = False)
  • Фикс: включить, валидировать SMTP-конфиг на старте.

W-8. time.sleep(1) в async POST /jobs

  • Файл: backend/app/api/v1/routes_jobs.py:151-152
  • Фикс: удалить debug-блок 149-165.

W-9. PM не может complete джобу

  • Проблема: complete_job требует PROD/ADMIN. PM не может сам доставить клиенту.
  • Фикс: разрешить PM org, если он Membership владелец проекта.

W-10. Final Review недоступен PM из sidebar

  • Файлы: Sidebar.tsx (/admin/final для reviewer/linguist/production/admin, не для PM)
  • Фикс: добавить PM в allowedRoles.

W-11. Production не имеет своего дашборда (упускали в первой версии аудита)

  • Файлы: frontend/src/routes/Dashboard.tsx:25-138 — нет ветки case 'production'
  • Проблема: Production падает в default. У него нет видимости очереди briefs от PM, текущих AI-процессов, failures, pending QC handoff. Сейчас Production вынужден scroll'ить общий JobsList и сам угадывать что нужно его действие.
  • Фикс: /dashboard/production со виджетами: Briefs awaiting upload / AI processing / Failures (TTS_FAILED, RENDER_FAILED, INGESTING stuck) / Pending QC handoff. Real-time updates через WS.

W-12. Нет "Brief from PM → upload by Production" workflow

  • Проблема: сегодня PM либо сам загружает (но это не его роль в идеале), либо лично пишет Production в Slack/email. В системе нет первоклассного объекта "Brief" / "Upload Request".
  • Фикс: новая сущность JobBrief (или Job в state awaiting_upload):
    • PM создаёт brief: project, languages, outputs, deadlines, team — БЕЗ файла
    • Production видит brief в inbox, кликает → /jobs/new pre-populated → загружает файл → brief конвертируется в нормальную джобу
    • Audit linkage сохраняется (кто briefed, кто uploaded)

W-13. Failure recovery UI разбросан и неинформативен

  • Файлы: JobDetail.tsx:670-684 (Retry TTS), routes_jobs.py (retry endpoints)
  • Проблема: "Retry TTS" — одна кнопка без deserialization причины (error_message/failed_at/retry_count). Production не знает: квота? один кью сломался? сетевой?
  • Фикс: Job.failure структура: {step, reason_code, reason_text, failed_at, retry_count, last_attempt_logs_url}. UI показывает structured info + action buttons (Retry / Skip step / Manual override / Escalate).

W-14. Нет bulk retry / bulk failure dashboard

  • Проблема: если Gemini вернул 503 на 10 джоб одновременно — Production должен открыть каждую и ткнуть Retry.
  • Фикс: /dashboard/production/failures → table all failed jobs → multi-select → "Retry all selected" с suspect-reason группировкой ("All failed because TTS API 503 — likely transient").

P1 — Сильные UX боли

PM workflow

  • PM-1. Нет project-level deadline. models/job.py без deadline. Только per-language linguist_deadline/reviewer_deadline. → Добавить Job.deadline + дисплей в JobsList с overdue-цветом.
  • PM-2. Нет clone проекта/джобы. → POST /projects/{id}/clone, POST /jobs/{id}/clone (без файла, копирует config).
  • PM-3. Project create зарыт в форму NewJob. → "+ New Project" кнопка на ClientDetail.
  • PM-4. Client + Project pickers в самом низу формы. NewJob.tsx:765-896. → Перенести наверх.
  • PM-5. Нет deadline picker на NewJob. → Добавить.
  • PM-6. Multi-upload теряет per-job titles. NewJob.tsx:574-582. → Editable.
  • PM-7. Нет bulk-assign. → "Apply project default to all languages" + "Assign linguist X to languages [select]".
  • PM-8. Reassign mid-job без warning о WIP. language_qc.py:121-213. → Снапшот VTT перед reassign + confirmation modal.
  • PM-9. Нет "needs my attention" фильтра в JobsList. → Preset filters: assigned to me, pending my final review, overdue, failed.
  • PM-10. PM не имеет bulk actions (canManageJobs = isAdmin || isProduction). JobsList.tsx:430. → Включить PM для своих org.
  • PM-11. Нет deadline column / sort. JobsList.tsx:212-227. → Добавить.
  • PM-12. JobsList фетчит size:10000. JobsList.tsx:123-127. → Серверная пагинация.
  • PM-13. Final review страница — generic. → Дедикейтед UI с per-language summary, links to deliverables, override notes.
  • PM-14. Notify client uncontrollable: автоматически на complete, нельзя править email/CC. tasks/notify.py:205. → "Preview & Send" UI.
  • PM-15. Нет share read-only ссылки клиенту. → POST /projects/{id}/share → токен с TTL → public read-only view.
  • PM-16. Glossary не в sidebar для PM. → Добавить.
  • PM-17. PM не уведомляется о comment на assigned джобу. language_qc.py:72-88. → Добавить PM в _qc_recipients.
  • PM-18. Audit log заперт за PROD/ADMIN. → Дать PM read для своих org.
  • PM-19. Cost tracker только в QCDetail. → На JobsList row + Project page.
  • PM-20. JobDetail.tsx без assignments tab. :164-170. → Добавить.
  • PM-21. Sidebar role-string mismatch (project_manager vs manager). Sidebar.tsx:43,73. → Унифицировать enum.
  • PM-22. Дашборд "Recent Activity" утекает другие org (см. MT-10).

Linguist workflow

  • L-1. Глоссарий НЕ показан в редакторе inline. VttEditor.tsx. → Подсветка preferred terms по hover, suggestion на edit.
  • L-2. Save VTT создаёт версию НА КАЖДЫЙ cue-edit. QCDetail.tsx:413-420, 430-437, routes_jobs.py:1313, 1399. → Debounce 30 сек или ручной "Save as version" button.
  • L-3. Нет optimistic locking — concurrent edits молча overwrite. routes_jobs.py:1278-1408. → If-Match: <updated_at> + 409 при mismatch + WS broadcast "user X is editing".
  • L-4. TimestampInput молча проглатывает parse errors. VttEditor.tsx:382-394. → Toast.
  • L-5. Millisecond rounding overflow. VttEditor.tsx:186 (Math.round((s-w)*1000)). → Хранить ms напрямую.
  • L-6. Нет CPS/speech-rate валидации. VttEditor.tsx:5-25. → Warn если cps > 20.
  • L-7. Нет diff "AI vs my edit". → Tag первой версии как ai_baseline, кнопка "Compare to AI".
  • L-8. Edit button только on hover. VttEditor.tsx:325-332. → Always visible.
  • L-9. "Edit text"-кнопка / hotkeys. → Cmd+Enter save cue, ]/[ next/prev.
  • L-10. Linguist может self-approve если reviewer не назначен. language_qc.py:973-977. (см. W-6).
  • L-11. start_linguist_work молча no-op. language_qc.py:362-363. → Если состояние не подходит — toast.
  • L-12. Нет "next assigned to me" между джобами. → Кнопка "Next →" в queue context.
  • L-13. Assignment email минимальный. emailer.py:156-178. → Добавить deadline, source-language, cue count, glossary link.
  • L-14. Реассайнутый linguist не получает уведомление. language_qc.py:241. → "Removed from assignment" email.
  • L-15. WS language_qc_assigned есть, но UI не слушает live. → Toast + queue refetch.
  • L-16. VTT version restore только PROD/ADMIN. routes_vtt_versions.py:95, VersionsTab.tsx:122. → Linguist может restore СВОИ версии.
  • L-17. Save без note. routes_jobs.py:1313, 1399 не передаёт note. → Field "note" в save modal.
  • L-18. Нет "send back to PM (source has issues)". → Endpoint POST /jobs/{id}/blocked-on-source с reason.

Production workflow

  • PR-1. Нет "Awaiting upload" inbox. → новая колонка/state, см. W-12.
  • PR-2. Multi-upload теряет per-file настройки. NewJob.tsx:574-582. → Per-file expandable rows: title / brand-context override / individual languages override.
  • PR-3. Нет server-side multi-upload progress aggregation. useMultiUpload.ts. → API возвращает batch-id, клиент опрашивает batch.
  • PR-4. Resumable upload (>500MB) — нет. → GCS resumable upload с client-side chunks (минимум — beforeunload warning).
  • PR-5. Production не видит cost per job / token usage в реальном времени. cost_tracker_project_id. → Колонка cost в JobsList для Production.
  • PR-6. Retry endpoints не имеют rate limit / circuit breaker. Production может zaspamить Gemini в "retry all failed" сценарии. → Rate-limit на retry-mutations, exponential backoff.
  • PR-7. Нет visibility на queue depth (Celery active/pending tasks). → Production-only widget "Workers: 4 active, 12 queued".
  • PR-8. Нет "skip step" / "manual override". Если AI не может справиться (например, источник на редком языке) — Production должен иметь возможность загрузить готовый VTT вручную и обойти AI шаг.
  • PR-9. Bulk download для своих org для Production уже есть, но без org-фильтра (см. MT-9).
  • PR-10. Нет "promote to QC" вручную. Бывают кейсы когда AI ушёл в edge-case, Production знает что результат норм и хочет вручную перевести в pending_qc.
  • PR-11. Audit log для Production-actions: должен быть scoped к его org (см. MT-9).
  • PR-12. Reassign не предупреждает что у linguist'а есть unsaved work. language_qc.py:121-213 (см. PM-8 — это и Production-action тоже).

Reviewer workflow

  • R-1. Bulk approve без read-through. QCList.tsx:81-92, 153. → Запретить bulk, оставить per-job.
  • R-2. Approve enabled моментально, без reviewed_cues tracking. QCDetail.tsx:906-913. → "X of N cues marked reviewed" gate.
  • R-3. Нет diff AI-original vs linguist edit. → См. L-7.
  • R-4. Reject reason free-text. QCDetail.tsx:989-995. → Категории (timing/mistranslation/terminology/profanity/length) + free-text.
  • R-5. Comment не WS-broadcastит. language_qc.py:822-837 vs :206-211. → Добавить broadcast.
  • R-6. Три approve-surface (per-language card, "Approve All Languages", bulk in QCList). → Удалить bulk и "Approve All".
  • R-7. Reviewer не слышит AD audio. QCDetail.tsx:1281-1303. → Per-cue MP3 playback.
  • R-8. Нет language competence чека. language_qc.py:246-327. → User.languages[] поле, warn при assign.
  • R-9. No VTT versions on review page. → Добавить tab.
  • R-10. Hotkey "A" триггерит "Approve All Languages" без confirm. QCDetail.tsx:335-341. → Confirmation modal.
  • R-11. submitted_for_review_at не показан reviewer'у. language_qc.py:426, QCDetail.tsx:830-876. → Колонка.
  • R-12. Reject не делает re-acknowledgement step для reviewer. language_qc.py:637. → При resubmit — clear reviewed_cues + new email.

P2 — Полировка / Tech Debt

  • T-1. Глобальные status labels — два расходящихся мапа (JobsList.tsx:22-38, StatusBadge.tsx:40-73). → Один словарь в lib/jobStatusMessages.ts.
  • T-2. Цветовая палитра статусов сломана: 3 разных state одним зелёным/красным. → 4-уровневая палитра.
  • T-3. Терминология плавает: Job/Video/Project. → Глоссарий + замена.
  • T-4. 24h URL TTL — magic number в 8+ местах. → core/config.py: SIGNED_URL_TTL_HOURS.
  • T-5. Heavy re-parse VTT треков: VideoWithCaptions.tsx:65-101. → Memoize.
  • T-6. Captions overlay кастомный <div>, не нативный <track>. VideoWithCaptions.tsx:204-222. → Native track API.
  • T-7. AD как отдельный <audio> без sync. VideoWithCaptions.tsx:271-286. → Bind play/pause/seek.
  • T-8. Mobile: caption bottom-16 коллидит c iOS Safari controls. → env(safe-area-inset-bottom).
  • T-9. Login error mapping грубый (Login.tsx:31). → 401/429/network как разные сообщения.
  • T-10. Login для уже залогиненного не редиректит. App.tsx:61. → Guard.
  • T-11. No CSRF на cookie-based refresh. api.ts:75. → SameSite=Strict + double-submit.
  • T-12. Microsoft SSO sync requests.get() в async. → httpx.AsyncClient.
  • T-13. Rate limit middleware silent-fail на отсутствии Redis. main.py:108. → Health-check кричит.
  • T-14. Unified upload size limit (1GB / 500MB / без лимита расходятся). → core/config.py: UPLOAD_MAX_BYTES.
  • T-15. Browser refresh kills upload. useMultiUpload.ts. → beforeunload warning + опционально resumable upload.
  • T-16. Lifespan-блок в main.py:97-101: миграция language_qc на каждый старт API. → Отдельный CLI скрипт + флаг "done".

Файловая карта (что трогаем)

Backend (auth/tenancy/data):

  • backend/app/core/authz.py — расширить OrgScopedQuery, удалить альтернативный путь
  • backend/app/core/dependencies.pyget_accessible_project_ids[] для staff без membership
  • backend/app/core/security.py — добавить org_ids в JWT
  • backend/app/models/job.pyorganization_id, team_id, deadline
  • backend/app/models/review_note.py, vtt_version.py, audit_log.py, glossary.pyorganization_id
  • backend/app/models/membership.py — добавить team_ids[]
  • backend/app/services/audit_logger.py — фильтр по org
  • backend/app/services/websocket.py — membership check на subscribe
  • backend/app/services/gcs.py — org-prefixed paths
  • backend/app/services/language_qc.py — org-isolated queries, enforce two-stage order, validate assignee org
  • backend/app/api/v1/routes_jobs.py — все detail/bulk handlers + удалить time.sleep
  • backend/app/api/v1/routes_review_notes.py, routes_vtt_versions.py, routes_glossaries.py, routes_admin.py, routes_clients.pyassert_*_in_user_org
  • backend/app/tasks/notify.py — включить email
  • backend/app/services/emailer.py — обогатить assignment email
  • backend/migrations/2026_05_add_organization_id.py — новая миграция

Frontend (workflow):

  • frontend/src/routes/Dashboard.tsx — PM-ветка, scope to org
  • frontend/src/routes/jobs/NewJob.tsx — team assignment matrix, project defaults autofill, deadline, перенести picker'ы
  • frontend/src/routes/jobs/JobsList.tsx — server pagination, deadline col, "needs my attention" filters, PM bulk
  • frontend/src/routes/jobs/JobDetail.tsx — assignments tab, role-aware actions
  • frontend/src/routes/admin/QCDetail.tsx — bulk-assign, two-stage gate, diff view, reviewed-cues tracking
  • frontend/src/routes/admin/QCList.tsx — убрать bulk approve
  • frontend/src/components/VttEditor.tsx — glossary inline, ms precision, CPS warn, diff view, hotkeys
  • frontend/src/components/Layout/Sidebar.tsx — role badges, fix role-string mismatch
  • frontend/src/lib/jobStatusMessages.ts — единый источник лейблов/цветов

Новое:

  • frontend/src/routes/dashboard/PMDashboard.tsx — focus PM
  • frontend/src/routes/qc/LinguistQueue.tsx — pre-existing, добавить badges/sorting
  • backend/app/api/v1/routes_organizations.pyGET /organizations/{id}/users?role=...
  • backend/app/api/v1/routes_share.py — share-token endpoints для CLIENT view

Подход к реализации (рекомендация)

5 PR'ов, порядок строгий (multi-tenancy первым, без неё всё остальное опасно деплоить):

PR-1 "Multi-tenancy foundations" (P0 MT-1..19)

  • Migration: добавить organization_id во все доменные модели
  • Унифицировать на core/authz.py, удалить get_accessible_project_ids
  • JWT с org_ids
  • Helper assert_*_in_user_org + применить везде
  • Org-prefixed GCS paths (lazy migration)
  • WebSocket membership check
  • Tests: test_cross_tenant_isolation.py — для каждого роута попытка cross-org access → 404
  • Не мерджить без E2E теста, который фейлит до фикса и проходит после.

PR-2 "Workflow blockers" (P0 W-1..10)

  • Email enable + smtp validation
  • Remove time.sleep
  • PM dashboard (новая ветка в Dashboard.tsx)
  • Role-based redirect on login
  • Sidebar badges
  • Team assignment matrix на NewJob
  • Project defaults autofill при выборе existing project
  • Two-stage QC order enforcement
  • PM в allowedRoles для Final Review
  • PM может complete джобу для своих org

PR-3 "PM productivity" (P1 PM-*)

  • Deadlines (column, sort, overdue colors)
  • Clone project/job
  • Bulk assign + apply defaults
  • Server pagination JobsList
  • "Needs my attention" filter presets
  • Audit log read для PM
  • PM bulk actions для своих org
  • Cost tracker visibility
  • Assignments tab в JobDetail
  • Project share read-only link

PR-4 "Linguist & Reviewer productivity" (P1 L-, R-)

  • Glossary inline в VttEditor
  • Diff view "AI vs current"
  • Optimistic locking + WS "user editing" indicator
  • Manual "Save as version" + note field
  • Reviewer reviewed-cues tracking + gate approve
  • Reject reason categories
  • Per-cue AD MP3 playback
  • Hotkeys
  • Language competence flag на User
  • Comment WS broadcast

PR-5 "Polish & tech debt" (T-*)

  • Unified status labels/colors
  • Native <track> для captions
  • AD sync с video
  • CSRF, SSO async
  • Unified upload size, beforeunload, resumable
  • 24h TTL central config

Verification

После PR-1 (КРИТИЧНО):

  • E2E: создать 2 организации, по 1 PM/Linguist каждой. Linguist org A пытается:
    • GET /jobs → видит только org A (пусто)
    • GET /jobs/{id_org_b} → 404
    • GET /jobs/{id_org_b}/review-notes → 404
    • GET /jobs/{id_org_b}/vtt/versions → 404
    • WebSocket subscribe org B job → reject
    • POST /jobs/bulk-delete с id из org B → 0 deleted, не 404
    • Signed download URL by guessed path → 403/404
  • Все эти случаи покрыты тестом, тест в CI блокирует merge.

После PR-2:

  • PM логинится → лендится на /dashboard/pm с правильными метриками своей org
  • Linguist логинится → редирект на /qc/queue, badges показывают N pending
  • На NewJob: выбрать existing project → languages/linguist/reviewer автозаполнены
  • Сабмит → джоба в pending_qc сразу с назначениями (ассайнмент НЕ ждёт ИИ)
  • Reviewer пытается approve до linguist submit → 403 с понятным сообщением

После PR-3:

  • В JobsList переключение фильтра "Pending my final review" — корректный count
  • Создать клон джобы → новый ID, тот же config, файла нет
  • Bulk assign linguist X на 5 языков → 1 клик
  • Просрочка deadline → красная подсветка строки

После PR-4:

  • Linguist открывает VttEditor → preferred term подсвечен
  • Двое linguists одновременно открывают одну lang → второй видит "user X is editing", save → 409
  • Reviewer без mark-as-reviewed на хотя бы 80% cues → approve disabled
  • Reject reason без категории → form invalid

После PR-5:

  • Native CC button в fullscreen работает (доказательство нативного <track>)
  • AD slider синхронизирован с video
  • Refresh during upload → browser confirm

Команды:

  • Backend: cd backend && ruff check . && mypy . && poetry run pytest tests/test_cross_tenant_isolation.py
  • Frontend: cd frontend && npm run type-check && npm run lint && npm run test:e2e -- --grep multi-tenant
  • Local: ./scripts/run-local.sh → seed двух org, прогнать сценарии вручную

Notes

  • Multi-tenancy — главный архитектурный долг. Все остальные улучшения опасны деплоить до PR-1, потому что любой новый эндпоинт унаследует дырявый authz и расширит surface утечки.
  • CLIENT не должен иметь UI в основном приложении вообще (кроме страницы шары). Их сценарий — открыть share-link с токеном (без логина), посмотреть/скачать. Любое присутствие CLIENT в Sidebar/JobsList/Dashboard сегодня — наследие старой модели и должно уходить.
  • Двухстадийный QC в коде уже реализован, но enforcement дырявый (W-6) — фиксить ОБЯЗАТЕЛЬНО, иначе бизнес-обещание "linguist → reviewer" не выполняется.
  • PM-роль наименее обслужена в UI — это поразительно учитывая что она самая загруженная. PR-3 должен это переломить.
  • Many issues stem from отсутствия единого источника истины (статусы, лимиты, TTL, role enums). PR-5 — рефакторинг этого.

Принципы UX-консистентности (применяются в каждом PR)

  1. Один объект — одно имя везде. Job/Video/Project не должны путаться. Договариваемся: Project = контейнер для серии работ от одного клиента. Job = одно видео в проекте. UI всегда говорит так. Никаких "video" в нав-меню.
  2. Один статус — один цвет — один лейбл. lib/jobStatusMessages.ts — единственный источник. Любое отображение статуса (badge, toast, email subject, notification) идёт через один helper.
  3. Один путь к действию. Если PM может assign на трёх экранах с тремя разными UI — это бага. Per-language assignment живёт в одном месте (assignments tab на JobDetail) с deep-link'ами туда из dashboard widgets и Final Review.
  4. Role-aware navigation, не скрытые кнопки. Нет смысла показывать кнопку "Retry TTS" клиенту/linguist'у и потом 403'ить. Если действие недоступно — кнопки нет.
  5. Любой list — фильтр "needs my action" по умолчанию для роли. PM по умолчанию видит "pending my final review", linguist — "pending me", reviewer — "pending review". Глобальный list — отдельный tab.
  6. Sidebar badges как единственный pull-mechanism. Никаких "пойди в JobsList и поищи нужный фильтр".
  7. Hotkeys для ежедневных действий. Save (Cmd+S), Next (Cmd+]), Approve (A с confirmation), Reject (R), Play cue (Cmd+P).
  8. Автоматический "Next" в editor/queue. После Submit/Approve — переход к следующему элементу очереди, без возврата на queue.
  9. Никогда не блокировать пользователя на "ждать AI". Назначения, дедлайны, конфигурация TTS — настраиваются ДО AI processing.
  10. Все деструктивные действия — с confirmation. Delete job, Reassign linguist, Approve all languages — модалка с явным impact описанием.
  11. Email = mirror in-app. Любая email-уведомление имеет deep-link на in-app экран. Любое in-app событие, требующее action, дублируется email'ом.
  12. Audit видим участникам. Linguist/Reviewer/PM видят историю изменений в job-scope. Cross-org audit — только ADMIN.

Sessions

2026-04-30 Asked the developer to review the

Asked: Asked the developer to review the project files in Obsidian, create a fix/improvement plan, and answer a question about editing closed captions on accessibility videos. Done: Reviewed project files, identified pre-existing B904 errors as non-critical, verified frontend compilation, and committed changes.

2026-04-30 Continue previous work, review project files

Asked: Continue previous work, review project files in Obsidian, create a fix/improvement plan, and confirm if closed captions can be edited on accessibility videos. Done: Reviewed project plan, completed multiple task blocks (validation, security debts, tests, quick wins, backend features), and deferred PR-3 resumable upload due to time constraints.

2026-04-30 Asked | How to edit/add closed

Asked: Asked | How to edit/add closed captions on Accessibility videos and review project files for fixes | (No files) Done: Done | Reviewed project files and identified fixes needed; confirmed closed captions can be edited and added to Accessibility videos | .env.production

2026-04-30 Reviewed project files and assessed fixes

Asked: Reviewed project files and assessed fixes needed; confirmed Closed Captions editing is possible on Accessibility videos Done: Fixed TimelinePreview TS error and achieved successful build with 999 modules

2026-04-30 Analyzed project files and accessibility video

Asked: Analyzed project files and accessibility video captions capability | Verified claude run command and passed timeline/VTT editor callbacks | TimelinePreview.tsx, ADVttEditor.tsx Done: Fixed callback props for timeline and VTT editor components | Added onPausePointAdd, insertAtTimeMs, and onInsertAtTimeDone props | TimelinePreview.tsx, ADVttEditor.tsx

2026-04-29 Continue video-accessibility project by implementing PM-7

Asked: Continue video-accessibility project by implementing PM-7 bulk assign linguist feature and subsequent tasks from the handover plan. Done: Reviewed project plan and handover documents, set up working directory and branch context for feature implementation.

2026-04-29 Continue video-accessibility project and update session

Asked: Continue video-accessibility project and update session documentation after completing all planned tasks. Done: Read plan and handover documents, reviewed task summary from previous context, and prepared to execute PM-7 task implementation.

2026-04-29 Analyze the client flow in the

Asked: Analyze the client flow in the application and identify all logic breaks, inconsistencies, and UX issues that need improvement. Done: Created a comprehensive audit plan in Obsidian with status tables, UX flows by role, and verification checklists.

2026-04-29 Asked | Review client-side application architecture

Asked: Asked | Review client-side application architecture and identify logic breaks, inconsistencies, and UX issues Done: Done | Analyzed application flow and documented findings in Obsidian plan file for refactoring

2026-04-29 Review the client-side application flow and

Asked: Review the client-side application flow and identify logic breaks, UX inconsistencies, and areas needing improvement. Done: Analyzed application flow in Obsidian and documented progress across 4 commits with PM features, deadline management, reject categories, and hotkey implementations.