fix(api,ui): a refused write says why, wherever the refusal happens

Round 5 taught `notifyActionError` to discard a plain-string `detail` in favour
of the caller's translated fallback, because `req` leaves 403/404/429 to the
caller and those routes hand-write English. Three places did not get the
message.

**The demo account lost its explanation.** `require_human` refused with a plain
string, so from that commit on the shared demo account got its caller's generic
"couldn't do that" and no hint that the demo account was the reason — and the
gate is not backed by a UI gate everywhere: the comfort shelf has no `is_demo`
check, so the pin button is live and simply fails. A structured `{code: "demo"}`
and an `errors.demo` string in both locales. The two auth routes that hand-roll
the same check were swept with it.

**`ChatThread` carried a second copy of the contract.** It filtered 404/429
itself and then did `e.detail || t(…)`, the pre-fix order, so the central fix
could not reach it and "That member hasn't set up messaging yet." stayed English
under a Hungarian UI. It goes through the shared helper now, and a contract test
pins the remaining raw-`detail` consumers as an explicit SET — never a count —
so a new copy fails the gate instead of shipping. The five on that list are
inline surfaces (a form field's error line, a setup banner) and are deferred
with the ~60 backend routes whose details they print.

**The Plex "Add to playlist" dialog was still silent.** Same shape as the
collection editor round 5 fixed and the YouTube twin that already had the fix:
no catch, so a playlist deleted on another device answered a structured 404 that
nothing reported — the tick never flipped, the gone row stayed, and the floating
promise became an unhandled rejection.

Also: the `ABORTED` sentinel's VALUE was asserted nowhere. The consumer test
hand-built the error it expected, so replacing `xhr.onabort`'s rejection with a
friendlier message left the lane green — and every upload the user cancels would
then raise a red "the upload failed" toast, which is what the sentinel prevents.
Both producer tests assert through `isAborted` now.

And two backend tests were pinning fewer sites than their own docstrings claimed:
the channel-404 loop said "all five" and issued three requests (explore and
subscribe unexercised, the excuse for subscribe naming a different call site
with a different reason slug), and the recipient/job test named three refusals
and requested two — `share_download`'s `not_found("recipient")` had no test in
the suite at all, nor did the locale keys added beside it. Every site named now
has a request behind it.

6 tests, mutation-checked against 7 mutations: the demo detail at both the gate
and an auth route, a re-hand-rolled ChatThread, the silent Plex dialog, the
abort sentinel, and each unpinned 404 — all red on the test that names them.
This commit is contained in:
2026-09-12 05:50:31 +02:00
parent d89d887a9b
commit a0114deff1
15 changed files with 266 additions and 21 deletions
+1
View File
@@ -1,6 +1,7 @@
{
"title": "Couldn't complete that",
"generic": "The server couldn't carry out that action. Please try again.",
"demo": "This isn't available in the demo account.",
"validation": "Some of the values you entered weren't valid. Please check them and try again.",
"server": "Server error",
"syncCooldown": "You've synced a few times just now — give it a few minutes. The YouTube budget is shared with everyone on this instance, and background syncing keeps running either way.",
+4 -1
View File
@@ -297,7 +297,10 @@
"create": "Create",
"none": "No playlists yet. Create one above.",
"count": "{{count}} items",
"groupProgress": "{{in}} / {{size}}"
"groupProgress": "{{in}} / {{size}}",
"addFailed": "Couldn't add it to the playlist.",
"removeFailed": "Couldn't remove it from the playlist.",
"createFailed": "Couldn't create the playlist."
},
"playlist": {
"section": "Playlists",
+1
View File
@@ -1,6 +1,7 @@
{
"title": "Nem sikerült",
"generic": "A szerver nem tudta végrehajtani a műveletet. Próbáld újra.",
"demo": "Ez a bemutató fiókban nem érhető el.",
"validation": "Néhány megadott érték érvénytelen volt. Ellenőrizd, majd próbáld újra.",
"server": "Szerverhiba",
"syncCooldown": "Az imént többször is szinkronizáltál — várj pár percet. A YouTube-keret közös az instance összes felhasználójával, a háttérszinkron pedig így is fut tovább.",
+4 -1
View File
@@ -297,7 +297,10 @@
"create": "Létrehozás",
"none": "Még nincs lista. Hozz létre egyet fent.",
"count": "{{count}} elem",
"groupProgress": "{{in}} / {{size}}"
"groupProgress": "{{in}} / {{size}}",
"addFailed": "Nem sikerült hozzáadni a lejátszási listához.",
"removeFailed": "Nem sikerült eltávolítani a lejátszási listáról.",
"createFailed": "Nem sikerült létrehozni a lejátszási listát."
},
"playlist": {
"section": "Lejátszási listák",