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
datetime
validation
2026-07-22T14:07:58Z
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. Empty string is allowed; any non-empty value must be a valid ISO-8601 datetime — invalid input renders general-eventStart-error.
Event end (UTC)
general-eventEnd
eventEndUtc
datetime-local input. Empty string is allowed; any non-empty value must be a valid ISO-8601 datetime — invalid input renders general-eventEnd-error. Must also be strictly after Event start (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.
Per-field datetime validation: if the user types (or leaves) a value
that is not parseable as an ISO-8601 datetime in either the Event start
or Event end input, the affected input flips aria-invalid="true" and
its inline error element (general-eventStart-error or
general-eventEnd-error) renders the message
"<Field> must be a valid ISO-8601 datetime.". Empty strings are
considered valid (the event window may be unconfigured), so clearing
both inputs removes the error. Save remains disabled while either input
is invalid.
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.
Event start / end inline error messages
The inline error elements under the Event start and Event end inputs
(general-eventStart-error and general-eventEnd-error) are driven by
two pure helpers exported from
frontend/src/app/features/admin/general.pure.ts:
isoDatetimeValidator(ctrl) — an Angular reactive-forms validator
that returns null for an empty value and { invalidDatetime: true }
for any non-empty value that Date.parse cannot parse. Empty is
intentionally valid so the event window can be cleared.
datetimeMessage(fieldLabel, value, controlErrors) — renders the
human-readable message "<Field> must be a valid ISO-8601 datetime."
either when controlErrors.invalidDatetime is set, or as a fallback
when the raw value itself is non-empty and unparseable.
The component wires both controls (eventStartUtc, eventEndUtc) with
this validator and keeps three local signals per field — Value,
Invalid, TouchedOrDirty — exactly mirroring the Page-title pattern.
The computed signals showEventStartError / showEventEndError and
eventStartMessage / eventEndMessage are then bound to:
[attr.aria-invalid] and [attr.aria-describedby] on each input so
assistive tech is informed the instant the field goes invalid.
An @if block that renders the inline <div class="field-error">
with data-testid="general-eventStart-error" /
data-testid="general-eventEnd-error".
Condition
Message
Empty value ('')
null (no error; the event window may be unconfigured).
Non-empty value that Date.parse accepts
null (no error).
Non-empty value that Date.parse rejects
"<Field> must be a valid ISO-8601 datetime."
Control carries invalidDatetime error
Same message (covered by the validator path).
The error is shown only when the control is both touched || dirty
AND invalid, so the inputs do not flash errors on initial load. After
a successful Save, both controls are reset to untouched/pristine via
resetTouchedState() and their TouchedOrDirty signals are cleared.
The cross-field endBeforeStart error (general-endBeforeStart) is
still emitted from the form-level superRefine/validator chain and is
unchanged — it now sits next to the new per-field messages when the
user supplies two non-empty values that are out of order.
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, per-field ISO-8601 datetime check on eventStartUtc/eventEndUtc (empty string allowed), 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.