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.
This commit is contained in:
vrubelroman 2026-09-18 09:39:18 +00:00
parent 5fa5a391d7
commit 0dc1336951
5 changed files with 94 additions and 19 deletions

View file

@ -1,17 +1,35 @@
import { useEffect, useRef, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Link, NavLink, useLocation, useNavigate } from 'react-router-dom'
import { getSyncStatus, listCategories } from '../api/client'
import { getSyncStatus, listCategories, syncSubscriptions, syncVideos } from '../api/client'
import { formatRelativeTime } from '../utils/format'
import Icon from './Icon'
function SyncIndicator() {
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
const queryClient = useQueryClient()
const { data } = useQuery({
queryKey: ['sync-status'], queryFn: getSyncStatus,
refetchInterval: (query) => query.state.data?.videos.running || query.state.data?.subscriptions.running ? 2000 : 60_000,
})
const syncAllMutation = useMutation({
mutationFn: async () => {
const ignoreInProgress = (error: unknown) => error instanceof Error && error.message.includes('already in progress')
try {
await syncSubscriptions()
} catch (error) {
if (!ignoreInProgress(error)) throw error
}
try {
await syncVideos()
} catch (error) {
if (!ignoreInProgress(error)) throw error
}
},
onMutate: () => { queryClient.invalidateQueries({ queryKey: ['sync-status'] }) },
onSettled: () => { queryClient.invalidateQueries({ queryKey: ['sync-status'] }) },
})
useEffect(() => {
if (!open) return
const close = (event: MouseEvent) => { if (!ref.current?.contains(event.target as Node)) setOpen(false) }
@ -19,6 +37,7 @@ function SyncIndicator() {
return () => document.removeEventListener('mousedown', close)
}, [open])
const running = data?.videos.running || data?.subscriptions.running
const busy = syncAllMutation.isPending || running
const failed = data?.videos.status === 'failed' || data?.subscriptions.status === 'failed'
const recent = [data?.videos.finished_at, data?.subscriptions.finished_at].filter((value): value is string => Boolean(value)).sort().at(-1)
const label = running ? 'Обновление...' : failed ? 'Ошибка синхронизации' : recent ? `Обновлено ${formatRelativeTime(recent)}` : 'Синхронизация'
@ -27,11 +46,16 @@ function SyncIndicator() {
<Icon name={failed ? 'alert' : running ? 'refresh' : 'check'} size={17} className={running ? 'spin' : ''} />
<span>{label}</span>
</button>
{open && <div className="sync-popover" role="status">
{open && <div className="sync-popover">
<strong>Синхронизация</strong>
<p>Подписки: {data?.subscriptions.running ? 'обновляются' : data?.subscriptions.finished_at ? formatRelativeTime(data.subscriptions.finished_at) : 'ещё не обновлялись'}</p>
<p>Видео: {data?.videos.running ? 'обновляются' : data?.videos.finished_at ? formatRelativeTime(data.videos.finished_at) : 'ещё не обновлялись'}</p>
{failed && <p className="error">Последнее обновление завершилось ошибкой. Подробности в настройках.</p>}
<button type="button" className="button-secondary sync-all-button" onClick={() => syncAllMutation.mutate()} disabled={busy}>
<Icon name="refresh" size={17} className={busy ? 'spin' : ''} />
{busy ? 'Обновляем…' : 'Обновить всё'}
</button>
{syncAllMutation.isError && <p className="error" role="alert">Не удалось запустить обновление. Попробуйте позже.</p>}
</div>}
</div>
}