feat: Admin Area General Settings and Categories 1.05
This commit is contained in:
@@ -1,36 +0,0 @@
|
||||
# Implementation Plan: Admin Area General Settings and Categories 1.04
|
||||
|
||||
## 1. Architectural Reconnaissance
|
||||
- **Codebase style & conventions:** Node.js monorepo with a NestJS 10 REST backend and Angular 17 standalone frontend. Frontend components use `inject()`, Angular Signals, reactive forms, `ChangeDetectionStrategy.OnPush`, and async service methods backed by `HttpClient`/`firstValueFrom`. Global bootstrap data and CSS theme application are owned by the root-scoped `BootstrapService`; admin form persistence is owned by `AdminGeneralComponent` and `AdminService`.
|
||||
- **Current flow and root cause:** Application startup calls `BootstrapService.load()`, stores the `/api/v1/bootstrap` payload, and writes its theme tokens to `document.documentElement.style`. Saving `/admin/general` successfully persists `themeKey` through `PUT /api/v1/admin/general/settings`, and the backend later resolves the chosen theme in bootstrap. However, `AdminGeneralComponent.onSubmit()` only patches the returned settings form; it never asks `BootstrapService` to refresh or apply the newly selected theme. The backend's `general` SSE event contains only `themeKey`, while the frontend event-status stream treats every hub payload as event-state data and has no theme propagation path. Thus the save response changes persistence but not the current document tokens.
|
||||
- **Data Layer:** TypeORM with SQLite (`better-sqlite3`). The existing `setting` table stores `themeKey`; no schema or migration change is required. Persistent runtime data remains under `/data/hipctf` through existing setup conventions; the planned focused frontend tests require no persistent data.
|
||||
- **Test Framework & Structure:** Root Jest 29 multi-project configuration with `ts-jest`; frontend tests run in jsdom from `tests/frontend/**/*.spec.ts`, backend tests run in Node from `tests/backend/**/*.spec.ts`, and all tests are runnable with `npm test`. The fix should follow a minimal red-green-refactor path and test logic rather than visual appearance or browser automation.
|
||||
- **Required Tools & Dependencies:** No new system tools, global CLIs, npm packages, or `/data` assets are required. Existing Node/npm, Angular, Jest, jsdom, TypeScript, and `setup.sh` are sufficient; `setup.sh` and lockfiles should remain unchanged.
|
||||
|
||||
## 2. Impacted Files
|
||||
- **To Modify:**
|
||||
- `frontend/src/app/core/services/bootstrap.service.ts` — expose a typed, reusable live theme application/refresh path that updates both bootstrap client state and document CSS custom properties without being blocked by the startup request cache.
|
||||
- `frontend/src/app/features/admin/general.component.ts` — inject `BootstrapService` and invoke the live theme refresh only after `updateGeneralSettings()` succeeds, before reporting Save success.
|
||||
- **To Create:**
|
||||
- `tests/frontend/bootstrap-theme.spec.ts` — focused jsdom tests for applying all theme token fields and refreshing to a saved non-default theme.
|
||||
|
||||
## 3. Proposed Changes
|
||||
1. **Database / Schema Migration:**
|
||||
- No migration or backend persistence change. `AdminGeneralService.updateSettings()` already saves `SETTINGS_KEYS.THEME_KEY`, and `SystemService.bootstrap()` already reads that key and returns the corresponding complete theme object.
|
||||
2. **Backend Logic & APIs:**
|
||||
- Keep the existing `PUT /api/v1/admin/general/settings` and `GET /api/v1/bootstrap` contracts unchanged. The save response intentionally contains settings rather than theme tokens; reuse bootstrap as the canonical theme-token source instead of duplicating the backend theme model in the admin response.
|
||||
- Do not add backend endpoints, dependencies, SSE handling, or theme-token duplication. The reported same-tab Save issue can be resolved deterministically from the successful save path, while existing persistence guarantees future reloads use the selected theme.
|
||||
3. **Frontend UI Integration:**
|
||||
- Replace the frontend `theme: any` shape with local typed `Theme`/`ThemeTokens` interfaces matching the bootstrap contract, including colors, font family, radii, and spacing scale. Type the theme application method accordingly and preserve the defensive no-token guard for malformed/absent payloads.
|
||||
- Make theme application a reusable service operation rather than startup-only private behavior. It must continue setting all existing CSS variables (`--color-primary`, `--color-secondary`, `--color-accent`, `--color-surface`, `--color-text`, `--color-success`, `--color-warning`, `--color-danger`, `--font-family`, and all three radius properties), including valid zero-valued radius strings such as Monochrome's `0`.
|
||||
- Add a force-refresh method in `BootstrapService` that performs a fresh `/api/v1/bootstrap` request instead of reusing `loadPromise`, validates HTTP success before consuming the payload, updates `payload` and `initialized`, and applies the returned theme to the live document. Keep startup `load()`/`ready()` request coalescing intact so guard/bootstrap race behavior does not regress.
|
||||
- Inject `BootstrapService` into `AdminGeneralComponent`. After `AdminService.updateGeneralSettings()` resolves and the form is patched, await the force-refresh method before setting `saveOk`. This orders persistence before re-fetch, applies the canonical saved theme immediately, and also synchronizes other global bootstrap-backed settings in the current tab.
|
||||
- Treat a failed refresh as a Save error rather than showing a false success: retain the persisted form response, surface the existing inline error path, and leave `submitting` cleanup in `finally`. Do not mutate theme tokens before the PUT succeeds, so failed saves leave the current live theme unchanged.
|
||||
- No template or categories changes are needed; the Global Theme select already includes the server-provided canonical choices, and category behavior is unrelated to this regression.
|
||||
|
||||
## 4. Test Strategy
|
||||
- **Target Unit Test File:** `tests/frontend/bootstrap-theme.spec.ts`, executed by the existing root `npm test` command (or selectively through the frontend Jest project during development). Do not add visual, browser, or persistence-volume tests.
|
||||
- **Mocking Strategy:** Use jsdom's real `document.documentElement.style` and mock only global `fetch`. Reset CSS properties and the fetch mock between tests to avoid shared state. Provide small typed bootstrap payload fixtures rather than Angular TestBed or a backend/database instance.
|
||||
- **Core success path:** Start with Classic CSS values, have the mocked refresh return a Monochrome bootstrap payload, call the refresh method, and assert the payload signal plus representative computed CSS variables change immediately to `--color-primary: #000000`, `--color-text: #0a0a0a`, and `--radius-md: 0`; also verify all token mappings in one compact table-driven assertion so every canonical theme token category is covered.
|
||||
- **Key error condition:** Mock a non-OK bootstrap refresh response and assert the method rejects without replacing the existing payload or applying partial/new CSS tokens. This supports the component's existing save-error handling while keeping the suite minimal.
|
||||
- **Implementation verification:** Run the focused frontend test first to prove the regression, then `npm test`, `npm run build`, and the repository's available TypeScript builds. No lint script is defined in the root or workspace package manifests, so no additional lint dependency or setup change should be introduced.
|
||||
@@ -0,0 +1,173 @@
|
||||
# Implementation Plan: Admin General Settings — Event Start/End Validation (Job 887)
|
||||
|
||||
## Status
|
||||
|
||||
NOT already implemented. Two distinct gaps are described in the Job:
|
||||
|
||||
1. **Backend (`PUT /api/v1/admin/general/settings`)** accepts any string for
|
||||
`eventStartUtc` / `eventEndUtc` and persists it. Only the cross-field
|
||||
`end > start` rule is enforced. Garbage payloads like
|
||||
`eventStartUtc='not-a-date'` return HTTP 200 and overwrite the stored
|
||||
schedule.
|
||||
2. **Frontend (`/admin/general`)** has no per-field validation feedback for the
|
||||
two `datetime-local` inputs. The cross-field rule fires correctly
|
||||
(`form.invalid`, Save disabled), but the inputs render no `title`,
|
||||
`aria-invalid`, `aria-describedby`, `validationMessage`, or `[data-testid=...-error]`
|
||||
element when either field is empty/invalid individually.
|
||||
|
||||
## 1. Architectural Reconnaissance
|
||||
|
||||
- **Codebase style & conventions:**
|
||||
- Backend: NestJS controllers, Zod schemas in `*.dto.ts`, validated by the
|
||||
shared `ZodValidationPipe` (`backend/src/common/pipes/zod-validation.pipe.ts`)
|
||||
which throws `ApiError.validation('Request validation failed', details)`
|
||||
mapped to HTTP 400 `VALIDATION_FAILED`.
|
||||
- Frontend: Angular 17+ standalone components, signal-based state, `OnPush`,
|
||||
`ReactiveFormsModule`, pure helper module at
|
||||
`frontend/src/app/features/admin/general.pure.ts`. Inline error display
|
||||
pattern is already used for the `pageTitle` field
|
||||
(`general-pageTitle-error` + `pageTitleMessage(...)` computed signal +
|
||||
`showPageTitleError()` computed) — replicate this exact pattern for the
|
||||
two date inputs.
|
||||
- **Data Layer:** SQLite via `better-sqlite3`. `SettingsService` stores every
|
||||
general-settings key as a single string row in the `setting` table. No
|
||||
schema migration is required for this job — we are tightening input
|
||||
validation, not changing storage.
|
||||
- **Test Framework & Structure:**
|
||||
- Jest 29 with `ts-jest`, root config at `tests/jest.config.js`. Two
|
||||
projects: `backend` (`tests/backend/`) and `frontend`
|
||||
(`tests/frontend/`). Tests live ONLY in `/repo/tests/`.
|
||||
- Single command `npm test` runs everything.
|
||||
- **Required Tools & Dependencies:** None. We use the already-installed
|
||||
`zod` (datetime check is built into zod ≥ 3.20) and the existing
|
||||
Angular reactive-forms stack. No `setup.sh` changes are required.
|
||||
|
||||
## 2. Impacted Files
|
||||
|
||||
- **To Modify:**
|
||||
- `backend/src/modules/admin/dto/general.dto.ts` — tighten
|
||||
`eventStartUtc` / `eventEndUtc` to require real ISO-8601 datetimes.
|
||||
- `frontend/src/app/features/admin/general.component.ts` — add per-field
|
||||
datetime error rendering and inline error elements under both inputs.
|
||||
- `frontend/src/app/features/admin/general.pure.ts` — add pure helpers
|
||||
(`isoDatetimeValidator`, `datetimeMessage`) so the same logic is
|
||||
unit-testable in isolation.
|
||||
- `tests/backend/admin-general-service.spec.ts` — add negative cases for
|
||||
non-datetime payloads.
|
||||
- `tests/frontend/admin-general-pure.spec.ts` — add tests for the new
|
||||
validators + message helpers.
|
||||
- **To Create:** None. All changes fit inside existing files.
|
||||
|
||||
## 3. Proposed Changes
|
||||
|
||||
### 3.1 Backend — strict ISO-8601 datetime validation
|
||||
|
||||
In `backend/src/modules/admin/dto/general.dto.ts`:
|
||||
|
||||
- Replace the two loose `z.string()` declarations with zod's built-in
|
||||
`.datetime({ offset: false })` check (or
|
||||
`z.string().datetime({ message: '...' })` with explicit messages), so that
|
||||
`GeneralSettingsSchema` rejects non-ISO-8601 strings with a clear
|
||||
`INVALID_DATETIME` message before the request reaches
|
||||
`SettingsService.set(...)`. This guarantees:
|
||||
- The previously valid schedule is unchanged when validation fails (the
|
||||
service is never called).
|
||||
- The error is surfaced via the standard
|
||||
`ApiError.validation('Request validation failed', details)` envelope
|
||||
produced by `ZodValidationPipe` (HTTP 400 `VALIDATION_FAILED`).
|
||||
- Keep the existing `superRefine` end > start rule; it now runs only on
|
||||
values that already passed the per-field datetime check.
|
||||
- Empty string handling: the current UI sends `''` when the user clears an
|
||||
input, and the current code accepts `''`. To avoid regressing the
|
||||
"unconfigured event" state, allow `z.union([z.string().datetime(...), z.literal('')])`
|
||||
(an empty string is a valid "no event scheduled" value). Anything that
|
||||
is neither empty nor a valid ISO datetime is rejected.
|
||||
|
||||
### 3.2 Frontend — per-field datetime error rendering
|
||||
|
||||
In `frontend/src/app/features/admin/general.component.ts`:
|
||||
|
||||
- Apply `isoDatetimeValidator` (new, see 3.3) as a per-control validator
|
||||
on both `eventStartUtc` and `eventEndUtc` (alongside the existing
|
||||
group-level `endAfterStartValidator`).
|
||||
- For each input, add the `aria-describedby` / `aria-invalid` attributes
|
||||
and an inline `<div class="field-error" data-testid="general-eventStart-error">…</div>`
|
||||
(and the matching `general-eventEnd-error`) that renders when the
|
||||
control is touched/dirty AND invalid, driven by a `computed` signal —
|
||||
exactly mirroring the existing `showPageTitleError` /
|
||||
`general-pageTitle-error` pattern in this same file.
|
||||
- The message text comes from the new `datetimeMessage(value, errors)`
|
||||
pure helper (see 3.3). Messages:
|
||||
- non-empty value that fails `Date.parse` →
|
||||
`"Event start must be a valid ISO-8601 datetime."` (or "Event end").
|
||||
- empty value → no message (empty is the intentional "unconfigured"
|
||||
state; the `endBeforeStart` group validator already stays quiet when
|
||||
one side is empty).
|
||||
- The existing `general-endBeforeStart` element stays put; it now
|
||||
coexists with the two new per-field error nodes.
|
||||
|
||||
### 3.3 Frontend pure helpers
|
||||
|
||||
In `frontend/src/app/features/admin/general.pure.ts`, add:
|
||||
|
||||
```ts
|
||||
export function isoDatetimeValidator(ctrl: { value: string | null }) {
|
||||
const v = ctrl?.value ?? '';
|
||||
if (v === '') return null; // unconfigured is allowed
|
||||
const t = Date.parse(v);
|
||||
return Number.isFinite(t) ? null : { invalidDatetime: true };
|
||||
}
|
||||
|
||||
export function datetimeMessage(
|
||||
fieldLabel: string,
|
||||
value: string | null | undefined,
|
||||
controlErrors: { [k: string]: unknown } | null | undefined,
|
||||
): string | null {
|
||||
if (controlErrors?.['invalidDatetime']) {
|
||||
return `${fieldLabel} must be a valid ISO-8601 datetime.`;
|
||||
}
|
||||
// also catch the case where the user typed something unparseable but
|
||||
// the validator did not run yet (defensive)
|
||||
const v = value ?? '';
|
||||
if (v !== '' && !Number.isFinite(Date.parse(v))) {
|
||||
return `${fieldLabel} must be a valid ISO-8601 datetime.`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
These are exported from the existing pure file so the component can call
|
||||
them directly and the spec file can exercise them in isolation.
|
||||
|
||||
### 3.4 No persistence side-effects on rejection
|
||||
|
||||
This is already enforced by the existing `ZodValidationPipe` (the
|
||||
service is never invoked when validation fails), so no further change is
|
||||
needed there.
|
||||
|
||||
## 4. Test Strategy
|
||||
|
||||
- **Target Unit Test Files:**
|
||||
- `tests/backend/admin-general-service.spec.ts` — extend the existing
|
||||
`GeneralSettingsSchema - validation rules` describe block with two
|
||||
cases:
|
||||
- `eventStartUtc = 'not-a-date'` → schema returns `success: false`.
|
||||
- `eventStartUtc = '2026-08-15T10:30:00.000Z'` and
|
||||
`eventEndUtc = 'also-not-a-date'` → schema returns
|
||||
`success: false`, and the issue message includes `'eventEndUtc'`
|
||||
/ contains the word `datetime` (to assert the new error is the
|
||||
source of the rejection, not the cross-field rule).
|
||||
- `tests/frontend/admin-general-pure.spec.ts` — extend with:
|
||||
- `isoDatetimeValidator` returns `null` for empty input.
|
||||
- `isoDatetimeValidator` returns `null` for a valid ISO string.
|
||||
- `isoDatetimeValidator` returns `{ invalidDatetime: true }` for
|
||||
`'not-a-date'`.
|
||||
- `datetimeMessage` returns the correct per-field message for
|
||||
`'Event start'` / `'Event end'` and returns `null` for empty /
|
||||
valid input.
|
||||
- **Mocking Strategy:** Pure-function tests need no mocks. The existing
|
||||
backend spec already constructs the schema in-process; we just feed
|
||||
it bad payloads and inspect `safeParse` results. No DB, no HTTP
|
||||
server, no Angular `TestBed` are required.
|
||||
- **Command:** `npm test` (or the narrower
|
||||
`npm run test:backend` / `npm run test:frontend`).
|
||||
Reference in New Issue
Block a user