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
+6 -5
View File
@@ -4,11 +4,12 @@
type Item = { href: string; label: string };
const items: Item[] = [
{ href: '/admin', label: 'Overview' },
{ href: '/admin/integrations', label: 'Integrations' },
{ href: '/admin/requests', label: 'Requests' },
{ href: '/admin/quarantine', label: 'Quarantine' },
{ href: '/admin/users', label: 'Users' }
{ href: '/admin', label: 'Overview' },
{ href: '/admin/integrations', label: 'Integrations' },
{ href: '/admin/requests', label: 'Requests' },
{ href: '/admin/quarantine', label: 'Quarantine' },
{ href: '/admin/playback-errors', label: 'Playback errors' },
{ href: '/admin/users', label: 'Users' }
];
function isActive(href: string): boolean {
+2 -1
View File
@@ -52,7 +52,7 @@ describe('AdminTabs', () => {
);
});
test('renders exactly five tabs', () => {
test('renders all six tabs in order', () => {
state.pageUrl = new URL('http://localhost/admin');
render(AdminTabs);
const links = screen.getAllByRole('link');
@@ -61,6 +61,7 @@ describe('AdminTabs', () => {
'Integrations',
'Requests',
'Quarantine',
'Playback errors',
'Users'
]);
});