--- type: guide title: Admin Shell & Side Navigation description: How an authenticated admin navigates the post-login admin area, including General, Challenges, Players, Categories, and Blog management pages. tags: [guide, admin, shell, navigation, tester] timestamp: 2026-07-23T10:12:24Z --- # When this view is available The admin shell is the post-login UI for users with `role === 'admin'`. It is gated by: | Layer | File | Check | |------------------|--------------------------------------------------------------------------------------------|------------------------------------| | Client route | `frontend/src/app/app.routes.ts` | `/admin` parent uses `adminGuard`; children inherit it. | | Client nav link | `frontend/src/app/features/home/home.component.ts` | Renders only when `showAdminNav()` is true. | | Client predicate | `frontend/src/app/features/home/home.shell.ts` (`shouldShowAdminNav`) | `isAuthenticated && role === 'admin'`. | | Server route | `backend/src/modules/admin/admin.controller.ts` + `admin-general.controller.ts` + `admin-categories.controller.ts` (mounted under `@UseGuards(AdminGuard)` + `@Roles('admin')`) | Requires admin JWT. | A non-admin (or unauthenticated) request to `/admin` is intercepted by the client guard *before* any HTTP call is made, so the page never renders. # How to access (tester steps) 1. Ensure the instance is initialized (`/api/v1/bootstrap` returns `initialized: true`). If not, follow [First-Run Bootstrap](/guides/bootstrap.md). 2. Sign in as a user with `role === 'admin'` via `/login`. 3. The browser lands on `/` and renders the **Home shell** with a header containing the page title and a sign-in status line. 4. Click the **Admin** nav link (`data-testid="nav-admin"`) in the shell header, or open the username menu and choose **Admin area**, or navigate directly to `/admin`. 5. The admin area renders inside the shell: - **Aside** (`data-testid="admin-aside"`): heading "Admin area" and a vertical side-nav (`data-testid="admin-nav"`). - **Body** (`data-testid="admin-body"`): holds the `` that renders the active child page. # Side-nav entries The side-nav is defined as a static `ENTRIES` array in `frontend/src/app/features/admin/admin-shell.component.ts`: | Label | Path | Enabled | Renders | |-------------|-----------------------|---------|----------------------------------------------------------------------------------------------| | General | `/admin/general` | yes | [Admin — General Settings](/guides/admin-general-settings.md) (default — `/admin` redirects here). | | Challenges | `/admin/challenges` | yes | [Admin — Challenges](/guides/admin-challenges.md) — sortable list, search, Add/Edit modal, file staging, import/export. | | Players | `/admin/players` | yes | User management page. | | Blog | `/admin/blog` | yes | [Blog Publishing and Reading](/guides/blog.md) — post list with draft/publish, edit, preview, and delete workflows. | | System | `/admin/system` | no | Greyed-out placeholder. No route registered. | Each `
  • ` carries `data-testid="admin-nav-{id}"`. Disabled entries have `cursor: not-allowed` and `opacity: 0.45`. Clicking a disabled entry is a no-op (`go()` returns early when `!e.enabled`). The active entry is highlighted by `[class.active]` whenever the current URL starts with the entry's path. General is the default child (`/admin` → `/admin/general`). # Expected behavior | User role | Visiting `/admin` | |----------------------|---------------------------------------------------------| | Unauthenticated | Redirected to `/login` by `adminGuard`. | | Authenticated player | Redirected to `/` by `adminGuard`. | | Authenticated admin | Page renders, defaults to `/admin/general`. | * When `BootstrapService.initialized()` is still `false` the guard redirects to `/bootstrap` instead of `/login`, so the first admin flow is honored. * The Admin nav link in the shell header is hidden for non-admin users — there is no visible UI affordance to reach `/admin` without the role. * All admin HTTP requests use the existing `authInterceptor` + `csrfInterceptor`, so no extra plumbing is required. # Visual elements | Element | Selector | Purpose | |------------------------|-------------------------------------------|--------------------------------------------------------------------------| | Shell aside | `[data-testid="admin-aside"]` | Side-nav container. Heading "Admin area". | | Side-nav list | `[data-testid="admin-nav"]` | `