From c3be0f3e6e22bfa42be783f568d3fc83574283bb Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 2 May 2026 15:01:46 -0400 Subject: [PATCH] refactor(web): consolidate error-copy table to JSON source of truth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three admin pages had divergent inline error-code → user-copy switch tables (with wording drift, including a stale "Settings → Integrations" that should have been Admin). Consolidate into web/src/lib/styles/error-copy.json, expose via web/src/lib/api/error-copy.ts (ERROR_COPY + copyForCode), and refactor the three pages to import the helper. Fixes the three-way drift; the only user-visible behavior change is the quarantine page now correctly says "Admin → Integrations". Sets up the JSON for the M7 Flutter client (#356) to consume the same table. Co-Authored-By: Claude Opus 4.7 (1M context) --- web/src/lib/api/error-copy.ts | 8 ++++++ web/src/lib/styles/error-copy.json | 16 +++++++++++ web/src/routes/admin/+page.svelte | 26 ++++------------- web/src/routes/admin/quarantine/+page.svelte | 28 ++++--------------- .../admin/quarantine/quarantine.test.ts | 2 +- web/src/routes/admin/requests/+page.svelte | 28 ++----------------- 6 files changed, 40 insertions(+), 68 deletions(-) create mode 100644 web/src/lib/api/error-copy.ts create mode 100644 web/src/lib/styles/error-copy.json diff --git a/web/src/lib/api/error-copy.ts b/web/src/lib/api/error-copy.ts new file mode 100644 index 00000000..37dfefae --- /dev/null +++ b/web/src/lib/api/error-copy.ts @@ -0,0 +1,8 @@ +import errorCopyJson from '../styles/error-copy.json'; + +export const ERROR_COPY: Readonly> = errorCopyJson; + +export function copyForCode(code: string | null | undefined): string { + if (!code) return ERROR_COPY.unknown ?? 'Something went wrong.'; + return ERROR_COPY[code] ?? ERROR_COPY.unknown ?? 'Something went wrong.'; +} diff --git a/web/src/lib/styles/error-copy.json b/web/src/lib/styles/error-copy.json new file mode 100644 index 00000000..a3c3e4cf --- /dev/null +++ b/web/src/lib/styles/error-copy.json @@ -0,0 +1,16 @@ +{ + "unknown": "Something went wrong.", + "unauthenticated": "Your session has ended. Please sign in again.", + "connection_refused": "Couldn't reach the server. Check the URL and try again.", + "lidarr_unreachable": "Lidarr is unreachable right now. Try again, or check Admin → Integrations.", + "lidarr_disabled": "Lidarr integration is not enabled.", + "lidarr_auth_failed": "Lidarr authentication failed.", + "lidarr_defaults_incomplete": "Lidarr is missing a default quality profile or root folder. Set them in Admin → Integrations.", + "lidarr_server_error": "Lidarr returned an error. Check Lidarr's logs for the cause.", + "lidarr_rejected": "Lidarr rejected the request. Check the server logs for the field-level reason.", + "lidarr_album_lookup_failed": "Lidarr doesn't recognize this album. Try Resolve or Delete file instead.", + "album_mbid_missing": "This track has no Lidarr album to remove.", + "request_not_pending": "This request is no longer pending.", + "request_not_found": "That request no longer exists.", + "track_not_found": "That track no longer exists." +} diff --git a/web/src/routes/admin/+page.svelte b/web/src/routes/admin/+page.svelte index 530a1e69..f9a0de41 100644 --- a/web/src/routes/admin/+page.svelte +++ b/web/src/routes/admin/+page.svelte @@ -12,6 +12,7 @@ deleteQuarantineViaLidarr } from '$lib/api/admin'; import { qk } from '$lib/api/queries'; + import { copyForCode } from '$lib/api/error-copy'; import type { AdminQuarantineRow, LidarrRequest, @@ -60,21 +61,6 @@ toastTimer = setTimeout(() => { toast = null; }, 5000); } - function errorCopy(code: string): string { - switch (code) { - case 'lidarr_unreachable': return "Lidarr is unreachable. Try again, or check Admin → Integrations."; - case 'lidarr_disabled': return 'Lidarr integration is not enabled.'; - case 'lidarr_auth_failed': return 'Lidarr authentication failed.'; - case 'lidarr_defaults_incomplete': return 'Lidarr defaults missing — set them in Admin → Integrations.'; - case 'lidarr_server_error': return "Lidarr returned an error. Check Lidarr's logs."; - case 'lidarr_rejected': return 'Lidarr rejected the request. Check the server logs for details.'; - case 'request_not_pending': return 'This request is no longer pending.'; - case 'request_not_found': return 'That request no longer exists.'; - case 'track_not_found': return 'That track no longer exists.'; - default: return code || 'Action failed.'; - } - } - // ---- Two-click destructive confirm for quarantine actions ---- // Map key is `${trackId}:${actionKey}`. First click sets a key; second // click within 3s fires the action; otherwise the key clears. @@ -134,7 +120,7 @@ await approveRequest(r.id); await invalidateRequests(); } catch (e) { - showToast(errorCopy((e as { code?: string }).code ?? '')); + showToast(copyForCode((e as { code?: string }).code)); } } async function onReject(r: LidarrRequest) { @@ -142,7 +128,7 @@ await rejectRequest(r.id); await invalidateRequests(); } catch (e) { - showToast(errorCopy((e as { code?: string }).code ?? '')); + showToast(copyForCode((e as { code?: string }).code)); } } @@ -158,7 +144,7 @@ await resolveQuarantine(row.track_id); await invalidateQuarantine(); } catch (e) { - showToast(errorCopy((e as { code?: string }).code ?? '')); + showToast(copyForCode((e as { code?: string }).code)); } } async function onDeleteFile(row: AdminQuarantineRow) { @@ -172,7 +158,7 @@ await deleteQuarantineFile(row.track_id); await invalidateQuarantine(); } catch (e) { - showToast(errorCopy((e as { code?: string }).code ?? '')); + showToast(copyForCode((e as { code?: string }).code)); } } async function onDeleteLidarr(row: AdminQuarantineRow) { @@ -186,7 +172,7 @@ await deleteQuarantineViaLidarr(row.track_id); await invalidateQuarantine(); } catch (e) { - showToast(errorCopy((e as { code?: string }).code ?? '')); + showToast(copyForCode((e as { code?: string }).code)); } } diff --git a/web/src/routes/admin/quarantine/+page.svelte b/web/src/routes/admin/quarantine/+page.svelte index 15cf81b1..08d3f5d8 100644 --- a/web/src/routes/admin/quarantine/+page.svelte +++ b/web/src/routes/admin/quarantine/+page.svelte @@ -8,13 +8,14 @@ deleteQuarantineViaLidarr } from '$lib/api/admin'; import { qk } from '$lib/api/queries'; + import { copyForCode } from '$lib/api/error-copy'; import { playRadio } from '$lib/player/store.svelte'; import type { AdminQuarantineRow, LidarrQuarantineReason } from '$lib/api/types'; // Aggregated triage queue. One row per track, with per-row resolution // actions: Resolve (clears reports), Delete file (Bronze; modal-confirm), // Delete via Lidarr (Oxblood; typed-confirm "DELETE"). Mirrors - // /admin/requests for shape — toast helper, errorCopy(), invalidateQueries. + // /admin/requests for shape — toast helper, copyForCode(), invalidateQueries. const client = useQueryClient(); @@ -78,23 +79,6 @@ }, 5000); } - function errorCopy(code: string): string { - switch (code) { - case 'lidarr_unreachable': - return "Lidarr is unreachable right now. Try again, or check Settings → Integrations."; - case 'lidarr_disabled': - return 'Lidarr integration is not enabled.'; - case 'lidarr_auth_failed': - return 'Lidarr authentication failed.'; - case 'lidarr_album_lookup_failed': - return "Lidarr doesn't recognize this album. Try Resolve or Delete file instead."; - case 'album_mbid_missing': - return 'This track has no Lidarr album to remove.'; - default: - return "Couldn't reach Lidarr."; - } - } - async function invalidate() { await client.invalidateQueries({ queryKey: qk.adminQuarantine() }); } @@ -105,7 +89,7 @@ await invalidate(); } catch (e) { const code = (e as { code?: string }).code ?? 'unknown'; - showToast(errorCopy(code)); + showToast(copyForCode(code)); } } @@ -124,7 +108,7 @@ await invalidate(); } catch (e) { const code = (e as { code?: string }).code ?? 'unknown'; - showToast(errorCopy(code)); + showToast(copyForCode(code)); } } @@ -153,9 +137,9 @@ const code = (e as { code?: string }).code ?? 'unknown'; // Inline error keeps the modal open so the operator sees the failure // alongside the album they were about to remove. - deleteLidarrError = errorCopy(code); + deleteLidarrError = copyForCode(code); // Also surface as toast so it's visible after dismissing the modal. - showToast(errorCopy(code)); + showToast(copyForCode(code)); } } diff --git a/web/src/routes/admin/quarantine/quarantine.test.ts b/web/src/routes/admin/quarantine/quarantine.test.ts index 68d7eedb..51f73ff7 100644 --- a/web/src/routes/admin/quarantine/quarantine.test.ts +++ b/web/src/routes/admin/quarantine/quarantine.test.ts @@ -153,7 +153,7 @@ describe('/admin/quarantine', () => { await waitFor(() => expect( screen.getByText( - 'Lidarr is unreachable right now. Try again, or check Settings → Integrations.' + 'Lidarr is unreachable right now. Try again, or check Admin → Integrations.' ) ).toBeInTheDocument() ); diff --git a/web/src/routes/admin/requests/+page.svelte b/web/src/routes/admin/requests/+page.svelte index 1acd5b37..36f1905f 100644 --- a/web/src/routes/admin/requests/+page.svelte +++ b/web/src/routes/admin/requests/+page.svelte @@ -8,6 +8,7 @@ approveRequest, rejectRequest } from '$lib/api/admin'; + import { copyForCode } from '$lib/api/error-copy'; import StatusPill from '$lib/components/StatusPill.svelte'; import type { LidarrRequest, LidarrRequestKind, LidarrRequestStatus } from '$lib/api/types'; @@ -67,29 +68,6 @@ }, 5000); } - function errorCopy(code: string): string { - switch (code) { - case 'lidarr_unreachable': - return "Lidarr is unreachable right now. Try again, or check Admin → Integrations."; - case 'lidarr_disabled': - return 'Lidarr integration is not enabled.'; - case 'lidarr_auth_failed': - return 'Lidarr authentication failed.'; - case 'lidarr_defaults_incomplete': - return 'Lidarr is missing a default quality profile or root folder. Set them in Admin → Integrations.'; - case 'lidarr_server_error': - return "Lidarr returned an error. Check Lidarr's logs for the cause."; - case 'lidarr_rejected': - return "Lidarr rejected the request. Check the server logs for the field-level reason."; - case 'request_not_pending': - return 'This request is no longer pending.'; - case 'request_not_found': - return 'That request no longer exists.'; - default: - return code ? code : "Couldn't reach Lidarr."; - } - } - function fallbackIcon(kind: LidarrRequestKind) { if (kind === 'artist') return Disc3; if (kind === 'album') return Album; @@ -136,7 +114,7 @@ await invalidate(); } catch (e) { const code = (e as { code?: string }).code ?? 'unknown'; - showToast(errorCopy(code)); + showToast(copyForCode(code)); } } @@ -177,7 +155,7 @@ await invalidate(); } catch (e) { const code = (e as { code?: string }).code ?? 'unknown'; - showToast(errorCopy(code)); + showToast(copyForCode(code)); } }