myYouTube/analytics/2026-09-17-sync-all-button.md
vrubelroman 0dc1336951 Move manual sync to a single 'Update all' button in the header popover
The feed/channels 'Update' buttons triggered a global sync of all channels,
which was misleading inside a specific category or page context. Now one
'Update all' button in the sync indicator popover runs subscriptions then
videos sequentially, tolerates an already-running sync (409), and pages
keep their finished_at-based cache invalidation.
2026-09-18 09:39:18 +00:00

9.7 KiB
Raw Blame History

Перенос ручного обновления синков в кнопку «Обновить всё» (попап 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 не меняется).