diff --git a/frontend/src/components/AuditLog.tsx b/frontend/src/components/AuditLog.tsx
index 106ccff1..c4feda96 100644
--- a/frontend/src/components/AuditLog.tsx
+++ b/frontend/src/components/AuditLog.tsx
@@ -54,7 +54,7 @@ export default function AuditLog() {
{t("audit.intro")}
- {q.isError ? (
+ {q.isError && !q.data ? (
q.refetch()}>
{t("common.loadError")}
diff --git a/frontend/src/components/ChannelDiscovery.tsx b/frontend/src/components/ChannelDiscovery.tsx
index 74c58945..124368bd 100644
--- a/frontend/src/components/ChannelDiscovery.tsx
+++ b/frontend/src/components/ChannelDiscovery.tsx
@@ -224,7 +224,7 @@ export default function ChannelDiscovery({
- {query.isError ? (
+ {query.isError && !query.data ? (
query.refetch()}>
{t("common.loadError")}
diff --git a/frontend/src/components/Channels.tsx b/frontend/src/components/Channels.tsx
index 439f8dd0..760d6c1b 100644
--- a/frontend/src/components/Channels.tsx
+++ b/frontend/src/components/Channels.tsx
@@ -710,7 +710,7 @@ export default function Channels() {
(status chips + pager) rides in the fixed band right above it (controlsInBand). The
card layout swaps the table for a virtualized grid over the same rows (its pager and
status chips ride in the fixed band above instead). */}
- {channelsQuery.isError ? (
+ {channelsQuery.isError && !channelsQuery.data ? (
channelsQuery.refetch()}>
{t("common.loadError")}
diff --git a/frontend/src/components/DataTable.tsx b/frontend/src/components/DataTable.tsx
index 9620dd4f..a1750074 100644
--- a/frontend/src/components/DataTable.tsx
+++ b/frontend/src/components/DataTable.tsx
@@ -4,7 +4,6 @@ import { sortRows, cycleSort, type SortState } from "../lib/columnSort";
import Pager, { hasPagerContent } from "./Pager";
import { PageToolbar } from "./PageShell";
import { ColumnHead, alignClass, filterRows, type Column, type FilterMap } from "./tableHeader";
-import { StateMessage } from "./QueryState";
// A reusable, client-side data table: per-column sort + filter (in the header) + pagination,
// with its sort/filter/page state optionally persisted to localStorage so a reload (F5) keeps
@@ -45,9 +44,6 @@ export default function DataTable({
pageSizeOptions = [10, 25, 50, 100],
persistKey,
emptyText,
- loading = false,
- error = false,
- onRetry,
rowClassName,
controlsPosition = "bottom",
controlsLeading,
@@ -65,12 +61,6 @@ export default function DataTable({
pageSizeOptions?: number[];
persistKey?: string;
emptyText?: string;
- // Honest states (R3): when the caller's query is loading or errored, pass these so the table
- // shows a loading / error(+retry) message instead of an "empty" list that lies about a failed
- // fetch. `error` suppresses the empty-text; `onRetry` wires the retry button.
- loading?: boolean;
- error?: boolean;
- onRetry?: () => void;
rowClassName?: (row: T) => string;
controlsPosition?: "top" | "bottom" | "both";
controlsLeading?: ReactNode;
@@ -256,17 +246,11 @@ export default function DataTable({
))}
- {error ? (
-
- {t("common.loadError")}
-
- ) : loading && paged.length === 0 ? (
- {t("common.loading")}
- ) : paged.length === 0 ? (
+ {paged.length === 0 && (
{emptyText ?? t("datatable.empty")}
- ) : null}
+ )}
{controlsPosition === "bottom" && controls}
{controlsPosition === "both" && bottomControls}
diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx
index 695e9686..22c02484 100644
--- a/frontend/src/components/Feed.tsx
+++ b/frontend/src/components/Feed.tsx
@@ -494,7 +494,7 @@ export default function Feed({
}
if (query.isLoading) return {t("feed.loading")}
;
- if (query.isError)
+ if (query.isError && !query.data)
return (
query.refetch()}>
{t("feed.loadError")}
diff --git a/frontend/src/components/Messages.tsx b/frontend/src/components/Messages.tsx
index 44d7b4b2..b3ab6ff8 100644
--- a/frontend/src/components/Messages.tsx
+++ b/frontend/src/components/Messages.tsx
@@ -225,7 +225,7 @@ function Directory({ onPick, onBack }: { onPick: (u: MessageUser) => void; onBac
placeholder={t("messages.searchPeople")}
className="w-full bg-card border border-border rounded-xl px-3 py-2 text-sm outline-none focus:border-accent"
/>
- {q.isError ? (
+ {q.isError && !q.data ? (
q.refetch()}>
{t("common.loadError")}
diff --git a/frontend/src/components/PlexBrowse.tsx b/frontend/src/components/PlexBrowse.tsx
index 5c7c272e..ecd4b32c 100644
--- a/frontend/src/components/PlexBrowse.tsx
+++ b/frontend/src/components/PlexBrowse.tsx
@@ -358,7 +358,7 @@ export default function PlexBrowse() {
)}
- {browseQ.isError ? (
+ {browseQ.isError && !browseQ.data ? (
browseQ.refetch()}>
{t("common.loadError")}
diff --git a/frontend/src/components/PlexPlaylistView.tsx b/frontend/src/components/PlexPlaylistView.tsx
index e02ba77b..0e847bd4 100644
--- a/frontend/src/components/PlexPlaylistView.tsx
+++ b/frontend/src/components/PlexPlaylistView.tsx
@@ -577,7 +577,7 @@ export default function PlexPlaylistView({
- {q.isError ? (
+ {q.isError && !q.data ? (
q.refetch()}>
{t("common.loadError")}
diff --git a/frontend/src/components/QueryState.tsx b/frontend/src/components/QueryState.tsx
index 4278bb23..57f42199 100644
--- a/frontend/src/components/QueryState.tsx
+++ b/frontend/src/components/QueryState.tsx
@@ -22,8 +22,9 @@ export function StateMessage({
return (
{tone === "error" && }
@@ -42,7 +43,8 @@ export function StateMessage({
);
}
-// The honest-state convention every page now follows: check `isError` BEFORE the empty branch so a
-// failed fetch renders this message (with Retry) instead of the page's "empty" text — the R3 bug
-// where a backend restart read as "No channels". `StateMessage` is the shared building block;
-// pages wire it into their existing loading/empty ternary (and DataTable takes loading/error props).
+// The honest-state convention every page now follows: check `isError && ` BEFORE the empty
+// branch, so a failed fetch renders this message (with Retry) instead of the page's "empty" text
+// — the R3 bug where a backend restart read as "No channels". The `&& ` guard matters: a
+// transient refetch error while data is already on screen keeps the data rather than blanking it to
+// an error. `StateMessage` is the shared building block pages wire into their loading/empty ternary.
diff --git a/frontend/src/i18n/locales/en/common.json b/frontend/src/i18n/locales/en/common.json
index 178b878b..deed2f72 100644
--- a/frontend/src/i18n/locales/en/common.json
+++ b/frontend/src/i18n/locales/en/common.json
@@ -19,6 +19,5 @@
"demoSharedNotice": "You're in the shared demo account. Everything you do here — playlists, hidden videos, settings — is communal and may be changed by other demo visitors at the same time.",
"backToTop": "Back to top",
"retry": "Retry",
- "loadError": "Couldn't load this — check your connection and try again.",
- "empty": "Nothing here yet."
+ "loadError": "Couldn't load this — check your connection and try again."
}
diff --git a/frontend/src/i18n/locales/hu/common.json b/frontend/src/i18n/locales/hu/common.json
index bb2c83a6..a3386834 100644
--- a/frontend/src/i18n/locales/hu/common.json
+++ b/frontend/src/i18n/locales/hu/common.json
@@ -19,6 +19,5 @@
"demoSharedNotice": "Egy közös demo fiókban vagy. Minden, amit itt csinálsz — lejátszási listák, elrejtett videók, beállítások — közös, és más demo látogatók egyszerre módosíthatják.",
"backToTop": "Vissza a tetejére",
"retry": "Újra",
- "loadError": "Nem sikerült betölteni — ellenőrizd a kapcsolatot, és próbáld újra.",
- "empty": "Itt még nincs semmi."
+ "loadError": "Nem sikerült betölteni — ellenőrizd a kapcsolatot, és próbáld újra."
}