9.1 KiB
9.1 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | |||||
|---|---|---|---|---|---|---|---|---|---|
| guide | Admin — General Settings | How an admin edits global platform settings (page title, logo, theme, event window, default challenge IP, registrations, welcome Markdown) from the /admin/general page. |
|
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 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)
- Sign in as an admin user (see First-Run Bootstrap if no admin exists).
- Open the username menu in the shell header and click Admin area,
or use the side-nav's General entry, or visit
/admin/generaldirectly. - The page renders a
Loading settings...placeholder while the initialGET /api/v1/admin/general/settings+GET .../themesrequests 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, non-blank, max 120 chars; trimmed server-side. |
| 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() === truerendersgeneral-loading. After both requests resolve, the form is patched with the values from the backend (UTC timestamps are converted todatetime-localstrings viatoDatetimeLocalso the native picker shows them). - Logo upload: selecting a file fires
POST /api/v1/uploads/logo, then writes the returnedpublicUrlinto the hiddenlogocontrol. If the upload fails,general-logo-errorshows the message; the previous logo is preserved. - Welcome Markdown preview: every keystroke in
general-welcometriggersMarkdownService.renderand updatesgeneral-welcome-previewsynchronously. - Event-state derivation: the disabled
general-event-togglelabel is computed fromderiveEventState(start, end):- both empty →
UNCONFIGURED now < start→COUNTDOWNstart <= now < end→RUNNINGnow >= end→STOPPED
- both empty →
- End-before-start validation: if the user picks an end that is not
strictly after the start, the form becomes invalid and
general-endBeforeStartappears under the end input. Save remains disabled. - Save: clicking Save sends
PUT /api/v1/admin/general/settingswith all fields. The Page title input is validated client-side for non-blank content; an empty or whitespace-only Page title disables the Save button and rendersgeneral-pageTitle-errorso the request is never issued. The server trims surrounding whitespace and re-validates against the same 1–120 character rule, so invalid payloads return400 VALIDATION_FAILEDand the stored Page title is unchanged. On success the form is patched with the response,general-save-okrenders briefly, and the backend emits an SSEgeneralevent viaSseHubServiceso other tabs refresh their theme. - Error states: load failures render
general-error; save failures rendergeneral-save-errorwith theerror.message(orerror.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
generalSSE 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
settingtable; the UI converts to/fromdatetime-localfor display. - Saving the form requires the page title and default challenge IP to
be non-empty; the form is
invalidand Save stays disabled until they are.
See also
- Admin Shell — side-nav layout and guard chain.
- Admin — Categories — categories management page that renders below General settings.
- Admin Endpoints —
GET/PUT /api/v1/admin/general/*reference. - Uploads Endpoints —
POST /api/v1/uploads/logoreference. - Backend Module Map