import { useEffect, useState } from 'react' import StyledButton from '~/components/StyledButton' import StyledSectionHeader from '~/components/StyledSectionHeader' import Alert from '~/components/Alert' import api from '~/lib/api' import Input from '~/components/inputs/Input' import Switch from '~/components/inputs/Switch' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useNotifications } from '~/context/NotificationContext' import { useContentAutoUpdateStatus } from '~/hooks/useContentAutoUpdateStatus' import { formatBytes } from '~/lib/util' const COOLOFF_OPTIONS = [ { value: 24, label: '24 hours (1 day)' }, { value: 48, label: '48 hours (2 days)' }, { value: 72, label: '72 hours (3 days)' }, { value: 168, label: '7 days' }, ] const BYTES_PER_GB = 1024 * 1024 * 1024 export default function ContentAutoUpdateSection() { const { addNotification } = useNotifications() const queryClient = useQueryClient() const { data: status, isLoading } = useContentAutoUpdateStatus() const [windowStart, setWindowStart] = useState('02:00') const [windowEnd, setWindowEnd] = useState('05:00') const [cooloff, setCooloff] = useState(72) // Data cap is stored in bytes but edited in GB (0 = unlimited). const [capGb, setCapGb] = useState('0') // Seed editable fields once the persisted status loads. useEffect(() => { if (status) { setWindowStart(status.windowStart) setWindowEnd(status.windowEnd) setCooloff(status.cooloffHours) setCapGb( status.maxBytesPerWindow > 0 ? String(Math.round((status.maxBytesPerWindow / BYTES_PER_GB) * 100) / 100) : '0' ) } }, [status?.windowStart, status?.windowEnd, status?.cooloffHours, status?.maxBytesPerWindow]) const enabled = status?.enabled ?? false const autoDisabled = !!status?.autoDisabledReason const toggleMutation = useMutation({ mutationFn: (value: boolean) => api.updateSetting('contentAutoUpdate.enabled', value), onSuccess: (_data, value) => { queryClient.invalidateQueries({ queryKey: ['content-auto-update-status'] }) addNotification({ type: 'success', message: value ? 'Automatic content updates enabled.' : 'Automatic content updates disabled.', }) }, onError: () => { addNotification({ type: 'error', message: 'Failed to update content auto-update setting.' }) }, }) const handleSaveSchedule = async () => { const parsedGb = Number(capGb) if (!Number.isFinite(parsedGb) || parsedGb < 0) { addNotification({ type: 'error', message: 'Data cap must be 0 or a positive number of GB.' }) return } const capBytes = Math.round(parsedGb * BYTES_PER_GB) try { await api.updateSetting('contentAutoUpdate.windowStart', windowStart) await api.updateSetting('contentAutoUpdate.windowEnd', windowEnd) await api.updateSetting('contentAutoUpdate.cooloffHours', String(cooloff)) await api.updateSetting('contentAutoUpdate.maxBytesPerWindow', String(capBytes)) queryClient.invalidateQueries({ queryKey: ['content-auto-update-status'] }) addNotification({ type: 'success', message: 'Content update schedule saved.' }) } catch { addNotification({ type: 'error', message: 'Failed to save content update schedule.' }) } } return ( <>
{autoDisabled && ( )} toggleMutation.mutate(value)} disabled={toggleMutation.isPending || isLoading} label="Enable Automatic Content Updates" description="Automatically download newer versions of your installed Information Library content (ZIM files) and maps during your chosen window. Content downloads can be very large, so set a per-window data cap to limit how much is pulled at once. We recommend allowing at least 0.5 GB per update window to ensure most updates can be pulled in a timely manner, but you can set a lower cap if you have very limited bandwidth and don't mind some updates being skipped (they will still appear in the UI and can be updated manually). If an update repeatedly fails to download within the window, it will be automatically disabled and require manual intervention to re-enable." />
setWindowStart(e.target.value)} disabled={!enabled} helpText="Local server time" /> setWindowEnd(e.target.value)} disabled={!enabled} helpText="Local server time" />

Delay after a new version appears

setCapGb(e.target.value)} disabled={!enabled} helpText="Per window. 0 = unlimited" />
Save Schedule
{enabled && status && (

Update window: {status.windowStart}โ€“{status.windowEnd} ( {status.withinWindow ? 'currently inside' : 'currently outside'}); cool-off{' '} {status.cooloffHours}h; data cap{' '} {status.maxBytesPerWindow > 0 ? formatBytes(status.maxBytesPerWindow) : 'unlimited'} {status.maxBytesPerWindow > 0 && ( <> ({formatBytes(status.windowBytesUsed)} used this window) )} . {status.lastResult && ( <> {' '} Last run: {status.lastResult} {status.lastAttemptAt ? ` (${new Date(status.lastAttemptAt).toLocaleString()})` : ''} )}

{status.lastError && (

Last error: {status.lastError}

)} {status.resources.length === 0 ? (

All installed content is up to date. New versions will appear here when detected.

) : (
    {status.resources.map((resource) => (
  • {resource.resource_id}{' '} {resource.resource_type}

    {resource.current_version} {resource.available_update_version ? ` โ†’ ${resource.available_update_version}` : ' (up to date)'} {resource.size_bytes ? ` ยท ${formatBytes(resource.size_bytes)}` : ''}

    {resource.auto_disabled_reason && (

    {resource.auto_disabled_reason}

    )}
    {resource.exceeds_cap ? 'Skipped โ€” exceeds data cap, update manually' : resource.reason}
  • ))}
)}
)}
) }