Compare commits
No commits in common. "5fa5a391d70e3d894830414d347bda23a8d5df3b" and "2e5ef8e52897c617fe5f8e94de3befc1ba29e658" have entirely different histories.
5fa5a391d7
...
2e5ef8e528
7 changed files with 49 additions and 236 deletions
|
|
@ -1,26 +0,0 @@
|
||||||
---
|
|
||||||
description: Task analyst. Before implementation, documents each task in analytics/*.md, updates README when needed, and reports ANALYST_DONE with the spec for Coder.
|
|
||||||
mode: subagent
|
|
||||||
---
|
|
||||||
|
|
||||||
# Analyst
|
|
||||||
|
|
||||||
You are the task analyst in an opencode agent team. The Orchestrator sends you a bounded task BEFORE Coder starts working. You write documentation only — never product code, tests, migrations, or configuration.
|
|
||||||
|
|
||||||
## Your job
|
|
||||||
|
|
||||||
1. Ground the task in reality: read `AGENTS.md`, the Orchestrator role (`.opencode/agent/orchestrator.md`), `analytics/README.md`, the relevant code and docs, and inspect the current worktree state (`git status`, `git diff`) enough to write an accurate spec.
|
|
||||||
2. Create or update the task's analytics document under `analytics/`:
|
|
||||||
- One file per task: `analytics/<YYYY-MM-DD>-<short-slug>.md`.
|
|
||||||
- Sections: «Задача» (goal), «Контекст» (why, relevant background), «Затронутые подсистемы и файлы», «Критерии приёмки», «План», «Риски и ограничения», «Журнал изменений».
|
|
||||||
- On follow-up rounds for the same task, UPDATE the existing document instead of creating a new file. Edit only the parts that need changing: append a journal entry and revise the affected sections; never rewrite the whole document or restyle untouched content.
|
|
||||||
- Documents are in Russian, concise but complete.
|
|
||||||
3. Update `README.md` when the task changes user-visible behavior, configuration, or project structure that README documents (feature list, env settings, project tree, agent team). Update README only where necessary — the specific sections affected; keep the rest untouched.
|
|
||||||
4. Verify your facts against the code; never invent endpoints, settings, or file paths.
|
|
||||||
|
|
||||||
## Final report format
|
|
||||||
|
|
||||||
- Analytics: file path(s) created or updated.
|
|
||||||
- Spec summary: scope, acceptance criteria, plan.
|
|
||||||
- README: updated, or why not.
|
|
||||||
- End the final response with `ANALYST_DONE`.
|
|
||||||
|
|
@ -1,33 +1,31 @@
|
||||||
---
|
---
|
||||||
description: Orchestrator / Tech Lead — coordinates analyst, coder, reviewer, and tester subagents and reports to the user.
|
description: Orchestrator / Tech Lead — coordinates coder, reviewer, and tester subagents and reports to the user.
|
||||||
mode: primary
|
mode: primary
|
||||||
---
|
---
|
||||||
|
|
||||||
# Orchestrator / Tech Lead
|
# Orchestrator / Tech Lead
|
||||||
|
|
||||||
You are the only agent who normally speaks with the user. Coordinate the opencode subagents `analyst`, `coder`, `reviewer`, and `tester` through the Task tool; do not edit product code yourself. The subagents share this worktree. Only `coder` changes product code; `analyst` may edit documentation (`analytics/**`, `README.md`).
|
You are the only agent who normally speaks with the user. Coordinate the opencode subagents `coder`, `reviewer`, and `tester` through the Task tool; do not edit product code yourself. The subagents share this worktree. Only `coder` changes tracked project files.
|
||||||
|
|
||||||
## Start every task
|
## Start every task
|
||||||
|
|
||||||
1. Read the repository's `AGENTS.md` and any relevant specification before planning. Inspect the current worktree (`git status`, `git diff`) and identify pre-existing changes.
|
1. Read the repository's `AGENTS.md` and any relevant specification before planning. Inspect the current worktree (`git status`, `git diff`) and identify pre-existing changes.
|
||||||
2. Turn the user's request into a bounded task: scope, acceptance criteria, files or subsystems likely involved, constraints, and validation expected. Resolve routine choices yourself. Ask the user only for genuinely missing decisions. The task goes to Analyst first, then to Coder.
|
2. Turn the user's request into a bounded task for Coder: scope, acceptance criteria, files or subsystems likely involved, constraints, and validation expected. Resolve routine choices yourself. Ask the user only for genuinely missing decisions.
|
||||||
3. Submit the task to Analyst (subagent_type `analyst`) first. Analyst grounds the task, writes or updates the analytics document under `analytics/`, and updates `README.md` when needed. Read its report (ends with `ANALYST_DONE`) before moving on.
|
3. Give Coder ownership of implementation. Do not send implementation work to Reviewer or Tester.
|
||||||
4. Give Coder ownership of implementation. Do not send implementation work to Reviewer or Tester.
|
4. Submit the task to Coder with the Task tool (subagent_type `coder`). Keep one code writer at a time: never run two Coder tasks concurrently and do not start a new Coder task while another is working.
|
||||||
5. Submit the task to Coder with the Task tool (subagent_type `coder`). Keep one code writer at a time: never run two Coder tasks concurrently and do not start a new Coder task while another is working.
|
5. Read Coder's report (ends with `CODER_DONE`) and inspect the diff yourself. Then run Reviewer and Tester on the result. Reviewer must not edit; Tester must not fix. They may run in parallel — their commands cannot interfere with each other's.
|
||||||
6. Read Coder's report (ends with `CODER_DONE`) and inspect the diff yourself. Then run Reviewer and Tester on the result. Reviewer must not edit; Tester must not fix. They may run in parallel — their commands cannot interfere with each other's. Collect Critical, Major, and Minor findings with evidence. Send actionable findings back to Coder as a follow-up task; if a follow-up substantially changes the task scope, first send Analyst an analytics update (a journal entry), otherwise the analytics update is optional. Repeat review and test on the changed result until Critical and Major findings are resolved, or report a concrete blocker to the user.
|
6. Collect Critical, Major, and Minor findings with evidence. Send actionable findings back to Coder as a follow-up task. Repeat review and test on the changed result until Critical and Major findings are resolved, or report a concrete blocker to the user.
|
||||||
6.5. When Critical and Major findings are closed and checks are green, do not ask for permission: deploy the changes to the test service right away with `docker compose up -d --build` (from the repository root; the container rebuilds the frontend from the working tree and applies migrations on startup). Then verify the container is up and `curl http://localhost:8080/api/health` responds OK.
|
6.5. When Critical and Major findings are closed and checks are green, do not ask for permission: deploy the changes to the test service right away with `docker compose up -d --build` (from the repository root; the container rebuilds the frontend from the working tree and applies migrations on startup). Then verify the container is up and `curl http://localhost:8080/api/health` responds OK.
|
||||||
7. After the deploy, give the user a concise final report: what Coder implemented, what Reviewer reviewed, what Tester tested (commands and results), review findings resolved or remaining, known limits, and worktree/branch. Remind the user to refresh the page with cache cleared (Ctrl+Shift+R) and explicitly say you are waiting for their feedback to verify the deployed changes. Do not claim visual or integration checks that were not performed.
|
7. After the deploy, give the user a concise final report: what Coder implemented, what Reviewer reviewed, what Tester tested (commands and results), review findings resolved or remaining, known limits, and worktree/branch. Remind the user to refresh the page with cache cleared (Ctrl+Shift+R) and explicitly say you are waiting for their feedback to verify the deployed changes. Do not claim visual or integration checks that were not performed.
|
||||||
|
|
||||||
## Working rules
|
## Working rules
|
||||||
|
|
||||||
- Do not merge, publish, or commit unless the user requested it or existing authorization covers it. Deploying to the test service is authorized by this workflow (step 6.5); deploying elsewhere still requires the user's go-ahead.
|
- Do not merge, publish, or commit unless the user requested it or existing authorization covers it. Deploying to the test service is authorized by this workflow (step 6.5); deploying elsewhere still requires the user's go-ahead.
|
||||||
- Analyst edits only documentation (`analytics/**`, `README.md`); Coder remains the only agent changing product code.
|
|
||||||
- For frontend work, include responsive behavior, accessibility, loading/error/empty states, and real browser verification when tooling exists in the acceptance criteria.
|
- For frontend work, include responsive behavior, accessibility, loading/error/empty states, and real browser verification when tooling exists in the acceptance criteria.
|
||||||
- For backend work, include data integrity, security, edge cases, and relevant API checks.
|
- For backend work, include data integrity, security, edge cases, and relevant API checks.
|
||||||
|
|
||||||
## Expected worker reports
|
## Expected worker reports
|
||||||
|
|
||||||
- Analyst ends with `ANALYST_DONE` and lists the analytics file(s), the spec summary, and README changes.
|
|
||||||
- Coder ends with `CODER_DONE` and lists changed files, implementation, verification, and limits.
|
- Coder ends with `CODER_DONE` and lists changed files, implementation, verification, and limits.
|
||||||
- Reviewer ends with `REVIEW_DONE` and lists findings by severity with file/line evidence, or states that no actionable findings were found.
|
- Reviewer ends with `REVIEW_DONE` and lists findings by severity with file/line evidence, or states that no actionable findings were found.
|
||||||
- Tester ends with `TEST_DONE` and lists commands, passes, failures, reproduction steps, expected and actual behavior, and severity.
|
- Tester ends with `TEST_DONE` and lists commands, passes, failures, reproduction steps, expected and actual behavior, and severity.
|
||||||
|
|
|
||||||
29
AGENT_TEAM.md
Normal file
29
AGENT_TEAM.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Агентская команда opencode
|
||||||
|
|
||||||
|
Одна сессия opencode в роли **Orchestrator** и три субагента — **Coder**, **Reviewer**, **Tester**, которых Orchestrator вызывает через Task tool. Роли лежат в [`.opencode/agent/`](./.opencode/agent/), Orchestrator назначен агентом по умолчанию в [`opencode.json`](./opencode.json).
|
||||||
|
|
||||||
|
## Запуск и работа
|
||||||
|
|
||||||
|
В корне репозитория запусти `opencode`. Каждая новая сессия стартует Orchestrator'ом: задачу пиши обычным сообщением. Он сам:
|
||||||
|
|
||||||
|
- декомпозирует задачу и отправляет её Coder'у через Task tool;
|
||||||
|
- после Coder'а запускает Reviewer (только чтение) и Tester (проверка без правок) параллельно;
|
||||||
|
- возвращает Coder'у actionable findings и повторяет цикл, пока Critical/Major не закрыты;
|
||||||
|
- когда Critical/Major закрыты и проверки зелёные — без вопроса деплоит на тестовый сервис (`docker compose up -d --build`, затем `curl http://localhost:8080/api/health`);
|
||||||
|
- отдаёт финальный отчёт уже после деплоя: что написано, просмотрено и протестировано, оставшиеся риски и просьба проверить в браузере с очисткой кэша (Ctrl+Shift+R).
|
||||||
|
|
||||||
|
Вручную писать субагентам не нужно — их вызывает только Orchestrator.
|
||||||
|
|
||||||
|
## Роли
|
||||||
|
|
||||||
|
- **Orchestrator** (primary, агент по умолчанию) — единственный общается с пользователем; сам код не правит.
|
||||||
|
- **Coder** (subagent) — единственный меняет отслеживаемые файлы; отчитывается `CODER_DONE`.
|
||||||
|
- **Reviewer** (subagent, `edit: deny`) — read-only review; отчитывается `REVIEW_DONE`.
|
||||||
|
- **Tester** (subagent, `edit: deny`) — прогоняет проверки; может создавать игнорируемые артефакты, но не меняет отслеживаемые файлы; отчитывается `TEST_DONE`.
|
||||||
|
|
||||||
|
## Файлы
|
||||||
|
|
||||||
|
- `.opencode/agent/*.md` — определения агентов команды;
|
||||||
|
- `opencode.json` — `default_agent: orchestrator`.
|
||||||
|
|
||||||
|
Конфиг и агенты читаются при старте opencode: после изменений перезапусти сессию.
|
||||||
27
README.md
27
README.md
|
|
@ -1,7 +1,7 @@
|
||||||
# MyYouTube
|
# MyYouTube
|
||||||
|
|
||||||
Персональный YouTube-клиент с категориями подписок и интеграцией с существующим MeTube.
|
Персональный YouTube-клиент с категориями подписок и интеграцией с существующим MeTube.
|
||||||
Работа агентской команды (opencode) описана в секции «Агентская команда (opencode)» ниже; роли агентов — в `.opencode/agent/`.
|
Работа агентской команды (opencode) описана в [руководстве по агентской команде](./AGENT_TEAM.md).
|
||||||
|
|
||||||
Текущий статус: **функционально готово** (основные этапы 1–6 и доработки по итогам тестирования). Развёрнуто и вручную протестировано на тестовом окружении (`testmyyoutube.vrubel.xyz` → hermesVM `192.168.8.173` → MeTube на mediaVM `192.168.8.177`).
|
Текущий статус: **функционально готово** (основные этапы 1–6 и доработки по итогам тестирования). Развёрнуто и вручную протестировано на тестовом окружении (`testmyyoutube.vrubel.xyz` → hermesVM `192.168.8.173` → MeTube на mediaVM `192.168.8.177`).
|
||||||
|
|
||||||
|
|
@ -47,23 +47,22 @@
|
||||||
|
|
||||||
### Агентская команда (opencode)
|
### Агентская команда (opencode)
|
||||||
|
|
||||||
|
Подробная инструкция: [AGENT_TEAM.md](./AGENT_TEAM.md).
|
||||||
|
|
||||||
Работа идёт в одной сессии opencode: она стартует агентом `orchestrator`
|
Работа идёт в одной сессии opencode: она стартует агентом `orchestrator`
|
||||||
(`default_agent` в `opencode.json`). Порядок работы:
|
(`default_agent` в `opencode.json`). Orchestrator сам декомпозирует задачу и
|
||||||
`orchestrator → analyst (аналитика/README) → coder → reviewer → tester`.
|
вызывает субагентов через Task tool: `coder` (единственный меняет
|
||||||
Orchestrator сам декомпозирует задачу и вызывает субагентов через Task tool:
|
отслеживаемые файлы), `reviewer` (read-only review, `edit: deny`) и `tester`
|
||||||
`analyst` (до Coder'а фиксирует задачу в `analytics/*.md` и при необходимости
|
(проверки без правок, `edit: deny`). Роли описаны в
|
||||||
актуализирует README), `coder` (единственный меняет код продукта), `reviewer`
|
[`.opencode/agent/`](./.opencode/agent/).
|
||||||
(read-only review, `edit: deny`) и `tester` (проверки без правок, `edit: deny`).
|
|
||||||
Роли описаны в [`.opencode/agent/`](./.opencode/agent/). Аналитика задач
|
|
||||||
ведётся в [`analytics/`](./analytics/) (см. `analytics/README.md`).
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
opencode
|
opencode
|
||||||
```
|
```
|
||||||
|
|
||||||
Задачу пиши обычным сообщением: Orchestrator передаст её сначала Analyst'у
|
Задачу пиши обычным сообщением: Orchestrator передаст её Coder'у, соберёт
|
||||||
(аналитика и README), затем Coder'у, соберёт review и QA, повторит цикл при
|
review и QA, повторит цикл при находках и вернёт финальный отчёт. Вручную
|
||||||
находках и вернёт финальный отчёт. Вручную писать субагентам не нужно.
|
писать субагентам не нужно.
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
|
|
@ -112,11 +111,11 @@ myyoutube/
|
||||||
├── frontend/ # React SPA
|
├── frontend/ # React SPA
|
||||||
├── migrations/ # Alembic migrations
|
├── migrations/ # Alembic migrations
|
||||||
├── tests/ # backend tests (pytest)
|
├── tests/ # backend tests (pytest)
|
||||||
├── analytics/ # аналитика задач агентской команды
|
|
||||||
├── Dockerfile
|
├── Dockerfile
|
||||||
├── compose.yml
|
├── compose.yml
|
||||||
├── .env / .env.example
|
├── .env / .env.example
|
||||||
└── .opencode/ # агенты команды (orchestrator/analyst/coder/reviewer/tester)
|
├── .opencode/ # агенты команды (orchestrator/coder/reviewer/tester)
|
||||||
|
└── AGENT_TEAM.md # руководство по агентской команде opencode
|
||||||
```
|
```
|
||||||
|
|
||||||
## Секреты
|
## Секреты
|
||||||
|
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
# Базовый документ проекта (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`, роли агентов.
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
# Удалить AGENT_TEAM.md и почистить ссылки на него
|
|
||||||
|
|
||||||
## Задача
|
|
||||||
|
|
||||||
Удалить `AGENT_TEAM.md` из репозитория и убрать/заменить все ссылки на него в `README.md` и `.opencode/agent/analyst.md`. Задача чисто документационная: код продукта не меняется.
|
|
||||||
|
|
||||||
## Контекст
|
|
||||||
|
|
||||||
`AGENT_TEAM.md` — человекочитаемое руководство по агентской команде (opencode). Оно стало избыточным: рабочий цикл и правила команды теперь описаны в `.opencode/agent/orchestrator.md` (порядок `orchestrator → analyst → coder → reviewer → tester`, правила деплоя) и в ролях субагентов (`.opencode/agent/analyst.md`, `coder.md`, `reviewer.md`, `tester.md`). Краткое описание команды для человека остаётся в `README.md` (секция «Локальная разработка → Агентская команда (opencode)», строки 48–67) — её нужно сохранить, убрав из неё ссылку-инструкцию на удаляемый файл.
|
|
||||||
|
|
||||||
Рабочее дерево чистое, ветка `master` впереди `origin/master` на 1 коммит. Коммит/пуш не входят в скоуп задачи.
|
|
||||||
|
|
||||||
## Затронутые подсистемы и файлы
|
|
||||||
|
|
||||||
Проверено `rg AGENT_TEAM --hidden --no-ignore` — в репозитории ровно 4 упоминания (плюс сам файл):
|
|
||||||
|
|
||||||
1. `AGENT_TEAM.md` (корень репозитория) — удалить целиком.
|
|
||||||
2. `README.md:4` — интро: «Работа агентской команды (opencode) описана в [руководстве по агентской команде](./AGENT_TEAM.md).» — переформулировать без ссылки (либо убрать предложение).
|
|
||||||
3. `README.md:50` — «Подробная инструкция: [AGENT_TEAM.md](./AGENT_TEAM.md).» в секции «Агентская команда (opencode)» — убрать строку; остальное описание команды (строки 52–67) сохранить, при необходимости дополнить упоминанием папки `analytics/` (аналитика задач, ведёт Analyst).
|
|
||||||
4. `README.md:121` — дерево проекта: `└── AGENT_TEAM.md # руководство по агентской команде opencode` — убрать строку.
|
|
||||||
5. `.opencode/agent/analyst.md:12` — шаг 1: «read `AGENTS.md`, `AGENT_TEAM.md`, the relevant code and docs…» — заменить `AGENT_TEAM.md` на актуальные источники (например, `README.md` и/или `.opencode/agent/`), оставив смысл шага.
|
|
||||||
|
|
||||||
## Критерии приёмки
|
|
||||||
|
|
||||||
- `AGENT_TEAM.md` удалён из репозитория.
|
|
||||||
- Нигде не осталось ссылок на удалённый файл: `rg AGENT_TEAM` по `README.md`, `.opencode/`, `backend/`, `frontend/` и т.д. — 0 совпадений (упоминания имени файла в тексте самой этой аналитики не считаются).
|
|
||||||
- В `README.md` ссылок на `AGENT_TEAM.md` не осталось; секция «Агентская команда (opencode)» сохранена и при необходимости дополнена упоминанием `analytics/`; дерево проекта не содержит удалённого файла.
|
|
||||||
- В `.opencode/agent/analyst.md` упоминание `AGENT_TEAM.md` заменено/убрано без потери смысла шага 1.
|
|
||||||
- Код продукта (`backend/`, `frontend/`, `migrations/`, `tests/`, `compose.yml`, `Dockerfile` и т.п.) не тронут.
|
|
||||||
- `git status` — ожидаемый набор: `deleted: AGENT_TEAM.md`, `modified: README.md`, `modified: .opencode/agent/analyst.md` (+ новый файл этой аналитики).
|
|
||||||
- Коммит/пуш не выполняется без явного запроса пользователя.
|
|
||||||
|
|
||||||
## План
|
|
||||||
|
|
||||||
1. Удалить `AGENT_TEAM.md`.
|
|
||||||
2. `README.md`: переформулировать интро (строка 4), убрать строку 50, при необходимости дополнить описание команды упоминанием `analytics/`, убрать строку 121 из дерева проекта. Остальное в README не трогать.
|
|
||||||
3. `.opencode/agent/analyst.md`: в шаге 1 заменить `AGENT_TEAM.md` на актуальные источники (`README.md`, роли в `.opencode/agent/`).
|
|
||||||
4. Проверить: `rg AGENT_TEAM` вне `analytics/` → 0 совпадений; `git status` — только ожидаемые файлы.
|
|
||||||
5. Деплой не требуется: изменены только документация и описание агентов, они не входят в Docker-образы; решение за Orchestrator.
|
|
||||||
|
|
||||||
## Риски и ограничения
|
|
||||||
|
|
||||||
- Не потерять информацию: рабочий цикл после удаления должен оставаться описан в `.opencode/agent/orchestrator.md` + ролях субагентов + секции «Агентская команда» в README.
|
|
||||||
- Задача документационная — ни один файл продукта не должен попасть в diff.
|
|
||||||
- Минимальные правки: не рестайлить нетронутые части README/analyst.md.
|
|
||||||
|
|
||||||
## Журнал изменений
|
|
||||||
|
|
||||||
- 2026-09-17 — создан документ задачи (Analyst). Факты сверены с репозиторием: найдено 4 упоминания `AGENT_TEAM` (README ×3, analyst.md ×1) + сам файл.
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
# Аналитика задач
|
|
||||||
|
|
||||||
Папку ведёт субагент **Analyst** (`.opencode/agent/analyst.md`): до начала реализации он фиксирует каждую задачу, а по ходу работы дополняет и актуализирует документы. Здесь только документация — никакого кода.
|
|
||||||
|
|
||||||
## Конвенция
|
|
||||||
|
|
||||||
- Один файл на задачу: `analytics/<YYYY-MM-DD>-<slug>.md` (дата и короткий идентификатор задачи).
|
|
||||||
- Обязательные секции:
|
|
||||||
- **Задача** — цель;
|
|
||||||
- **Контекст** — зачем это нужно и важный фон;
|
|
||||||
- **Затронутые подсистемы и файлы**;
|
|
||||||
- **Критерии приёмки**;
|
|
||||||
- **План**;
|
|
||||||
- **Риски и ограничения**;
|
|
||||||
- **Журнал изменений**.
|
|
||||||
- Документы на русском, кратко, но полно.
|
|
||||||
- Follow-up (повторный раунд по той же задаче) дополняет существующий файл, а не создаёт новый.
|
|
||||||
- Документ не переписывается целиком: дополняй журнал и правь только затронутые секции, остальное не трогай.
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue