diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 4c4c06dc..804c01d2 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -31,6 +31,7 @@ import PageShell from "./components/PageShell";
import GlassTuner from "./components/GlassTuner";
import ChatDock from "./components/ChatDock";
import Toaster from "./components/Toaster";
+import { StateMessage } from "./components/QueryState";
import ErrorDialog from "./components/ErrorDialog";
import VersionBanner from "./components/VersionBanner";
import DemoBanner from "./components/DemoBanner";
@@ -294,8 +295,10 @@ export default function App() {
// network/5xx failure, and no data means "not signed in" → the public landing.
if (meQuery.error)
return (
-
- {t("common.somethingWrong")}
+
+ meQuery.refetch()}>
+ {t("common.somethingWrong")}
+
);
if (!meQuery.data) return
;
diff --git a/frontend/src/components/Playlists.tsx b/frontend/src/components/Playlists.tsx
index 1483dc9e..2d7ee7f7 100644
--- a/frontend/src/components/Playlists.tsx
+++ b/frontend/src/components/Playlists.tsx
@@ -32,7 +32,7 @@ import {
Upload,
Youtube,
} from "lucide-react";
-import { api, type Video } from "../lib/api";
+import { api, HttpError, type Video } from "../lib/api";
import { notify } from "../lib/notifications";
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
import { playlistName } from "../lib/playlistName";
@@ -489,10 +489,14 @@ export default function Playlists() {
setSelectedId(remaining[0]?.id ?? null);
try {
await api.deletePlaylist(deletedId, onYoutube);
- } catch {
+ } catch (err) {
+ // Prefer the server's specific reason (e.g. a local vs YouTube-side failure) over a blanket
+ // "couldn't delete on YouTube" that misleads when the failure wasn't the YouTube side.
notify({
level: "warning",
- message: onYoutube ? t("playlists.deleteYtFailed") : t("playlists.deleteFailed"),
+ message:
+ (err instanceof HttpError && err.detail) ||
+ (onYoutube ? t("playlists.deleteYtFailed") : t("playlists.deleteFailed")),
});
}
qc.removeQueries({ queryKey: ["playlist", deletedId] });
diff --git a/frontend/src/lib/youtubeErrors.ts b/frontend/src/lib/youtubeErrors.ts
index 5d8440ee..22d07af5 100644
--- a/frontend/src/lib/youtubeErrors.ts
+++ b/frontend/src/lib/youtubeErrors.ts
@@ -11,7 +11,9 @@ export function notifyYouTubeActionError(
fallbackMessage: string,
onConnect?: () => void,
): void {
- if (err instanceof HttpError && err.status === 403) {
+ const httpErr = err instanceof HttpError ? err : null;
+ if (httpErr?.status === 403) {
+ // A 403 on a YouTube write action means the write scope isn't granted — offer to connect.
notify({
level: "error",
message: i18n.t("channels.notify.needYouTube"),
@@ -19,7 +21,9 @@ export function notifyYouTubeActionError(
? { label: i18n.t("channels.notify.connect"), onClick: onConnect }
: undefined,
});
- } else {
- notify({ level: "error", message: fallbackMessage });
+ return;
}
+ // Any other error: prefer the server's own reason (e.g. "Not enough quota left today") over the
+ // caller's generic fallback — the useful detail was being discarded (U-3.2.8).
+ notify({ level: "error", message: httpErr?.detail || fallbackMessage });
}