From af6a07bd1f49dfd9dbd2637bfa366af573b22b0c Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Thu, 25 Jun 2026 20:13:10 +0200 Subject: [PATCH] feat(nav): give admins a mobile entry point to the admin area The admin area was reachable only from the desktop header's is_admin link; on mobile there was no tab or row, so admins had to type the URL and no tab highlighted. Add a conditional Admin row to the mobile Account hub and `/admin` to the Account tab's match prefixes, so the tab stays highlighted while an admin is in the admin area. e2e: an admin session shows an Admin row linking to /admin; a non-admin never sees it. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/e2e/mobile-account-library.spec.ts | 20 +++++++++++++++++-- frontend/src/routes/+layout.svelte | 4 +++- .../src/routes/profile/account/+page.svelte | 13 ++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/frontend/e2e/mobile-account-library.spec.ts b/frontend/e2e/mobile-account-library.spec.ts index 04d562e..1890f4f 100644 --- a/frontend/e2e/mobile-account-library.spec.ts +++ b/frontend/e2e/mobile-account-library.spec.ts @@ -11,7 +11,7 @@ const DESKTOP = { width: 1280, height: 720 } as const; async function mockSession( page: Page, - opts: { authed?: boolean; logoutCalls?: { count: number } } = {} + opts: { authed?: boolean; admin?: boolean; logoutCalls?: { count: number } } = {} ) { const authed = opts.authed ?? false; await page.route('**/api/v1/auth/config', (route) => @@ -31,7 +31,7 @@ async function mockSession( id: 'u1', username: 'fabian', created_at: '2026-01-05T00:00:00Z', - is_admin: false + is_admin: opts.admin ?? false } }) : JSON.stringify({ @@ -175,11 +175,27 @@ test.describe('mobile account hub', () => { await expect(page.getByTestId('account-row-preferences')).toBeVisible(); await expect(page.getByTestId('account-row-change-password')).toBeVisible(); await expect(page.getByTestId('account-row-logout')).toBeVisible(); + // A non-admin never sees the Admin entry. + await expect(page.getByTestId('account-row-admin')).toHaveCount(0); // Desktop card MUST not render — only the hub view is active // on mobile so we don't double-up the password form testids. await expect(page.getByTestId('account-desktop-card')).toHaveCount(0); }); + test('phone viewport admin: the account hub offers an Admin row linking to /admin', async ({ + page + }) => { + // Admin is reachable from the desktop header but had no mobile entry + // point — admins had to type the URL. The Account hub now carries it. + await mockSession(page, { authed: true, admin: true }); + await page.setViewportSize(MOBILE); + await page.goto('/profile/account'); + + const adminRow = page.getByTestId('account-row-admin'); + await expect(adminRow).toBeVisible(); + await expect(adminRow).toHaveAttribute('href', '/admin'); + }); + test('phone viewport authed: Change password row opens the bottom sheet', async ({ page }) => { diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index c621b41..87c2119 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -82,7 +82,9 @@ label: 'Account', href: '/profile/account', icon: User, - match: ['/profile', '/profile/preferences'] + // `/admin` keeps the Account tab highlighted while an admin is in + // the admin area — its only mobile entry point is the Account hub. + match: ['/profile', '/profile/preferences', '/admin'] } ]; diff --git a/frontend/src/routes/profile/account/+page.svelte b/frontend/src/routes/profile/account/+page.svelte index c5513ad..dae522c 100644 --- a/frontend/src/routes/profile/account/+page.svelte +++ b/frontend/src/routes/profile/account/+page.svelte @@ -10,6 +10,7 @@ import SlidersHorizontal from '@lucide/svelte/icons/sliders-horizontal'; import KeyRound from '@lucide/svelte/icons/key-round'; import LogOut from '@lucide/svelte/icons/log-out'; + import Shield from '@lucide/svelte/icons/shield'; import ChevronRight from '@lucide/svelte/icons/chevron-right'; let currentPassword = $state(''); @@ -193,6 +194,18 @@ + {#if session.user.is_admin} + + + {/if} +