feat(web): admin playback-errors inbox
test-web / test (push) Successful in 34s

Surfaces client-reported playback failures from /api/admin/playback-errors
in a new admin tab. Tabs: Unresolved (default) / Resolved. Each row
shows track + artist + album, error kind badge, who hit it, when,
optional client-supplied detail, and the absolute file path so the
operator can grep the library mount without leaving the page.

Per-row actions (RowActionsMenu):
- Resolve (primary) — modal with Fixed / Ignored dropdown for the
  "no further action taken" cases.
- Copy — JSON payload to clipboard with track_id / file_path / kind
  / detail / reporter / client_id / occurred_at. Matches the
  operator's "logs with a copy-out function" ask.
- Delete file (danger, modal-confirm) — uses the existing
  /api/admin/quarantine/{track_id}/delete-file endpoint AND
  auto-stamps resolution='deleted' so a single click closes both
  the file and the inbox row.

Deferred to a follow-up: Hide (the existing quarantine flow is
per-user-flag, not a true library-hide), and Re-request via Lidarr
(needs album MBID join — not in the current ListAdminPlaybackErrors
projection).

Also: admin tab list grows from five to six; AdminTabs.test.ts
updated.
This commit is contained in:
2026-06-02 11:34:46 -04:00
parent 99e1df4920
commit de61305fde
6 changed files with 384 additions and 7 deletions
+31 -1
View File
@@ -3,6 +3,7 @@ import { api } from './client';
import { qk } from './queries';
import type {
ActionResult,
AdminPlaybackError,
AdminQuarantineRow,
LidarrConfig,
LidarrMetadataProfile,
@@ -11,7 +12,8 @@ import type {
LidarrRequest,
LidarrRequestStatus,
LidarrRootFolder,
LidarrTestResult
LidarrTestResult,
PlaybackErrorResolution
} from './types';
// Admin Lidarr config -----------------------------------------------------
@@ -174,6 +176,34 @@ export function createQuarantineActionsQuery(limit: number = 50) {
});
}
// Admin playback errors ---------------------------------------------------
export async function listAdminPlaybackErrors(
resolved: boolean = false
): Promise<AdminPlaybackError[]> {
return api.get<AdminPlaybackError[]>(
`/api/admin/playback-errors?resolved=${resolved}`
);
}
export async function resolvePlaybackError(
id: string,
resolution: PlaybackErrorResolution
): Promise<{ id: string }> {
return api.post<{ id: string }>(
`/api/admin/playback-errors/${id}/resolve`,
{ resolution }
);
}
export function createAdminPlaybackErrorsQuery(resolved: boolean = false) {
return createQuery({
queryKey: qk.adminPlaybackErrors(resolved),
queryFn: () => listAdminPlaybackErrors(resolved),
staleTime: 30_000
});
}
// Admin cover art ---------------------------------------------------------
export type RefetchAlbumCoverResponse = {