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

15 KiB
Raw Blame History

type, title, description, tags, timestamp
type title description tags timestamp
guide Admin — General Settings How an admin edits required platform-wide settings, including the validated event window, from the /admin/general page.
guide
admin
settings
general
tester
datetime
validation
2026-07-22T14:24:08Z

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, pageTitleError, pageTitleMessage) Pure helpers for event-state derivation and Page-title error/message mapping.
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 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, 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 Required valid ISO-8601 datetime. Empty or malformed input renders general-eventStart-error and disables Save.
Event end (UTC) general-eventEnd eventEndUtc Required valid ISO-8601 datetime. Empty or malformed input renders general-eventEnd-error; it 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 < startCOUNTDOWN
    • start <= now < endRUNNING
    • now >= endSTOPPED
  • 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: clearing a field or supplying a value that cannot be parsed as an ISO-8601 datetime makes that control invalid. The affected input receives aria-invalid="true", references its inline error through aria-describedby, and renders "<Field> must be a valid ISO-8601 datetime.". Save remains disabled until both timestamps are valid and ordered correctly.
  • 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 1120 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 only when Date.parse accepts the value. Empty and malformed values return { invalidDatetime: true }.
  • datetimeMessage(fieldLabel, value, controlErrors) — renders the human-readable message "<Field> must be a valid ISO-8601 datetime." when invalidDatetime is set or when the raw value is 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 ('') "<Field> must be a valid ISO-8601 datetime."
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 emitted by the form-level validator whenever both valid timestamps are present and end is not strictly after start. The Event end input also receives aria-invalid="true", references both possible error elements, and exposes Event end must be after event start. as its title.

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"]
Event start inline error [data-testid="general-eventStart-error"]
Event end inline error [data-testid="general-eventEnd-error"]
Page-title inline error [data-testid="general-pageTitle-error"]

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 requires ISO-8601 values for both event-window fields and enforces 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 valid timestamps are stored as ISO-8601 UTC strings in the setting table; the UI converts to/from datetime-local for display.
  • Both timestamps are required by the General Settings form and API. The UNCONFIGURED derived display remains a defensive state for missing or invalid data, but clearing either input makes the form unsavable.
  • 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