55 KiB
| title | date | tags | aliases | status | ||||
|---|---|---|---|---|---|---|---|---|
| Video Accessibility — Full Audit & Implementation Plan | 2026-04-29 |
|
|
in-progress |
Staff Workflow & Multi-Tenancy Audit — Video Accessibility Platform
Branch state (на момент аудита)
- Текущая ветка:
main(HEADa168af1, 2026-04-29) origin/dev— строгий предокmain(HEAD02c4240). Ничего уникального на 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)
Аудит покрыл:
- Multi-tenant isolation — есть ли утечки данных между организациями/командами
- PM workflow — создание клиента/проекта, назначение команд, мониторинг, финальный ревью, доставка
- Production workflow — upload, конфигурация джобы, мониторинг пайплайна, восстановление после фейлов
- Linguist workflow — назначение, очередь, VTT-редактор, глоссарий, версионирование
- Reviewer workflow — двухстадийный QC, апрув/реджект, диффы
Найдено 100+ дефектов. Главный архитектурный вывод: изоляция по тенантам в коде сегодня дырявая — staff-роли (LINGUIST/REVIEWER/PRODUCTION) видят джобы, файлы, аудит, ноты ВСЕХ компаний из-за того, что get_accessible_project_ids для них возвращает None (что значит "без ограничений"). До запуска SaaS это надо чинить.
Сквозной критерий редизайна: программа, интерфейс и последовательность действий должны быть логичными, последовательными и удобными для каждой роли при ежедневной работе. Каждая правка ниже мерится тремя вопросами:
- Логично ли? — интуитивно ли понятен следующий шаг? нет ли скрытых блокеров?
- Последовательно ли? — один и тот же объект выглядит/называется/ведёт себя одинаково везде?
- Удобно ли? — для частых действий минимум кликов, минимум перепрыгивания между страницами, минимум контекст-свитчинга?
Идеальный 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) vscore/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-15vsmodels/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
selectedProjectId—setValue('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. FrontendcanApproveAll && 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в stateawaiting_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-languagelinguist_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_managervsmanager).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+Entersave 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_cuestracking.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-837vs: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 — clearreviewed_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. →beforeunloadwarning + опционально 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.py—get_accessible_project_ids→[]для staff без membershipbackend/app/core/security.py— добавить org_ids в JWTbackend/app/models/job.py—organization_id,team_id,deadlinebackend/app/models/review_note.py,vtt_version.py,audit_log.py,glossary.py—organization_idbackend/app/models/membership.py— добавитьteam_ids[]backend/app/services/audit_logger.py— фильтр по orgbackend/app/services/websocket.py— membership check на subscribebackend/app/services/gcs.py— org-prefixed pathsbackend/app/services/language_qc.py— org-isolated queries, enforce two-stage order, validate assignee orgbackend/app/api/v1/routes_jobs.py— все detail/bulk handlers + удалитьtime.sleepbackend/app/api/v1/routes_review_notes.py,routes_vtt_versions.py,routes_glossaries.py,routes_admin.py,routes_clients.py—assert_*_in_user_orgbackend/app/tasks/notify.py— включить emailbackend/app/services/emailer.py— обогатить assignment emailbackend/migrations/2026_05_add_organization_id.py— новая миграция
Frontend (workflow):
frontend/src/routes/Dashboard.tsx— PM-ветка, scope to orgfrontend/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 bulkfrontend/src/routes/jobs/JobDetail.tsx— assignments tab, role-aware actionsfrontend/src/routes/admin/QCDetail.tsx— bulk-assign, two-stage gate, diff view, reviewed-cues trackingfrontend/src/routes/admin/QCList.tsx— убрать bulk approvefrontend/src/components/VttEditor.tsx— glossary inline, ms precision, CPS warn, diff view, hotkeysfrontend/src/components/Layout/Sidebar.tsx— role badges, fix role-string mismatchfrontend/src/lib/jobStatusMessages.ts— единый источник лейблов/цветов
Новое:
frontend/src/routes/dashboard/PMDashboard.tsx— focus PMfrontend/src/routes/qc/LinguistQueue.tsx— pre-existing, добавить badges/sortingbackend/app/api/v1/routes_organizations.py—GET /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}→ 404GET /jobs/{id_org_b}/review-notes→ 404GET /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)
- Один объект — одно имя везде. Job/Video/Project не должны путаться. Договариваемся:
Project= контейнер для серии работ от одного клиента.Job= одно видео в проекте. UI всегда говорит так. Никаких "video" в нав-меню. - Один статус — один цвет — один лейбл.
lib/jobStatusMessages.ts— единственный источник. Любое отображение статуса (badge, toast, email subject, notification) идёт через один helper. - Один путь к действию. Если PM может assign на трёх экранах с тремя разными UI — это бага. Per-language assignment живёт в одном месте (assignments tab на JobDetail) с deep-link'ами туда из dashboard widgets и Final Review.
- Role-aware navigation, не скрытые кнопки. Нет смысла показывать кнопку "Retry TTS" клиенту/linguist'у и потом 403'ить. Если действие недоступно — кнопки нет.
- Любой list — фильтр "needs my action" по умолчанию для роли. PM по умолчанию видит "pending my final review", linguist — "pending me", reviewer — "pending review". Глобальный list — отдельный tab.
- Sidebar badges как единственный pull-mechanism. Никаких "пойди в JobsList и поищи нужный фильтр".
- Hotkeys для ежедневных действий. Save (Cmd+S), Next (Cmd+]), Approve (A с confirmation), Reject (R), Play cue (Cmd+P).
- Автоматический "Next" в editor/queue. После Submit/Approve — переход к следующему элементу очереди, без возврата на queue.
- Никогда не блокировать пользователя на "ждать AI". Назначения, дедлайны, конфигурация TTS — настраиваются ДО AI processing.
- Все деструктивные действия — с confirmation. Delete job, Reassign linguist, Approve all languages — модалка с явным impact описанием.
- Email = mirror in-app. Любая email-уведомление имеет deep-link на in-app экран. Любое in-app событие, требующее action, дублируется email'ом.
- 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.