fix(ui): a write that fails says what failed and why, wherever it fails

`req` opens its blocking dialog for 400/409/422/5xx only. A 403/404/429 was left to the caller and a
502-504 or lost connection got just the self-clearing connection notice, so 34 mutations without a
reporting onError and 15 direct api writes with a bare or missing catch failed in silence: a hidden
channel that snapped back, a share link that never revoked, a confirmed account delete that ended
with nothing said.

- lib/queryClient: one MutationCache reporter. react-query awaits it BEFORE the mutation's own
  onError, so it waits a task and skips a failure the site already reported; the toast title comes
  from `meta.failTitle`, `meta.errorShownInline` opts out.
- notifyActionError: title names WHAT failed, the line under it says WHY (`failureReason`: a
  structured localized detail, else the status). It marks the error reported (`markReported`), so
  nothing reports one failure twice; `notifyKnownFailure` for a site's own specific sentence.
- Every silent site named (45 new HU/EN titles, 4 reasons). Stale "the global dialog shows it"
  comments corrected on the job actions, channel block, Plex sync, cookies, download enqueue,
  account delete and usePref.
- Channels passed the tag hook's channel id to the reporter as the error; the tag and priority hooks
  now report themselves, with the real error.
- A contract test keeps every useMutation naming its failure; CLAUDE.md gains the pattern row.
- history.dom.test: the "navigates only once the entries are back" assertion raced an async act's
  macrotask flush (1 in 5 runs); it now asserts from a later popstate listener.
