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 }); }