refactor(web): consolidate error-copy table to JSON source of truth
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) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
import errorCopyJson from '../styles/error-copy.json';
|
||||
|
||||
export const ERROR_COPY: Readonly<Record<string, string>> = 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.';
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user