60 lines
9.7 KiB
Markdown
60 lines
9.7 KiB
Markdown
|
|
# Перенос ручного обновления синков в кнопку «Обновить всё» (попап SyncIndicator)
|
|||
|
|
|
|||
|
|
## Задача
|
|||
|
|
|
|||
|
|
Собрать ручное обновление синков в единую кнопку «Обновить всё» в попапе индикатора синхронизации в шапке (`SyncIndicator` в `AppShell`). Убрать кнопки «Обновить» из заголовков страниц «Лента» (`Feed.tsx`) и «Каналы» (`Channels.tsx`). Backend не менять.
|
|||
|
|
|
|||
|
|
## Контекст
|
|||
|
|
|
|||
|
|
- Кнопка в ленте вызывает `POST /api/sync/videos` — глобальный синк всех каналов, а не текущей категории; вводит в заблуждение. Глобальное действие семантически принадлежит глобальному индикатору «Обновлено X назад» в шапке.
|
|||
|
|
- Порядок «подписки → видео» важен: `sync_subscriptions` заполняет `uploads_playlist_id` новых каналов, а `sync_videos` выбирает только каналы с непустым `uploads_playlist_id` (`backend/app/services/sync.py:169-173`) — новые каналы сразу получают видео.
|
|||
|
|
- Автотриггер: каждый аутентифицированный запрос (`require_session` → `maybe_trigger_videos_sync`) может запустить фоновый синк видео (`backend/app/services/sync_trigger.py`). Важно: сам `POST /api/sync/subscriptions` тоже проходит через `require_session`, поэтому сразу после клика «Обновить всё» видео-синк может уже стартовать в фоне, и следующий `POST /api/sync/videos` вернёт 409 — это штатный сценарий, не ошибка.
|
|||
|
|
- Поведение backend при уже идущем синке: `sync.py` бросает `SyncInProgress`, API отдаёт **409** (`backend/app/api/sync.py`), detail: `"Subscriptions sync already in progress"` / `"Videos sync already in progress"`. Фронтовый `request()` кидает `Error(detail)` (`frontend/src/api/client.ts:27`).
|
|||
|
|
- Поллинг статуса уже есть: query `['sync-status']` с `refetchInterval` 2 с при `running`, иначе 60 с (`AppShell.tsx:13`). `Feed.tsx` и `Channels.tsx` инвалидируют `['feed']` / `['channels']` в `useEffect` при смене `finished_at` — эта механика не зависит от того, откуда запущен синк, и продолжит работать после переноса кнопки.
|
|||
|
|
- Синк подписок может быть занят и без пользователя: он запускается по расписанию (`backend/app/services/scheduler.py:15`).
|
|||
|
|
- Backend не трогаем: `POST /api/sync/subscriptions` и `POST /api/sync/videos` остаются — пригодятся API-клиентам.
|
|||
|
|
|
|||
|
|
## Затронутые подсистемы и файлы
|
|||
|
|
|
|||
|
|
- `frontend/src/components/AppShell.tsx` — `SyncIndicator`: кнопка «Обновить всё» в попапе, последовательный запуск подписки → видео, толерантность к 409, disabled/спиннер, показ ошибки в попапе, инвалидация `['sync-status']`.
|
|||
|
|
- `frontend/src/pages/Feed.tsx` — удалить `syncMutation`, кнопку (строка 84) и notice ошибки мутации (строка 97); сохранить notice по `sync-status` «Последнее обновление видео завершилось ошибкой…» (строка 98) и оба `useEffect`-инвалидации.
|
|||
|
|
- `frontend/src/pages/Channels.tsx` — удалить `syncMutation` (строка 20), кнопку в заголовке (строка 37) и notice ошибки мутации (строка 38); сохранить notice о `failed` (строка 39); заменить подсказку пустого состояния (строка 45) на указание на индикатор в шапке.
|
|||
|
|
- `frontend/src/api/client.ts` — `syncSubscriptions`/`syncVideos` начнёт использовать `AppShell`; сигнатуры не меняются.
|
|||
|
|
- `frontend/src/App.css` — стиль кнопки внутри `.sync-popover`; удалить неиспользуемые правила `.refresh-button` (media query, строки 248–249), если после чистки не останется использований.
|
|||
|
|
- Backend — не изменяется.
|
|||
|
|
|
|||
|
|
## Критерии приёмки
|
|||
|
|
|
|||
|
|
1. Единственная кнопка ручного обновления — «Обновить всё» в попапе `SyncIndicator` (шапка).
|
|||
|
|
2. Запуск строго последовательный: сначала `POST /api/sync/subscriptions`, затем `POST /api/sync/videos`.
|
|||
|
|
3. 409 при запуске видео-синка (в т.ч. из-за автотриггера) — не ошибка: повторный запуск пропускается, состояние показывает поллинг; попап не показывает ошибку в этом случае.
|
|||
|
|
4. Кнопка disabled со спиннером, пока идёт мутация или любой из синков `running` (по `sync-status`); прочие ошибки (400/502/503/сеть) показываются в попапе.
|
|||
|
|
5. `Feed.tsx` и `Channels.tsx` очищены: нет кнопок «Обновить», нет мутаций/вызовов `syncVideos`/`syncSubscriptions`, нет notice ошибок мутаций; notice «…завершилось ошибкой» (по `sync-status`) и `useEffect`-инвалидации кэша по `finished_at` сохранены и работают.
|
|||
|
|
6. Пустое состояние «Каналы» ссылается на индикатор в шапке (текст с «Обновить всё»).
|
|||
|
|
7. `npm run lint` и `npm run build` чистые; `git diff` не затрагивает `backend/`; `pytest` зелёный.
|
|||
|
|
8. Деплой по правилу команды: `docker compose up -d --build`, `curl http://localhost:8080/api/health` → OK.
|
|||
|
|
|
|||
|
|
## План
|
|||
|
|
|
|||
|
|
1. `AppShell.tsx` (SyncIndicator):
|
|||
|
|
- `useMutation` с `mutationFn`: `await syncSubscriptions()`, затем `await syncVideos()`; при ошибке видео, содержащей `already in progress` (409), «проглотить» и не считать ошибкой; 409 подписок — аналогично не ошибка.
|
|||
|
|
- Кнопка «Обновить всё» в попапе: `disabled` при `isPending` или `data?.videos.running || data?.subscriptions.running`, спиннер при pending/running.
|
|||
|
|
- `onSettled`: `invalidateQueries(['sync-status'])`; блок ошибки мутации в попапе (`role="status"` у попапа при наличии кнопки стоит пересмотреть: интерактивный элемент внутри status-региона — вынести кнопку или убрать role).
|
|||
|
|
2. `Feed.tsx`: удалить `syncMutation`, кнопку, notice мутации и импорты `useMutation`/`syncVideos`; оставить `syncStatusQuery`, оба `useEffect`, notice о `failed`.
|
|||
|
|
3. `Channels.tsx`: то же + новый текст подсказки пустого состояния со ссылкой на индикатор в шапке.
|
|||
|
|
4. `App.css`: стили кнопки в попапе; вычистить `.refresh-button`.
|
|||
|
|
5. Проверка: `npm run lint`, `npm run build`, `pytest`; ручная проверка сценариев (клик → подписки затем видео; повторный клик при идущем видео-синке не даёт ошибки; статусы обновляются поллингом; notice о failed остаётся на страницах).
|
|||
|
|
6. Деплой по правилу команды.
|
|||
|
|
|
|||
|
|
## Риски и ограничения
|
|||
|
|
|
|||
|
|
- Распознавание 409 по тексту `Error.message` (`already in progress`) — хрупко, если backend изменит detail; приемлемо, т.к. backend не меняется. Альтернатива — типизованные ошибки в `client.ts`, но без изменения API.
|
|||
|
|
- 409 подписок (фоновый часовой синк): трактовать симметрично (пропустить, поллинг покажет статус). Если при этом продолжить и запускать видео-синк, он может не увидеть каналы, чьи плейлисты ещё импортируются — приемлемо, следующий синк подхватит.
|
|||
|
|
- После удаления кнопки из `page-heading` правая часть заголовков страниц опустеет — проверить, что вёрстка не ломается (внутренний `div` с заголовком остаётся).
|
|||
|
|
- Попап закрывается по клику вне `ref`; кнопка внутри попапа не должна его закрывать — существующий обработчик это уже учитывает.
|
|||
|
|
- Мобильная вёрстка: попап имеет адаптивное правило (`App.css:278`), кнопка должна быть удобной на узких экранах.
|
|||
|
|
|
|||
|
|
## Журнал изменений
|
|||
|
|
|
|||
|
|
- 2026-09-18: документ создан перед реализацией (первичный спек, backend не меняется).
|