Files
HIPCTF2/docs/guides/admin-general-settings.md
T

127 lines
8.7 KiB
Markdown

---
type: guide
title: Admin — General Settings
description: How an admin edits global platform settings (page title, logo, theme, event window, default challenge IP, registrations, welcome Markdown) from the /admin/general page.
tags: [guide, admin, settings, general, tester]
timestamp: 2026-07-22T12:00:00Z
---
# When this view is available
The General Settings page is rendered at `/admin/general` for users with
`role === 'admin'` once the instance is initialized. It is reached from
the [Admin Shell](/guides/admin-shell.md) side-nav (`General` entry) or
by navigating directly to the URL.
| Layer | File | Check |
|------------------|----------------------------------------------------------------------------|------------------------------------------------|
| Client route | `frontend/src/app/app.routes.ts` | `/admin/general` child of `adminGuard`. |
| Client component | `frontend/src/app/features/admin/general.component.ts` | `AdminGeneralComponent.ngOnInit` fetches settings + themes. |
| Client predicate | `frontend/src/app/features/admin/general.pure.ts` (`deriveEventState`) | Computes the read-only event-state label. |
| Server route | `backend/src/modules/admin/admin-general.controller.ts` | `GET/PUT /api/v1/admin/general/settings` + `GET /api/v1/admin/general/themes`. |
# How to access (tester steps)
1. Sign in as an admin user (see [First-Run Bootstrap](/guides/bootstrap.md)
if no admin exists).
2. Open the username menu in the shell header and click **Admin area**,
or use the side-nav's **General** entry, or visit `/admin/general`
directly.
3. The page renders a `Loading settings...` placeholder while the
initial `GET /api/v1/admin/general/settings` + `GET .../themes`
requests are in flight.
# Fields
The page is a single reactive form with these controls (every
`data-testid` listed is asserted in the existing test suite):
| Label | `data-testid` | Backend field | Notes |
|------------------------|---------------------------|-------------------------|-------|
| Page title | `general-pageTitle` | `pageTitle` | Required, max 120 chars. |
| Logo (file picker) | `general-logo-file` | `logo` (public URL) | Uploads via `POST /api/v1/uploads/logo`; the returned `publicUrl` is bound to a hidden input `general-logo`. |
| Logo upload status | `general-logo-uploading` / `general-logo-error` / `general-logo-current` | — | Inline status text under the file picker. |
| Global theme | `general-themeKey` | `themeKey` | `<select>` populated from `/themes`. Value is one of `THEME_IDS`. |
| Event start (UTC) | `general-eventStart` | `eventStartUtc` | `datetime-local` input converted to ISO UTC on save. |
| Event end (UTC) | `general-eventEnd` | `eventEndUtc` | Must be strictly after Event start. Invalid pair renders `general-endBeforeStart`. |
| Default challenge IP | `general-defaultIp` | `defaultChallengeIp` | Required, max 255 chars. |
| Enable registrations | `general-registrations` | `registrationsEnabled` | Boolean checkbox. When `false`, the public register endpoint returns `REGISTRATIONS_DISABLED`. |
| Welcome description | `general-welcome` | `welcomeMarkdown` | Multi-line textarea; a live preview is rendered into `general-welcome-preview`. |
| Event controls | `general-event-toggle` | (derived) | Disabled button whose text is the derived event state (`UNCONFIGURED` / `COUNTDOWN` / `RUNNING` / `STOPPED`). |
| Save button | `general-save` | — | Disabled while `submitting()` or `form.invalid`. |
| Save error / success | `general-save-error` / `general-save-ok` | — | Inline status. |
# Expected behavior
* **Initial load:** `loading() === true` renders `general-loading`. After
both requests resolve, the form is patched with the values from the
backend (UTC timestamps are converted to `datetime-local` strings via
`toDatetimeLocal` so the native picker shows them).
* **Logo upload:** selecting a file fires `POST /api/v1/uploads/logo`,
then writes the returned `publicUrl` into the hidden `logo` control.
If the upload fails, `general-logo-error` shows the message; the
previous logo is preserved.
* **Welcome Markdown preview:** every keystroke in `general-welcome`
triggers `MarkdownService.render` and updates `general-welcome-preview`
synchronously.
* **Event-state derivation:** the disabled `general-event-toggle` label
is computed from `deriveEventState(start, end)`:
* both empty → `UNCONFIGURED`
* `now < start``COUNTDOWN`
* `start <= now < end``RUNNING`
* `now >= end``STOPPED`
* **End-before-start validation:** if the user picks an end that is not
strictly after the start, the form becomes invalid and
`general-endBeforeStart` appears under the end input. Save remains
disabled.
* **Save:** clicking Save sends `PUT /api/v1/admin/general/settings`
with all fields. On success the form is patched with the response,
`general-save-ok` renders briefly, and the backend emits an SSE
`general` event via `SseHubService` so other tabs refresh their theme.
* **Error states:** load failures render `general-error`; save failures
render `general-save-error` with the `error.message` (or
`error.error.message`) from the standard envelope.
# Visual elements
| Element | Selector |
|-------------------------------|-----------------------------------------------------|
| Page section | `[data-testid="admin-general"]` |
| Loading placeholder | `[data-testid="general-loading"]` |
| Load error | `[data-testid="general-error"]` |
| Form | `[data-testid="general-form"]` |
| Welcome preview | `[data-testid="general-welcome-preview"]` |
| End-before-start message | `[data-testid="general-endBeforeStart"]` |
# Architecture map
| Step | Where | What happens |
|------|------------------------------------------------------|---------------------------------------------------------------------------|
| 1 | `frontend/src/app/app.routes.ts` | `/admin/general` lazy-loads `AdminGeneralComponent`. |
| 2 | `frontend/src/app/features/admin/general.component.ts` | `ngOnInit` calls `AdminService.getGeneralSettings()` + `listAdminThemes()` in parallel. |
| 3 | `frontend/src/app/core/services/admin.service.ts` | `getGeneralSettings()``GET /api/v1/admin/general/settings`; `listAdminThemes()``GET /api/v1/admin/general/themes`; `updateGeneralSettings()``PUT .../settings`; `uploadLogo()``POST /api/v1/uploads/logo`. |
| 4 | `backend/src/modules/admin/admin-general.controller.ts` | `AdminGuard` + `@Roles('admin')` on every handler. |
| 5 | `backend/src/modules/admin/general.service.ts` | `getSettings` reads 8 keys via `SettingsService`; `updateSettings` writes all 8, emits `{ topic: 'general', themeKey }` via `SseHubService`. |
| 6 | `backend/src/modules/admin/dto/general.dto.ts` | `GeneralSettingsSchema` enforces string lengths, `themeKey` enum, and `eventEndUtc > eventStartUtc` via `superRefine`. |
# Notes
* The `general` SSE event (`{ topic: 'general', themeKey }`) is a
lightweight signal so authenticated tabs can pick up the new theme
without polling. Other tabs do not auto-refresh settings values.
* The "Event controls" toggle is intentionally a derived display, not
an editable control — adjust the UTC timestamps to change state.
* All timestamps are stored as ISO-8601 UTC strings in the `setting`
table; the UI converts to/from `datetime-local` for display.
* Saving the form requires the page title and default challenge IP to
be non-empty; the form is `invalid` and Save stays disabled until
they are.
# See also
- [Admin Shell](/guides/admin-shell.md) — side-nav layout and guard chain.
- [Admin — Categories](/guides/admin-categories.md) — categories management page that renders below General settings.
- [Admin Endpoints](/api/admin.md) — `GET/PUT /api/v1/admin/general/*` reference.
- [Uploads Endpoints](/api/uploads.md) — `POST /api/v1/uploads/logo` reference.
- [Backend Module Map](/architecture/backend-modules.md)