How an admin edits global platform settings (page title, logo, theme, event window, default challenge IP, registrations, welcome Markdown) from the /admin/general page.
guide
admin
settings
general
tester
2026-07-22T12:44:45Z
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.
<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. The Page title input is validated client-side for
non-blank content; an empty or whitespace-only Page title disables the
Save button and renders general-pageTitle-error so the request is
never issued. The server trims surrounding whitespace and re-validates
against the same 1–120 character rule, so invalid payloads return
400 VALIDATION_FAILED and the stored Page title is unchanged.
On success the form is patched with the response, the Page-title
control is marked untouched/pristine (so a freshly-loaded valid
form does not flash an error), 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.
Page-title inline error message
The inline error message under the Page-title field
(general-pageTitle-error) is driven by the pure helper
pageTitleMessage(value, controlErrors, maxLength = 120) exported from
frontend/src/app/features/admin/general.pure.ts:10-22. The component
keeps three local signals — pageTitleValue, pageTitleInvalid,
pageTitleTouchedOrDirty — and subscribes to the reactive form
control's valueChanges and statusChanges (via
takeUntilDestroyed(this.destroyRef)) so the computeds for
showPageTitleError and pageTitleMessage re-evaluate when the user
types or blurs the input. The helper returns:
Condition
Message
Empty or whitespace-only value
Page title is required and cannot contain only whitespace.
Value length > 120
Page title must be 120 characters or fewer.
Control has required, maxlength, or whitespace error
Matching human-readable message (whitespace falls back to "required" wording).
Valid value
null (no message rendered, Save becomes enabled).
As a result, the general-pageTitle-error element renders as soon as
the Page-title control becomes both touched || dirty AND invalid,
without requiring a full component re-render. After a successful
Save, the control is reset to untouched/pristine and the
pageTitleTouchedOrDirty signal is cleared, so reloading valid
settings does not flash a stale error.
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.
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.