vault backup: 2026-04-29 19:56:12

This commit is contained in:
Vadym Samoilenko 2026-04-29 19:56:12 +01:00
parent 351cd2383c
commit 360e4e543c
3 changed files with 263 additions and 71 deletions

View file

@ -9,8 +9,6 @@ tags:
# Next Session Start Prompt
> Copy the block below and paste as the first message in the new Claude Code session.
---
```
@ -20,93 +18,59 @@ tags:
Платформа — AI SaaS для генерации accessibility-материалов (CC, AD, SDH) из видео.
Рабочая директория: /Users/ai_leed/Documents/Projects/Oliver/video-accessibility
Текущая ветка: feat/pm-productivity (от main)
Последний коммит: 1bf0fb9
Текущая ветка: feat/pm-productivity → мержим в main, новая ветка: feat/pr7-security-and-briefs
Последний коммит: 08fcb4d
## Полный план
## Документы для чтения (В ЭТОМ ПОРЯДКЕ)
Читай план: /Users/ai_leed/.claude/plans/snoopy-watching-fountain.md
(копия в Obsidian: [[Full-Implementation-Plan]])
1. Хендовер (история всего сделанного):
/Users/ai_leed/Library/Mobile Documents/iCloud~md~obsidian/Documents/VadymSamoilenko/01 Projects/video-accessibility/Multi-Tenant Audit — Handover.md
Хендовер с историей что сделано и что осталось:
/Users/ai_leed/Library/Mobile Documents/iCloud~md~obsidian/Documents/VadymSamoilenko/01 Projects/video-accessibility/Multi-Tenant Audit — Handover.md
2. План на эту сессию (PR-7):
/Users/ai_leed/Library/Mobile Documents/iCloud~md~obsidian/Documents/VadymSamoilenko/01 Projects/video-accessibility/PR-7-Plan.md
## Что уже сделано (не трогать)
### PR-1 (merged): Multi-tenancy foundations
- org-изоляция во всех эндпоинтах (assert_job_in_user_org)
- WebSocket membership check
- organization_id в моделях
- миграция 2026_05_add_organization_id.py
### PR-2 (commit c7a6f13): PM/Production dashboards, workflow blockers
### PR-3+4+5 (commit 1bf0fb9): PM tools, deadline, clone, glossary, VTT diff, CSRF
### PR-6 (commit 08fcb4d): WS real-time updates, per-cue AD playback, beforeunload guard,
team auto-assignment, broadcast_to_job/broadcast_to_user bugfix
### PR-2 (pushed feat/workflow-blockers, commit c7a6f13):
- PM dashboard + Production dashboard
- role-based login redirect (linguist/reviewer → /qc/queue)
- sidebar badges (live QC count)
- two-stage QC enforcement (_assert_can_approve)
- email notifications enabled
- PM в complete_job, Final Review, Audit Log
## Задачи этой сессии (PR-7)
### PR-3+4+5 (pushed feat/pm-productivity, commit 1bf0fb9):
- W-5: project defaults autofill в NewJob
- PM-9: quick filter presets в JobsList
- PM-10: PM в canManageJobs / New Job button / Final Review action
- PM-12: server pagination (size:50, page state, pagination controls)
- PM-1: deadline field в Job model + JobsList column + overdue red + NewJob date picker
- PM-2: POST /jobs/{id}/clone + useCloneJob hook + Clone button
- R-4: reject categories (pill-select) в QCDetail reject modal
- R-2: reviewed_cues/total_cues + progress bar + 80% approve gate + /mark-cue-reviewed
- L-9: Cmd+S saves full VTT, Escape closes modals
- T-1: JOB_STATUS_LABELS / getJobStatusLabel unified in jobStatusMessages.ts
- T-14: settings.upload_max_video_bytes + settings.upload_signed_url_ttl_hours
КРИТИЧНО — делать первыми:
## Что осталось (приоритет сверху вниз)
1. MT-11: Cross-org assignment (language_qc.py:182 assign_linguist + :307 assign_reviewer)
→ добавить проверку job_org == linguist_org перед сохранением
### PR-3 (продолжение)
- PM-7: Bulk assign linguist
- Бэкенд: нет endpoint. Нужен POST /jobs/{id}/languages/bulk-assign с {linguist_id, reviewer_id, language_codes[]}
- Фронт: кнопка "Assign defaults to all languages" в QCDetail (берёт PM defaults из проекта) + select-all checkbox
- Файлы: backend/app/services/language_qc.py, routes_language_qc.py, frontend/src/routes/admin/QCDetail.tsx
2. MT-12: Legacy PM bypass (routes_clients.py:487-488 _assert_client_access)
→ удалить строку `if user.role in (CLIENT, PROJECT_MANAGER): return`
- PM-15: Read-only share link для CLIENT
- Новый файл backend/app/api/v1/routes_share.py
- POST /share/projects/{id} → создаёт ShareToken (TTL 7d, read-only) в MongoDB
- GET /share/{token} → публичная страница (без auth) с video player + download links
- Новый фронт компонент frontend/src/routes/share/ShareView.tsx
- Зарегистрировать роут в App.tsx (публичный, не обёрнут в PrivateRoute)
3. MT-13: Glossary без org-чека (routes_glossaries.py:47-56)
→ вынести _assert_client_access в core/dependencies.py, добавить в glossary handlers
### PR-4 (остаток)
- L-1/L-6: Glossary inline в VttEditor + CPS warning
- Файл: frontend/src/components/VttEditor.tsx
- Глоссарий: подтянуть через apiClient.getGlossaries(clientId), подсветить preferred terms в cue text
- CPS: warn если characters_per_second > 20 рядом с cue
4. MT-15 (partial): Заменить get_accessible_project_ids на get_membership_context
только в routes_jobs.py list_jobs + get_job, и routes_clients.py
- L-3: Optimistic locking (concurrent edits → 409)
- Backend: routes_jobs.py PATCH /jobs/{id}/vtt добавить If-Match check по updated_at
- Frontend: передавать заголовок If-Match: {updated_at}, показывать toast при 409
ПОТОМ:
- L-7: Diff AI baseline vs current
- Backend: при первом сохранении VTT после AI проставить тег "ai_baseline" на VTT version
- Frontend: кнопка "Compare to AI" в QCDetail, side-by-side diff view
5. W-13: Поле Job.failure + structured failure UI в JobDetail.tsx
### PR-5 (остаток)
- T-6: Native <track> для captions в VideoWithCaptions.tsx
- Убрать кастомный div overlay (строки ~215-221)
- Заменить на <track kind="captions" src={blobUrl}> с Blob URL из VTT контента
6. W-14: GET /admin/production/failures + bulk-retry endpoint + /dashboard/production/failures
- T-7: AD audio sync с video в VideoWithCaptions.tsx
- <audio> для AD уже есть (~строки 272-286) но без синхронизации
- Привязать play/pause/seek через ref на <video> и <audio>
7. W-12: Job Brief workflow — новая коллекция job_briefs + routes_briefs.py + BriefsList.tsx + NewJob интеграция
- T-11: CSRF
- Backend: double-submit cookie pattern в main.py
- Frontend: api.ts interceptor добавляет X-CSRF-Token из cookie
## Deploy-блокеры (напомни мне про них перед пушем)
## Инструкции
- Запустить 5 миграций: 2026-04-27-000000 через 2026-04-28-000003
- Проверить db.memberships.countDocuments() > 0
- Проверить organization_id на старых jobs
1. Перед началом прочти хендовер в Obsidian (ссылка выше)
2. Работаем на ветке feat/pm-productivity
3. После каждого блока работы: коммит → пуш → обновить Obsidian хендовер
4. Использовать скиллы: fastapi-expert для backend, react-expert для frontend, security-reviewer для CSRF
5. После завершения всего — создать финальный хендовер с инструкцией по деплою
## Команды
cd /Users/ai_leed/Documents/Projects/Oliver/video-accessibility
git checkout -b feat/pr7-security-and-briefs
cd backend && ruff check .
cd frontend && npm run type-check && npm run lint
```

