feat(admin): expand /admin overview into an actionable workbench

The admin overview was just two stat cards. Promotes it to a proper
workbench by surfacing the top-5 of each actionable queue with inline
handlers, while keeping the dedicated /admin/requests and
/admin/quarantine pages as the deep-action surfaces.

Changes:

- Top stats grow from 2 cards to 3 (requests / quarantine / lidarr).
- New "Pending requests" preview list — top-5 rows with inline Approve
  and Reject buttons that call the same approveRequest/rejectRequest
  endpoints used on the dedicated page (default profiles, no override
  modal — that flow stays on /admin/requests for power users).
- New "Quarantine" preview list — top-5 rows with three inline actions
  matching the dedicated page: Resolve (single click), Delete file
  (two-click confirm with inline "Confirm?" state), Delete via Lidarr
  (two-click confirm). The two-click confirm avoids modal duplication
  on the overview while still gating the irreversible operations.
- "View all <N> →" link surfaces beneath each preview only when the
  full queue exceeds the preview limit, pointing to the dedicated page.
- Shared toast surface with errorCopy mapping for the same lidarr_*
  codes the dedicated requests page handles, so failed actions surface
  meaningful text instead of "unknown".

Test fixture migrated from per-test dynamic imports to top-level
imports — the previous pattern was producing stale-mock symptoms
where mockReturnValue was set after the page bound against the
original mocks. 12 tests cover stat counts, empty states, links,
each handler binding, the two-click confirm pattern, and the
"View all" surfacing rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-02 12:51:57 -04:00
parent 1c39001482
commit 7b91718fc9
2 changed files with 481 additions and 43 deletions
+318 -3
View File
@@ -1,5 +1,34 @@
<script lang="ts">
import { createAdminRequestsQuery, createLidarrConfigQuery } from '$lib/api/admin';
import { Disc3, Album, Music2, Check, X, RotateCcw, Trash2, Cloud, ChevronRight } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import {
createAdminRequestsQuery,
createLidarrConfigQuery,
createAdminQuarantineQuery,
approveRequest,
rejectRequest,
resolveQuarantine,
deleteQuarantineFile,
deleteQuarantineViaLidarr
} from '$lib/api/admin';
import { qk } from '$lib/api/queries';
import type {
AdminQuarantineRow,
LidarrRequest,
LidarrRequestKind,
LidarrQuarantineReason
} from '$lib/api/types';
// Top-of-admin overview. Shows the connection card + the most recent
// 5 of each actionable queue (requests, quarantine) with inline
// handlers for the common case. The dedicated pages keep the full
// modal flows (override, typed-confirm); the overview offers a faster
// path for the common-case actions, with a two-click confirm on
// destructive quarantine actions to avoid modal duplication.
const PREVIEW_LIMIT = 5;
const client = useQueryClient();
const requestsStore = createAdminRequestsQuery('pending');
const requests = $derived($requestsStore);
@@ -7,16 +36,168 @@
const configStore = createLidarrConfigQuery();
const config = $derived($configStore);
const quarantineStore = createAdminQuarantineQuery();
const quarantine = $derived($quarantineStore);
const pendingCount = $derived(requests.data?.length ?? 0);
const quarantineCount = $derived(quarantine.data?.length ?? 0);
const lidarrConnected = $derived(config.data?.enabled === true);
const requestPreview = $derived(
(requests.data ?? []).slice(0, PREVIEW_LIMIT)
);
const quarantinePreview = $derived(
(quarantine.data ?? []).slice(0, PREVIEW_LIMIT)
);
// ---- Toast (shared) ----
let toast = $state<string | null>(null);
let toastTimer: ReturnType<typeof setTimeout> | null = null;
function showToast(msg: string) {
if (toastTimer) clearTimeout(toastTimer);
toast = msg;
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.
let confirmingKey = $state<string | null>(null);
let confirmTimer: ReturnType<typeof setTimeout> | null = null;
function confirmKey(trackID: string, action: 'delete-file' | 'delete-lidarr'): string {
return `${trackID}:${action}`;
}
function armConfirm(trackID: string, action: 'delete-file' | 'delete-lidarr') {
confirmingKey = confirmKey(trackID, action);
if (confirmTimer) clearTimeout(confirmTimer);
confirmTimer = setTimeout(() => { confirmingKey = null; }, 3000);
}
function clearConfirm() {
confirmingKey = null;
if (confirmTimer) clearTimeout(confirmTimer);
}
// ---- Helpers ----
function fallbackIcon(kind: LidarrRequestKind) {
if (kind === 'artist') return Disc3;
if (kind === 'album') return Album;
return Music2;
}
function rowTitle(r: LidarrRequest): string {
if (r.kind === 'artist') return r.artist_name;
if (r.kind === 'album') return r.album_title ?? '—';
return r.track_title ?? '—';
}
function rowSubtitle(r: LidarrRequest): string {
if (r.kind === 'artist') return 'Artist';
if (r.kind === 'album') return `Album · ${r.artist_name}`;
return `Track · ${r.artist_name}`;
}
const REASON_LABELS: Record<LidarrQuarantineReason, string> = {
bad_rip: 'Bad rip',
wrong_file: 'Wrong file',
wrong_tags: 'Wrong tags',
duplicate: 'Duplicate',
other: 'Other'
};
// ---- Action handlers ----
async function invalidateRequests() {
await Promise.all([
client.invalidateQueries({ queryKey: qk.adminRequests('pending') }),
client.invalidateQueries({ queryKey: qk.adminRequests('approved') }),
client.invalidateQueries({ queryKey: qk.adminRequests('rejected') }),
client.invalidateQueries({ queryKey: qk.adminRequests('completed') })
]);
}
async function onApprove(r: LidarrRequest) {
try {
await approveRequest(r.id);
await invalidateRequests();
} catch (e) {
showToast(errorCopy((e as { code?: string }).code ?? ''));
}
}
async function onReject(r: LidarrRequest) {
try {
await rejectRequest(r.id);
await invalidateRequests();
} catch (e) {
showToast(errorCopy((e as { code?: string }).code ?? ''));
}
}
async function invalidateQuarantine() {
await Promise.all([
client.invalidateQueries({ queryKey: qk.adminQuarantine() }),
client.invalidateQueries({ queryKey: qk.adminQuarantineActions() })
]);
}
async function onResolve(row: AdminQuarantineRow) {
try {
await resolveQuarantine(row.track_id);
await invalidateQuarantine();
} catch (e) {
showToast(errorCopy((e as { code?: string }).code ?? ''));
}
}
async function onDeleteFile(row: AdminQuarantineRow) {
const key = confirmKey(row.track_id, 'delete-file');
if (confirmingKey !== key) {
armConfirm(row.track_id, 'delete-file');
return;
}
clearConfirm();
try {
await deleteQuarantineFile(row.track_id);
await invalidateQuarantine();
} catch (e) {
showToast(errorCopy((e as { code?: string }).code ?? ''));
}
}
async function onDeleteLidarr(row: AdminQuarantineRow) {
const key = confirmKey(row.track_id, 'delete-lidarr');
if (confirmingKey !== key) {
armConfirm(row.track_id, 'delete-lidarr');
return;
}
clearConfirm();
try {
await deleteQuarantineViaLidarr(row.track_id);
await invalidateQuarantine();
} catch (e) {
showToast(errorCopy((e as { code?: string }).code ?? ''));
}
}
</script>
<div class="space-y-6">
<div class="space-y-8">
<header>
<h2 class="font-display text-2xl font-medium text-text-primary">Overview</h2>
</header>
<div class="grid gap-4 sm:grid-cols-2">
<!-- Top stats -->
<div class="grid gap-4 sm:grid-cols-3">
<a
href="/admin/requests"
class="block rounded-xl border border-border bg-surface p-5 transition-colors hover:bg-surface-hover"
@@ -24,6 +205,13 @@
<div class="text-sm text-text-secondary">Pending requests</div>
<div class="font-display mt-1 text-3xl font-medium text-text-primary">{pendingCount}</div>
</a>
<a
href="/admin/quarantine"
class="block rounded-xl border border-border bg-surface p-5 transition-colors hover:bg-surface-hover"
>
<div class="text-sm text-text-secondary">Quarantined tracks</div>
<div class="font-display mt-1 text-3xl font-medium text-text-primary">{quarantineCount}</div>
</a>
<a
href="/admin/integrations"
class="block rounded-xl border border-border bg-surface p-5 transition-colors hover:bg-surface-hover"
@@ -34,4 +222,131 @@
</div>
</a>
</div>
<!-- Pending requests preview -->
<section class="space-y-3">
<header class="flex items-baseline justify-between">
<h3 class="font-display text-xl font-medium text-text-primary">Pending requests</h3>
{#if pendingCount > PREVIEW_LIMIT}
<a href="/admin/requests" class="text-sm text-accent hover:underline">View all {pendingCount}</a>
{/if}
</header>
{#if requests.isPending}
<p class="text-sm text-text-secondary">Loading…</p>
{:else if requestPreview.length === 0}
<p class="text-sm text-text-secondary">No pending requests.</p>
{:else}
<ul class="divide-y divide-border rounded-xl border border-border bg-surface">
{#each requestPreview as r (r.id)}
{@const Icon = fallbackIcon(r.kind)}
<li class="flex items-center gap-3 p-3">
<Icon size={20} strokeWidth={1.5} class="shrink-0 text-text-muted" />
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-medium text-text-primary">{rowTitle(r)}</div>
<div class="truncate text-xs text-text-secondary">{rowSubtitle(r)}</div>
</div>
<button
type="button"
onclick={() => onApprove(r)}
aria-label={`Approve ${rowTitle(r)}`}
class="flex h-8 items-center gap-1 rounded-md bg-action-primary px-3 text-sm text-text-primary hover:opacity-90"
>
<Check size={14} strokeWidth={1.5} /> Approve
</button>
<button
type="button"
onclick={() => onReject(r)}
aria-label={`Reject ${rowTitle(r)}`}
class="flex h-8 items-center gap-1 rounded-md border border-border px-3 text-sm text-text-secondary hover:text-text-primary hover:bg-surface-hover"
>
<X size={14} strokeWidth={1.5} /> Reject
</button>
</li>
{/each}
</ul>
{/if}
</section>
<!-- Quarantine preview -->
<section class="space-y-3">
<header class="flex items-baseline justify-between">
<h3 class="font-display text-xl font-medium text-text-primary">Quarantine</h3>
{#if quarantineCount > PREVIEW_LIMIT}
<a href="/admin/quarantine" class="text-sm text-accent hover:underline">View all {quarantineCount}</a>
{/if}
</header>
{#if quarantine.isPending}
<p class="text-sm text-text-secondary">Loading…</p>
{:else if quarantinePreview.length === 0}
<p class="text-sm text-text-secondary">No quarantined tracks.</p>
{:else}
<ul class="divide-y divide-border rounded-xl border border-border bg-surface">
{#each quarantinePreview as row (row.track_id)}
{@const fileKey = confirmKey(row.track_id, 'delete-file')}
{@const lidarrKey = confirmKey(row.track_id, 'delete-lidarr')}
{@const fileArmed = confirmingKey === fileKey}
{@const lidarrArmed = confirmingKey === lidarrKey}
<li class="flex items-center gap-3 p-3">
<Music2 size={20} strokeWidth={1.5} class="shrink-0 text-text-muted" />
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-medium text-text-primary">{row.track_title}</div>
<div class="truncate text-xs text-text-secondary">
{row.artist_name} · {row.album_title}
· {row.report_count} {row.report_count === 1 ? 'report' : 'reports'}
{#if row.reports[0]}
· {REASON_LABELS[row.reports[0].reason] ?? row.reports[0].reason}
{/if}
</div>
</div>
<button
type="button"
onclick={() => onResolve(row)}
aria-label={`Resolve ${row.track_title}`}
class="flex h-8 items-center gap-1 rounded-md border border-border px-3 text-sm text-text-secondary hover:text-text-primary hover:bg-surface-hover"
>
<RotateCcw size={14} strokeWidth={1.5} /> Resolve
</button>
<button
type="button"
onclick={() => onDeleteFile(row)}
aria-label={fileArmed ? `Confirm delete file ${row.track_title}` : `Delete file ${row.track_title}`}
class="flex h-8 items-center gap-1 rounded-md px-3 text-sm transition-colors {fileArmed
? 'bg-action-secondary text-text-primary'
: 'border border-border text-text-secondary hover:text-text-primary hover:bg-surface-hover'}"
>
<Trash2 size={14} strokeWidth={1.5} /> {fileArmed ? 'Confirm?' : 'Delete file'}
</button>
<button
type="button"
onclick={() => onDeleteLidarr(row)}
aria-label={lidarrArmed ? `Confirm delete via Lidarr ${row.track_title}` : `Delete via Lidarr ${row.track_title}`}
class="flex h-8 items-center gap-1 rounded-md px-3 text-sm transition-colors {lidarrArmed
? 'bg-action-destructive text-text-primary'
: 'border border-border text-text-secondary hover:text-text-primary hover:bg-surface-hover'}"
>
<Cloud size={14} strokeWidth={1.5} /> {lidarrArmed ? 'Confirm?' : 'Delete via Lidarr'}
</button>
<a
href="/admin/quarantine"
aria-label="Open in quarantine queue"
class="text-text-secondary hover:text-text-primary"
>
<ChevronRight size={16} strokeWidth={1.5} />
</a>
</li>
{/each}
</ul>
{/if}
</section>
</div>
{#if toast}
<div
role="alert"
class="fixed bottom-4 right-4 z-50 max-w-sm rounded-md border border-border bg-surface px-4 py-2 text-sm text-text-primary shadow"
>
{toast}
</div>
{/if}
+163 -40
View File
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import { render, screen, fireEvent, cleanup } from '@testing-library/svelte';
import { mockQuery } from '../../test-utils/query';
// Mutable mock-user handle so individual tests can flip is_admin / null
@@ -18,10 +18,25 @@ vi.mock('$app/state', () => ({
vi.mock('$lib/api/admin', () => ({
createAdminRequestsQuery: vi.fn(),
createLidarrConfigQuery: vi.fn()
createLidarrConfigQuery: vi.fn(),
createAdminQuarantineQuery: vi.fn(),
approveRequest: vi.fn().mockResolvedValue({}),
rejectRequest: vi.fn().mockResolvedValue({}),
resolveQuarantine: vi.fn().mockResolvedValue({}),
deleteQuarantineFile: vi.fn().mockResolvedValue({}),
deleteQuarantineViaLidarr: vi.fn().mockResolvedValue({})
}));
vi.mock('@tanstack/svelte-query', async (orig) => {
const actual = (await orig()) as Record<string, unknown>;
return {
...actual,
useQueryClient: () => ({ invalidateQueries: vi.fn() })
};
});
afterEach(() => {
cleanup();
vi.clearAllMocks();
userState.current = null;
});
@@ -31,8 +46,6 @@ describe('admin/+layout.ts load gate', () => {
userState.current = { id: 'u1', username: 'alice', is_admin: false };
const { load } = await import('./+layout');
await expect(async () =>
// The SvelteKit `load` signature wants a LoadEvent; we don't read any of
// it inside the function, so an empty cast is sufficient for this gate.
load({} as Parameters<typeof load>[0])
).rejects.toMatchObject({ status: 302, location: '/' });
});
@@ -48,55 +61,165 @@ describe('admin/+layout.ts load gate', () => {
test('admin user passes the gate', async () => {
userState.current = { id: 'u1', username: 'root', is_admin: true };
const { load } = await import('./+layout');
// load() is synchronous in the happy path; it only throws on the gate.
expect(load({} as Parameters<typeof load>[0])).toEqual({});
});
});
// Top-level imports — Svelte/Vite caches the module graph across tests,
// and dynamic re-imports inside each test produced "stale-mock" symptoms
// where the page picked up empty data despite `mockReturnValue` being set.
// Importing at module load means the mocks attach to the same handles the
// page binds against.
import {
createAdminRequestsQuery,
createLidarrConfigQuery,
createAdminQuarantineQuery,
approveRequest,
rejectRequest,
resolveQuarantine,
deleteQuarantineFile,
deleteQuarantineViaLidarr
} from '$lib/api/admin';
import OverviewPage from './+page.svelte';
function setupOverview(opts: {
requests?: unknown[];
quarantine?: unknown[];
lidarr?: { enabled: boolean };
} = {}) {
(createAdminRequestsQuery as ReturnType<typeof vi.fn>).mockReturnValue(
mockQuery({ data: opts.requests ?? [] })
);
(createAdminQuarantineQuery as ReturnType<typeof vi.fn>).mockReturnValue(
mockQuery({ data: opts.quarantine ?? [] })
);
(createLidarrConfigQuery as ReturnType<typeof vi.fn>).mockReturnValue(
mockQuery({ data: opts.lidarr ?? { enabled: false } })
);
}
describe('admin Overview page', () => {
test('renders pending requests count and Lidarr status (connected)', async () => {
const { createAdminRequestsQuery, createLidarrConfigQuery } = await import('$lib/api/admin');
(createAdminRequestsQuery as ReturnType<typeof vi.fn>).mockReturnValue(
mockQuery({ data: [{ id: '1' }, { id: '2' }] })
);
(createLidarrConfigQuery as ReturnType<typeof vi.fn>).mockReturnValue(
mockQuery({ data: { enabled: true } })
);
const { default: OverviewPage } = await import('./+page.svelte');
test('renders top stat cards (requests, quarantine, lidarr)', async () => {
setupOverview({
requests: [{ id: '1' }, { id: '2' }],
quarantine: [{ track_id: 't1', report_count: 2, reports: [] }],
lidarr: { enabled: true }
});
render(OverviewPage);
expect(screen.getByText('Pending requests')).toBeInTheDocument();
expect(screen.getByText('2')).toBeInTheDocument();
expect(screen.getByText('Lidarr')).toBeInTheDocument();
// "Pending requests" appears as both stat-card label and section
// header — assert via the link role to disambiguate to the stat card.
const reqLink = screen.getByRole('link', { name: /pending requests/i });
expect(reqLink).toHaveTextContent('2');
const quarLink = screen.getByRole('link', { name: /quarantined tracks/i });
expect(quarLink).toHaveTextContent('1');
expect(screen.getByText(/connected/i)).toBeInTheDocument();
});
test('shows zero pending and "Unset" when nothing is configured', async () => {
const { createAdminRequestsQuery, createLidarrConfigQuery } = await import('$lib/api/admin');
(createAdminRequestsQuery as ReturnType<typeof vi.fn>).mockReturnValue(
mockQuery({ data: [] })
);
(createLidarrConfigQuery as ReturnType<typeof vi.fn>).mockReturnValue(
mockQuery({ data: { enabled: false } })
);
const { default: OverviewPage } = await import('./+page.svelte');
test('shows empty-state copy when both queues are empty', async () => {
setupOverview({
requests: [],
quarantine: [],
lidarr: { enabled: false }
});
render(OverviewPage);
expect(screen.getByText('0')).toBeInTheDocument();
expect(screen.getByText(/no pending requests/i)).toBeInTheDocument();
expect(screen.getByText(/no quarantined tracks/i)).toBeInTheDocument();
expect(screen.getByText(/unset/i)).toBeInTheDocument();
});
test('Overview cards link to their sub-pages', async () => {
const { createAdminRequestsQuery, createLidarrConfigQuery } = await import('$lib/api/admin');
(createAdminRequestsQuery as ReturnType<typeof vi.fn>).mockReturnValue(
mockQuery({ data: [] })
);
(createLidarrConfigQuery as ReturnType<typeof vi.fn>).mockReturnValue(
mockQuery({ data: { enabled: false } })
);
const { default: OverviewPage } = await import('./+page.svelte');
test('top stat cards link to their sub-pages', async () => {
setupOverview();
render(OverviewPage);
const reqLink = screen.getByRole('link', { name: /pending requests/i });
expect(reqLink).toHaveAttribute('href', '/admin/requests');
const lidarrLink = screen.getByRole('link', { name: /lidarr/i });
expect(lidarrLink).toHaveAttribute('href', '/admin/integrations');
expect(screen.getByRole('link', { name: /pending requests/i }))
.toHaveAttribute('href', '/admin/requests');
expect(screen.getByRole('link', { name: /quarantined tracks/i }))
.toHaveAttribute('href', '/admin/quarantine');
expect(screen.getByRole('link', { name: /^lidarr/i }))
.toHaveAttribute('href', '/admin/integrations');
});
test('clicking Approve on a request row calls approveRequest', async () => {
setupOverview({
requests: [
{ id: 'r1', kind: 'artist', artist_name: 'Boards of Canada', status: 'pending' }
]
});
render(OverviewPage);
await fireEvent.click(screen.getByRole('button', { name: /approve boards of canada/i }));
expect(approveRequest).toHaveBeenCalledWith('r1');
});
test('clicking Reject on a request row calls rejectRequest', async () => {
setupOverview({
requests: [
{ id: 'r1', kind: 'artist', artist_name: 'Boards of Canada', status: 'pending' }
]
});
render(OverviewPage);
await fireEvent.click(screen.getByRole('button', { name: /reject boards of canada/i }));
expect(rejectRequest).toHaveBeenCalledWith('r1');
});
test('Resolve fires immediately on a quarantine row', async () => {
setupOverview({
quarantine: [
{ track_id: 't1', track_title: 'Roygbiv', artist_name: 'BoC',
album_title: 'Geogaddi', report_count: 1, reports: [] }
]
});
render(OverviewPage);
await fireEvent.click(screen.getByRole('button', { name: /resolve roygbiv/i }));
expect(resolveQuarantine).toHaveBeenCalledWith('t1');
});
test('Delete file requires two clicks (inline confirm)', async () => {
setupOverview({
quarantine: [
{ track_id: 't1', track_title: 'Roygbiv', artist_name: 'BoC',
album_title: 'Geogaddi', report_count: 1, reports: [] }
]
});
render(OverviewPage);
// First click: arms the confirm; no API call yet.
await fireEvent.click(screen.getByRole('button', { name: /^delete file roygbiv$/i }));
expect(deleteQuarantineFile).not.toHaveBeenCalled();
// Second click on the now-armed button fires the action.
await fireEvent.click(
screen.getByRole('button', { name: /confirm delete file roygbiv/i })
);
expect(deleteQuarantineFile).toHaveBeenCalledWith('t1');
});
test('Delete via Lidarr requires two clicks (inline confirm)', async () => {
setupOverview({
quarantine: [
{ track_id: 't1', track_title: 'Roygbiv', artist_name: 'BoC',
album_title: 'Geogaddi', report_count: 1, reports: [] }
]
});
render(OverviewPage);
await fireEvent.click(
screen.getByRole('button', { name: /^delete via lidarr roygbiv$/i })
);
expect(deleteQuarantineViaLidarr).not.toHaveBeenCalled();
await fireEvent.click(
screen.getByRole('button', { name: /confirm delete via lidarr roygbiv/i })
);
expect(deleteQuarantineViaLidarr).toHaveBeenCalledWith('t1');
});
test('"View all" link surfaces only when count exceeds the preview limit', async () => {
// 6 requests > limit of 5; quarantine empty.
const six = Array.from({ length: 6 }, (_, i) => ({
id: `r${i}`,
kind: 'artist' as const,
artist_name: `Artist ${i}`,
status: 'pending' as const
}));
setupOverview({ requests: six });
render(OverviewPage);
expect(screen.getByRole('link', { name: /view all 6/i })).toHaveAttribute(
'href', '/admin/requests'
);
});
});