From 72d8ec9cbadcb1b74d032deb5e5eea96e785d9c4 Mon Sep 17 00:00:00 2001 From: ph4nt0mer Date: Thu, 16 Jul 2026 12:30:00 +0800 Subject: [PATCH] fix: auto-refresh remote backup directory when cache is stale (#312) When entering the cloud backup page, the remote backup directory list only showed cached data and required a manual click of the refresh button to see new backup files. This change adds a TTL-based auto-refresh that fetches fresh data when the cache is older than 5 minutes. Changes: - Added refreshedAt tracking per cache key in persisted state - Added REMOTE_BROWSER_REFRESH_TTL_MS (5 min) constant - Added useEffect that triggers auto-refresh when destination is selected and cached data is stale - Stamped refresh timestamps after successful API responses - Cleaned up timestamps on destination delete and settings save --- webapp/src/components/BackupCenterPage.tsx | 21 ++++++++++++++++++++- webapp/src/lib/backup-center.ts | 5 +++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/webapp/src/components/BackupCenterPage.tsx b/webapp/src/components/BackupCenterPage.tsx index 771d074..650a5a0 100644 --- a/webapp/src/components/BackupCenterPage.tsx +++ b/webapp/src/components/BackupCenterPage.tsx @@ -13,6 +13,7 @@ import { } from '@/lib/api/backup'; import { REMOTE_BROWSER_ITEMS_PER_PAGE, + REMOTE_BROWSER_REFRESH_TTL_MS, compareRemoteItems, createDraftBackupSettings, createDraftDestinationRecord, @@ -217,6 +218,7 @@ export default function BackupCenterPage(props: BackupCenterPageProps) { const [remoteBrowserCache, setRemoteBrowserCache] = useState>(persistedRemoteState.cache); const [remoteBrowserPathByDestination, setRemoteBrowserPathByDestination] = useState>(persistedRemoteState.pathByDestination); const [remoteBrowserPageByKey, setRemoteBrowserPageByKey] = useState>(persistedRemoteState.pageByKey); + const [remoteBrowserRefreshedAt, setRemoteBrowserRefreshedAt] = useState>(persistedRemoteState.refreshedAt || {}); const [showAddChooser, setShowAddChooser] = useState(false); const visibleDestinations = getVisibleDestinations(settings); @@ -308,8 +310,22 @@ export default function BackupCenterPage(props: BackupCenterPageProps) { pathByDestination: remoteBrowserPathByDestination, pageByKey: remoteBrowserPageByKey, selectedDestinationId, + refreshedAt: remoteBrowserRefreshedAt, }); - }, [props.currentUserId, remoteBrowserCache, remoteBrowserPageByKey, remoteBrowserPathByDestination, selectedDestinationId]); + }, [props.currentUserId, remoteBrowserCache, remoteBrowserPageByKey, remoteBrowserPathByDestination, remoteBrowserRefreshedAt, selectedDestinationId]); + + useEffect(() => { + if (!savedSelectedDestination) return; + const destinationId = savedSelectedDestination.id; + const path = remoteBrowserPathByDestination[destinationId] || ''; + const cacheKey = getRemoteBrowserCacheKey(destinationId, path); + const lastRefreshed = remoteBrowserRefreshedAt[cacheKey] || 0; + const isStale = Date.now() - lastRefreshed > REMOTE_BROWSER_REFRESH_TTL_MS; + if (isStale) { + void loadRemoteBrowser(destinationId, path, { force: true }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [savedSelectedDestination?.id]); useEffect(() => { if (!restoreProgress) { @@ -398,6 +414,7 @@ export default function BackupCenterPage(props: BackupCenterPageProps) { }; setRemoteBrowserCache((current) => ({ ...current, [cacheKey]: nextBrowser })); setRemoteBrowserPageByKey((current) => ({ ...current, [cacheKey]: 1 })); + setRemoteBrowserRefreshedAt((current) => ({ ...current, [cacheKey]: Date.now() })); } catch (error) { const message = error instanceof Error ? error.message : t('txt_backup_remote_load_failed'); setLocalError(message); @@ -543,6 +560,7 @@ export default function BackupCenterPage(props: BackupCenterPageProps) { ).cache); setRemoteBrowserPathByDestination((current) => Object.fromEntries(Object.entries(current).filter(([key]) => key !== destinationIdToDelete))); setRemoteBrowserPageByKey((current) => Object.fromEntries(Object.entries(current).filter(([key]) => !key.startsWith(`${destinationIdToDelete}:`)))); + setRemoteBrowserRefreshedAt((current) => Object.fromEntries(Object.entries(current).filter(([key]) => !key.startsWith(`${destinationIdToDelete}:`)))); setSelectedDestinationId(nextSelected); setConfirmDeleteDestinationOpen(false); props.onNotify('success', t('txt_backup_destination_deleted')); @@ -670,6 +688,7 @@ export default function BackupCenterPage(props: BackupCenterPageProps) { setRemoteBrowserCache((current) => Object.fromEntries(Object.entries(current).filter(([key]) => !key.startsWith(`${destinationIdToInvalidate}:`)))); setRemoteBrowserPathByDestination((current) => Object.fromEntries(Object.entries(current).filter(([key]) => key !== destinationIdToInvalidate))); setRemoteBrowserPageByKey((current) => Object.fromEntries(Object.entries(current).filter(([key]) => !key.startsWith(`${destinationIdToInvalidate}:`)))); + setRemoteBrowserRefreshedAt((current) => Object.fromEntries(Object.entries(current).filter(([key]) => !key.startsWith(`${destinationIdToInvalidate}:`)))); } setSelectedDestinationId(nextSelected); props.onNotify('success', t('txt_backup_settings_saved')); diff --git a/webapp/src/lib/backup-center.ts b/webapp/src/lib/backup-center.ts index e07f5ea..e2cb815 100644 --- a/webapp/src/lib/backup-center.ts +++ b/webapp/src/lib/backup-center.ts @@ -14,10 +14,12 @@ export interface PersistedRemoteBrowserState { pathByDestination: Record; pageByKey: Record; selectedDestinationId: string | null; + refreshedAt: Record; } export const REMOTE_BROWSER_STORAGE_KEY = 'nodewarden.backup.remote-browser.v1'; export const REMOTE_BROWSER_ITEMS_PER_PAGE = 10; +export const REMOTE_BROWSER_REFRESH_TTL_MS = 5 * 60 * 1000; // 5 minutes export const COMMON_TIME_ZONES = [ 'UTC', @@ -148,6 +150,7 @@ export function loadPersistedRemoteBrowserState(userId?: string | null): Persist pathByDestination: {}, pageByKey: {}, selectedDestinationId: null, + refreshedAt: {}, }; } const parsed = JSON.parse(raw) as Partial; @@ -156,6 +159,7 @@ export function loadPersistedRemoteBrowserState(userId?: string | null): Persist pathByDestination: parsed.pathByDestination && typeof parsed.pathByDestination === 'object' ? parsed.pathByDestination : {}, pageByKey: parsed.pageByKey && typeof parsed.pageByKey === 'object' ? parsed.pageByKey : {}, selectedDestinationId: typeof parsed.selectedDestinationId === 'string' ? parsed.selectedDestinationId : null, + refreshedAt: parsed.refreshedAt && typeof parsed.refreshedAt === 'object' ? parsed.refreshedAt as Record : {}, }; } catch { return { @@ -163,6 +167,7 @@ export function loadPersistedRemoteBrowserState(userId?: string | null): Persist pathByDestination: {}, pageByKey: {}, selectedDestinationId: null, + refreshedAt: {}, }; } }