From 7e5c083ceff02a940d065b1c88b291f947a24072 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Tue, 14 Jul 2026 13:25:31 +0200 Subject: [PATCH] fix(web): surface a toast on failed mutations (#14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several client mutations either ignored a non-ok response (silent no-op) or toasted success unconditionally even when the request failed. Sweep them so every mutation gives feedback: - artists: toggle auto-monitor, unfollow → error toast on failure. - discography + wanted: toggle monitor / search-now → success or error toast. - discover suggestion follow/want/dismiss → error toast (was toasting success even on failure). - The Floor: queue request + retry → error toast on failure. - artist-modal + preview follow/want, Last.fm want → error toast on failure (closes the deferred "want() shows no toast on non-ok" item). web 153 tests, tsc + build clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/src/app/_ui/artist-modal.tsx | 4 ++++ .../app/artists/[id]/discography-client.tsx | 9 ++++++--- web/src/app/artists/artists-client.tsx | 8 +++++--- .../discover/artist/[mbid]/preview-client.tsx | 4 ++++ web/src/app/discover/discover-client.tsx | 8 ++++++-- web/src/app/lastfm/lastfm-client.tsx | 2 ++ web/src/app/queue.tsx | 18 ++++++++++++------ web/src/app/wanted/wanted-client.tsx | 4 ++-- 8 files changed, 41 insertions(+), 16 deletions(-) diff --git a/web/src/app/_ui/artist-modal.tsx b/web/src/app/_ui/artist-modal.tsx index c61ffde..38ebc86 100644 --- a/web/src/app/_ui/artist-modal.tsx +++ b/web/src/app/_ui/artist-modal.tsx @@ -102,6 +102,8 @@ export function ArtistModal({ if (res.ok || res.status === 409) { setFollowed(true); toast(`Following ${name}`); + } else { + toast(`Couldn't follow ${name}`); } } @@ -122,6 +124,8 @@ export function ArtistModal({ if (res.ok) { setWanted((s) => new Set(s).add(r.rgMbid)); toast(`Added ${r.album}`); + } else { + toast(`Couldn't add ${r.album}`); } } diff --git a/web/src/app/artists/[id]/discography-client.tsx b/web/src/app/artists/[id]/discography-client.tsx index 8c0617c..ce2959b 100644 --- a/web/src/app/artists/[id]/discography-client.tsx +++ b/web/src/app/artists/[id]/discography-client.tsx @@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react"; import { PageHead } from "../../_ui/page-head"; import { CoverArt } from "../../_ui/cover-art"; import { AlbumModal } from "../../_ui/album-modal"; +import { toast } from "../../_ui/toast"; import { categoryOf, TAB_ORDER, type Category } from "../../_ui/categories"; type Release = { @@ -39,15 +40,17 @@ export function DiscographyClient({ id }: { id: string }) { }, [id]); async function toggleMonitor(r: Release) { - await fetch(`/api/releases/${r.id}`, { + const res = await fetch(`/api/releases/${r.id}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ monitored: !r.monitored }), - }); + }).catch(() => null); + if (!res?.ok) toast(`Couldn't update ${r.album}`); refresh(); } async function searchNow(r: Release) { - await fetch(`/api/releases/${r.id}/search`, { method: "POST" }); + const res = await fetch(`/api/releases/${r.id}/search`, { method: "POST" }).catch(() => null); + toast(res?.ok ? `Searching for ${r.album}` : `Couldn't search for ${r.album}`); refresh(); } diff --git a/web/src/app/artists/artists-client.tsx b/web/src/app/artists/artists-client.tsx index e4b187e..3d1602f 100644 --- a/web/src/app/artists/artists-client.tsx +++ b/web/src/app/artists/artists-client.tsx @@ -95,16 +95,18 @@ export function ArtistsClient() { } async function toggleAuto(a: Artist) { - await fetch(`/api/artists/${a.id}`, { + const res = await fetch(`/api/artists/${a.id}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ autoMonitorFuture: !a.autoMonitorFuture }), - }); + }).catch(() => null); + if (!res?.ok) toast(`Couldn't update ${a.name}`); refresh(); } async function unfollow(a: Artist) { - await fetch(`/api/artists/${a.id}`, { method: "DELETE" }); + const res = await fetch(`/api/artists/${a.id}`, { method: "DELETE" }).catch(() => null); + if (!res?.ok) toast(`Couldn't unfollow ${a.name}`); refresh(); } diff --git a/web/src/app/discover/artist/[mbid]/preview-client.tsx b/web/src/app/discover/artist/[mbid]/preview-client.tsx index 8bc9073..951e7ac 100644 --- a/web/src/app/discover/artist/[mbid]/preview-client.tsx +++ b/web/src/app/discover/artist/[mbid]/preview-client.tsx @@ -55,6 +55,8 @@ export function PreviewClient({ mbid, initialName }: { mbid: string; initialName if (res.ok || res.status === 409) { setFollowed(true); toast(`Following ${name}`); + } else { + toast(`Couldn't follow ${name}`); } } @@ -75,6 +77,8 @@ export function PreviewClient({ mbid, initialName }: { mbid: string; initialName if (res.ok) { setWanted((s) => new Set(s).add(r.rgMbid)); toast(`Added ${r.album}`); + } else { + toast(`Couldn't add ${r.album}`); } } diff --git a/web/src/app/discover/discover-client.tsx b/web/src/app/discover/discover-client.tsx index 4698963..cc7886b 100644 --- a/web/src/app/discover/discover-client.tsx +++ b/web/src/app/discover/discover-client.tsx @@ -103,11 +103,15 @@ export function DiscoverClient() { } async function act(id: string, action: "follow" | "want" | "dismiss") { - await fetch(`/api/discover/${id}`, { + const res = await fetch(`/api/discover/${id}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action }), - }); + }).catch(() => null); + if (!res?.ok) { + toast(`Couldn't ${action}`); + return; + } toast(action === "follow" ? "Following" : action === "want" ? "Added to wanted" : "Dismissed"); loadFeed(); } diff --git a/web/src/app/lastfm/lastfm-client.tsx b/web/src/app/lastfm/lastfm-client.tsx index ebb166f..6f035da 100644 --- a/web/src/app/lastfm/lastfm-client.tsx +++ b/web/src/app/lastfm/lastfm-client.tsx @@ -174,6 +174,8 @@ export function LastfmClient() { toast(`Wanted ${match.title}`); setWantedSet((s) => new Set(s).add(match.rgMbid)); setAlbumTarget(null); + } else { + toast(`Couldn't add ${match.title}`); } } diff --git a/web/src/app/queue.tsx b/web/src/app/queue.tsx index 0499a9f..6fc0e2a 100644 --- a/web/src/app/queue.tsx +++ b/web/src/app/queue.tsx @@ -76,22 +76,28 @@ export function Queue() { }, []); async function retry(id: string) { - const res = await fetch(`/api/requests/${id}/retry`, { method: "POST" }); - if (res.ok) { - toast("Retrying…"); - refresh(); + const res = await fetch(`/api/requests/${id}/retry`, { method: "POST" }).catch(() => null); + if (!res?.ok) { + toast("Couldn't retry"); + return; } + toast("Retrying…"); + refresh(); } async function submit(e: React.FormEvent) { e.preventDefault(); if (!artist.trim() || !album.trim()) return; const label = album.trim(); - await fetch("/api/requests", { + const res = await fetch("/api/requests", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ artist, album }), - }); + }).catch(() => null); + if (!res?.ok) { + toast(`Couldn't queue ${label}`); + return; + } toast(`Queued ${label}`); setArtist(""); setAlbum(""); diff --git a/web/src/app/wanted/wanted-client.tsx b/web/src/app/wanted/wanted-client.tsx index 18698ea..a735ad9 100644 --- a/web/src/app/wanted/wanted-client.tsx +++ b/web/src/app/wanted/wanted-client.tsx @@ -60,8 +60,8 @@ export function WantedClient() { } async function searchNow(w: Wanted) { - await fetch(`/api/releases/${w.id}/search`, { method: "POST" }); - toast(`Searching for ${w.album}`); + const res = await fetch(`/api/releases/${w.id}/search`, { method: "POST" }).catch(() => null); + toast(res?.ok ? `Searching for ${w.album}` : `Couldn't search for ${w.album}`); refresh(); }