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.
9.7 KiB
9.7 KiB
Перенос ручного обновления синков в кнопку «Обновить всё» (попап 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']сrefetchInterval2 с при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 — не изменяется.
Критерии приёмки
- Единственная кнопка ручного обновления — «Обновить всё» в попапе
SyncIndicator(шапка). - Запуск строго последовательный: сначала
POST /api/sync/subscriptions, затемPOST /api/sync/videos. - 409 при запуске видео-синка (в т.ч. из-за автотриггера) — не ошибка: повторный запуск пропускается, состояние показывает поллинг; попап не показывает ошибку в этом случае.
- Кнопка disabled со спиннером, пока идёт мутация или любой из синков
running(поsync-status); прочие ошибки (400/502/503/сеть) показываются в попапе. Feed.tsxиChannels.tsxочищены: нет кнопок «Обновить», нет мутаций/вызововsyncVideos/syncSubscriptions, нет notice ошибок мутаций; notice «…завершилось ошибкой» (поsync-status) иuseEffect-инвалидации кэша поfinished_atсохранены и работают.- Пустое состояние «Каналы» ссылается на индикатор в шапке (текст с «Обновить всё»).
npm run lintиnpm run buildчистые;git diffне затрагиваетbackend/;pytestзелёный.- Деплой по правилу команды:
docker compose up -d --build,curl http://localhost:8080/api/health→ OK.
План
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).
Feed.tsx: удалитьsyncMutation, кнопку, notice мутации и импортыuseMutation/syncVideos; оставитьsyncStatusQuery, обаuseEffect, notice оfailed.Channels.tsx: то же + новый текст подсказки пустого состояния со ссылкой на индикатор в шапке.App.css: стили кнопки в попапе; вычистить.refresh-button.- Проверка:
npm run lint,npm run build,pytest; ручная проверка сценариев (клик → подписки затем видео; повторный клик при идущем видео-синке не даёт ошибки; статусы обновляются поллингом; notice о failed остаётся на страницах). - Деплой по правилу команды.
Риски и ограничения
- Распознавание 409 по тексту
Error.message(already in progress) — хрупко, если backend изменит detail; приемлемо, т.к. backend не меняется. Альтернатива — типизованные ошибки вclient.ts, но без изменения API. - 409 подписок (фоновый часовой синк): трактовать симметрично (пропустить, поллинг покажет статус). Если при этом продолжить и запускать видео-синк, он может не увидеть каналы, чьи плейлисты ещё импортируются — приемлемо, следующий синк подхватит.
- После удаления кнопки из
page-headingправая часть заголовков страниц опустеет — проверить, что вёрстка не ломается (внутреннийdivс заголовком остаётся). - Попап закрывается по клику вне
ref; кнопка внутри попапа не должна его закрывать — существующий обработчик это уже учитывает. - Мобильная вёрстка: попап имеет адаптивное правило (
App.css:278), кнопка должна быть удобной на узких экранах.
Журнал изменений
- 2026-09-18: документ создан перед реализацией (первичный спек, backend не меняется).