myYouTube/analytics/2026-09-17-project-baseline.md
vrubelroman 5fa5a391d7 Remove AGENT_TEAM.md, add project baseline analytics
The agent team workflow lives in .opencode/agent role files; the
human-readable guide is redundant and removed. README and analyst role
updated accordingly. Added analytics baseline documenting the current
project state as a starting point for future tasks.
2026-09-17 21:42:30 +00:00

120 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Базовый документ проекта (baseline)
## Задача
Зафиксировать фактическое состояние проекта MyYouTube на 2026-09-17 как точку отсчёта для будущих задач агентской команды. Документ описывает: обзор, архитектуру, интеграции, ключевые решения с причинами из git-истории, известные ограничения/техдолг и устройство команды агентов. Факты сверены с кодом, `README.md`, `AGENTS.md`, миграциями и `git log`.
## 1. Обзор проекта
Персональный single-user YouTube-клиент с категориями подписок и интеграцией с существующим MeTube. Скачивание выполняет MeTube на `mediaVM`; этот сервис ничего не скачивает сам и не хранит видео локально (только проксирует/линкует `media_url`).
- **Статус:** функционально готово. Этапы 1–6 реализованы (каркас, OAuth, категории, синк подписок/видео, лента, воспроизведение, MeTube-интеграция), плюс доработки по итогам live-тестирования. Развёрнуто на тестовом окружении: `testmyyoutube.vrubel.xyz` → hermesVM `192.168.8.173` (порт 8080) → MeTube на mediaVM `192.168.8.177:8081`.
- **Стек:** Python 3.13 + FastAPI + SQLAlchemy 2.x + Alembic + Pydantic Settings + APScheduler; React 19 + TypeScript + Vite 8 + React Router 7 + TanStack Query 5; PostgreSQL 16; Docker Compose.
- **Масштаб:** single-user — вход разрешён только `ALLOWED_GOOGLE_EMAIL`; фронтенд не ходит в Google API и MeTube write API напрямую (только через backend).
- **Реализованные возможности:** Google OAuth (allow-list) → синк подписок (APScheduler) и видео (по активности); категории (CRUD, reorder, many-to-many с каналами, постоянные URL); лента с курсорной пагинацией, поиском по видео/каналам и фильтром «только новые»; YouTube-плеер; MeTube-интеграция (download/delete, Socket.IO live-статусы, recovery после рестарта); раздел «На сервере» (`/local`, перенаправление с `/saved`); отписка на YouTube; удаление скачанной копии; тёмный адаптивный UI с мобильным меню; скрытие отписанных каналов; счётчики подписчиков и бейджи «N новых» (окно 2 дня).
- **Быстрая проверка (prod):** `docker compose up -d --build` → `curl http://localhost:8080/api/health` (возвращает `status`/`database`/`metube`).
- **Состояние worktree на момент фиксации:** ветка `master` впереди `origin/master` на 1 коммит (f0ca7b9 не запушен); в работе документационная задача «Удалить AGENT_TEAM.md» (`analytics/2026-09-17-remove-agent-team-guide.md`, staged-удаление `AGENT_TEAM.md`, правки `README.md` и `.opencode/agent/analyst.md`).
## 2. Архитектура
### Backend (`backend/app/`)
- **`main.py`** — FastAPI app: `SessionMiddleware` (cookie `myyoutube_session`, 30 дней, `same_site=lax`, `https_only` по `APP_BASE_URL`), lifespan = reconcile download jobs по `/history` MeTube + старт APScheduler + Socket.IO-листенер MeTube (переподключения). Статика собранного фронта монтируется в `/` с fallback на `index.html` (`SPAStaticFiles`).
- **`api/`** (группы, кроме `auth/status`, `auth/google/*` и `health` — за `require_session`):
- `health` — `GET /api/health` (status, database, metube).
- `auth` — `GET /auth/status`, `/auth/google/start`, `/auth/google/callback` (state-проверка, allow-list, background initial sync), `POST /auth/logout`.
- `channels` — `GET /channels` (фильтры `subscribed/search/category_id/uncategorized`), `GET /channels/{id}`, `PUT /channels/{id}/categories`, `POST /channels/{id}/unsubscribe` (реальная отписка на YouTube).
- `categories` — CRUD + `POST /categories/reorder` (slug-генерация, проверка дублей имён).
- `feed` — `GET /feed` (курсорная пагинация base64 `published_at|id`, фильтры category/uncategorized/channel/downloaded/search/new_only, лимит 30/100), `GET /feed/saved-counts`.
- `videos` — `GET /videos/{youtube_video_id}`, `POST .../download`, `DELETE .../download`, `GET .../download-status`, `POST .../recheck-local`.
- `sync` — `POST /sync/subscriptions`, `POST /sync/videos`, `GET /sync/status`.
- **`services/`** — `google_oauth` (flow, allow-list, кэш токена), `youtube_client` (все вызовы YouTube API, ошибки квоты/scope), `sync` (подписки и видео, thread-lock'и), `sync_trigger` (синк по активности), `scheduler` (APScheduler, только подписки), `metube_client` (HTTP+Socket.IO к MeTube), `download_jobs` (машина состояний, reconcile), `state` (AppSetting get/set), `video_presentation` (сериализация).
- **`core/`** — `auth_dependency.require_session` (+триггер синка видео), `crypto` (Fernet для refresh token), `duration` (ISO 8601 → секунды), `slugify`.
- **Модели БД:** `AppSetting` (key/value — статусы синков), `OAuthCredentials` (singleton `id=1`, `encrypted_refresh_token`), `Channel` (`youtube_channel_id`, `youtube_subscription_id`, `uploads_playlist_id`, `subscriber_count`, `subscribed`, `last_synced_at`), `Video` (`youtube_video_id` unique, `published_at`, `duration_seconds`, `youtube_url`), `Category` (name/slug unique, `sort_order`), `channel_categories` (m2m, PK-пара), `DownloadJob` (статусы `queued/downloading/postprocessing/completed/failed/deleted/unknown`, `metube_job_id`, `metube_filename`, `media_url`).
- **Миграции (Alembic, `migrations/versions/`):** 0001 `app_settings` → 0002 `oauth_credentials` + `channels` → 0003 `categories` + `channel_categories` → 0004 `videos` (+индексы) → 0005 `download_jobs` → 0006 `channels.youtube_subscription_id` → 0007 drop `access_token_expires_at` (мёртвая после in-process кэша) → 0008 `channels.subscriber_count` (BigInteger, из `statistics`).
### Frontend (`frontend/src/`)
- **Роуты (`App.tsx`):** `/` Feed, `/category/:categoryId`, `/uncategorized`, `/local` (скачанные), `/search`, `/saved` → redirect `/local`, `/channels`, `/channels/:channelId/videos`, `/video/:youtubeVideoId`, `/settings`, `/settings/categories` (и `/categories` → redirect), 404. До входа — страница `Connect`.
- **Страницы:** `Connect`, `Feed` (infinite-query, фильтры, бейджи новых, кнопка «Обновить»), `Channels` (поиск, `CategoryNav`, назначение категорий, отписка), `ChannelVideos` (счётчик подписчиков), `VideoPage`, `Categories` (CRUD, reorder, confirm-диалог), `Settings` (health MeTube, статусы синков, переподключение, logout).
- **Компоненты:** `AppShell` (сайдбар с категориями и бейджами новых, поиск по видео/каналам, индикатор синка, мобильное drawer-меню), `VideoCard`, `ChannelCard` (popover категорий, создание категории на лету), `DownloadButton` (polling 2 с только для активных статусов), `Player` (локальная копия через `media_url`, при ошибке — fallback на iframe `youtube-nocookie.com` + `recheck-local`), `Icon`.
- **API-клиент** — `api/client.ts`, fetch с `credentials: include`, все эндпоинты типизированы; `utils/format.ts` — форматирование длительности/времени/счётчиков подписчиков.
- **Стили/состояние:** `App.css`/`index.css` (тёмная адаптивная тема); TanStack Query с invalidation по ключам `['feed']`, `['channels']`, `['categories']`, `['download-status', id]` и т.д.; позиция скролла ленты восстанавливается через `sessionStorage` (`feed-scroll:*`).
### Деплой
- **`Dockerfile`** — multi-stage: node:22-slim собирает фронт (`npm ci && npm run build`), python:3.13-slim — бэкенд; dist → `backend/static`; **non-root** пользователь `uid 10001`; `EXPOSE 8080`.
- **`entrypoint.sh`** — `alembic upgrade head` → `uvicorn app.main:app` (`--proxy-headers --forwarded-allow-ips="*"`, порт `APP_PORT`).
- **`compose.yml`** — `app` (env_file `.env`, healthcheck `curl /api/health`) + `postgres:16` (volume `postgres_data`, healthcheck `pg_isready`). Миграции применяются при старте контейнера.
- **`.env`** — секреты: `APP_SECRET_KEY`, `TOKEN_ENCRYPTION_KEY` (Fernet, менять нельзя), `GOOGLE_CLIENT_SECRET`, `POSTGRES_PASSWORD`; все внешние URL (Google/YouTube/MeTube) и параметры синка — в env (см. `.env.example`).
- **Локальная разработка:** backend — `uv venv`/`uv pip install -r requirements-dev.txt` + `alembic upgrade head` + `uvicorn app.main:app --app-dir backend --port 8080`; тесты — `pytest tests/ -v` (SQLite in-memory, Google/MeTube mock). Frontend — `npm install` + `npm run dev`, Vite проксирует `/api/*` на `127.0.0.1:8080` (`vite.config.ts`); lint — `oxlint`, сборка — `tsc -b && vite build`.
### Ключевые настройки (`config.py`, все переопределяются env)
- Google/YouTube endpoints: `google_auth_uri`, `google_token_uri`, `google_userinfo_uri`, `google_revoke_uri`, `youtube_api_base_url` (`https://www.googleapis.com/youtube/v3`), `youtube_watch_url_template`.
- MeTube: `metube_api_base_url` (дефолт `http://127.0.0.1:8081`, реально `http://192.168.8.177:8081`), `metube_public_base_url` (для браузера), `metube_container_download_dir` (`/downloads`), `metube_request_timeout_seconds` (30).
- Синк: `subscriptions_sync_interval_hours=6` (APScheduler), `videos_sync_idle_hours=2` (триггер по активности), `videos_backfill_cap=200`, `videos_known_stop_threshold=50`, `new_videos_window_days=2`.
## 3. Интеграции
### Google OAuth / синк
- **Scope:** полный `https://www.googleapis.com/auth/youtube` (read/write — ради `subscriptions.delete` при отписке; сознательное отступление от `youtube.readonly` по явному запросу пользователя) + `openid`, `userinfo.email`, `userinfo.profile`. Старые readonly-токены не покрывают отписку → нужен один переподключение.
- **Single-user:** email из userinfo сверяется с `ALLOWED_GOOGLE_EMAIL`; недопустимый аккаунт — `revoke_token` + редирект с `auth_error=account_not_allowed`. Redirect URI фиксирован: `{APP_BASE_URL}/api/auth/google/callback` (должен быть в Authorized redirect URIs Google-клиента).
- **Кэш токена:** refresh token хранится в БД шифрованным (Fernet, `TOKEN_ENCRYPTION_KEY`); access token — в памяти процесса под `threading.Lock`, проактивный refresh за 60 с до expiry (`OAUTHLIB_RELAX_TOKEN_SCOPE=1` снимает ложный scope-mismatch при расширении scope). Обмен кода на токен с `access_type=offline`, `prompt=consent`.
- **После подключения** — фоновый initial sync (подписки + видео) через `BackgroundTasks`.
### YouTube API (`youtube_client.py`)
- Ресурсы: `subscriptions.list` (`mine=true`, пагинация по 50), `channels.list` (`snippet,contentDetails,statistics` — uploads playlist и подписчики, батчи по 50), `playlistItems.list` (`contentDetails`, лимит `MAX_PLAYLIST_PAGES=10` страниц), `videos.list` (`snippet,contentDetails,status`, батчи по 50), `subscriptions.delete`.
- Ошибки → HTTP: `quotaExceeded/dailyLimitExceeded/rateLimitExceeded` → 503 «quota exhausted»; insufficient scope → 403 с подсказкой переподключиться; прочее → 502.
- **Квота (оценка из паттерна вызовов):** видео-синк на канал — 1 юнит `playlistItems.list` + 1 юнит `videos.list` на каждые ≤50 новых id → практически **~2 юнита на канал за синк видео** при малом числе новых видео; синк подписок — 1 юнит `subscriptions.list` на 50 подписок + 1 юнит `channels.list` на 50 каналов. Расход пропорционален числу каналов.
### MeTube (`metube_client.py`)
- Весь HTTP/Socket.IO к MeTube инкапсулирован в `MeTubeClient` (правило AGENTS.md): `POST /add` (url, `download_type=video`, codec auto, format mp4, quality best, `auto_start`, `custom_name_prefix` = youtube_video_id), `GET /history`, `POST /delete` (**ключ — URL, не job id**; `where=done`), `build_media_url` (публичный `/download/<filename>` с защитой от path traversal), `check_media` (HEAD), `health`.
- **Socket.IO-события** `added/updated/completed/canceled/cleared` (payload — JSON-строка от `DownloadInfo.to_public_dict()`); маппинг статусов MeTube (`pending/preparing/scheduled/downloading/postprocessing/finished/error`) → свои. Только событие `completed` — авторитетный терминальный статус (yt-dlp шлёт `finished` по отдельным потокам → иначе flapping). `media_url` строится из точного `filename` события (не из title) и отдаётся только при `completed`.
- **Recovery:** при старте `reconcile_on_startup` сверяет активные/`unknown` job'ы с `/history`; completed с `media_url` не трогаются. «cleared»/«canceled» матчатся по URL, несовпавшие игнорируются.
### Процессы синка (`services/sync.py`)
- **Подписки:** `subscriptions.list` (пагинация) → upsert каналов (заполняется и `youtube_subscription_id` для будущей отписки); каналы, которых нет в ответе API, помечаются `subscribed=False` (скрываются в UI). Затем батчами по 50 `channels.list` добираются `uploads_playlist_id` и `subscriber_count`; неудача этой фазы не валит весь синк. Запуск: APScheduler каждые 6 ч + ручной `POST /sync/subscriptions`; параллельный запуск блокируется `threading.Lock` → 409 `SyncInProgress`.
- **Видео:** по каждому подписанному каналу с uploads-плейлистом — инкрементальный `playlistItems.list` (ранняя остановка на 50 подряд известных, кап 200 новых на канал, максимум 10 страниц на плейлист); известные id собраны заранее одним запросом к БД (без N+1). Для неизвестных id — `videos.list` батчами по 50 и insert. Существующие видео не обновляются. Запуск: триггер по активности (`maybe_trigger_videos_sync` в `require_session`, idle ≥2 ч, фоновый поток) + ручной `POST /sync/videos`.
- **Статусы синков** персистятся в `app_settings` (JSON: status/started_at/finished_at/error + счётчики), отдаются через `GET /api/sync/status` (плюс флаг `running` из lock'а); UI опрашивает каждые 2 с пока идёт синк, иначе раз в 60 с.
## 4. Ключевые решения и политики (из git log)
- **2026-09-16, `0ed20bb`** — этапы 1–5: каркас, OAuth, категории, синк видео/лента, воспроизведение.
- **2026-09-16, `fe16c08`** — этап 6: MeTube-интеграция (download/delete, live-статусы).
- **2026-09-16, `e333296`/`10c16ba`** — статусы загрузки: терминален только `completed`; кнопка не откатывается в «Скачать» после завершения.
- **2026-09-16, `3089202`+`8ddea87`** — реальная отписка (`subscriptions.delete`) и удаление скачанного — сверх MVP по запросу пользователя; из-за этого scope расширен до полного `youtube` (потребовался `OAUTHLIB_RELAX_TOKEN_SCOPE`).
- **2026-09-16, `62f8fb7`/`441a3ef`** — `DELETE /videos/{id}/download`: ключ `/delete` — URL (по id — тихий no-op); после запроса проверяем фактическое удаление файла (настройка `DELETE_FILE_ON_TRASHCAN` MeTube вне нашего контроля → 409).
- **2026-09-16, `b29bcfb`…`90eb012`** — отписанные каналы скрываются; вкладка Saved; счётчики каналов/сохранённых в сайдбарах.
- **2026-09-16, `c4a772b`** — тёмный адаптивный редизайн интерфейса.
- **2026-09-16, `6c704ca`** — переход на opencode-команду агентов (Codex/Herdr выброшены).
- **2026-09-17, `fde9a43`** — review-фиксы: in-process кэш access token (+миграция 0007), обработка cleared/canceled по URL, email в `/auth/status` только при валидной сессии, Google base URLs в настройки, **non-root контейнер**; убран «Без категории» из сайдбара (фильтр остался на странице каналов), локальные фильтры в query key, Content-Type только с телом.
- **2026-09-17, `e10df8d`** — канальные фичи: подписчики из `statistics` (0008), бейджи «N новых» (окно **`NEW_VIDEOS_WINDOW_DAYS=2`**), **синк видео по активности** (idle **`VIDEOS_SYNC_IDLE_HOURS=2`** вместо ежечасного планировщика), **инкрементальная догрузка**: ранняя остановка после **50 подряд известных** id (`VIDEOS_KNOWN_STOP_THRESHOLD`), жёсткий кап **200 последних видео на канал** (`VIDEOS_BACKFILL_CAP`); клик по бейджу фильтрует категорию (`new_only`).
- **2026-09-17, `2e5ef8e`** — `.env.example` с комментариями к каждому полю.
- **2026-09-17, `f0ca7b9`** — введён субагент Analyst и конвенция `analytics/`.
## 5. Известные ограничения и техдолг
- **Квота YouTube:** основной расход — по каналу за видео-синк (~1–2 юнита, см. п. 3); при текущем числе подписок практический запас — **порядка 2–4 полных синков видео в день** (оценка, зависит от числа каналов). Исчерпание → 503, обновления пропускаются до сброса суточной квоты; ручной «Обновить» в UI не «пробьёт» квоту.
- **Браузерная проверка только пользователем:** в команде нет Playwright/скриншот-инструментов; reviewer/tester честно помечают визуальное как неверифицированное, финальную визуальную приёмку делает пользователь после деплоя.
- **Edge-кейсы MeTube:** удаление записи вне нашего приложения (`cleared` done-записи) матчится по URL и может быть не связано с нашими job'ами; re-download после удаления создаёт новый `DownloadJob`, значимым считается последний; после рестарта активные job'ы сверяются с `/history` (иначе → `unknown`, пользователь видит «Загрузить снова»).
- **Отсутствует CI и E2E:** тесты — только backend `pytest` (`tests/`, 12 файлов, SQLite in-memory, Google/MeTube замоканы; реальные вызовы к MeTube — opt-in). GitHub Actions нет, E2E/снапшотов нет.
- **Single-worker lock:** защита от параллельных синков — `threading.Lock` в одном процессе (uvicorn запускается одним worker'ом); при scale-out и триггер по активности, и блокировки перестанут быть глобальными — сознательное упрощение single-user сервиса.
- **Синк видео не обновляет метаданные** уже известных видео (`videos_updated` всегда 0, обновляются только новые id).
- **Сознательно не реализовано:** YouTube Recommendations/Home (запрещено), PWA, watch later, Shorts-фильтр, SponsorBlock; полная история канала не бэкапится (кап 200).
- **Удаление файла на диске** зависит от `DELETE_FILE_ON_TRASHCAN` MeTube (вне нашего контроля, правило 11); чужие (не наши) файлы MeTube не удаляем никогда (правило 10).
## 6. Команда агентов
- **Конфиг:** `opencode.json` → `default_agent: orchestrator`. Работа — в одной сессии opencode; задача пишется обычным сообщением.
- **Роли (`.opencode/agent/`):** `orchestrator` (primary; декомпозиция, единственный говорит с пользователем, собирает отчёт, деплой на тестовый сервис `docker compose up -d --build` после закрытия Critical/Major), `analyst` (subagent; только документация `analytics/**` и точечные правки `README.md`), `coder` (subagent; единственный меняет код продукта), `reviewer` (subagent, `edit: deny`; read-only review диффа, severity Critical/Major/Minor), `tester` (subagent, `edit: deny`; прогон проверок, ничего не чинит).
- **Цикл:** `orchestrator → analyst (фиксация задачи) → coder → reviewer + tester (параллельно) → follow-up по находкам → деплой → отчёт пользователю`. Отчёты субагентов заканчиваются маркерами `ANALYST_DONE` / `CODER_DONE` / `REVIEW_DONE` / `TEST_DONE`. Коммит/пуш — только по явному запросу.
- **Конвенция `analytics/` (`analytics/README.md`):** один файл на задачу `analytics/<YYYY-MM-DD>-<slug>.md`; секции: Задача, Контекст, Затронутые подсистемы и файлы, Критерии приёмки, План, Риски и ограничения, Журнал изменений; follow-up дополняет существующий файл (журнал + только затронутые секции); документы на русском, кратко, но полно; код не пишется.
## Журнал изменений
- 2026-09-17 — создан baseline-документ (Analyst). Сверено с кодом: модули backend/frontend, все миграции 0001–0008, настройки `.env.example`/`config.py`, `git log -25`, роли агентов.