From 1c6b7a9a5aa442036ddcdd62277b01dcae091687 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Wed, 22 Jul 2026 04:43:02 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20R3=20S3=20=E2=80=94=20surface=20swa?= =?UTF-8?q?llowed=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - notifyYouTubeActionError: on any non-403 error, show the server's own reason (err.detail, e.g. "Not enough quota left today") instead of the caller's generic fallback, which was discarded (U-3.2.8). 403 still offers the Connect affordance. - App boot error now offers a Retry (StateMessage) instead of a dead "Something went wrong" (U-4.2). - deletePlaylist prefers the server's reason over a blanket "couldn't delete on YouTube" that misleads when the failure wasn't the YouTube side. - Already covered in earlier sprints (verified): the live-search "Load more" already surfaces the quota error inline via err.detail, and ChannelPage subscribe/unsubscribe already route through notifyYouTubeActionError. --- frontend/src/App.tsx | 7 +++++-- frontend/src/components/Playlists.tsx | 10 +++++++--- frontend/src/lib/youtubeErrors.ts | 10 +++++++--- 3 files changed, 19 insertions(+), 8 deletions(-) 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 }); }