This commit is contained in:
2026-09-13 01:45:28 +02:00
parent b4aea7fa5b
commit 8a50538275
45 changed files with 851 additions and 132 deletions
+52
View File
@@ -20,6 +20,58 @@
"recipient": "There's no user with that email address.",
"job": "This job doesn't exist any more."
},
"reason": {
"unreachable": "The server couldn't be reached, so the change didn't go through. Try again.",
"forbidden": "You don't have permission to do that.",
"rateLimited": "Too many requests at once — wait a moment and try again.",
"unexpected": "Something went wrong in the app. Try again."
},
"writeFailed": {
"hideChannel": "Couldn't change whether the channel is hidden",
"blockChannel": "Couldn't change the channel's block",
"unblockChannel": "Couldn't unblock the channel",
"channelSettings": "Couldn't save the channel's settings",
"channelPriority": "Couldn't save the channel's priority",
"exploreChannel": "Couldn't load the channel's videos",
"downloadQuota": "Couldn't save the download quota",
"downloadQuotaReset": "Couldn't reset the download quota",
"job": {
"pause": "Couldn't pause the download",
"resume": "Couldn't resume the download",
"cancel": "Couldn't cancel the download",
"delete": "Couldn't delete the download",
"redownload": "Couldn't start the download again"
},
"removeShared": "Couldn't remove the shared item",
"downloadDetails": "Couldn't save the download's details",
"enqueueDownload": "Couldn't queue the download",
"downloadProfileSave": "Couldn't save the download profile",
"downloadProfileDelete": "Couldn't delete the download profile",
"downloadCookiesRemove": "Couldn't remove those cookies",
"notifRead": "Couldn't mark the notification as read",
"notifReadAll": "Couldn't mark the notifications as read",
"notifDismiss": "Couldn't dismiss the notification",
"notifClear": "Couldn't clear the notifications",
"createPlaylist": "Couldn't create the playlist",
"schedulerRun": "Couldn't start the job",
"schedulerRunAll": "Couldn't start the jobs",
"maintenanceBatch": "Couldn't save the maintenance batch size",
"purgeDiscovery": "Couldn't clear the discovery data",
"plexWatchLink": "Couldn't change the Plex watch-state sync",
"plexWatchImport": "Couldn't import the watch state from Plex",
"shareLinkDownload": "Couldn't change whether the link allows downloading",
"shareLinkRevoke": "Couldn't revoke the link",
"shareLinkPassword": "Couldn't change the link's password",
"syncPause": "Couldn't pause syncing",
"syncResume": "Couldn't resume syncing",
"prefs": "Couldn't save your settings",
"deleteAccount": "Couldn't delete your account",
"videoState": "Couldn't change the video's state",
"videoReset": "Couldn't reset the video",
"watchLaterAdd": "Couldn't add it to Watch later",
"watchLaterRemove": "Couldn't remove it from Watch later",
"clearSearch": "Couldn't clear the search results"
},
"ok": "OK",
"offline": {
"title": "Connection lost",
+52
View File
@@ -20,6 +20,58 @@
"recipient": "Nincs felhasználó ezzel az e-mail címmel.",
"job": "Ez a feladat már nem létezik."
},
"reason": {
"unreachable": "A művelet nem ment végbe: a szerver nem volt elérhető. Próbáld újra.",
"forbidden": "Ehhez nincs jogosultságod.",
"rateLimited": "Túl sok kérés egyszerre — várj egy kicsit, és próbáld újra.",
"unexpected": "Váratlan hiba történt az alkalmazásban. Próbáld újra."
},
"writeFailed": {
"hideChannel": "Nem sikerült módosítani a csatorna elrejtését",
"blockChannel": "Nem sikerült módosítani a csatorna tiltását",
"unblockChannel": "Nem sikerült feloldani a csatorna tiltását",
"channelSettings": "Nem sikerült menteni a csatorna beállításait",
"channelPriority": "Nem sikerült menteni a csatorna prioritását",
"exploreChannel": "Nem sikerült betölteni a csatorna videóit",
"downloadQuota": "Nem sikerült menteni a letöltési kvótát",
"downloadQuotaReset": "Nem sikerült visszaállítani a letöltési kvótát",
"job": {
"pause": "Nem sikerült szüneteltetni a letöltést",
"resume": "Nem sikerült folytatni a letöltést",
"cancel": "Nem sikerült megszakítani a letöltést",
"delete": "Nem sikerült törölni a letöltést",
"redownload": "Nem sikerült újraindítani a letöltést"
},
"removeShared": "Nem sikerült eltávolítani a megosztott elemet",
"downloadDetails": "Nem sikerült menteni a letöltés adatait",
"enqueueDownload": "Nem sikerült sorba állítani a letöltést",
"downloadProfileSave": "Nem sikerült menteni a letöltési profilt",
"downloadProfileDelete": "Nem sikerült törölni a letöltési profilt",
"downloadCookiesRemove": "Nem sikerült eltávolítani ezeket a sütiket",
"notifRead": "Nem sikerült olvasottnak jelölni az értesítést",
"notifReadAll": "Nem sikerült olvasottnak jelölni az értesítéseket",
"notifDismiss": "Nem sikerült elvetni az értesítést",
"notifClear": "Nem sikerült törölni az értesítéseket",
"createPlaylist": "Nem sikerült létrehozni a lejátszási listát",
"schedulerRun": "Nem sikerült elindítani a feladatot",
"schedulerRunAll": "Nem sikerült elindítani a feladatokat",
"maintenanceBatch": "Nem sikerült menteni a karbantartási köteg méretét",
"purgeDiscovery": "Nem sikerült törölni a felfedezési adatokat",
"plexWatchLink": "Nem sikerült módosítani a Plex megtekintésiállapot-szinkront",
"plexWatchImport": "Nem sikerült importálni a megtekintési állapotot a Plexből",
"shareLinkDownload": "Nem sikerült módosítani, hogy a link engedi-e a letöltést",
"shareLinkRevoke": "Nem sikerült visszavonni a linket",
"shareLinkPassword": "Nem sikerült módosítani a link jelszavát",
"syncPause": "Nem sikerült szüneteltetni a szinkronizálást",
"syncResume": "Nem sikerült folytatni a szinkronizálást",
"prefs": "Nem sikerült menteni a beállításaidat",
"deleteAccount": "Nem sikerült törölni a fiókodat",
"videoState": "Nem sikerült módosítani a videó állapotát",
"videoReset": "Nem sikerült alaphelyzetbe állítani a videót",
"watchLaterAdd": "Nem sikerült hozzáadni a Megnézem később listához",
"watchLaterRemove": "Nem sikerült eltávolítani a Megnézem később listából",
"clearSearch": "Nem sikerült törölni a keresési találatokat"
},
"ok": "OK",
"offline": {
"title": "Megszakadt a kapcsolat",