View file

@ -0,0 +1,225 @@
---
title: PR-7 — Security Fixes + Failure UI + Job Brief
date: 2026-04-29
tags:
- video-accessibility
- plan
- in-progress
status: planned
---
# PR-7 — Следующая сессия
> [!warning] Приоритет: Security-фиксы ПЕРЕД любыми новыми фичами
> MT-11, MT-12, MT-13 — реальные дыры в multi-tenancy. Без них нельзя деплоить.
---
## Блок 1 — Security (MT-11 / MT-12 / MT-13)
### MT-11 — Cross-org assignment
> [!danger] Критично
> **Файл:** `backend/app/services/language_qc.py` — функции `assign_linguist` (строка 182) и `assign_reviewer` (строка 307)
> **Проблема:** PM org A назначает linguist из org B → linguist видит чужой job в очереди.
**Фикс** — после `linguist_doc = await db.users.find_one(...)` добавить:
```python
job_org = job_doc.get("organization_id")
linguist_org = linguist_doc.get("organization_id")
if job_org and linguist_org and job_org != linguist_org:
raise HTTPException(
status_code=400,
detail="Assignee is not in the same organization as this job"
)
```
То же самое в `assign_reviewer`.
---
### MT-12 — PM/CLIENT legacy bypass
> [!danger] Критично
> **Файл:** `backend/app/api/v1/routes_clients.py` — функция `_assert_client_access` (строка 476)
> **Проблема:** строка `if user.role in (CLIENT, PROJECT_MANAGER): return` пускает ЛЮБОГО PM к ЛЮБОМУ client.
**Текущий код (конец функции):**
```python
mem = await db.memberships.find_one({"user_id": str(user.id), "organization_id": client_id})
if mem:
return
# Legacy fallback for pre-migration users
if user.role == UserRole.PROJECT_MANAGER and client_id in (user.pm_client_ids or []):
return
if user.role in (UserRole.CLIENT, UserRole.PROJECT_MANAGER): # УДАЛИТЬ ЭТУ СТРОКУ
return
raise HTTPException(status_code=403, detail="Insufficient permissions")
```
**Фикс:** удалить последнее `if user.role in (...)` перед `raise`. Строку с pm_client_ids оставить.
---
### MT-13 — Glossary без org-чека
> [!danger] Критично
> **Файл:** `backend/app/api/v1/routes_glossaries.py:47-56`
> **Проблема:** `GET /clients/{client_id}/glossaries` не вызывает `_assert_client_access` — любой аутентифицированный пользователь читает глоссарий чужой org.
**Фикс:**
1. Вынести `_assert_client_access` из `routes_clients.py` в `core/dependencies.py` как shared helper
2. Импортировать в `routes_glossaries.py`, добавить вызов в начало каждого handler'а с `client_id` path param
---
## Блок 2 — MT-15: Частичная консолидация authz
> [!bug] Два параллельных authz-стека
> `core/authz.py` (новый, `MembershipContext`) используется только в `routes_organizations.py`.
> `core/dependencies.py` (legacy, `get_accessible_project_ids`) — во всех остальных роутах.
**Стратегия — не ломать всё сразу:**
1. В `routes_jobs.py` — в `list_jobs` и `get_job` заменить `get_accessible_project_ids` на `get_membership_context` + `accessible_org_ids()`
2. В `routes_clients.py` — заменить `_assert_client_access` на dependency `require_org_role(OrgRole.VIEWER)`
3. `get_accessible_project_ids` помечаем deprecated, не удаляем — остальные роуты переводить в следующих PR
---
## Блок 3 — W-13: Failure Recovery UI
> [!tip] Backend
> **Новое поле `Job.failure`** в `backend/app/models/job.py`:
> ```python
> class JobFailure(BaseModel):
> step: str # "ai_processing" | "translation" | "tts" | "render"
> reason_code: str # "GEMINI_503" | "TTS_QUOTA" | "GCS_TIMEOUT" ...
> reason_text: str # human-readable
> failed_at: datetime
> retry_count: int = 0
> ```
>
> В Celery tasks (`ingest_and_ai.py`, `translate_and_synthesize.py`) при поимке исключения записывать в `job.failure`.
> [!tip] Frontend
> **Файл:** `frontend/src/routes/jobs/JobDetail.tsx:670-684`
>
> Вместо одной кнопки "Retry TTS":
> - Step name: AI Processing / Translation / TTS / Render
> - Reason code + human text
> - Failed at (datetime)
> - Retry count (предупреждение если >3)
> - Кнопки: **Retry** / **Escalate**
---
## Блок 4 — W-14: Bulk Failures Dashboard
> [!tip] Backend
> - `GET /admin/production/failures` — все jobs в `*_failed` статусах, с `failure` полем
> - `POST /admin/production/bulk-retry``{job_ids: []}`, перезапускает воркер
> [!tip] Frontend
> - Новый роут `/dashboard/production/failures`
> - Table: title / status / error step / reason / failed_at
> - Multi-select + "Retry selected"
> - Группировка по `reason_code` (accordion) — "10 jobs: TTS API 503"
> - Sidebar badge у Production: кол-во failed jobs
---
## Блок 5 — W-12: Job Brief Workflow *(самый большой)*
> [!note] Концепция
> PM создаёт brief (без файла) → Production видит в inbox → берёт в работу → загружает файл → brief конвертируется в обычный Job
**Новая коллекция `job_briefs`:**
```python
class JobBrief(BaseModel):
id: str
organization_id: str
project_id: str
created_by: str # PM user_id
title: str
languages: list[str]
outputs: RequestedOutputs
deadline: Optional[datetime]
team: dict # {linguist_id?, reviewer_id?}
brand_context: Optional[str]
status: Literal["open", "claimed", "converted"]
claimed_by: Optional[str] # Production user_id
claimed_at: Optional[datetime]
job_id: Optional[str] # заполняется после convert
created_at: datetime
updated_at: datetime
```
**Backend endpoints — новый `routes_briefs.py`:**
| Method | Path | Кто | Что |
|--------|------|-----|-----|
| POST | /briefs | PM/ADMIN | создать brief |
| GET | /briefs | Production/PM/ADMIN | список (Production видит open, PM — свои) |
| GET | /briefs/{id} | - | детали |
| POST | /briefs/{id}/claim | Production | взять в работу (status → claimed) |
| POST | /briefs/{id}/unclaim | Production | отказаться (status → open) |
| POST | /briefs/{id}/convert | Production | загрузить файл → создать Job (brief.job_id, status → converted) |
**Job model:** добавить `brief_id: Optional[str]`
**Frontend:**
- `BriefsList.tsx` — Production inbox (open briefs), кнопка "Claim"
- `BriefDetail.tsx` — детали + upload form (берём NewJob.tsx как основу)
- `NewJob.tsx``?brief_id=xxx` pre-populate поля
- PM Dashboard — секция "My Briefs" (3 колонки: open / claimed / converted)
- Sidebar badge Production — кол-во unclaimed briefs
---
## Deploy Checklist
> [!warning] Обязательно ПЕРЕД первым деплоем
```bash
# Внутри backend-контейнера:
python -m app.migrations.runner
# Должны выполниться (по порядку):
# 2026-04-27-000000 add_project_manager_role
# 2026-04-28-000000 create_memberships_collection
# 2026-04-28-000001 backfill_memberships
# 2026-04-28-000002 create_invitations_collection
# 2026-04-28-000003 backfill_job_organization_id
```
После миграции проверить:
1. `db.memberships.countDocuments()` > 0
2. Каждый linguist/reviewer/production имеет >= 1 membership запись
3. Выборка 5-10 старых jobs — все имеют `organization_id`
---
## Что НЕ делаем в этой сессии
- **MT-14** (GCS org-prefix в путях) — требует lazy migration существующих файлов в GCS, отдельный PR
- **MT-16** (JWT с org_ids) — требует force re-login всех пользователей, нужно явное ОК от стейкхолдеров
- **MT-17** (Membership без team_id) — низкий приоритет
- **PR-2/3** (per-file multi-upload settings, server-side progress) — UX, не безопасность
- **PR-7/8** (queue depth widget, skip step) — следующий Production-PR
---
## Порядок работы
```
[ ] 1. Блок 1: MT-11 → MT-12 → MT-13 (3 небольших security-фикса, ~1ч)
[ ] 2. Блок 2: MT-15 partial (jobs + clients, ~1ч)
[ ] 3. ruff check + type-check + commit
[ ] 4. Блок 3: W-13 failure field + UI (~2ч)
[ ] 5. Блок 4: W-14 failures dashboard (~2ч)
[ ] 6. ruff check + type-check + commit
[ ] 7. Блок 5: W-12 Job Brief (~4ч, самый долгий)
[ ] 8. Финальный ruff + lint + commit + push
[ ] 9. Обновить хендовер
```

View file

@ -482,3 +482,6 @@ tags: [daily]
- 19:29 | `aimpress`
- **Asked:** Set up Forgejo, Plane, and Maybe Finance on HomeLab with Docker and add them to home.ai-impress.com.
- **Done:** Restarted Plane container and identified need for OpenAI API key configuration in Maybe Finance setup.
- 19:55 | `video-accessibility`
- **Asked:** Developer requested to continue video-accessibility project work on fe branch with plan and handover review.
- **Done:** Retrieved project context and Obsidian CLI documentation to prepare for task implementation.