diff --git a/apps/web/src/components/LoadError.tsx b/apps/web/src/components/LoadError.tsx
new file mode 100644
index 0000000..954c475
--- /dev/null
+++ b/apps/web/src/components/LoadError.tsx
@@ -0,0 +1,22 @@
+import { friendlyError } from "../format.ts";
+
+/**
+ * A failed load. With cached data on screen (`stale`), a small inline banner sits above it so the user
+ * keeps what they had; with nothing cached, it is the whole state.
+ */
+export function LoadError({ error, retry, stale, className }: { error: unknown; retry: () => void; stale?: boolean; className?: string }) {
+ if (stale) {
+ return (
+
+
Can't refresh right now. {friendlyError(error)}
+
+
+ );
+ }
+ return (
+
+
{friendlyError(error)}
+
+
+ );
+}
diff --git a/apps/web/src/routes/library.tsx b/apps/web/src/routes/library.tsx
index 120e8ea..05b6cc2 100644
--- a/apps/web/src/routes/library.tsx
+++ b/apps/web/src/routes/library.tsx
@@ -2,6 +2,7 @@ import { useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-q
import { useRef, useState, type FormEvent } from "react";
import { Link, useLocation } from "wouter";
import { api, type EntryView } from "../api.ts";
+import { LoadError } from "../components/LoadError.tsx";
import { Cover } from "../components/Cover.tsx";
import { EntryDetail } from "../components/EntryDetail.tsx";
import { Icon } from "../components/Icon.tsx";
@@ -165,12 +166,12 @@ function ListPane({ selectedId }: { selectedId: string | null }) {
))}
- ) : entries.error ? (
+ ) : entries.error && !entries.data ? (