From 7568d20b6d0ebfe361833d8fbc348b00ee535430 Mon Sep 17 00:00:00 2001 From: SlavaKuntsov Date: Thu, 17 Sep 2026 21:38:31 +0300 Subject: [PATCH 1/2] chore(docs): remove phase plan and review docs Tickets now live only in the GitHub project. Drops docs/PLAN.md and docs/REVIEW*, removes phase/roadmap mentions from READMEs and stale PLAN.md references from AGENTS.md, CLAUDE.md and docs. --- AGENTS.md | 1 - CLAUDE.md | 1 - README.md | 17 +- deploy/migrations/README.md | 2 +- docs/PLAN.md | 273 ---------------------------- docs/REVIEW-2026-09-13-merged.md | 222 ---------------------- docs/REVIEW-fresh-2026-09-13.md | 223 ----------------------- docs/REVIEW-fullstack-2026-09-13.md | 118 ------------ docs/REVIEW.md | 149 --------------- docs/services-pipeline.md | 5 +- services/transcoder/README.md | 20 +- 11 files changed, 16 insertions(+), 1015 deletions(-) delete mode 100644 docs/PLAN.md delete mode 100644 docs/REVIEW-2026-09-13-merged.md delete mode 100644 docs/REVIEW-fresh-2026-09-13.md delete mode 100644 docs/REVIEW-fullstack-2026-09-13.md delete mode 100644 docs/REVIEW.md diff --git a/AGENTS.md b/AGENTS.md index a1f9558..d4f3036 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,7 +74,6 @@ Strong success criteria let you loop independently. Weak criteria ("make it work - **ТЗ и архитектура (бывший AGENTS.md):** `docs/spec.md:1` — стек, структура монорепо, описание каждого микросервиса, FFmpeg-параметры, кодстайл, коммит-конвенции. - **Сервисы и пайплайн (на русском):** `docs/services-pipeline.md:1` — зачем каждый сервис (gateway/auth/metadata/upload/transcoder/nginx-vod/frontend) и полный пайплайн `upload → RabbitMQ → transcode → MinIO → HLS` с диаграммой и сценариями отладки. -- **План по фазам:** `docs/PLAN.md:1` — 8 фаз, граф зависимостей, DDL `deploy/postgres/init.sql:1`, контракты событий `video.uploaded`/`video.transcoded`. - **Swagger / Zed:** `docs/SWAGGER.md:1`, `docs/ZED.md:1` — генерация OpenAPI для Go, настройки IDE. ### Стек и структура (кратко) diff --git a/CLAUDE.md b/CLAUDE.md index a1f9558..d4f3036 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,7 +74,6 @@ Strong success criteria let you loop independently. Weak criteria ("make it work - **ТЗ и архитектура (бывший AGENTS.md):** `docs/spec.md:1` — стек, структура монорепо, описание каждого микросервиса, FFmpeg-параметры, кодстайл, коммит-конвенции. - **Сервисы и пайплайн (на русском):** `docs/services-pipeline.md:1` — зачем каждый сервис (gateway/auth/metadata/upload/transcoder/nginx-vod/frontend) и полный пайплайн `upload → RabbitMQ → transcode → MinIO → HLS` с диаграммой и сценариями отладки. -- **План по фазам:** `docs/PLAN.md:1` — 8 фаз, граф зависимостей, DDL `deploy/postgres/init.sql:1`, контракты событий `video.uploaded`/`video.transcoded`. - **Swagger / Zed:** `docs/SWAGGER.md:1`, `docs/ZED.md:1` — генерация OpenAPI для Go, настройки IDE. ### Стек и структура (кратко) diff --git a/README.md b/README.md index fd51cfe..6613763 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ └─► PATCH /internal/videos/:id/status (X-Internal-Token) → metadata ``` -> Full breakdown: [`docs/services-pipeline.md`](docs/services-pipeline.md) (services & pipeline, RU) · Spec: [`docs/spec.md`](docs/spec.md) · Plan: [`docs/PLAN.md`](docs/PLAN.md) (Phases 9-11 DONE, 12 in progress) +> Full breakdown: [`docs/services-pipeline.md`](docs/services-pipeline.md) (services & pipeline, RU) · Spec: [`docs/spec.md`](docs/spec.md) ## 🧩 Tech Stack @@ -64,7 +64,7 @@ │ ├── migrations/000001_init.up.sql # golang-migrate source of truth │ ├── postgres/init.sql # legacy fallback │ └── nginx/{nginx.conf,nginx.prod.conf,Dockerfile} # envsubst INTERNAL_TOKEN -├── docs/ # spec.md, services-pipeline.md, PLAN.md (Phases 9-11 DONE), ZED.md, SWAGGER.md +├── docs/ # spec.md, services-pipeline.md, ZED.md, SWAGGER.md ├── scripts/e2e.sh # upload → poll ready → master.m3u8 → ffprobe aligned segments → gateway HLS └── Makefile # migrate-up/down/create, lint/test, e2e, swagger ``` @@ -116,18 +116,7 @@ Zed IDE auto-fix on save — see [`.zed/settings.json`](.zed/settings.json) and ## 🗺 Roadmap -- [x] Phase 0–1 — infra & DB schema (`deploy/migrations/000001_init.up.sql` golang-migrate, `init.sql` legacy) -- [x] Phase 2 — Auth + Metadata (chi + FastAPI, JWT HS256/Argon2, `owner_email` JOIN) -- [x] Phase 3 — Upload (multipart `MaxBytesReader 5GB` → MinIO raw + `video.uploaded`, `video/*` allowlist) -- [x] Phase 4 — Transcoder (pika + FFmpeg, aligned GOP `-g fps*2 -force_key_frames`, shared `audio.m4a`, `heartbeat 600`) -- [x] Phase 5 — Streaming nginx-vod JIT (`vod_mode mapped`, 3 MP4 → HLS, `vod_segment_duration 2000`, `INTERNAL_TOKEN` envsubst) -- [x] Phase 6 — Gateway (reverse proxy, CORS, rate-limit, JWT, `InternalAuth` proxy, `MaxBytesReader`) -- [x] Phase 7 — Frontend (Next 14, `hls.js` `maxBufferLength 4` seamless, `/watch/[id]` + `playbackRate` + owner badge + delete) -- [x] Phase 8 — Integration & hardening (`scripts/e2e.sh` + `healthcheck` + `docker-compose.prod.yml` CDN) -- [x] Phase 9 — Security & Upload streaming (P0) — `INTERNAL_TOKEN`, `MaxBytesReader 5GB`, `private` raw, `DELETE` + S3 cleanup -- [x] Phase 10 — Transcoder limits & reliability (P0) — sequential, `threads 2` `veryfast`, `fps` preserve, `heartbeat 600`, `4G` limits -- [ ] Phase 10b — Queue DLX/retries & idempotency → Phase 11 presigned multipart → Phase 12 adaptive ladder/HW accel (fan-out, NVENC) -- See [`docs/PLAN.md`](docs/PLAN.md) — 15 phases, event contracts `video.uploaded` / `video.transcoded` +Roadmap and tickets are tracked in the GitHub project. ## 🤝 Contributing diff --git a/deploy/migrations/README.md b/deploy/migrations/README.md index 8ff81bf..5491af7 100644 --- a/deploy/migrations/README.md +++ b/deploy/migrations/README.md @@ -7,4 +7,4 @@ Single source of truth — `deploy/migrations/*.sql` (golang-migrate). - Apply: `make migrate-up` (local) or `migrate` service in `deploy/docker-compose.yml` (auto on `make up`). - Auth's `services/auth/migrations` (alembic) is kept for Python autogen, but **not used in prod** — prod uses this directory. If you add a column via SQLAlchemy, generate alembic version then port SQL to a new `deploy/migrations/00000N_*.up.sql`. -See `Makefile: migrate-*` and `docs/PLAN.md: Фаза 9`. +See `Makefile: migrate-*`. diff --git a/docs/PLAN.md b/docs/PLAN.md deleted file mode 100644 index 0bd99ca..0000000 --- a/docs/PLAN.md +++ /dev/null @@ -1,273 +0,0 @@ -# Flowix — План реализации - -> Дата: 2026-08-30 (переписан после senior review 2026-08-29) · Обновлен: 2026-08-30 — Фазы 9,10,10b,11 DONE (очередь DLX+удаление видео) -> Стек: RabbitMQ + chi `go-chi/chi/v5` + nginx-vod JIT + Next.js 14 -> Исходники: `docs/spec.md:1` (ТЗ), `docs/services-pipeline.md:1` (пайплайн), `AGENTS.md:1` (гайдлайны) -> Ревью-отчет: см. чат 2026-08-29 (P0/P1/P2 находки) - -## Легенда приоритетов - -| Приоритет | Смысл | SLA | -| --------------- | --------------------------------------------------- | --------------- | -| **P0 Critical** | Прод-блокер: OOM, потеря данных, дыра безопасности | до след. релиза | -| **P1 High** | Масштаб/надежность: большие файлы, N видео, ресурсы | 1-2 спринта | -| **P2 Medium** | Оптимизация/UX/DX: экономия, наблюдаемость, техдолг | бэклог | - -Фазы 0-8 — завершены (MVP). Фазы 9+ — план харденинга и масштабирования. - -## Граф зависимостей (текущий + план) - -``` -[Infra: Postgres, MinIO, RabbitMQ, Docker] - ↓ -[Auth (Py)] ←→ [Metadata (Go)] — независимы - ↓ ↓ - [Upload (Go)] → MQ → [Transcoder (Py)] → MinIO - ↓ - [Streaming nginx-vod] ← MinIO - ↓ - [Gateway (Go)] ← Auth/Metadata/Upload - ↓ - [Frontend (Next)] ← Gateway + /hls - - План харденинга: - Фаза 9 (P0: Upload/Metadata sec) ─┐ - Фаза 10 (P0: Transcoder limits+heartbeat) ──┼→ Фаза 11 (P1: presigned upload) → Фаза 13 (P1: HLS auth+CDN) - Фаза 10b (P0: Queue DLX+idempotency) ─────────┘ ↓ - Фаза 12 (P1: adaptive ladder/HW + фан-аут) - ↓ - Фаза 14 (P1: observability) - ↓ - Фаза 15 (P2: storage/CDN cost) -``` - ---- - -## Завершенные фазы (0-8) — кратко, без изменений - -### Фаза 0 — Фундамент монорепо ✅ - -Скелет `services/gateway|metadata|upload|auth|transcoder`, `frontend/`, `deploy/docker-compose.yml` (postgres:16 :5432, minio :9000/:9001, rabbitmq :15672, nginx-vod :8081), `.env.example`, `Makefile`. Проверка: `make up && make ps`. - -### Фаза 1 — Контракты и БД ✅ (миграции — 2026-08-30) - -`deploy/migrations/000001_init.up.sql:1` — source of truth (golang-migrate), `deploy/postgres/init.sql:1` — legacy fallback для fresh volume. `services/auth/migrations/env.py:1` (alembic) — `versions/0001_init.py` no-op. `video_status ENUM`, `users`, `videos (thumbnail_s3_key)`, `video_renditions`. - -### Фаза 2a — Auth (Py FastAPI) ✅ - -`services/auth/src/routers/auth.py:1` — register/login/refresh/me, JWT HS256 15m/7d, argon2. `src/core/security.py:34`. - -### Фаза 2b — Metadata (Go chi) ✅ - -`services/metadata/cmd/server/main.go:1` — CRUD `/api/v1/videos`, `PATCH /internal/videos/:id/status`, `GET /internal/videos/:id/vod` (`internal/handler/video.go:124`). - -### Фаза 3 — Upload (Go chi) ✅ - -`services/upload/internal/handler/upload.go:57` — multipart `POST /api/v1/videos/upload` → MinIO `raw/{id}/original.mp4` → RabbitMQ. - -### Фаза 4 — Transcoder (pika + FFmpeg) ✅ - -`services/transcoder/app/consumer.py:1` — download → ffprobe → 1× ffmpeg audio (`audio.m4a`, общий для всех рипов) → 3× ffmpeg `-g 60 -force_key_frames expr:gte(t,n_forced*2) -c:a copy` → `renditions/{id}/{360,720,1080}.mp4` → PATCH ready. - -### Фаза 5 — Streaming nginx-vod JIT ✅ - -`deploy/nginx/nginx.conf:1` — `vod_mode mapped`, `proxy_pass minio:9000`, `vod_segment_duration 2000`. - -### Фаза 6 — Gateway (Go chi) ✅ - -`services/gateway/cmd/server/main.go:1` — `ReverseProxy` (`internal/proxy/proxy.go:15`), CORS, `RateLimit 20/40` (`internal/middleware/ratelimit.go:19`), JWT. - -### Фаза 7 — Frontend (Next 14) ✅ (удаление — 2026-08-30) - -`frontend/src/components/VideoPlayer.tsx:1` — `hls.js` + `quality/speed` (`frontend/src/lib/api.ts:15`), `frontend/src/lib/api.ts:122` `deleteVideo(id)`, `watch/[id]/page.tsx:15` кнопка «Удалить» (owner only) + `VideoCard.tsx:1` `✕` на карточке, `services/metadata/internal/storage/minio.go:1` чистка S3 `raw/renditions/thumbnails`. - -### Фаза 8 — Интеграция и харденинг ✅ - -`scripts/e2e.sh:1` — `upload→poll ready→master.m3u8→ffprobe aligned segments→gateway /hls`, `make lint-*` зеленые, `deploy/docker-compose.prod.yml:1` (CDN Cache-Control). - ---- - -## Фаза 9 — P0: Безопасность и Upload streaming ✅ DONE 2026-08-30 - -**Приоритет: P0 Critical** · Оценка: 2-3 дня · Зависит от: 0-8 ✅ · **Выполнено** - -**Проблемы из ревью:** - -- `services/upload/internal/handler/upload.go:66` `ParseMultipartForm(200<<20)` грузит большие файлы в память/диск upload-пода → OOM на 5-10GB, блокирует хэндлер (нарушает `docs/spec.md:252` — heavy work в очереди). -- `deploy/docker-compose.yml:43` `mc anonymous set public` — `raw/` и `renditions` публично читаемы. -- `services/metadata/cmd/server/main.go:87` `PATCH /internal/videos/:id/status` без auth — любой может подделать `ready` + `GET /internal/videos/:id/vod` отдает маппинг. -- Нет лимита размера запроса ни в gateway, ни в upload. - -**Выполнено 2026-08-30 (лимит 5-6GB как просил, большие файлы — Фаза 11 presigned):** - -1. `upload`: `MaxBytesReader 5GB` (`UPLOAD_MAX_BYTES` env, `upload.go:65`) + `ParseMultipartForm 32MB` + MIME allowlist `video/*` →413/400. `"internal/middleware/internal.go:1"` не трогали — узкое место. -2. `gateway`: `maxBytesMw` для `POST /api/v1/videos/upload` (`gateway/cmd/server/main.go:98`) + прокидка `X-Internal-Token` в `metadataProxy/vodProxy` Director. -3. `metadata`: `InternalAuth` (`services/metadata/internal/middleware/internal.go:5`) — закрывает `Group(/internal/*)` (`metadata/cmd/server/main.go:86`), `transcoder/consumer.py:43` шлет заголовок, `deploy/nginx/nginx.conf:40` + `Dockerfile:33` envsubst. -4. `deploy/docker-compose.yml:50` `minio-setup` → `private` + `download` для `renditions/thumbnails`, `raw` private (проверено: `curl raw` 403, rendition 200, HLS 200). -5. MIME валидация: `allowedCT` + `isAllowedContentType` (`upload.go:135`). - -**Затронуто:** `services/upload/internal/handler/upload.go`, `services/gateway/cmd/server/main.go`, `services/metadata/cmd/server/main.go`, `services/metadata/internal/middleware/internal.go`, `services/transcoder/app/consumer.py`, `deploy/docker-compose.yml`, `deploy/nginx/nginx.conf`, `deploy/nginx/Dockerfile`, `.env.example:27` (`INTERNAL_TOKEN`, `UPLOAD_MAX_BYTES`). - -**Проверка:** `curl /internal/.../vod` без токена 401 / с токеном 404 — ok; `POST /upload image/jpeg` 400; `small.mp4` 201 → ready → HLS 200; `raw` 403 / `renditions` 200; `go test ./...` ok. - ---- - -## Фаза 10 — P0: Transcoder — лимиты ресурсов и надежность ✅ DONE 2026-08-30 - -**Приоритет: P0 Critical** · Оценка: 3-4 дня · Зависит от: Фаза 9 ✅ DONE · **Выполнено** - -**Проблемы:** - -- `services/transcoder/app/consumer.py:213` `ThreadPoolExecutor(max_workers=3)` — 3× `libx264` = OOM, `timeout=300` убивает длинные видео `consumer.py:139`, `fget_object` качает целиком в `/tmp` `consumer.py:188` — диск кончается на больших файлах. -- Нет лимитов в `deploy/docker-compose.prod.yml` для transcoder, `HEALTHCHECK pgrep` `services/transcoder/Dockerfile:18` ложный. -- `consumer.py:139` форсирует `-r 30` даже для 24fps → лишний CPU. - -**Выполнено 2026-08-30 (в т.ч. фикс 2GB heartbeat):** - -1. Последовательный транскод: убран `ThreadPoolExecutor(max_workers=3)` (`consumer.py:213`), цикл `for q,w,h,br in RENDITIONS_SPEC: transcode_one(..., fps=fps)` — проверено: 24fps видео → 3× ffmpeg последовательно. -2. `transcode_one`: `-threads 2 -preset veryfast` (`FFMPEG_THREADS/PRESET` env, `consumer.py:140`), fps из probe (`_fps_from_probe` `consumer.py:108` — preserve ≤30, cap >30→30, fallback 30), `-g/-keyint_min fps*2`, `-maxrate 1.10×` (`consumer.py:155`), `timeout 900`, общий `audio` (`audio.m4a` + `-c:a copy`). -3. Диск-чек: `shutil.disk_usage(tmp).free <500MB → abort` (`consumer.py:210`), `fget_object` остался (pipe — Фаза 12). -4. `SIGTERM` graceful: `signal.signal(SIGTERM/SIGINT)` + `_shutdown` flag (`consumer.py:332`), `timeout 900`, `celery` legacy (удаление — Фаза 10b), `healthcheck` `pgrep app.consumer`. -5. **Heartbeat fix для больших файлов:** `params.heartbeat=600 blocked_connection_timeout=300` (`consumer.py:344`, было `60→104` на `2GB` `5м20с`), идемпотентность `ready→skip` (`consumer.py:248` `_get_status`), `0f1e8...` `19:33→19:39` теперь без ре-транскода. -6. `deploy/docker-compose.yml:179` env `FFMPEG_THREADS/PRESET`, `deploy/docker-compose.prod.yml:44` limits `cpus 2 / mem 4G` + reservations 1G, `healthcheck pgrep` only. -7. `.env.example:30` `FFMPEG_THREADS=2`, `FFMPEG_PRESET=veryfast`, `Makefile:84` `dev-transcoder` → `python -m app.consumer` (legacy `dev-transcoder-celery`). - -**Затронуто:** `services/transcoder/app/consumer.py`, `services/transcoder/pyproject.toml:8` (коммент legacy), `services/transcoder/README.md`, `deploy/docker-compose.yml`, `deploy/docker-compose.prod.yml`, `.env.example`, `.env`, `Makefile`. - -**Проверка:** `24fps` исходник → `master.m3u8 FRAME-RATE=24.000` (не 30), `g 48`, `threads 2 preset veryfast` в логах, `small.mp4 2s → ready` за 2 polling, `docker stats` <4G, `make e2e` (6s sample) зеленый, `kill -TERM` graceful. - ---- - -## Фаза 10b — P0: Очередь — DLX, ретраи, идемпотентность ✅ DONE 2026-08-30 - -**Приоритет: P0 Critical** · Оценка: 1-2 дня · Зависит от: Фаза 10 (можно параллельно с 9) · **Выполнено** - -**Проблемы:** - -- `services/transcoder/app/consumer.py:272` `basic_nack(requeue=False)` теряет задачу навсегда, `services/upload/internal/queue/publisher.go:94` публикует `Persistent` но без `DLX`, `consumer.py:264` бесконечный `while True` без backoff на `AMQPConnectionError`. -- Нет идемпотентности: повтор `video.uploaded` → дубли `video_renditions`. - -**Выполнено 2026-08-30:** - -1. `upload` `queue/publisher.go:59` `declareTopology` — `dlx` exchange `direct`, `video.uploaded.dlq` + `video.uploaded.retry` (`x-message-ttl 30000`, `x-dead-letter-routing-key video.uploaded`), main queue `video.uploaded` с `x-dead-letter-exchange dlx → dlq`; авто-хил precondition (удаление stale queue без DLX). -2. `transcoder` `app/consumer.py:256` `declare_topology` + `main:434` ретрай 3× `x-retry-count` header → `basic_publish retry` → ack, после 3× `basic_nack requeue=False` → DLQ + `update_status failed`; `process_message` теперь `raise` для transient ошибок, `ready/failed` идемпотентность. -3. `metadata` `repository/video.go:125` `UpdateStatus` — `SELECT ... FOR UPDATE` + guard `ready→processing/uploaded` skip, `ON CONFLICT` для renditions сохранен; `handler/video_test.go:106` fakeStore mirror + `TestUpdateStatusIdempotencyReadyNotDowngraded`. -4. `transcoder` тесты `tests/test_consumer.py:82` `test_process_message_minio_failure_raises_for_retry` + `test_declare_topology`, `test_process_message_idempotent_*`. - -**Затронуто:** `services/upload/internal/queue/publisher.go`, `services/transcoder/app/consumer.py`, `services/transcoder/tests/test_consumer.py`, `services/metadata/internal/repository/video.go`, `services/metadata/internal/handler/video_test.go`. - -**Проверка:** `uv run pytest services/transcoder/tests/test_consumer.py -v` 16 passed; `go test ./...` metadata/upload green; `rabbitmqadmin list queues` после `kill -9 transcoder` → retry 30s → DLQ + `status=failed`. - ---- - -## Фаза 11 — P1: Масштабируемый upload (presigned multipart) ✅ DONE 2026-08-30 - -**Приоритет: P1 High** · Оценка: 4-5 дней · Зависит от: Фазы 9, 10b · **Выполнено** - -**Проблема:** upload через Go прокси — узкое место для больших файлов и N параллельных загрузок (1 под = 1 файл в памяти). Нет resumable. - -**Выполнено 2026-08-30 (single PUT presigned, multipart ladder — backlog):** - -1. Новый флоу `POST /api/v1/videos/presign` (`services/upload/internal/handler/presign.go:28` → `storage/minio.go:48` `PresignedPutObject` 1h + `MINIO_PUBLIC_ENDPOINT` rewrite) возвращает `{id, video_id, s3_key, method, url, expires_in}`; `POST /api/v1/videos/{id}/complete` проверяет `StatObject` + публикует `video.uploaded` (идемпотентно). Легковесный single PUT покрывает 5GB bypass без S3 CreateMultipartUpload (multipart чистит бэклог). -2. Совместимость сохранена: `POST /api/v1/videos/upload` оставлен (`upload.go:61` deprecated, <100MB fallback в FE). -3. Gateway `services/gateway/cmd/server/main.go:112` прокси `presign/complete` без `MaxBytesReader`, `CORS` уже `*`; MinIO `deploy/docker-compose.yml:32` `MINIO_API_CORS_ALLOW_ORIGIN=*`. -4. Upload `services/upload/cmd/server/main.go:86` регистрирует `presign/complete`, `.env.example:34` `MINIO_PUBLIC_ENDPOINT=http://localhost:9000`. -5. FE `frontend/src/lib/api.ts:126` `presignVideo/completeVideo/putToPresignedUrl` + `uploadVideo` с `localStorage` resume, `retry 3×`, `AbortSignal`, fallback на `uploadViaGateway` для <100MB; `frontend/src/app/upload/page.tsx:4` текст 5GB direct. - -**Затронуто:** `services/upload/internal/storage/minio.go`, `services/upload/internal/handler/presign.go` (+ `presign_test.go`), `services/upload/cmd/server/main.go`, `services/gateway/cmd/server/main.go`, `frontend/src/lib/api.ts`, `frontend/src/app/upload/page.tsx`, `deploy/docker-compose.yml`, `.env.example`. - -**Проверка:** `go test ./...` upload handler 6 passed; `npm run lint` frontend ok; `curl POST /api/v1/videos/presign` → 201 + `url` с `X-Amz-Signature`, `PUT` в `url` → `POST /complete` → `video.uploaded` → `ready` → HLS; сеть показывает прямое `PUT https://localhost:9000` без прокси, обрыв → resume из `localStorage`. - ---- - -## Фаза 12 — P1: Адаптивный ladder + HW accel + фан-аут (обновлено 2026-08-30, анализ 2GB 5м20с) - -**Приоритет: P1 High** · Оценка: 3-5 дней · Зависит от: Фаза 10, 11 · **Доп.: анализ 2GB 3248×2004 275с → 360p 100с+720p 100с+1080p 106с + heartbeat 104** - -**Проблемы:** - -- Фиксированный ladder `consumer.py:31` апскейлит 480p→1080p, тратит CPU на мыло. -- `libx264` CPU-only — `2GB` `19:33:41→19:39` ~5м20с, `heartbeat 60с` → `ConnectionResetError 104` → ре-транскод ×2. -- Последовательно 3 рипа в 1 воркере → `max = sum`, хотя `cpu 2 / mem 4G` `deploy/docker-compose.prod.yml:44`. - -**Задачи (расширено):** - -1. **Адаптивный ladder** (`probe` `consumer.py:62`): `<720p→[360p]`, `720p→[360p,720p]`, `≥1080p→[360p,720p,1080p]` (+ опц. `144p`). Экономия -33% CPU для `3K` без `360p` миса. -2. **CRF 23 + maxrate** вместо CBR — -20-30% битрейта, `env: ENCODE_MODE`. -3. **HW accel** (`Dockerfile:4` `jrottenberg/ffmpeg:*-nvidia` или `--enable-nvenc/qsv`), `transcode_one` `h264_nvenc -preset p4 -rc vbr` если `nvidia-smi`, иначе `libx264` — **5-10×** быстрее (`2GB` → 60-90с). -4. **Фан-аут по рипам** — 3 очереди `video.transcode.360p/720p/1080p`, 3 воркера по 1 рипу (`prefetch 1`) → `total = max(100с)` вместо `sum 300с`. + **720p first** — `master.m3u8` из 2 рипов, `1080p` докидывается позже (юзер видит через 100с, а не 300с). -5. **Стрим без `/tmp`** — `ffmpeg -i pipe:0` + `MinIO GetObject` stream (сейчас `fget_object` `consumer.py:272` 2GB на диск) → экономия диска/N параллельных. -6. **Чанк-транскод** (опц. для >10м видео) — режем по `force_key_frames 2с` на чанки, пул 4 воркера, `concat`. -7. **Heartbeat fix** уже в Фазе 10: `params.heartbeat=600` `consumer.py:344` (было `104`), идемпотентность `ready→skip`. -8. Thumbnail из `360p` рипа, `ultrafast` для `>1GB` (`FFMPEG_PRESET` env). - -**Проверка:** `480p→[360p]`, `3K 2GB→[720p,1080p]` ~90с с NVENC, `fan-out` `master` с 2 рипами за 100с, `next HLS` без freeze (буфер 4с `VideoPlayer.tsx:35`). - ---- - -## Фаза 13 — P1: Доступ к HLS и CDN - -**Приоритет: P1 High** · Оценка: 2-3 дня · Зависит от: Фаза 9 - -**Проблемы:** - -- `services/gateway/cmd/server/main.go:115` `r.Handle("/hls/*", vodProxy)` публично, `deploy/nginx/nginx.conf:61` `Cache-Control: public` — приватные видео доступны подбором UUID. -- Нет `signed URL`, нет проверки владельца. - -**Задачи:** - -1. Gateway middleware `HLSAuth`: `GET /hls/{id}/*?token=...` (JWT с `video_id` + `exp` 1h) или проверка `Authorization: Bearer` + `metadata owner_id`. Для публичных видео — пропуск. Для приватных — 403 без токена. -2. `metadata` добавить `visibility: public|private|unlisted` в `videos` (`deploy/postgres/init.sql`), `GET /internal/videos/:id/vod` проверяет visibility. -3. `deploy/nginx/nginx.prod.conf` — `Cache-Control: public, max-age=86400` для public, `private, max-age=10` для private, `CDN-Cache-Control` уже есть в `prod.yml` — дописать. -4. Добавить `Range` кеш на CDN (CloudFront/Cloudflare) + `vod_response_cache 128m` уже есть `nginx.conf:18` — увеличить до 512m для N видео. - -**Проверка:** `curl /hls/{private_id}/master.m3u8` без токена → 403, с токеном → 200, `curl -I` показывает `Cache-Control: private`. - ---- - -## Фаза 14 — P1: Наблюдаемость - -**Приоритет: P1 High** · Оценка: 2 дня · Можно параллельно с 11-13 - -**Проблемы:** `RequestID` не прокидывается (`proxy.go:18`), логи `zerolog/slog/logging` вперемешку, нет метрик. - -**Задачи:** - -1. Gateway `proxy.go:18` прокидывать `X-Request-ID` (chi `middleware.RequestID`), `X-User-ID`, `X-Forwarded-For` во все апстримы. `metadata`/`upload` логировать `request_id`. -2. Добавить `prometheus` + `grafana` в `deploy/docker-compose.yml`, метрики: `rabbitmq_queue_depth`, `ffmpeg_duration_seconds`, `upload_bytes`, `vod_cache_hit`. -3. `auth`/`metadata`/`upload`/`gateway` — единый JSON лог формат (`zerolog` уже json в prod `gateway/main.go:35`), добавить `trace_id`. -4. Алерты: queue depth >100, transcoder OOM, MinIO disk >80%. - -**Проверка:** `curl -H X-Request-ID:test123 /api/v1/videos` → логи всех сервисов содержат `test123`, `http://localhost:9090/metrics` отвечает. - ---- - -## Фаза 15 — P2: Стоимость и хранение - -**Приоритет: P2 Medium** · Оценка: 1-2 дня · Зависит от: Фазы 11, 12 - -**Задачи:** - -1. MinIO lifecycle: удалять `raw/{id}/original.mp4` через 7d после `ready` (или сразу, оставлять только если `keep_raw=true`). Добавить `mc ilm add --expiry-days 7`. -2. Добавить `pgbouncer` перед postgres, `pool_pre_ping=True` уже есть `services/auth/src/core/db.py:26` — настроить `pool_size`. -3. `frontend` `next.config.mjs` — `output: standalone` для меньшего образа, `VideoPlayer.tsx:34` `maxBufferLength:10` уже экономит память клиента — оставить. -4. DASH опционально (nginx-vod уже умеет `vod_dash`), но после HLS харденинга. - -**Проверка:** `raw/` удаляется, `videos` таблица имеет `updated_at`, `docker images` FE <200MB. - ---- - -## Бэклог (не в фазах) - -- ~~`golang-migrate` / `alembic` миграции вместо `init.sql` на проде.~~ ✅ **Выполнено 2026-08-30:** `deploy/migrations/000001_init.up.sql` (golang-migrate) + сервис `migrate` в `deploy/docker-compose.yml:22`, `deploy/postgres/init.sql` помечен legacy, `Makefile: migrate-up/down/create`, `services/auth/migrations/env.py` + `versions/0001_init.py` (alembic). См. `deploy/migrations/README.md:1`. -- ~~Удаление видео.~~ ✅ **Выполнено 2026-08-30:** `DELETE /api/v1/videos/:id` (owner only, `metadata/internal/handler/video.go:213` + `storage/minio.go:29` + `RemovePrefix` для orphans mid-transcode, `metadata/cmd/server/main.go:1` MinIO env), `gateway` уже проксировал `DELETE`, `frontend` `deleteVideo` + кнопки на `watch` и `VideoCard`. -- `Content-Range` resumable fallback для presign (если MinIO multipart не поддержан). -- Rate-limit на `auth` login (brute-force) — `redis` + `slowapi`. -- E2E `scripts/e2e.sh:189` расширить на presign + private HLS. - -## Чеклист приемки харденинга - -- [ ] `make up && make e2e` зеленый (старый флоу + новый presign) -- [ ] `make lint-go lint-py lint-front && make test-go test-py` зеленые -- [ ] Нагрузочный: 5× параллельных 1GB загрузок + 10 видео в очереди — без OOM, DLQ работает -- [ ] Security: `internal` без токена 401, `raw` не публичен, `hls` private 403 без JWT diff --git a/docs/REVIEW-2026-09-13-merged.md b/docs/REVIEW-2026-09-13-merged.md deleted file mode 100644 index 7eb83f3..0000000 --- a/docs/REVIEW-2026-09-13-merged.md +++ /dev/null @@ -1,222 +0,0 @@ -# Code Review — Flowix. Сводный отчёт трёх ревью (2026-09-13) - -> Объединение и дедупликация трёх независимых ревью одного дня: -> 1. `docs/REVIEW.md:1` — построчный аудит безопасности/корректности + архитектура. -> 2. `docs/REVIEW-fullstack-2026-09-13.md:1` — фокус на оптимизацию, архитектурные варианты и визуал. -> 3. `docs/REVIEW-fresh-2026-09-13.md:1` — свежий проход: data-plane обход приватности, CVE, resumable DoS. -> -> Ссылки на строки актуальны на коммит `91efb5b` (main). Контракты — `docs/spec.md:1`, пайплайн — `docs/services-pipeline.md:1`. - -## Вердикт - -Ядро правильное: `upload → RabbitMQ → transcode → MinIO → HLS`, выровненный GOP, общий `audio.m4a`, DLX+retry с `x-retry-count`, идемпотентный `UpdateStatus` (tx + `FOR UPDATE`), golangci-lint чист. Но: - -1. **Заявленная фича «private video» не работает** — приватность обходится целиком на data plane (CRITICAL, цепочка). -2. Слой upload-сервиса — главный очаг IDOR и DoS. -3. Транскодер: зависшие видео и зомби-ffmpeg на пути к отказоустойчивости. -4. Фронт — самый дешёвый рычаг UX: без UI-кита, react-query, silent refresh. - -Счёт: **1 CRITICAL, 3 HIGH, 8 MEDIUM, ~15 LOW, 6 improvements.** - ---- - -## 1. CRITICAL - -### Приватность обходится на уровне data plane -**Где:** `deploy/docker-compose.yml:88-91,70,139`; `deploy/nginx/nginx.conf:56-63`; `services/metadata/internal/repository/video.go:64`; `services/gateway/cmd/server/main.go:129,145` - -Вся авторизация видео живёт только в gateway (`HLSAuth`), но data plane торчит наружу мимо него: - -1. **MinIO** (`:9000` опубликован): префиксы `renditions/` и `thumbnails/` объявлены анонимно скачиваемыми (`mc anonymous set download`). Зная UUID, любой качает `http://:9000/videos/renditions/{id}/720p.mp4` — ни JWT, ни HLS-токен не нужны. -2. **nginx-vod** (`:8081` опубликован): в `location ^~ /hls/` нет проверки — `HLSAuth` реализован в gateway, которого здесь нет. -3. **Публичный `GET /api/v1/videos`** отдаёт ID приватных видео (List без фильтра visibility), замыкая цепочку. -4. **`/thumbnails/*`** проксируется в MinIO без auth — превью приватных видео публичны. - -**Fix:** `mc anonymous set private` на весь bucket; убрать наружную публикацию `:9000`/`:8081`; visibility-фильтр в `List`/`Get` (`WHERE (visibility='public' OR owner_id=$me)`); thumbnails за тем же `HLSAuth` или signed URL с TTL. -**Refs:** OWASP A01:2021, CWE-732. - ---- - -## 2. HIGH - -### H1. IDOR в upload: complete/status/resumable не проверяют владельца -**Где:** `services/upload/internal/handler/presign.go:126-143`, `resumable.go:47,98` - -`Complete`, `GET/PUT .../resumable` не сверяют владельца: `ownerID` из контекста доступен, но не используется. Любой залогиненный юзер, зная UUID, дозапишет/перезапишет чужой `raw/{id}/original.mp4` и запустит транскод чужого видео. **Fix:** сверять owner через internal-эндпоинт metadata, как в `HLSTokenHandler`; заодно сверять size/content-type (`complete` сейчас проверяет только существование объекта). - -### H2. Resumable upload: неограниченное тело + файл целиком в RAM -**Где:** `services/upload/internal/handler/resumable.go:114,156`; `services/gateway/cmd/server/main.go:122`; `frontend/src/lib/api.ts:224` - -На gateway-роуте `PUT .../resumable` нет `maxBytesMw` (он только на `/upload`), в хендлере `MaxBytesReader` тоже нет, при этом `io.ReadAll(r.Body)` читает всё. Фронт шлёт «resumable» файл одним чанком (`chunkSize = total - offset`) → 5GB целиком буферизуется в RAM воркера, один запрос кладёт контейнер. Побочно: объект раздувается сверх `UPLOAD_MAX_BYTES` (лимит per-request, не per-object). **Fix:** `MaxBytesReader` на роут + S3 multipart upload с чанками 5–10MB (он же убивает H3). - -### H3. Resumable O(n²): каждый чанк перечитывает и перезаливает весь объект -**Где:** `services/upload/internal/handler/resumable.go:170-183` - -Каждый чанк скачивает весь существующий объект в память, аппендит и перезаливает. На 5GB-файле — гигабайты перетрафика. **Fix:** S3 multipart (совместно с H2). - -### H4. python-jose ≤ 3.3.0: CVE-2024-33663 + CVE-2024-33664 -**Где:** `services/auth/pyproject.toml`, `services/auth/src/core/security.py` - -CVE-2024-33663 (algorithm confusion) и CVE-2024-33664 (JWT-бомба, DoS в `decode()`); все версии ≤ 3.3.0 затронуты, проект не поддерживается. DoS реален: `/refresh` и `/me` декодируют произвольные байты клиента. **Fix:** PyJWT или joserfc; минимум — лимит размера токена до decode. passlib тоже unmaintained → `argon2-cffi`. - ---- - -## 3. MEDIUM - -### M1. List/Get отдают приватные видео всем (источник UUID для CRITICAL) -**Где:** `services/metadata/internal/repository/video.go:64`, `handler/video.go:110`, `gateway/cmd/server/main.go:129` -**Fix:** `WHERE (visibility='public' OR owner_id=$me)`; `Get` — проверка visibility/owner. Учесть `unlisted` (недоступен в List, доступен по ссылке). - -### M2. Токены не обновляются никогда -**Где:** `frontend/src/store/auth.ts:39`, `frontend/src/lib/api.ts:76-93` -`refresh_token` кладётся в localStorage и ни разу не читается; при истечении access — молчаливый logout. Нет 401-обработки и refresh-ретея. **Fix:** 401-интерцептор + refresh retry + очередь запросов. - -### M3. Токены в localStorage; JWT в query string -**Где:** `frontend/src/auth.ts:29,48`; `gateway/internal/middleware/hlsauth.go:161,265` -XSS-кража токенов; HLS-токен (1 час) в URL попадает в access-логи/promtail/Referer. **Fix:** на фронте — httpOnly cookies через BFF; в gateway — токен только в теле ответа, заголовочные варианты уже работают. - -### M4. CORS `*` + `Allow-Credentials: true` -**Где:** `gateway/internal/middleware/cors.go:52-58` -Фактически «любой origin с куками». **Fix:** явный allowlist origin из env. - -### M5. Rate-limit обходится -**Где:** `gateway/internal/middleware/ratelimit.go:19-99`, `auth/src/core/limiter.py:15-33` -In-memory по IP, доверяет клиентскому `X-Forwarded-For` (первый IP подделывается); auth slowapi ключует по IP гейтвея — один абузер блокирует всех. Redis задеплоен, но не используется. **Fix:** rate-limit на Redis, доверенный XFF только от известного прокси (или считать по RemoteAddr, раз gateway — точка входа). - -### M6. JWT: секрет с дефолтом, `exp` не обязателен, INTERNAL_TOKEN пустой = open -**Где:** `gateway/cmd/server/main.go:24`, `metadata/cmd/server/main.go:57`, `upload/cmd/server/main.go:38`, `auth/src/core/config.py:7`; все три Go auth-мидлвари; `metadata/internal/middleware/internal.go:11` -Забытая переменная = тихо небезопасный деплой; токен без `exp` валиден навсегда; пустой `INTERNAL_TOKEN` делает `/internal/*` публично доступными (порт metadata опубликован) и молча отключает идемпотентность transcoder. **Fix:** fail fast при старте; `jwt.WithExpirationRequired()`; InternalAuth — constant-time compare. - -### M7. Видео навсегда «processing» -**Где:** `transcoder/app/consumer.py:832-926` -Если `update_status("ready")` упал, transcoder всё равно ack-ает — ретрая не будет никогда. **Fix:** ack только после успешного update_status; иначе basic_nack → retry-очередь. - -### M8. Redis наружу без пароля -**Где:** `deploy/docker-compose.yml:100-108` -`redis:7-alpine` без `--requirepass`, порт на 0.0.0.0 — классический путь компрометации. **Fix:** requirepass + пароль в REDIS_URL. Рядом: дефолтные креды (MinIO minioadmin, Postgres flowix, Grafana admin/admin, md5-аутентификация Postgres — сознательный tradeoff под pgbouncer, нужен план на scram). - -### M9. Регистрация: нет политики пароля, регистрозависимый email, гонка -**Где:** `services/auth/src/schemas.py:5-11`, `routers/auth.py:46-51` -Пароль любой длины (включая пустой); `EmailStr` не нормализует регистр, а `users.email UNIQUE` регистрозависим — `Ivan@x.com` и `ivan@x.com` — два аккаунта; SELECT-then-INSERT без ловли `IntegrityError` — конкурентная регистрация даёт 500. **Fix:** `min_length=8`, email в lowercase (CITEXT), `IntegrityError` → 409. - -### M10. Порты сервисов торчат наружу -**Где:** `deploy/docker-compose.yml` (auth 8001, metadata 8002, upload 8003) -Клиенты идут мимо гейтвея, минуя rate-limit и CORS. **Fix:** публиковать только gateway (+frontend), остальные — internal. - -### M11. Пресigned PUT без лимита размера -**Где:** `upload/internal/storage/minio.go:65-105` -`Content-Length` не подписан — URL позволяет залить произвольный объём за 1 час. **Fix:** presign с условием размера (POST policy) или сверка в `complete` (см. H1). - ---- - -## 4. LOW (сгруппировано) - -**Transcoder:** зомби-ffmpeg в pipe-режиме при ошибке стрима/таймауте (`consumer.py:518-559`); heartbeat 600с против ffmpeg 900с×3 — соединение умрёт на 5GB (`consumer.py:877-980`); проверка MagicMock в продовом коде (`:694`); pipe-режим качает файл всё равно + ещё раз на каждый рип (`:719-783`); `FANOUT_QUEUES` объявлены, но не потребляются (`:65,973`). - -**Go-сервисы:** ошибки через сравнение строк `err.Error() == "forbidden"`, любая ошибка БД → 404 (`metadata/internal/handler/video.go:220-228`); интерполяция `err.Error()` в рукописный JSON — битый JSON + утечка инфры (`upload.go:86,114`, `presign.go:87,101`, `resumable.go:120`); create-в-метадате валидирует после вставки (`video.go:84-98`); SigV4-баг в fallback-presign (host после подписи — `minio.go:87-96`, частично починен в `PresignedPutObjectExternal`); `QueueDelete(ifEmpty=false)` при лечении topology удаляет живую очередь с сообщениями (`publisher.go:58`, `consumer.py` main-loop); `respWriter` без `http.Flusher`; `http.Client` без таймаута (`upload/internal/client/metadata.go:16`); proxy стирает `Vary` у всех ответов (`proxy.go:67`); presign-сирота при упавшем presign (`presign.go:106-110`). - -**Frontend:** двойной сабмит при `progress === 0` (`upload/page.tsx:82`); утечка поллинга при ошибке (`watch/[id]/page.tsx:44-47`); presigned URL в localStorage (`api.ts:274`); `as any` и приватный `BUFFER_FLUSHING`-хак (`VideoPlayer.tsx:110,141-146`). - -**Инфра/наблюдаемость:** `/metrics`, `/docs`, `/openapi.json` без auth; мёртвые метрики (`upload_bytes`, `vod_cache_hit`, `rabbitmq_queue_depth`, `ffmpeg_duration` в auth) — объявлены, никогда не инкрементятся; e2e-логин с паролем `string`. - ---- - -## 5. Архитектура - -### Go-сервисы -- **Тройная копипаста** `AuthMiddleware`/logger/metrics/writeJSON/bootstrap, версии уже разошлись (jwt v5.3.1 vs v5.2.2, go 1.25.0 vs 1.23.0, minio v7.3.0 vs v7.0.70). → shared-модуль через `go.work`. -- **Нет graceful shutdown и таймаутов** `http.Server` ни в одном сервисе (slowloris + обрыв загрузок при деплое). -- **HLS-auth сам себе DDoS:** HTTP в metadata на каждый `.ts`-сегмент, новый `http.Client` без пула (`hlsauth.go:98-107`). → LRU `video_id→meta` 10–15с + negative cache + переиспользуемый клиент (~2 порядка меньше запросов). -- **Update в репозитории:** read-check-write (TOCTOU) + до 3 UPDATE без транзакции (`repository/video.go:85-116`) → один `UPDATE ... WHERE id AND owner_id RETURNING`. `UpdateStatus` — образец, так и надо. -- `Create` делает лишний синхронный `SELECT email` на каждую вставку (`video.go:29`). -- Мёртвый код: `OptionalAuth`, `NewWithPrefix`, `RemoveObject`, дублированный `writeJSON`. - -### Python-сервисы -- **`consumer.py` — god-file 998 строк**: разбить на `queue.py/ffmpeg.py/pipeline.py`, один `build_ffmpeg_argv` (сейчас `transcode_one`/`transcode_one_pipe` дублируют ~60% и уже разъехались). -- **Мёртвый Celery**: `celery_app.py`/`tasks.py` — стабы, пакеты `celery/redis/boto3/httpx/python-multipart` не используются. Удалить. -- **Нет событий как контрактов**: `video.uploaded` парсится сырым `json.loads`; `video.transcoded` не существует. → versioned Pydantic-модели; позже SSE-пуш вместо поллинга на watch. -- **Нет publisher confirms** (`confirm_delivery()`) — retry-publish может тихо теряться. -- **Split-brain миграций**: alembic в auth — no-op, схема живёт в golang-migrate. Выбрать один. -- Auth: нет ротации refresh-токенов (нет `jti`, старый жив до 7 дней), `/refresh` не проверяет юзера, лимиты только на login. - -### Стратегические варианты (из ревью 2) -1. **nginx-vod — риск** (модуль Kaltura заброшен). Вариант A (рекомендация для MVP): прегенерить HLS в транскодере, MinIO + CDN. Вариант B: пиннуть форк + fallback на static. -2. **Пуш вместо поллинга**: `video.transcoded` → gateway SSE `/api/v1/videos/{id}/events` → фронт. -3. **Единый контракт**: `openapi.json` как source of truth + `oapi-codegen` + `openapi-typescript` (Go struct / Pydantic / TS `Video` дрейфуют). -4. **Compose-профили** (`core / monitoring / gpu`), FE standalone + distroless. -5. **Наблюдаемость**: OTel gateway→services + `trace_id`, Sentry FE/BE, pprof. - ---- - -## 6. Фронтенд и визуал (главный рычаг UX) - -Сейчас: голый Tailwind, `confirm()/alert()`, смесь RU/EN, `` с eslint-disable, ручные `useEffect`-фетчи, кастомный плеер 194 строки. - -| Задача | Взять | Почему | -|--------|-------|--------| -| Дизайн-система | **shadcn/ui + Radix** | Вендорится в репо, Tailwind уже есть, a11y. Заменяет `confirm/alert`, хардкод `bg-red-500/bg-black` | -| Плеер | **Vidstack** (hls.js внутри) | Готовые качество/скорость/PiP; убирает ~120 строк ручного `nextLevel`-кода. Минимум — убрать `BUFFER_FLUSHING`-хак | -| Данные | **TanStack Query** | Кеш ленты, `refetchInterval` вместо ручного `setInterval`, инфинит-скролл | -| Формы | **react-hook-form + zod** | login/register/upload валидация | -| Загрузка | **Uppy / react-dropzone + tus** | Drag&drop, чанки, resume, отмена | -| Картинки | **next/image** | Вместо `` в `VideoCard.tsx:44` | - -Также: Server Components для фида/watch + `generateMetadata` (OG), `loading/error/not-found.tsx`, `middleware.ts` защита `/upload`, тёмная YouTube-like тема, единый язык UI (next-intl), dynamic import hls.js (~130KB gz из бандла), zustand-селекторы (каждый `VideoCard` сейчас ререндерится на изменение auth), фичи: поиск, пагинация, «мои видео», отмена загрузки. - ---- - -## 7. Инфраструктура и CI - -- **CI не покрывает фронтенд** — нет `lint-front`/`build` джобов. -- **Нет `.dockerignore` во frontend** — `COPY . .` тащит `node_modules`/`.next`; `npm ci 2>/dev/null || npm install` глушит ошибки. -- **Next 14.2.5** — security-фиксы в 14.2.25+ (middleware bypass); поднять до последней 14.2.x/15. -- Redis задеплоен, но не используется ни gateway (rate-limit), ни auth (ревокация) — либо использовать, либо убрать. -- Тесты, которых не хватает: pgx-репозиторий (testcontainers), `HLSAuth`/`HLSTokenHandler`, `on_message` retry/DLQ (самая критичная логика трансформатера), e2e на resumable-путь, auth на реальном Postgres. - ---- - -## 8. Improvements (upside-пасс) - -1. **[HIGH] Shared Go-модуль** — копии уже разошлись; убирает класс «пофиксил в двух из трёх». -2. **[HIGH] `Update` в metadata → один атомарный UPDATE** — 6 запросов → 1, TOCTOU исчезает. -3. **[MED] HLSAuth: LRU-кеш + общий http.Client** — ~2 порядка меньше запросов к metadata. -4. **[MED] Один builder ffmpeg-аргументов** — половина `consumer.py` короче. -5. **[MED] Убрать socket-пробу Redis из `limiter.py`** — slowapi сам деградирует при ConnectionError. -6. **[LOW] Удалить мёртвые метрики** — дашборды пустые, код вводит в заблуждение. - ---- - -## 9. План правок (объединённый) - -> Порядок выполнения задан фазами **16 → 17 → 18** (лейблы `phase:16/17/18` на issues #43–64, продолжение нумерации после фазы 15 из `docs/PLAN.md`). Внутри фазы — по приоритету P0→P2. Фаза 17 начинается со shared Go-модуля (#63): формально он P2, но CORS/rate-limit/HLSAuth-правки (P1) лучше делать уже в общем модуле, чтобы не чинить три копии дважды. Долгие фазы C/D (UI-кит, стратегия) — вне этого бэклога, по мере готовности 16–18. - -### Phase 16 — security hotfix (первая неделя) - -`phase:16`: **#43** MinIO-политики + порты 9000/8081 → **#44** visibility-фильтр List/Get → **#45** thumbnails за auth → **#46** IDOR в complete/resumable → **#47** лимит тела resumable → **#48** python-jose → pyjwt → **#53** fail fast на секретах + exp обязателен. - -Verify: приватное видео недоступно ни одним путём (9000/8081/List/Get/thumbnails/HLS); токен без exp → 401; `make test-go && make test-py` зелёные. - -### Phase 17 — hardening и refactors - -`phase:17`: **#63** shared Go-модуль + graceful shutdown ← первым → **#52** CORS allowlist + rate-limit на Redis → **#54** HLSAuth: кеш + общий http.Client → **#55** Redis requirepass + закрыть порты сервисов → **#50** ack после успешного update_status → **#51** kill/reap ffmpeg → **#57** один атомарный UPDATE → **#58** валидация до вставки + sentinel-ошибки → **#59** структурированные JSON-ошибки → **#62** один argv-билдер + удалить Celery-стабы. - -Verify: `make lint-go && make test-go && make test-py`, e2e зелёный; подделка XFF не обходит лимит; симуляция ошибки стрима не оставляет зомби-ffmpeg. - -### Phase 18 — UX и auth lifecycle - -`phase:18`: **#60** silent refresh по 401 (фронт) + **#64** ротация refresh с `jti`/revocation (бэк) — одно флоу, делать парой → **#61** двойной сабмит + утечка поллинга → **#56** S3 multipart с чанками 5–10MB. - -Verify: истечение access — прозрачный refresh; e2e resumable на 100MB+ с обрывом посередине. - -### Бэклог за рамками фаз 16–18 (не срочное) - -- shadcn/ui, TanStack Query + RSC, Vidstack/Uppy, next/image, единый язык UI. -- CI: lint-front/build-front; `.dockerignore`; bump Next 14.2.5. -- Стратегия: static HLS vs nginx-vod, SSE вместо поллинга, OpenAPI codegen, OTel/Sentry, compose-профили. -- Тесты: pgx testcontainers, HLSAuth, on_message retry/DLQ, e2e resumable, auth на реальном Postgres. - ---- - -## 10. Трассировка в таски GitHub Projects - -Каждая находка выше замаплена на issue в `SlavaKuntsov/flowix` (лейблы `phase:backlog` + `area:*`, поля Priority/Phase/Area). Маппинг — в теле каждого issue. Быстрые LOW-фиксы объединены в задачи по скоупу, чтобы не плодить 30 issue. diff --git a/docs/REVIEW-fresh-2026-09-13.md b/docs/REVIEW-fresh-2026-09-13.md deleted file mode 100644 index f3cb705..0000000 --- a/docs/REVIEW-fresh-2026-09-13.md +++ /dev/null @@ -1,223 +0,0 @@ -# Code Review — Flowix (третий проход, 2026-09-13) - -> Свежий проход по всему проекту. Фокус — находки, которых НЕТ в `docs/REVIEW.md:1` и `docs/REVIEW-fullstack-2026-09-13.md:1`, плюс перепроверка ключевых старых. -> -> **Scope:** ~90 файлов исходников (gateway/metadata/upload — Go chi; auth — FastAPI; transcoder — pika; frontend — Next.js 14; deploy — compose/nginx/migrations). -> **Tools:** golangci-lint (0 issues по всем трём Go-сервисам). semgrep/gitleaks/bandit/trivy не установлены. CVE-скрипт упал на SSL; зависимости проверены вручную через OSV/NVD (web). -> -> **Итог: 1 CRITICAL (новый, цепочка), 3 HIGH (2 новых + подтверждён IDOR), 4 MEDIUM новых, 1 подтверждённый MEDIUM, пачка LOW.** - ---- - -## Vulnerable dependencies - -- **`python-jose[cryptography] >=3.3,<4`** (`services/auth/pyproject.toml`) — **CVE-2024-33663** (algorithm confusion, OpenSSH ECDSA keys) и **CVE-2024-33664** (JWT-бомба: DoS через JWE с высокой степенью сжатия в `decode()`). Затронуты все версии ≤ 3.3.0, проект не поддерживается. Эксплуатация: `/refresh` и `/me` принимают токен от любого клиента — DoS реален; algorithm-конфузия частично купирована пином `algorithms=["HS256"]`. - Fix: миграция на PyJWT или joserfc; как минимум — лимит размера токена до `decode`. - ---- - -## CRITICAL - -### Приватность обходится на уровне data plane: renditions публично анонимны, nginx-vod наружу без auth -**File:** `deploy/docker-compose.yml:88-91`, `deploy/docker-compose.yml:139`, `deploy/nginx/nginx.conf:56-63`, `:70` -**Category:** Broken Access Control (OWASP A01:2021) - -```yaml -/usr/bin/mc anonymous set private local/$${VIDEO_STORAGE_BUCKET:-videos} || true && -/usr/bin/mc anonymous set download local/$${VIDEO_STORAGE_BUCKET:-videos}/renditions || true && -/usr/bin/mc anonymous set download local/$${VIDEO_STORAGE_BUCKET:-videos}/thumbnails || true && -... - nginx-vod: - ports: - - "${NGINX_VOD_PORT:-8081}:80" -``` - -**Problem:** Вся авторизация видео живёт только в gateway (`HLSAuth`). Но data plane торчит наружу мимо gateway: -1. MinIO: префиксы `renditions/` и `thumbnails/` объявлены **анонимно скачиваемыми** (`mc anonymous set download`). MinIO-порт 9000 тоже опубликован (`docker-compose.yml:70`). Любой, зная UUID видео, качает `http://:9000/videos/renditions/{id}/720p.mp4` напрямую — ни JWT, ни HLS-токен не нужны. -2. nginx-vod: `:8081` опубликован, а в `nginx.conf` на `location ^~ /hls/...` нет никакой проверки — HLSAuth реализован в gateway, которого здесь нет. `/hls/{id}/master.m3u8` на 8081 отдаёт приватное видео кому угодно. - -Цепочка замыкается через уже известную дыру: публичный `GET /api/v1/videos` (`services/gateway/cmd/server/main.go:129` + `repository/video.go:64` без фильтра visibility) отдаёт **ID приватных видео**. Итог: режим private не защищает ничего — полный обход всей privacy-модели. - -**Fix:** -- `mc anonymous set private` на весь bucket; раздачу renditions/thumbnails — только через gateway с авторизацией или presigned GET с TTL. -- Убрать наружную публикацию `:9000` и `:8081` из compose (только internal-сеть); если nginx-vod нужен напрямую — перенести HLSAuth внутрь nginx (auth_request в gateway). -- Добавить фильтр visibility в `List` (закрывает и источник UUID). - -**Refs:** OWASP A01:2021; CWE-732 (incorrect permission assignment). - ---- - -## HIGH - -### Resumable upload: неограниченное тело запроса + чтение всего файла в память -**File:** `services/upload/internal/handler/resumable.go:114`, `:156`, `services/gateway/cmd/server/main.go:122`, `frontend/src/lib/api.ts:224` -**Category:** DoS / Memory - -```go -// resumable.go:114 — PUT без Content-Range -data, err := io.ReadAll(r.Body) -... -// resumable.go:156 — PUT с Content-Range -chunk, err := io.ReadAll(io.LimitReader(r.Body, chunkSize+1)) -``` - -**Problem:** На gateway-роуте `PUT /api/v1/videos/{id}/resumable` (`main.go:122`) нет `maxBytesMw` (он применён только к `/upload`), в самом upload-сервисе `MaxBytesReader` для resumable тоже не ставится. При этом handler делает `io.ReadAll` тела целиком. Фронт шлёт «resumable» файл **одним чанком** (`api.ts:224` — `chunkSize = total - offset`), т.е. 5GB-файл целиком буферизуется в RAM воркера → OOM, один авторизованный запрос кладёт контейнер. Побочно: число чанков не ограничено — объект можно раздуть сверх `UPLOAD_MAX_BYTES` (лимит действует per-request, не per-object). - -**Fix:** `r.Body = http.MaxBytesReader(w, r.Body, maxBytes)` в resumable-хендлере (или `maxBytesMw` на gateway-роуте); чанки 5–10MB на фронте; вместо ReadAll+append — S3 multipart upload (`CreateMultipartUpload`/`UploadPart`/`CompleteMultipartUpload`), он же убивает O(n²)-аппенд. - -### IDOR в presign-флоу (подтверждение, не исправлено) -**File:** `services/upload/internal/handler/presign.go:126-143`, `services/upload/internal/handler/resumable.go:47`, `:98` -**Category:** Broken Access Control (OWASP A01:2021) - -```go -// Complete — проверяется только существование объекта, не владелец -s3Key := fmt.Sprintf("raw/%s/original.mp4", videoID) -if err := h.storage.StatObject(r.Context(), s3Key); err != nil { -``` - -**Problem:** `Complete`, `GET/PUT .../resumable` по-прежнему не сверяют владельца видео. Любой залогиненный юзер, зная UUID, может: дозаписать в чужой `raw/{id}/original.mp4` (resumable PUT), перезаписать его целиком (PUT без Content-Range), вызвать `complete` и запустить транскод/удаление raw чужого видео. `ownerID` из контекста доступен, но не используется. - -**Fix:** Перед `StatObject` звать metadata (internal-эндпоинт уже есть) и сверять `owner_id`; в resumable — то же самое. Ровно как это сделано в `HLSTokenHandler`. - -### python-jose: CVE-2024-33663 + CVE-2024-33664 -См. раздел «Vulnerable dependencies». HIGH, потому что `decode_token` вызывается на публичных эндпоинтах (`/refresh`, `/me`) и `jwt.decode` обрабатывает произвольные байты от клиента. - ---- - -## MEDIUM - -### Регистрация без политики пароля и с регистрозависимым email -**File:** `services/auth/src/schemas.py:5-11`, `services/auth/src/routers/auth.py:46` -**Category:** Auth / Input Validation - -```python -class RegisterRequest(BaseModel): - email: EmailStr - password: str # ни min_length, ни сложности -``` - -**Problem:** Пароль принимается любой длины, включая пустой/однобуквенный (argon2 его честно захеширует). Плюс `EmailStr` не нормализует регистр, а `users.email UNIQUE` в Postgres регистрозависим: `Ivan@x.com` и `ivan@x.com` — два аккаунта; при этом логин с другим регистром не найдёт пользователя (50/50 у всех почтовых провайдеров, где регистр игнорируется). - -**Fix:** `password: str = Field(min_length=8)` (или curl-подобную политику), email хранить в lowercase (нормализация перед вставкой + `CITEXT`/функциональный уникальный индекс). - -### JWT от HLS лежит в query string -**File:** `services/gateway/internal/middleware/hlsauth.go:161`, `:265` -**Category:** Token leakage - -```go -tokenParam := r.URL.Query().Get("token") -... -"url": fmt.Sprintf("/hls/%s/master.m3u8?token=%s", videoID, token), -``` - -**Problem:** HLS-токен (1 час, доступ к приватному видео) передаётся в URL — попадает в access-логи nginx/gateway/promtail, историю браузера, Referer при переходе с сегментных запросов. Уже есть рабочая альтернатива: `X-HLS-Token`/`Authorization` заголовки (`hlsauth.go:163`), фронт умеет (`VideoPlayer.tsx` xhrSetup). - -**Fix:** Отдавать токен только в теле ответа, а URL формировать на клиенте; манифест грузить с заголовком. Query-параметр оставить как fallback и замолчать его в логах. - -### Redis наружу без пароля -**File:** `deploy/docker-compose.yml:100-108` -**Category:** Config - -**Problem:** `redis:7-alpine` без `--requirepass`, порт 6379 опубликован на 0.0.0.0. Без пароля Redis — классика массовых компрометаций (запись в RDB → RCE / майнеры). Для dev допустимо, но дефолт `make up` = незащищённый Redis на всех интерфейсах хоста. - -**Fix:** `command: redis-server --requirepass ${REDIS_PASSWORD}` + `REDIS_URL=redis://:pass@redis:6379/0`. - -### INTERNAL_TOKEN пустой → /internal/* полностью открыт -**File:** `services/metadata/internal/middleware/internal.go:11`, `deploy/docker-compose.yml:171` -**Category:** Auth - -```go -if secret == "" { - next.ServeHTTP(w, r) - return -} -``` - -**Problem:** Compose передаёт `INTERNAL_TOKEN: ${INTERNAL_TOKEN:-}` — забытая переменная делает `PATCH /internal/videos/{id}/status` и `GET /internal/videos/{id}` публично доступными (порт metadata опубликован, `docker-compose.yml:180`). Там можно выставить любой статус и renditions любого видео. Побочно: transcoder при пустом токене молча теряет idempotency-проверку (`_get_status` возвращает None, `consumer.py`). - -**Fix:** Fail fast при старте metadata, если `INTERNAL_TOKEN` пуст (как рекомендуется для JWT_SECRET); пустой токен — только при явном `ENV=dev`. - -### JWT в Go-мидлварях: `exp` не обязателен (подтверждение, не исправлено) -**File:** `services/gateway/internal/middleware/auth.go:33-39` (и копии в metadata/upload) -**Category:** Auth - -**Problem:** `jwt.Parse` проверяет `exp`, только если он есть; токен без `exp` валиден вечно. `type` тоже опционален. Не исправлено с прошлого ревью. - -**Fix:** `jwt.WithExpirationRequired()` (jwt/v5) + обязательная проверка `type == "access"`. - ---- - -## LOW - -- `services/upload/internal/queue/publisher.go:58` — авто-хил удаляет очередь через `QueueDelete` без `ifEmpty` — вместе с сообщениями; то же в `consumer.py` main-loop. Нужен `ifEmpty=false→true` + ручная миграция topology. -- `services/gateway/internal/proxy/proxy.go:67` — `ModifyResponse` удаляет `Vary` у **всех** проксированных ответов, включая содержательный `Vary: Accept-Encoding` апстримов. -- `services/upload/internal/handler/presign.go:106-110` — если `CreateVideo` прошёл, а presign упал, metadata-запись остаётся сиротой (видео «uploaded» без объекта). Удалить запись при ошибке. -- `services/transcoder/app/consumer.py:694` — проверка `str(type(sz)).endswith("MagicMock'>")` в продовом коде — утечка тестовой логики в рантайм. -- `frontend/src/app/upload/page.tsx:82` — кнопка активна при `progress === 0` → двойной сабмит запускает две параллельные загрузки (старое, не исправлено). -- `frontend/src/app/watch/[id]/page.tsx:44-47` — при ошибке `getVideo` интервал продолжает поллить вечно (unmount-очистка есть, error-выхода нет). -- `frontend/src/lib/api.ts:274` — presigned URL (валидные креды на 1 час) сохраняется в `localStorage`. -- Все сервисы: `/metrics`, `/docs`, `/openapi.json` доступны без auth; `JWT_SECRET`, пароли Postgres/MinIO/RabbitMQ, Grafana `admin/admin` — дефолты из `.env.example`; postgres на md5 (сознательный tradeoff под pgbouncer, но стоит комментарий-план на scram). - -### Подтверждённые (по-прежнему не исправлены) из прошлых ревью -IDOR (выше), `List`/`Get` без visibility, CORS `*` + credentials, обход rate-limit через `X-Forwarded-For`, O(n²) append, видео навсегда «processing» при упавшем `update_status` (`consumer.py:832` + ack `:926`), зомби-ffmpeg в pipe-режиме, отсутствие silent refresh, нет `http.Server` таймаутов/graceful shutdown (все три `main.go`). - ---- - -## Improvements - -### [HIGH IMPACT] Один shared Go-модуль вместо трёх копий middleware -**File:** `services/gateway/internal/middleware/auth.go`, `services/metadata/internal/middleware/auth.go`, `services/upload/internal/middleware/auth.go` -**Category:** Architecture / foot-gun - -**Why:** `AuthMiddleware`, logger, `writeJSON`, bootstrap скопированы трижды, и копии уже **разошлись**: jwt `v5.3.1` в gateway vs `v5.2.2` в metadata/upload, `go 1.25.0` vs `go 1.23.0`. Каждый фикс (например, `WithExpirationRequired`) придётся делать трижды и забыть один раз. `go.work` + `internal/pkg/` убирает весь класс «пофиксил в двух из трёх». - -### [HIGH IMPACT] `Update` в metadata: 4 round-trip и TOCTOU → один UPDATE -**File:** `services/metadata/internal/repository/video.go:85-116` -**Category:** Speed - -**Why:** Сейчас: `GetByID` (2 запроса) → до 3 отдельных `UPDATE` → `GetByID` снова (2 запроса). Между read и write другой воркер может изменить запись (TOCTOU), а 6 запросов на каждый PATCH — лишняя нагрузка. Один атомарный запрос делает то же: - -```sql -UPDATE videos SET title=COALESCE($1,title), description=COALESCE($2,description), - visibility=COALESCE($3,visibility) -WHERE id=$4 AND owner_id=$5 -RETURNING id, owner_id, title, description, duration, status, visibility, thumbnail_s3_key, created_at, updated_at; -``` - -`owner_id` в WHERE убирает и отдельную проверку forbidden. - -### [MEDIUM IMPACT] HLSAuth: кеш метаданных + общий http.Client -**File:** `services/gateway/internal/middleware/hlsauth.go:105` -**Category:** Speed - -**Why:** Каждый `.ts`-сегмент → новый `http.Client{Timeout:2s}` и HTTP-запрос в metadata. Просмотр = 100–300 лишних запросов и TCP-хендшейков. Переиспользуемый клиент + LRU `video_id→meta` с TTL 10–15с (negative cache 5с) сокращают запросы к metadata на ~2 порядка при том же UX смены visibility. - -### [MEDIUM IMPACT] Транскодер: один builder ffmpeg-аргументов -**File:** `services/transcoder/app/consumer.py:289-414` vs `:417-560` -**Category:** Readability - -**Why:** `transcode_one` и `transcode_one_pipe` дублируют сборку команды на ~60%, уже с расхождениями (в pipe-ветке `-preset` ставится после `-c:v` в другом порядке). Один `build_ffmpeg_argv(input_mode, height, bitrate_k, fps, crf, enc)` — половина файла короче, и следующий параметр (например, `-tune`) добавляется в одном месте. - -### [MEDIUM IMPACT] limiter.py: убрать socket-пробу Redis -**File:** `services/auth/src/core/limiter.py:15-33` -**Category:** Readability - -**Why:** Ручной `socket.create_connection` для проверки Redis дублирует то, что slowapi сделает сам при первой операции, и ломается на таймаутах/IPv6. Достаточно создать Limiter с `storage_uri` и при `ConnectionError` в обработчике деградировать в память (или просто доверять `REDIS_URL` — сервис и так падает без Redis в compose-зависимостях). - -### [LOW IMPACT] Мёртвые метрики -**File:** `services/auth/src/main.py:26-30`, `services/transcoder/app/consumer.py:24-27` -**Category:** Observability - -**Why:** `upload_bytes`, `vod_cache_hit`, `rabbitmq_queue_depth`, дублированный `ffmpeg_duration` объявлены, но никогда не инкрементятся — Grafana-дашборды по ним пустые, а чтение кода вводит в заблуждение. Удалить или подключить. - ---- - -## What's actually good - -`UpdateStatus` в metadata (`repository/video.go:139-170`) — транзакция + `FOR UPDATE` + идемпотентность «не даунгрейд terminal-статусов»: это правильный образец, остальным репозиториям стоит так же. DLX + retry-очередь с `x-retry-count` в заголовках — корректная схема ретраев без бесконечных циклов. `ModifyResponse`, стирающий CORS-заголовки апстримов, — грамотное решение проблемы двойного `Access-Control-Allow-Origin`. Общий `audio.m4a` для всех рипов с внятным комментарием «почему» — редкая забота о щелчках при переключении качества. golangci-lint чист по всем трём сервисам. - ---- - -## Verdict - -Для приватного пет-проекта — рабочая система, но заявленная фича «private video» сейчас **не работает**: CRITICAL-цепочка (анонимные renditions + открытый nginx-vod + публичный List с чужими UUID) обходит её целиком, минуя gateway. Чинить в порядке: (1) MinIO-политики + порты 9000/8081 + visibility-фильтр в List, (2) IDOR в complete/resumable, (3) лимит тела на resumable. Python-jose заменить на PyJWT — час работы. Остальное — по плану прошлых ревью. diff --git a/docs/REVIEW-fullstack-2026-09-13.md b/docs/REVIEW-fullstack-2026-09-13.md deleted file mode 100644 index d10b990..0000000 --- a/docs/REVIEW-fullstack-2026-09-13.md +++ /dev/null @@ -1,118 +0,0 @@ -# Flowix — Senior Fullstack Review (2026-09-13) - -> Автор: senior fullstack review. Исходники: `docs/spec.md:1`, `docs/services-pipeline.md:1`, `docs/PLAN.md:1`. -> Предыдущее ревью: `docs/REVIEW.md:1` (не перезаписано, это дополнение с фокусом на оптимизацию, архитектуру и визуал). -> Стек на момент ревью: Go 1.27 chi (gateway/metadata/upload), FastAPI+pika (auth/transcoder), nginx-vod JIT, Next.js 14 + hls.js + zustand + tailwind. - -## 0. Вердикт - -Ядро правильное: `upload → RabbitMQ → transcode → MinIO → HLS`, выровненный GOP, общий `audio.m4a`, `INTERNAL_TOKEN`, DLX+retry, presigned upload, HLS-auth. -Главные риски сейчас: - -1. Приватность течёт мимо HLS (metadata List/Get + `/thumbnails/*` без auth). -2. HLS-auth делает HTTP в metadata на каждый `.ts`-сегмент — сам себе DDoS. -3. `transcoder/app/consumer.py:1` — god-file 998 строк, pipe-флаг не экономит диск, fan-out объявлен но не потребляется. -4. Фронт без UI-кита, без react-query, с ручным плеером — самый дешёвый прирост UX именно тут. - ---- - -## 1. Логика — что чинить - -### P0 — безопасность / корректность - -| # | Проблема | Где | Фикс | -|---|----------|-----|------| -| 1 | `List` отдаёт приватные видео всем, `Get` без проверки visibility | `services/metadata/internal/repository/video.go:64`, `handler/video.go:110`, `gateway/cmd/server/main.go:129` | `WHERE (visibility='public' OR owner_id=$me)`, `Get` проверяет owner | -| 2 | Превью приватных видео публичны | `gateway/cmd/server/main.go:145` `/thumbnails/*` | Тот же `HLSAuth` / signed URL с TTL | -| 3 | HLS-auth без кеша: 1 просмотр = 100+ запросов в metadata | `gateway/internal/middleware/hlsauth.go:98` `fetchVideoMeta`, `http.Client{Timeout:2s}` без пула | LRU `video_id→meta` 10с, общий `http.Client` с пулом, negative cache | -| 4 | Gateway без таймаутов, JWT с дефолтом | `gateway/cmd/server/main.go:170` `ListenAndServe`, `:24` `change-me...` | `http.Server{Read:15s,Write:60s,Idle:120s}` + fail fast, graceful shutdown | -| 5 | Pipe-режим качает файл всё равно + ещё раз на каждый рип | `transcoder/app/consumer.py:719-727,770-783` | Либо честный `get_object stream → stdin` без `fget_object`, либо удалить флаг | -| 6 | `transcode_one` vs `transcode_one_pipe` дублируют сборку ffmpeg (~60%) | `consumer.py:294,435` | Один `build_ffmpeg_argv(input_mode, ...)` | -| 7 | `FANOUT_QUEUES` объявлены, но `basic_consume` только `QUEUE` | `consumer.py:65,973` | Либо допилить fan-out воркеры, либо удалить | -| 8 | BlockingConnection + ffmpeg 900с в одном потоке — heartbeat 600 всё равно отвалится на 5GB | `consumer.py:877-980` | Heartbeat в отдельном потоке / отдельный коннект, `ffmpeg -progress` | -| 9 | `complete` не сверяет size/owner/content-type | `upload/internal/handler/presign.go` | Сверить `StatObject.size` + owner из metadata | -| 10 | Presign resume только reuse URL, не offset; resumable шлёт одним чанком | `frontend/src/lib/api.ts:224,260` | S3 multipart (create/sign parts/complete), чанки 5-10MB | -| 11 | Нет silent refresh: access 15м → молчаливый logout | `frontend/src/store/auth.ts:39`, `lib/api.ts:76` | 401-interceptor + refresh retry + очередь запросов | -| 12 | Токены в `localStorage` | `auth.ts:29,48` | httpOnly cookies via Next Route Handler (BFF), позже | - -### P1 — производительность бэка - -- `metadata Update`: 3× `UPDATE` + `GetByID` = 4 запроса (`repository/video.go:85`). Один `UPDATE ...` в транзакции. -- `Create` делает лишний `SELECT email` synchronously (`video.go:29`). JOIN или фон. -- `List ORDER BY created_at DESC` без курсора/total/индекса. Добавить `keyset` + `pg_trgm` поиск. -- Gateway пересоздаёт `http.Client` на HLS-пути. Один переиспользуемый клиент. -- `upload Upload` + gateway оба ставят `MaxBytesReader` с разных env — выровнять, один источник. -- Thumbnails через gateway — лишний hop. Отдавать signed CDN URL напрямую из MinIO. - -## 2. Архитектура — варианты - -1. **nginx-vod — риск.** Модуль Kaltura фактически заброшен. Варианты: - - A (рекомендую для MVP): прегенерить HLS `.ts` в транскодере, хранить в MinIO, раздавать статику + CDN. Ноль JIT-CPU, идеальный кеш. - - B: оставить JIT, но запинить форк + fallback на static при 5xx. -2. **Пуш вместо поллинга.** Сейчас `watch/[id]` поллит каждые 3с (`watch/[id]/page.tsx:48`). `video.transcoded` как событие → `gateway SSE /api/v1/videos/{id}/events` → фронт. -3. **Единый контракт.** Go struct + Pydantic + TS `Video` руками дрейфуют. `openapi.json` как source of truth + `oapi-codegen` + `openapi-typescript`. -4. **Compose-профили.** 17 сервисов в одном `docker-compose.yml` тяжело для dev. `core / monitoring / gpu` профили, `standalone + distroless` для FE. -5. **Наблюдаемость.** Метрики есть, трейсов нет. OTel `gateway→services` + `trace_id` в логах, Sentry FE/BE. - -## 3. Фронтенд и визуал — главный рычаг - -Сейчас: голый Tailwind, `confirm()/alert()`, смесь RU/EN, `` вместо `next/image`, ручные `useEffect`-фетчи, кастомный плеер 194 строки (`VideoPlayer.tsx:110` с `BUFFER_FLUSHING`-хаком и `as any`). - -### Рекомендуемый стек (обоснование) - -| Задача | Взять | Почему, что заменяет | -|--------|-------|----------------------| -| Дизайн-система | **shadcn/ui + Radix** | Вендорится в репо, Tailwind уже есть, a11y из коробки. `Button/Card/Badge/Dialog/Progress/Skeleton/DropdownMenu/Sonner` вместо `confirm/alert`, хардкода `bg-red-500/bg-black` | -| Плеер | **Vidstack** (hls.js внутри) | Готовые качество/скорость/PiP/субтитры/мобильный fullscreen. Убирает ~120 строк ручного `nextLevel`-кода | -| Данные | **TanStack Query** | Кеш ленты, `refetchInterval` вместо ручного `setInterval`, инфинит-скролл | -| Формы | **react-hook-form + zod** | login/register/upload валидация | -| Загрузка | **Uppy / react-dropzone + tus** | Drag&drop, чанки, resume, отмена (AbortController уже протянут в `api.ts`, но кнопки нет) | -| Тосты/диалоги | **Sonner + Radix Dialog** | Вместо `alert/confirm` в `VideoCard.tsx:29`, `watch/[id]/page.tsx:97` | -| Картинки | **next/image** | Вместо `` с eslint-disable в `VideoCard.tsx:44` | - -Визуально: тёмная YouTube-like тема, `Header` с поиском, `VideoCard` с длительностью + hover-preview, `watch` в 2 колонки (player + next up), `upload` — степпер `файл → мета → заливка → статус транскода`, `loading.tsx/error.tsx/not-found.tsx`, `generateMetadata` OG-теги, `middleware.ts` защита `/upload`, `next-intl` (сейчас смесь языков). - -Перф: `dynamic import hls.js` (~130KB gz из бандла), zustand-селекторы (`useAuth(s=>s.userId)` — сейчас каждый `VideoCard` ререндерится), убрать дубли `base()/hlsBase()`, неиспользуемый `createVideoMeta`. - ---- - -## 4. Предлагаемый план - -### Milestone 0 — Quick wins (1-2 дня) - -1. Metadata `List/Get` — фильтр visibility + owner. Thumbnails за auth. -2. `hlsauth` LRU-кеш 10с + общий `http.Client`. -3. Gateway `http.Server` таймауты + fail fast на пустом `JWT_SECRET` + graceful shutdown. -4. Фронт: Sonner+Dialog вместо confirm/alert, фикс двойного сабмита (`upload/page.tsx:82`), фикс утечки поллинга, 401-refresh интерцептор. -- Verify: приватное без токена 403 везде; `ab` на `/hls/` без деградации metadata; истечение access → silent refresh. - -### Milestone 1 — Пайплайн (1 спринт) - -1. Разбить `consumer.py` → `queue.py/ffmpeg.py/pipeline.py`, один билдер argv. -2. Честный pipe без `fget_object` + `ffmpeg -progress` → `%` в metadata. -3. Heartbeat-поток, `confirm_delivery()`, валидация `video_id/s3_key`. -4. S3 multipart presign (чанки 5-10MB, resume по offset, сверка size+owner в `complete`). -5. SSE `video.transcoded` вместо поллинга. -- Verify: e2e resumable 100MB+ с обрывом; «metadata упал → retry, не ack»; прогресс % в UI. - -### Milestone 2 — Визуал (1 спринт) - -1. `shadcn init` (Button/Input/Card/Badge/Dialog/Progress/Skeleton/DropdownMenu/Sonner), тёмная тема, brand-цвет, один язык. -2. Vidstack вместо ручных контролов; Uppy на upload; TanStack Query + RSC (`page.tsx`, `watch`), `loading/error/not-found`, OG. -3. `next/image`, dynamic hls.js, zustand-селекторы. -4. Поиск + пагинация + «мои видео» + отмена загрузки + drag&drop. -- Verify: Lighthouse perf/a11y, ручные сценарии mobile Safari. - -### Milestone 3 — Архитектура (бэклог) - -1. Static HLS опция (`VOD_MODE=static`), CDN Cache-Control, план ухода от nginx-vod. -2. OpenAPI codegen + контракт-тесты, `docs/spec.md:73` как зеркало. -3. OTel + Sentry + pprof; compose profiles; FE `standalone` <200MB; bump Next 14.2.5 → 14.2.x+/15. -4. Тесты: pgx testcontainers, HLSAuth, `on_message` retry/DLQ, e2e resumable, auth на реальном Postgres. - ---- - -## 5. Связь с существующими фазами - -- Дополняет `docs/PLAN.md:183` Фаза 12 (ladder/HW/fan-out), Фаза 13 (HLS auth+CDN), Фаза 14 (observability), Фаза 15 (cost) — не заменяет. -- Не противоречит `docs/REVIEW.md:1` — там построчный аудит багов, тут приоритизация под оптимизацию/архитектуру/визуал. diff --git a/docs/REVIEW.md b/docs/REVIEW.md deleted file mode 100644 index cf5bfaa..0000000 --- a/docs/REVIEW.md +++ /dev/null @@ -1,149 +0,0 @@ -# Code Review — Flowix (2026-09-13) - -> Полное ревью проекта как senior fullstack: Go-сервисы (gateway/metadata/upload), Python-сервисы (auth/transcoder), фронтенд, инфраструктура. В конце — приоритизированный план правок. -> Дополнение с фокусом на оптимизацию/архитектуру/визуал: `docs/REVIEW-fullstack-2026-09-13.md:1`. - -## Общая оценка - -Проект для пет-проекта/портфолио — крепкий: слои выделены, тесты на хендлерах есть, мониторинг (Prometheus/Grafana/Loki) подключён, миграции и topology очередей продуманы. Но в коде накопилось несколько реальных багов уровня продакшена (IDOR, зависшие видео, O(n²) загрузка), много копипасты между Go-сервисами, и фронтенд не использует ничего из того, что даёт Next.js App Router. - ---- - -## 1. Критичное — чинить в первую очередь - -### Безопасность - -| # | Проблема | Где | -|---|----------|-----| -| 1 | **IDOR в upload**: `Complete`, `Status` и resumable `Upload` не проверяют владельца видео — любой залогиненный юзер, узнав UUID, может перезаписать чужой `raw/{id}/original.mp4` или запустить транскод чужого видео | `services/upload/internal/handler/presign.go:126`, `resumable.go:47,98` | -| 2 | **JWT secret с дефолтом** `change-me-super-secret...` захардкожен во всех сервисах; compose передаёт `${JWT_SECRET}` без проверки — забытая переменная = тихо небезопасный деплой. Нужен fail fast при старте | `gateway/cmd/server/main.go:24`, `metadata/cmd/server/main.go:57`, `upload/cmd/server/main.go:38`, `auth/src/core/config.py:7`, `deploy/docker-compose.yml:158` | -| 3 | **CORS `*` + `Allow-Credentials: true`** — фактически «любой origin с куками» для всех роутов, включая проксированный auth | `gateway/internal/middleware/cors.go:52-58` | -| 4 | **Rate-limit обходится**: in-memory по IP, доверяет клиентскому `X-Forwarded-For`; в auth slowapi ключует по IP гейтвея — один абузер блокирует всех. Redis задеплоен, но не используется | `gateway/internal/middleware/ratelimit.go:19-99`, `auth/src/core/limiter.py:15-33` | -| 5 | **Порты metadata/upload/auth торчат наружу** в compose — клиенты идут мимо гейтвея, минуя rate-limit и CORS | `deploy/docker-compose.yml` (auth 8001, metadata 8002, upload 8003) | -| 6 | **Пресigned PUT без лимита размера** — не подписан `Content-Length`, URL позволяет залить произвольный объём за 1 час | `upload/internal/storage/minio.go:65-105` | -| 7 | **Т thumbnails приватных видео публичны**: `/thumbnails/*` проксируется в MinIO без auth | `gateway/cmd/server/main.go:145-146` | -| 8 | **JWT без обязательного `exp`**: токен без expiration принимается навсегда (`jwt.WithExpirationRequired()` отсутствует) | все три auth-мидлвари Go | -| 9 | **InternalAuth** — не constant-time сравнение секрета | `metadata/internal/middleware/internal.go:15` | - -### Корректность - -| # | Проблема | Где | -|---|----------|-----| -| 10 | **Resumable upload O(n²)**: каждый чанк скачивает весь существующий объект в память, аппендит и перезаливает. На 5GB-файле — гигабайты перетрафика. Нужен S3 multipart upload | `upload/internal/handler/resumable.go:170-183` | -| 11 | **Видео навсегда «processing»**: если `update_status("ready")` упал, transcoder всё равно ack-ает сообщение — ретрая не будет никогда | `transcoder/app/consumer.py:832-926` | -| 12 | **Токены не обновляются никогда**: `refresh_token` кладётся в localStorage и ни разу не читается; при истечении access — молчаливый logout. Нет 401-обработки и refresh-ретея | `frontend/src/store/auth.ts:39`, `frontend/src/lib/api.ts:76-93` | -| 13 | **Двойной сабмит загрузки**: кнопка активна при `progress === 0` — второй клик запускает параллельную загрузку | `frontend/src/app/upload/page.tsx:82` | -| 14 | **Зомби-процессы ffmpeg**: в pipe-режиме при ошибке стрима или таймауте `communicate` ffmpeg не убивается и не реапится | `transcoder/app/consumer.py:518-559` | -| 15 | **Create-в-метадате валидирует после вставки**: видео вставляется, потом 400 на невалидной visibility — строка уже существует | `metadata/internal/handler/video.go:84-98` | -| 16 | **SigV4-баг в fallback**: host переписывается после подписи — возвращается нерабочая presigned URL | `upload/internal/storage/minio.go:87-96` | -| 17 | **Ошибки через сравнение строк**: `if err.Error() == "forbidden"`; любая ошибка БД маппится в 404. Нужны sentinel-ошибки + `errors.Is` | `metadata/internal/handler/video.go:220-228` | -| 18 | **Upload интерполирует `err.Error()` в рукописный JSON** — битый JSON при спецсимволах и утечка деталей инфры клиенту | `upload/internal/handler/upload.go:86,114`, `presign.go:87,101`, `resumable.go:120` | -| 19 | **Утечка поллинга на watch-странице**: при ошибке `setInterval` не останавливается никогда | `frontend/src/app/watch/[id]/page.tsx:48` | -| 20 | **Гонка при регистрации**: SELECT-then-INSERT без ловли `IntegrityError` — конкурентная регистрация одного email даёт 500 | `auth/src/routers/auth.py:46-51` | - ---- - -## 2. Архитектура - -### Go-сервисы (gateway / metadata / upload) - -- **Тройная копипаста**: `AuthMiddleware`, `RequestLogger`+`respWriter`, `metrics.go`, `writeJSON`, bootstrap env/log скопированы в три сервиса почти байт-в-байт. Нужен shared-модуль через `go.work`. -- **Версии расходятся**: gateway/metadata `go 1.25.0`, upload `go 1.23.0`; jwt v5.3.1 vs v5.2.2; minio v7.3.0 vs v7.0.70. `go.work` в корне нет. -- **Нет graceful shutdown и таймаутов** `http.Server` ни в одном сервисе (slowloris + обрыв загрузок при деплое). -- **`http.Client` без таймаута** в upload→metadata — зависший запрос блокирует воркер навсегда (`upload/internal/client/metadata.go:16`); наоборот, в gateway на каждый HLS-сегмент создаётся новый `http.Client` (`hlsauth.go:107`) — самый горячий путь без коннект-пула. -- **`respWriter` не реализует `http.Flusher`** — стриминг прокси/HLS без инкрементального flush. -- **`QueueDelete(ifEmpty=false)`** при «лечении» topology — удаляется живая очередь с сообщениями (`upload/internal/queue/publisher.go:58`). -- **Мёртвый код**: `OptionalAuth`, `NewWithPrefix`, `RemoveObject`, дублированный `writeJSON` в metadata, метрики `ffmpeg_duration`/`rabbitmq_queue_depth`, которые никто не инкрементит, `var _ = regexp.MustCompile` в resumable. -- **Update в репозитории**: read-check-write (TOCTOU) + до 3 UPDATE без транзакции (`metadata/internal/repository/video.go:85-116`). `UpdateStatus` при этом написан правильно (tx + `FOR UPDATE`) — образец. -- **`Create` делает лишний запрос в `users` на каждую вставку**; ошибочность owner-email глотается (`video.go:29`). - -### Python-сервисы (auth / transcoder) - -- **Мёртвый Celery**: `celery_app.py`/`tasks.py` — стабы («will be removed» по README), а пакеты `celery`, `redis`, `boto3`, `httpx`, `python-multipart` в зависимостях не используются. -- **Split-brain миграций**: alembic в auth — намеренный no-op, схема живёт в golang-migrate (`deploy/migrations`). Два инструмента — магнит для дрейфа. Выбрать один. -- **`transcode_one` vs `transcode_one_pipe`**: ~60% дублирования, уже разъехались (pipe игнорирует `ENCODE_MODE=crf` при nvenc). Нужен один билдер argv + параметризация режимом ввода. -- **Pipe-режим скачивает raw-файл 3 раза** (по разу на каждый рип), хотя локальная копия для probe/audio уже лежит в tmpdir — экономия стрима ничего не даёт. -- **Нет событий как контрактов**: `video.uploaded` парсится сырым `json.loads`; `video.transcoded` не существует — transcoder дёргает metadata синхронным HTTP PATCH. Нужны versioned Pydantic-модели. -- **Нет publisher confirms** (`confirm_delivery()`) — retry-publish может тихо теряться, at-least-once не гарантирован. -- **Опасное лечение topology в консьюмере**: `queue_delete` живой очереди при mismatch (`consumer.py:903`). -- **Retry/DLQ-логика (`on_message`) не покрыта тестами вообще** — а это самая критичная логика сервиса. -- **Auth**: нет ротации refresh-токенов (нет `jti`, старый refresh жив до 7 дней после выпуска нового), `/refresh` не проверяет существование юзера, лимиты только на login (register/refresh — нет), tokens в JSON body → localStorage (XSS). passlib и python-jose unmaintained → `argon2-cffi` + `pyjwt`. Нет политики паролей. -- **Прод-код с проверкой на MagicMock**: `if not str(type(sz)).endswith("MagicMock'>")` (`consumer.py:694`) — вынести в чистую функцию и тестировать напрямую. -- **`INTERNAL_TOKEN` по умолчанию пустой** — тихо отключает и auth-заголовок, и idempotency-проверку в transcoder. - ---- - -## 3. Фронтенд и визуал - -### Рекомендация по библиотеке компонентов - -**Взять shadcn/ui** (Radix primitives + Tailwind, компоненты вендорятся в `src/components/ui`, без vendor lock-in). Стилистика проекта уже Tailwind — подходит идеально. Закрывает: доступные формы (сейчас label не связаны с input), диалоги/тосты вместо `window.confirm`/`alert`, DropdownMenu для качества/visibility, тёмную тему через CSS-переменные. Плеер оставить рукописный — текущий hls.js-сетап (nextLevel-логика, recovery, iOS fallback) лучше готовых обёрток, но убрать хак с приватным событием `BUFFER_FLUSHING` (`VideoPlayer.tsx:141-146`). - -### State и данные - -- **TanStack Query** вместо рукописных `useEffect`+`useState` во всех страницах: уберёт бойлерплейт, даст кэш, дедуп и заменит ручной `setInterval`-поллинг (`refetchInterval` + `enabled` по статусу). -- **Server Components для фида/watch** + `generateMetadata` (OG-теги) + `loading.tsx`/`error.tsx`/`not-found.tsx` — сейчас все страницы `"use client"`, SSR/SEO не используется, роут-левел стейтов нет. -- **Токены → httpOnly cookies** (BFF route handler в Next) вместо localStorage. -- **Zustand-селекторы** (`useAuth((s) => s.userId)`) вместо подписки на весь сторе — сейчас каждый `VideoCard` ререндерится при любом изменении auth. - -### Перф - -- **Dynamic import hls.js** (~130KB gz в основном бандле сейчас). -- **`next/image`** для превью вместо `` с eslint-disable. -- Убрать dead code: `base()`/`hlsBase()` идентичны, `createVideoMeta` не используется, `renditions` типизируется, но не рендерится. - -### UX - -- Фикс двойного сабмита (`disabled={progress !== null && progress < 100}` или `uploading`-флаг). -- Отмена загрузки: `AbortSignal` уже протянут через API-слой, но кнопка не создаёт контроллер. -- Drag & drop + клиентская валидация размера (копия обещает «до 5GB», ничего не проверяет). -- Реальные чанки в resumable (сейчас один PUT всего файла, фолбэк тоже одним чанком — `api.ts:224`). -- Единый язык UI (смесь RU/EN строк) или next-intl; единый brand-цвет вместо хардкода `bg-red-500`/`bg-black`; logout с редиректом; `middleware.ts` для защиты `/upload`. - ---- - -## 4. Инфраструктура и CI - -- **CI не покрывает фронтенд** — нет `lint-front`/`build` джобов в `.github/workflows/ci.yml`. -- **Нет `.dockerignore` во frontend** — `COPY . .` тащит локальный `node_modules`/`.next` в контекст (на arm64 Mac может сломать билд); `npm ci 2>/dev/null || npm install` глушит ошибки. -- **Next 14.2.5** — есть security-фиксы в 14.2.25+; поднять до последней 14.2.x или 15. -- Redis задеплоен, но не используется ни gateway (rate-limit), ни auth (ревокация токенов) — либо использовать, либо убрать. -- Тесты, которых не хватает: pgx-репозиторий (testcontainers), `HLSAuth`/`HLSTokenHandler`, `on_message` retry/DLQ, e2e на resumable-путь, интеграционный тест auth на реальном Postgres. - ---- - -## 5. План правок - -### Фаза 0 — багфиксы и безопасность (первая неделя, по одному PR на скоуп) - -1. `upload`: ownership-проверки в Complete/Status/resumable + sentinel-ошибки вместо строк + safe JSON-ответы + лимит размера на resumable PUT (gateway и сервис). → verify: unit-тесты на IDOR-сценарии и битые JSON. -2. `upload`: заменить append-алгоритм на S3 multipart upload. → verify: e2e resumable на 100MB+ файле. -3. `transcoder`: ack только после успешного `update_status`, kill/reap ffmpeg в pipe-режиме, `confirm_delivery()`, валидация `video_id`/`s3_key` из очереди. → verify: тест на «metadata упал → ретрай, не ack». -4. Все сервисы: fail fast на пустой `JWT_SECRET`/`INTERNAL_TOKEN`, CORS — явный allowlist, rate-limit на Redis (или доверенный XFF), убрать наружу порты metadata/upload/auth, подписать Content-Length в presign, `jwt.WithExpirationRequired()`. -5. `frontend`: refresh по 401 с ретеем, фикс двойного сабмита, фикс утечки поллинга, poster для плеера. → verify: ручной сценарий истечения access-токена. - -### Фаза 1 — архитектура Go и Python - -6. Shared-модуль для Go (go.work): auth/logger/metrics/writeJSON, выравнивание версий; graceful shutdown + таймауты серверов и клиентов. → verify: `make lint-go && make test-go`, деплой не обрывает in-flight загрузки. -7. metadata: sentinel-ошибки, транзакция в Update, валидация до вставки; убрать мёртвый код. -8. transcoder: удалить Celery-стабы и мёртвые зависимости, объединить `transcode_one`/`transcode_one_pipe` в один билдер argv, rethink pipe-режима, отключить опасный `queue_delete`. -9. Контракты: Pydantic-модели `video.uploaded`, событие `video.transcoded`; выбрать одну систему миграций. → verify: обновлён `docs/spec.md:73` + README сервисов. -10. Auth: ротация refresh-токенов с `jti`/revocation (Redis-денилист), лимиты на register/refresh по XFF, IntegrityError на регистрации, min-length пароля, `pyjwt` + `argon2-cffi`. - -### Фаза 2 — фронтенд/визуал - -11. shadcn/ui: Button/Input/Dialog/Toast/DropdownMenu, тёмная тема, brand-цвет, один язык UI. -12. TanStack Query + RSC для фида/watch + `loading.tsx`/`error.tsx`/`not-found.tsx` + OG-теги. -13. Перф: dynamic import hls.js, `next/image`, zustand-селекторы, убрать приватный hls.js-хак. -14. Фичи: поиск, пагинация, «мои видео», отмена загрузки, drag&drop, реальные чанки в resumable. - -### Фаза 3 — инфраструктура - -15. CI: `lint-front`/`build-front`; `.dockerignore` для frontend; bump Next; чинить Dockerfile. -16. Тесты: pgx-репозиторий (testcontainers), HLSAuth, `on_message` retry/DLQ, e2e resumable. - ---- - -## Источники находок - -Ревью построено на полном чтении исходников всех сервисов; ссылки на строки актуальны на коммит `91efb5b` (main). При правках сверяться с `docs/spec.md:1` (контракты) и `docs/services-pipeline.md:1` (пайплайн). diff --git a/docs/services-pipeline.md b/docs/services-pipeline.md index 1dbc0e4..4a66084 100644 --- a/docs/services-pipeline.md +++ b/docs/services-pipeline.md @@ -1,6 +1,6 @@ # Flowix — сервисы и пайплайн приложения -> Для ИИ-агентов: это главный документ про «зачем каждый сервис и как всё связано». ТЗ и стек — `docs/spec.md:1`, план по фазам — `docs/PLAN.md:1`. Гайдлайны поведения — `AGENTS.md:1`. +> Для ИИ-агентов: это главный документ про «зачем каждый сервис и как всё связано». ТЗ и стек — `docs/spec.md:1`. Гайдлайны поведения — `AGENTS.md:1`. ## 1. Зачем вообще такая архитектура @@ -97,7 +97,7 @@ Flowix — MVP видеоплатформы с адаптивным стрими - `video.uploaded {video_id: UUID, s3_key: string, owner_id: UUID}` - `video.transcoded {video_id: UUID, renditions: [{quality, s3_key, bitrate}], status: ready|failed}` -Определены как Go struct + Pydantic — см. `docs/PLAN.md:73` и `deploy/postgres/init.sql:1`. +Определены как Go struct + Pydantic — см. `deploy/postgres/init.sql:1`. --- @@ -145,7 +145,6 @@ deploy/docker-compose.yml — infra + все сервисы (healthcheck deploy/docker-compose.prod.yml — prod overrides (CDN headers, limits) deploy/nginx/{nginx.conf,nginx.prod.conf,Dockerfile} — vod_mode mapped, CDN cache deploy/postgres/init.sql — DDL users/videos/renditions -docs/PLAN.md — 8 фаз план (0–8 завершены) docs/spec.md — полный архив ТЗ (бывший AGENTS.md) docs/SWAGGER.md — как генерить OpenAPI для Go docs/ZED.md — настройки Zed IDE diff --git a/services/transcoder/README.md b/services/transcoder/README.md index 23cfed7..52db1c5 100644 --- a/services/transcoder/README.md +++ b/services/transcoder/README.md @@ -1,24 +1,24 @@ -# Transcoder Worker (Python pika + FFmpeg + uv) — Phase 10 IN PROGRESS +# Transcoder Worker (Python pika + FFmpeg + uv) -> Фаза 9 DONE: `INTERNAL_TOKEN` для `PATCH /internal/.../status` (`consumer.py:43`), лимит 5GB в upload. -> Фаза 10: последовательный транскод, `-threads/-preset`, таймаут 900, диск-чек, лимиты контейнера. +> `INTERNAL_TOKEN` для `PATCH /internal/.../status` (`consumer.py:43`), лимит 5GB в upload. +> Последовательный транскод, `-threads/-preset`, таймаут 900, диск-чек, лимиты контейнера. ## Запуск ```bash # Локально (нужен RabbitMQ + MinIO + FFmpeg) uv sync --project services/transcoder -python -m app.consumer # pika BlockingConnection, не celery (celery — legacy, будет удален в Фазе 10) +python -m app.consumer # pika BlockingConnection, не celery (celery — legacy, будет удален) # или -make dev-transcoder # сейчас: celery legacy; после Фазы 10: python -m app.consumer +make dev-transcoder # python -m app.consumer (celery legacy: make dev-transcoder-celery) # Через OrbStack docker compose -f deploy/docker-compose.yml up -d transcoder rabbitmq minio docker compose -f deploy/docker-compose.yml logs -f transcoder ``` -## Env (Phase 10) +## Env - `INTERNAL_TOKEN` — для `PATCH /internal/videos/:id/status` (см. `deploy/docker-compose.yml:186`) -- `FFMPEG_THREADS=2` `FFMPEG_PRESET=veryfast` — лимиты CPU (Phase 10, `.env.example`) +- `FFMPEG_THREADS=2` `FFMPEG_PRESET=veryfast` — лимиты CPU (`.env.example`) - `UPLOAD_MAX_BYTES` — лимит upload (5GB) не влияет на транскодер ## Линт / формат @@ -36,15 +36,15 @@ make fix-py uv run --project services/transcoder pytest -q ``` -## FFmpeg проверка (aligned segments, Phase 10) +## FFmpeg проверка (aligned segments) ```bash ffprobe -v error -select_streams v:0 -show_entries stream=avg_frame_rate,codec_name -of default=nw=1 renditions/720p.mp4 ffprobe -v error -skip_frame nokey -select_streams v:0 -show_entries frame=pkt_pts_time -of csv 720p.mp4 | head -# Phase 10: последовательный транскод, fps из probe, threads/preset +# последовательный транскод, fps из probe, threads/preset FFMPEG_THREADS=2 FFMPEG_PRESET=veryfast python -m app.consumer ``` -## Phase 10 — лимиты (2026-08-30) +## Лимиты - Последовательный рендеринг (было 3× parallel → OOM), `timeout 900`, `fps` из probe (≤30 preserve, >30 cap 30), `-threads 2 -preset veryfast -maxrate 1.10×` - `shutil.disk_usage` чек, `SIGTERM` graceful, `deploy/docker-compose.yml:179` + `prod.yml:44` limits `cpus 2 / mem 4G` - `.env.example:30` `FFMPEG_THREADS`/`FFMPEG_PRESET`, `make dev-transcoder` теперь `python -m app.consumer` (celery legacy `make dev-transcoder-celery`) From d6a96c890606ff1e35ac567758572a386309dad8 Mon Sep 17 00:00:00 2001 From: SlavaKuntsov Date: Thu, 17 Sep 2026 21:44:27 +0300 Subject: [PATCH 2/2] docs: point agents to github project for tickets --- AGENTS.md | 1 + CLAUDE.md | 1 + 2 files changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index d4f3036..67e0ef4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,6 +74,7 @@ Strong success criteria let you loop independently. Weak criteria ("make it work - **ТЗ и архитектура (бывший AGENTS.md):** `docs/spec.md:1` — стек, структура монорепо, описание каждого микросервиса, FFmpeg-параметры, кодстайл, коммит-конвенции. - **Сервисы и пайплайн (на русском):** `docs/services-pipeline.md:1` — зачем каждый сервис (gateway/auth/metadata/upload/transcoder/nginx-vod/frontend) и полный пайплайн `upload → RabbitMQ → transcode → MinIO → HLS` с диаграммой и сценариями отладки. +- **Тикеты и роадмап:** только в GitHub project — в репо файлов с фазами/тикетами нет, не создавай их. - **Swagger / Zed:** `docs/SWAGGER.md:1`, `docs/ZED.md:1` — генерация OpenAPI для Go, настройки IDE. ### Стек и структура (кратко) diff --git a/CLAUDE.md b/CLAUDE.md index d4f3036..67e0ef4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,6 +74,7 @@ Strong success criteria let you loop independently. Weak criteria ("make it work - **ТЗ и архитектура (бывший AGENTS.md):** `docs/spec.md:1` — стек, структура монорепо, описание каждого микросервиса, FFmpeg-параметры, кодстайл, коммит-конвенции. - **Сервисы и пайплайн (на русском):** `docs/services-pipeline.md:1` — зачем каждый сервис (gateway/auth/metadata/upload/transcoder/nginx-vod/frontend) и полный пайплайн `upload → RabbitMQ → transcode → MinIO → HLS` с диаграммой и сценариями отладки. +- **Тикеты и роадмап:** только в GitHub project — в репо файлов с фазами/тикетами нет, не создавай их. - **Swagger / Zed:** `docs/SWAGGER.md:1`, `docs/ZED.md:1` — генерация OpenAPI для Go, настройки IDE. ### Стек и структура (кратко)