AI Implementation feature(889): Admin Area General Settings and Categories 1.07 (#29)

This commit was merged in pull request #29.
This commit is contained in:
2026-07-22 14:53:01 +00:00
parent 3cf86b6233
commit 282fcbab94
11 changed files with 359 additions and 115 deletions
+3 -3
View File
@@ -3,7 +3,7 @@ type: database
title: Challenge Tables
description: category, challenge, challenge_file, and solve tables — how CTF challenges and scoring are stored.
tags: [database, challenge, category, solve]
timestamp: 2026-07-21T14:18:00Z
timestamp: 2026-07-22T14:50:25Z
---
# Tables
@@ -18,8 +18,8 @@ timestamp: 2026-07-21T14:18:00Z
| `abbreviation` | TEXT | Short label (e.g. `CRY`), 26 chars, server-uppercased. Globally unique — enforced by `uq_category_abbreviation`. |
| `description` | TEXT | Optional description. |
| `icon_path` | TEXT | Default icon URL (e.g. `/uploads/icons/CRY.png`). |
| `created_at` | TEXT | ISO 8601 timestamp the row was created (added by `AddCategoryTimestampsAndUniqueAbbrev1700000000200`). |
| `updated_at` | TEXT | ISO 8601 timestamp the row was last updated (added by the same migration; bumped on `PUT /api/v1/admin/categories/:id`). |
| `created_at` | TEXT | ISO 8601 timestamp the row was created. Declared in the initial `InitSchema1700000000000` migration (defaulting to `strftime('%Y-%m-%dT%H:%M:%fZ','now')`); the `AddCategoryTimestampsAndUniqueAbbrev1700000000200` migration adds it as a no-op for older pre-existing databases. |
| `updated_at` | TEXT | ISO 8601 timestamp the row was last updated (bumped on `PUT /api/v1/admin/categories/:id`). Same provenance as `created_at`. |
System rows are seeded by the
`UpdateSystemCategoryKeys1700000000300` migration so the canonical
+7 -1
View File
@@ -3,7 +3,7 @@ type: database
title: Database Schema Overview
description: SQLite (better-sqlite3) schema for HIPCTF: tables, relationships, indexes, and WAL journal mode.
tags: [database, sqlite, typeorm, schema]
timestamp: 2026-07-21T14:18:00Z
timestamp: 2026-07-22T14:50:25Z
---
# Overview
@@ -17,6 +17,12 @@ The schema is created by a single migration
(`backend/src/database/migrations/1700000000000-InitSchema.ts`) and seeded
by a second migration
(`backend/src/database/migrations/1700000000100-SeedSystemData.ts`).
The initial migration declares every column used by the entities
(including `category.created_at` and `category.updated_at`), so the
later `AddCategoryTimestampsAndUniqueAbbrev1700000000200` and
`UpdateSystemCategoryKeys1700000000300` migrations only patch legacy
databases that pre-date those columns and reseed the canonical system
rows.
# Tables
+25 -13
View File
@@ -3,7 +3,7 @@ type: guide
title: Admin — General Settings
description: How an admin edits required platform-wide settings, including the validated event window, from the /admin/general page.
tags: [guide, admin, settings, general, tester, datetime, validation]
timestamp: 2026-07-22T14:24:08Z
timestamp: 2026-07-22T14:50:25Z
---
# When this view is available
@@ -17,7 +17,7 @@ by navigating directly to the URL.
|------------------|----------------------------------------------------------------------------|------------------------------------------------|
| 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. |
| Client predicate | `frontend/src/app/features/admin/general.pure.ts` (`deriveEventState`, `pageTitleError`, `pageTitleMessage`, `eventEndFieldMessage`) | Pure helpers for event-state derivation, Page-title error/message mapping, and the merged Event End field message (per-field `invalidDatetime` + cross-field `endBeforeStart`). |
| 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)
@@ -43,7 +43,7 @@ The page is a single reactive form with these controls (every
| 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`). |
| 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 — when it is not, the same `general-eventEnd-error` region displays "Event end must be after event start." |
| 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`. |
@@ -71,9 +71,9 @@ The page is a single reactive form with these controls (every
* `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.
strictly after the start, the form becomes invalid and the
`general-eventEnd-error` region under the end input renders
"Event end must be after event start." 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
@@ -136,6 +136,16 @@ two pure helpers exported from
* `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.
* `eventEndFieldMessage(controlErrors, crossFieldErrors)` — returns the
canonical message for the Event End field's own error region. It
prefers the per-field ISO message
(`"Event end must be a valid ISO-8601 datetime."`) when
`controlErrors?.['invalidDatetime']` is set, otherwise it surfaces the
cross-field `endBeforeStart` message
(`"Event end must be after event start."`), otherwise `null`. The
component reads this helper through the `eventEndFieldMessageText`
computed signal so both error kinds render inside the single
`general-eventEnd-error` element.
The component wires both controls (`eventStartUtc`, `eventEndUtc`) with
this validator and keeps three local `signal`s per field — `Value`,
@@ -160,11 +170,14 @@ 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.
The cross-field `endBeforeStart` error is emitted by the form-level
validator whenever both valid timestamps are present and end is not
strictly after start. The Event end input receives `aria-invalid="true"`,
references `general-eventEnd-error` via `aria-describedby`, and exposes
`Event end must be after event start.` as its title — the message itself
is rendered inside the per-field error region
(`data-testid="general-eventEnd-error"`) by the
`eventEndFieldMessage` helper.
# Visual elements
@@ -175,9 +188,8 @@ and exposes `Event end must be after event start.` as its title.
| 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"]` |
| Event end inline error | `[data-testid="general-eventEnd-error"]` (renders both the per-field `invalidDatetime` message and the cross-field `endBeforeStart` "Event end must be after event start." message) |
| Page-title inline error | `[data-testid="general-pageTitle-error"]` |
# Architecture map
+1 -1
View File
@@ -11,7 +11,7 @@ scoreboard, an event window with a public countdown, theming, and admin
controls.
The docs below are organized by purpose so agents can pull just the slice
they need. Last regenerated 2026-07-22T14:24:08Z.
they need. Last regenerated 2026-07-22T14:50:25Z.
# Architecture