From 91428bb08d9711d6187b8af5a65828d281d5d9a8 Mon Sep 17 00:00:00 2001 From: Chris Sherwood Date: Mon, 13 Jul 2026 15:51:17 -0700 Subject: [PATCH] fix(content): refresh installed ZIMs when a download completes to prune ghost entries The Content Explorer remote list already filters out installed ZIMs client-side (flatData excludes localNames), but the installed-files query has refetchOnWindowFocus disabled and was never invalidated on download completion. So when a ZIM finished downloading it dropped off the active-downloads list (isDownloading -> false) while localFiles stayed stale (isPresent -> false), letting the just-installed ZIM reappear as a ghost entry until the page remounted. Add an effect that invalidates the ['zim-files'] query whenever a job drops off the polled downloads list, so the completed install is picked up and pruned. Fix originally diagnosed by @johno10661 in #771; reimplemented focused onto current dev (the accumulated-page/filter groundwork already landed via #731). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../pages/settings/zim/remote-explorer.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/admin/inertia/pages/settings/zim/remote-explorer.tsx b/admin/inertia/pages/settings/zim/remote-explorer.tsx index a19d9b3..2f90584 100644 --- a/admin/inertia/pages/settings/zim/remote-explorer.tsx +++ b/admin/inertia/pages/settings/zim/remote-explorer.tsx @@ -175,6 +175,26 @@ export default function ZimRemoteExplorer() { }, [data, downloads, localFiles]) const hasMore = useMemo(() => data?.pages[data.pages.length - 1]?.has_more || false, [data]) + // When a ZIM download drops off the polled downloads list it has completed (or been + // cancelled). The installed-files query (refetchOnWindowFocus is off) won't otherwise + // refresh, so a just-installed ZIM stays absent from `localFiles` and reappears in the + // accumulated remote pages as a ghost entry. Refresh it so the item is pruned. + const prevDownloadKeysRef = useRef>(new Set()) + useEffect(() => { + const currentKeys = new Set((downloads ?? []).map((d) => d.jobId)) + let anyRemoved = false + for (const key of prevDownloadKeysRef.current) { + if (!currentKeys.has(key)) { + anyRemoved = true + break + } + } + prevDownloadKeysRef.current = currentKeys + if (anyRemoved) { + queryClient.invalidateQueries({ queryKey: [ZIM_FILES_KEY] }) + } + }, [downloads, queryClient]) + const fetchOnBottomReached = useCallback( (parentRef?: HTMLDivElement | null) => { if (parentRef) {