diff --git a/apps/client/src/common/hooks-query/useCustomFields.ts b/apps/client/src/common/hooks-query/useCustomFields.ts
index 15dde9194..d0a5fe028 100644
--- a/apps/client/src/common/hooks-query/useCustomFields.ts
+++ b/apps/client/src/common/hooks-query/useCustomFields.ts
@@ -4,6 +4,7 @@ import { CustomFields } from 'ontime-types';
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { CUSTOM_FIELDS } from '../api/constants';
import { getCustomFields } from '../api/customFields';
+import { deriveQueryStatus } from '../utils/queryUtils';
const placeholder: CustomFields = {};
@@ -15,5 +16,5 @@ export default function useCustomFields() {
refetchInterval: queryRefetchIntervalSlow,
});
- return { data: data ?? placeholder, status, isFetching, isError, refetch };
+ return { data: data ?? placeholder, status: deriveQueryStatus(status, data), isFetching, isError, refetch };
}
diff --git a/apps/client/src/common/hooks-query/useProjectData.ts b/apps/client/src/common/hooks-query/useProjectData.ts
index f806f1a17..03a022e6f 100644
--- a/apps/client/src/common/hooks-query/useProjectData.ts
+++ b/apps/client/src/common/hooks-query/useProjectData.ts
@@ -4,6 +4,7 @@ import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { PROJECT_DATA } from '../api/constants';
import { getProjectData, postProjectData } from '../api/project';
import { projectDataPlaceholder } from '../models/ProjectData';
+import { deriveQueryStatus } from '../utils/queryUtils';
export default function useProjectData() {
const { data, status, isFetching, isError, refetch } = useQuery({
@@ -13,7 +14,7 @@ export default function useProjectData() {
refetchInterval: queryRefetchIntervalSlow,
});
- return { data: data ?? projectDataPlaceholder, status, isFetching, isError, refetch };
+ return { data: data ?? projectDataPlaceholder, status: deriveQueryStatus(status, data), isFetching, isError, refetch };
}
export function useUpdateProjectData() {
diff --git a/apps/client/src/common/hooks-query/useRundown.ts b/apps/client/src/common/hooks-query/useRundown.ts
index 4912f688b..4a5cd599b 100644
--- a/apps/client/src/common/hooks-query/useRundown.ts
+++ b/apps/client/src/common/hooks-query/useRundown.ts
@@ -6,6 +6,7 @@ import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { CURRENT_RUNDOWN_QUERY_KEY, getRundownQueryKey } from '../api/constants';
import { fetchCurrentRundown, fetchRundown } from '../api/rundown';
import { useSelectedEventId } from '../hooks/useSocket';
+import { deriveQueryStatus } from '../utils/queryUtils';
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
import { useProjectRundowns } from './useProjectRundowns';
@@ -50,7 +51,7 @@ export default function useRundown() {
queryClient.removeQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true });
}, [loadedRundownId, queryClient]);
- return { data: data ?? cachedRundownPlaceholder, status, isError, refetch, isFetching };
+ return { data: data ?? cachedRundownPlaceholder, status: deriveQueryStatus(status, data), isError, refetch, isFetching };
}
export function useRundownWithMetadata() {
@@ -135,5 +136,5 @@ export function useRundownById(rundownId: string | null | undefined) {
refetchInterval: queryRefetchIntervalSlow,
});
- return { data: data ?? cachedRundownPlaceholder, status, isError, refetch, isFetching };
+ return { data: data ?? cachedRundownPlaceholder, status: deriveQueryStatus(status, data), isError, refetch, isFetching };
}
diff --git a/apps/client/src/common/hooks-query/useSettings.ts b/apps/client/src/common/hooks-query/useSettings.ts
index e14676424..d6103c50d 100644
--- a/apps/client/src/common/hooks-query/useSettings.ts
+++ b/apps/client/src/common/hooks-query/useSettings.ts
@@ -4,6 +4,7 @@ import { unobfuscate } from 'ontime-utils';
import { APP_SETTINGS } from '../api/constants';
import { getSettings } from '../api/settings';
import { ontimePlaceholderSettings } from '../models/OntimeSettings';
+import { deriveQueryStatus } from '../utils/queryUtils';
export default function useSettings() {
const { data, status, isFetching, isError, refetch } = useQuery({
@@ -22,5 +23,5 @@ export default function useSettings() {
},
});
- return { data: data ?? ontimePlaceholderSettings, status, isFetching, isError, refetch };
+ return { data: data ?? ontimePlaceholderSettings, status: deriveQueryStatus(status, data), isFetching, isError, refetch };
}
diff --git a/apps/client/src/common/hooks-query/useViewSettings.ts b/apps/client/src/common/hooks-query/useViewSettings.ts
index dc3a3b621..05532674e 100644
--- a/apps/client/src/common/hooks-query/useViewSettings.ts
+++ b/apps/client/src/common/hooks-query/useViewSettings.ts
@@ -5,6 +5,7 @@ import { getViewSettings, postViewSettings } from '../../common/api/viewSettings
import { ontimeQueryClient } from '../../common/queryClient';
import { VIEW_SETTINGS } from '../api/constants';
import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
+import { deriveQueryStatus } from '../utils/queryUtils';
export default function useViewSettings() {
const { data, status } = useQuery({
@@ -24,5 +25,5 @@ export default function useViewSettings() {
},
});
- return { data: data ?? viewsSettingsPlaceholder, status, mutateAsync };
+ return { data: data ?? viewsSettingsPlaceholder, status: deriveQueryStatus(status, data), mutateAsync };
}
diff --git a/apps/client/src/common/utils/__tests__/queryUtils.test.ts b/apps/client/src/common/utils/__tests__/queryUtils.test.ts
new file mode 100644
index 000000000..65018ec8c
--- /dev/null
+++ b/apps/client/src/common/utils/__tests__/queryUtils.test.ts
@@ -0,0 +1,16 @@
+import { deriveQueryStatus } from '../queryUtils';
+
+test('keeps pending and success statuses unchanged', () => {
+ expect(deriveQueryStatus('pending', undefined)).toBe('pending');
+ expect(deriveQueryStatus('success', { some: 'data' })).toBe('success');
+});
+
+test('keeps error status when there is no data', () => {
+ expect(deriveQueryStatus('error', undefined)).toBe('error');
+});
+
+test('downgrades error to success when data is still available', () => {
+ expect(deriveQueryStatus('error', { some: 'data' })).toBe('success');
+ expect(deriveQueryStatus('error', [])).toBe('success');
+ expect(deriveQueryStatus('error', 0)).toBe('success');
+});
diff --git a/apps/client/src/common/utils/queryUtils.ts b/apps/client/src/common/utils/queryUtils.ts
new file mode 100644
index 000000000..a4ee7e7fb
--- /dev/null
+++ b/apps/client/src/common/utils/queryUtils.ts
@@ -0,0 +1,13 @@
+import type { QueryStatus } from '@tanstack/react-query';
+
+/**
+ * A background refetch failure still leaves the last successfully fetched data in place.
+ * In that case we want callers to keep treating the query as usable rather than erroring out,
+ * so we only report 'error' when we have never received data for this query.
+ */
+export function deriveQueryStatus(status: QueryStatus, data: unknown): QueryStatus {
+ if (status === 'error' && data !== undefined) {
+ return 'success';
+ }
+ return status;
+}
diff --git a/apps/client/src/features/operator/Operator.tsx b/apps/client/src/features/operator/Operator.tsx
index e389cc554..f1b6ffc41 100644
--- a/apps/client/src/features/operator/Operator.tsx
+++ b/apps/client/src/features/operator/Operator.tsx
@@ -36,8 +36,7 @@ export default function OperatorLoader() {
return ;
}
- // only show the error state if we have never received data to fall back on
- if (status === 'error' && data.rundown.revision === -1) {
+ if (status === 'error') {
return ;
}