diff --git a/webui/src/routes/import/-route.test.tsx b/webui/src/routes/import/-route.test.tsx
index 35566c14..679341c5 100644
--- a/webui/src/routes/import/-route.test.tsx
+++ b/webui/src/routes/import/-route.test.tsx
@@ -9,6 +9,7 @@ import { createShellBridge } from '@/test/shell-bridge';
import type { ImportStagingFile } from './-import.types';
+import { autoImportResultsQueryOptions, autoImportStatusQueryOptions } from './-import.api';
import { resetImportWorkflowStore } from './-import.store';
function renderImportRoute(initialEntries = ['/import']) {
@@ -19,6 +20,7 @@ function renderImportRoute(initialEntries = ['/import']) {
return {
history,
router,
+ queryClient,
...render(),
};
}
@@ -209,6 +211,26 @@ describe('import route', () => {
expect(window.SoulSyncWebShellBridge?.setActivePageChrome).toHaveBeenCalledWith('import');
});
+ it('keeps the import page rendering when staging files fail to load', async () => {
+ server.use(
+ http.get('/api/import/staging/files', () =>
+ HttpResponse.json(
+ {
+ success: false,
+ error: 'Import folder unavailable',
+ },
+ { status: 500 },
+ ),
+ ),
+ );
+
+ renderImportRoute();
+
+ expect(await screen.findByTestId('import-page')).toBeInTheDocument();
+ expect(await screen.findByText('Import Music')).toBeInTheDocument();
+ expect(await screen.findByText('Import folder: error')).toBeInTheDocument();
+ });
+
it('stores the active tab in nested route paths', async () => {
const { history } = renderImportRoute();
@@ -318,4 +340,56 @@ describe('import route', () => {
false,
);
});
+
+ it('keeps cached auto-import status visible when a refetch fails', async () => {
+ const { queryClient } = renderImportRoute(['/import/auto']);
+
+ expect(await screen.findByText('Watching')).toBeInTheDocument();
+
+ server.use(
+ http.get('/api/auto-import/status', () =>
+ HttpResponse.json(
+ {
+ success: false,
+ error: 'Auto-import unavailable',
+ },
+ { status: 500 },
+ ),
+ ),
+ );
+
+ await queryClient.refetchQueries({
+ queryKey: autoImportStatusQueryOptions().queryKey,
+ });
+
+ expect(screen.getByText('Watching')).toBeInTheDocument();
+ expect(screen.queryByText(/Auto-import is unavailable:/)).not.toBeInTheDocument();
+ });
+
+ it('keeps cached auto-import results visible when a refetch fails', async () => {
+ const { queryClient } = renderImportRoute(['/import/auto?autoFilter=pending']);
+
+ expect(await screen.findByRole('button', { name: /^Needs Review\s*1$/ })).toBeInTheDocument();
+ expect(screen.getAllByText('Album A').length).toBeGreaterThan(0);
+
+ server.use(
+ http.get('/api/auto-import/results', () =>
+ HttpResponse.json(
+ {
+ success: false,
+ error: 'Auto-import results unavailable',
+ },
+ { status: 500 },
+ ),
+ ),
+ );
+
+ await queryClient.refetchQueries({
+ queryKey: autoImportResultsQueryOptions().queryKey,
+ });
+
+ expect(screen.getByRole('button', { name: /^Needs Review\s*1$/ })).toBeInTheDocument();
+ expect(screen.getAllByText('Album A').length).toBeGreaterThan(0);
+ expect(screen.queryByText(/Failed to load imports:/)).not.toBeInTheDocument();
+ });
});
diff --git a/webui/src/routes/import/-ui/auto-import-tab.tsx b/webui/src/routes/import/-ui/auto-import-tab.tsx
index 761a08df..245861e3 100644
--- a/webui/src/routes/import/-ui/auto-import-tab.tsx
+++ b/webui/src/routes/import/-ui/auto-import-tab.tsx
@@ -10,7 +10,7 @@ import {
Select,
Switch,
} from '@/components/form/form';
-import { Badge } from '@/components/primitives';
+import { Badge, Notice } from '@/components/primitives';
import type {
ImportAutoFilter,
@@ -151,17 +151,18 @@ export function AutoImportPanel({
const counts = getAutoImportCounts(allResults);
const activeLines = getActiveImportLines(statusQuery.data);
const statusTone = getAutoImportStatusTone(statusQuery.data);
-
- if (statusQuery.error) {
- return (
-
- Auto-import is unavailable: {getErrorMessage(statusQuery.error)}
-
- );
- }
+ const statusError =
+ statusQuery.error && !statusQuery.data ? getErrorMessage(statusQuery.error) : '';
+ const resultsError =
+ resultsQuery.error && !resultsQuery.data ? getErrorMessage(resultsQuery.error) : '';
return (
<>
+ {statusError ? (
+
+ Auto-import is unavailable: {statusError}
+
+ ) : null}
@@ -293,10 +294,10 @@ export function AutoImportPanel({
) : null}
- {resultsQuery.error ? (
-
- Failed to load imports: {getErrorMessage(resultsQuery.error)}
-
+ {resultsError ? (
+
+ Failed to load imports: {resultsError}
+
) : allResults.length === 0 ? (
No imports yet. Drop album folders or single tracks into your import folder.
diff --git a/webui/src/routes/import/-ui/import-shared.tsx b/webui/src/routes/import/-ui/import-shared.tsx
index dfa5bdda..61b3b93e 100644
--- a/webui/src/routes/import/-ui/import-shared.tsx
+++ b/webui/src/routes/import/-ui/import-shared.tsx
@@ -1,6 +1,6 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
-import type { ImportQueueJob } from '../-import.types';
+import type { ImportQueueJob, ImportStagingFile } from '../-import.types';
import {
importStagingFilesQueryOptions,
@@ -11,6 +11,8 @@ import {
import { getTrackDisplayInfo, IMPORT_PLACEHOLDER_IMAGE } from '../-import.helpers';
import { useImportQueueWorkflow, useImportWorkflowStore } from '../-import.store';
+const EMPTY_STAGING_FILES: ImportStagingFile[] = [];
+
export function useImportStaging() {
const queryClient = useQueryClient();
const clearFinishedJobs = useImportWorkflowStore((state) => state.clearFinishedJobs);
@@ -23,7 +25,8 @@ export function useImportStaging() {
clearFinishedJobs();
await invalidateImportStagingQueries(queryClient);
},
- stagingFiles: stagingQuery.data?.files ?? [],
+ // Keep the empty fallback stable so staging-driven effects do not loop while loading.
+ stagingFiles: stagingQuery.data?.files ?? EMPTY_STAGING_FILES,
stagingPath: stagingQuery.data?.staging_path || 'Not configured',
stagingQuery,
};
diff --git a/webui/src/routes/import/route.tsx b/webui/src/routes/import/route.tsx
index 5cb5e60d..e91f9658 100644
--- a/webui/src/routes/import/route.tsx
+++ b/webui/src/routes/import/route.tsx
@@ -13,8 +13,10 @@ export const Route = createFileRoute('/import')({
throw redirect({ href: getProfileHomePath(bridge), replace: true });
}
},
- loader: async ({ context }) => {
- await context.queryClient.ensureQueryData(importStagingFilesQueryOptions());
+ loader: ({ context }) => {
+ // Warm the staging query if possible, but never block the route on a transient fetch
+ // failure. The page owns the in-place error state for that case.
+ void context.queryClient.prefetchQuery(importStagingFilesQueryOptions());
},
component: ImportPage,
});