From 73cfbb9af0dea9527845eee66d2eec145ef02d8f Mon Sep 17 00:00:00 2001 From: OpenVelo Agent Date: Wed, 22 Jul 2026 19:19:43 +0000 Subject: [PATCH] docs: update documentation to OKF v0.1 format --- docs/api/uploads.md | 15 +++++++++- docs/architecture/key-files.md | 6 ++-- docs/guides/admin-categories.md | 49 +++++++++++++++++++++++++++++---- docs/index.md | 2 +- 4 files changed, 61 insertions(+), 11 deletions(-) diff --git a/docs/api/uploads.md b/docs/api/uploads.md index 79c96a5..f8a604b 100644 --- a/docs/api/uploads.md +++ b/docs/api/uploads.md @@ -3,7 +3,7 @@ type: api title: Uploads Endpoints description: Admin-only multipart upload endpoints for site logos, category icons, and challenge files. tags: [api, uploads, multipart, admin] -timestamp: 2026-07-22T13:05:30Z +timestamp: 2026-07-22T19:18:00Z --- # Endpoints @@ -37,6 +37,19 @@ valid admin session and the standard CSRF cookie/header pair. * An accepted logo keeps its sanitized filename, is written directly under `UPLOAD_DIR`, and returns `{ publicUrl, originalFilename }`, with `publicUrl` in the form `/uploads/{safeFilename}`. +* Category icons are decoded with Sharp and normalized to a 128-by-128 PNG. + When `categoryId` is supplied, the file is written to + `UPLOAD_DIR/icons/{categoryId}.png` (overwriting any previous icon for that + category) and the response carries `{ id: "{categoryId}.png", publicUrl, + width: 128, height: 128, mimeType: "image/png", storedPath, size, + originalFilename }`. +* Category-icon validation is strict: a missing `file`, an oversized upload, + a corrupt/non-image payload, or a buffer that Sharp cannot decode returns + `400 Bad Request`. Invalid category-icon payloads use the message + `Category icon must be a valid PNG, JPEG, GIF, or WebP image.` and **are + not persisted** — the existing icon file (if any) on disk is left untouched. + The multipart MIME type, the filename extension, and any browser `accept` + metadata are not trusted as proof of validity. * A missing `file` part, an oversized upload, a corrupt image, or a decoded logo in another format returns `400 Bad Request`. Invalid logo payloads use the message `Logo must be a valid PNG, JPEG, GIF, or WebP image.` and are not diff --git a/docs/architecture/key-files.md b/docs/architecture/key-files.md index 0a2c4d3..d7e3a95 100644 --- a/docs/architecture/key-files.md +++ b/docs/architecture/key-files.md @@ -3,7 +3,7 @@ type: architecture title: Key Files Index description: One-line responsibility for important source and contract-test files, including strict event-window validation and the public bootstrap SSE listener. tags: [architecture, key-files, event-window, validation, default-challenge-ip, sse, bootstrap, migrations] -timestamp: 2026-07-22T18:37:00Z +timestamp: 2026-07-22T19:18:00Z --- # Backend @@ -25,7 +25,7 @@ timestamp: 2026-07-22T18:37:00Z | `backend/src/modules/admin/general.service.ts` | Reads/writes global settings, filters available theme files, and publishes the `general` SSE notification after updates. | | `backend/src/modules/auth/auth.controller.ts` | Registers authentication and account endpoints. | | `backend/src/modules/auth/auth.service.ts` | Handles sessions, authentication, registration, and password changes. | -| `backend/src/modules/uploads/uploads.controller.ts` | Registers admin-only multipart uploads, including Sharp-backed site-logo format validation. | +| `backend/src/modules/uploads/uploads.controller.ts` | Registers admin-only multipart uploads, including Sharp-backed site-logo format validation and strict category-icon decode (rejects undecodable / non-image buffers with HTTP 400 before any filesystem write so existing icons are preserved). | | `backend/src/modules/admin/dto/general.dto.ts` | Zod contract for `PUT /api/v1/admin/general/settings`; both event timestamps are required ISO-8601 values and end must be strictly after start. | | `tests/backend/admin-general-event-window.spec.ts` | Focused contract tests for valid, empty, malformed, equal, and reversed event-window timestamps. | | `tests/backend/admin-general-service.spec.ts` | General-settings schema and service tests, including per-field empty datetime failures and settings-event emission. | @@ -52,7 +52,7 @@ timestamp: 2026-07-22T18:37:00Z | `frontend/src/app/features/shell/tabs/quick-tabs.component.ts` | Main shell navigation tabs. | | `frontend/src/app/features/shell/change-password/change-password-modal.component.ts` | Change-password form modal. | | `frontend/src/app/features/admin/general.component.ts` | `AdminGeneralComponent` reactive form for `/admin/general` — per-field inline error rendering (page-title + event-start + event-end), logo upload wiring, welcome Markdown preview, event-state derivation, and SSE `general` event handling. | -| `frontend/src/app/features/admin/categories/category-form-modal.component.ts` | Standalone OnPush modal for create + edit; owns the `CategoryFormGroup`, exposes the pure `syncCategoryForm` helper, and reacts to `open` / `mode` / `category` signal inputs via a `markForCheck` effect so edit prefill reaches the DOM. | +| `frontend/src/app/features/admin/categories/category-form-modal.component.ts` | Standalone OnPush modal for create + edit; owns the `CategoryFormGroup`, exposes the pure `syncCategoryForm` helper, binds `[formGroup]` on its template `
`, accepts `errorMessage` + `saving` parent inputs (renders `cf-error` and disables OK while saving), and reacts to `open` / `mode` / `category` signal inputs via a `markForCheck` effect so edit prefill reaches the DOM. | | `frontend/src/app/features/admin/general.pure.ts` | Pure General Settings helpers, including required datetime validation, field messages, UTC conversion, end-after-start validation, and the default-challenge-address IPv4/hostname validator/normalizer/message trio. | | `tests/frontend/admin-general-pure.spec.ts` | Pure client-contract tests for required event timestamps, datetime messaging, UTC conversion, event-window ordering, and default-challenge-address validation, error mapping, and normalization. | | `tests/frontend/admin-categories-form-modal.spec.ts` | Tests the pure `syncCategoryForm` helper that drives the edit/create prefill in `CategoryFormModalComponent`: system-row abbreviation lock, user-row unlock, re-population on second invocation, clearing on create, and iconPreview passthrough. | diff --git a/docs/guides/admin-categories.md b/docs/guides/admin-categories.md index e9780f2..ba7869d 100644 --- a/docs/guides/admin-categories.md +++ b/docs/guides/admin-categories.md @@ -3,7 +3,7 @@ type: guide title: Admin — Categories description: How an admin lists, creates, edits, and deletes challenge categories from the /admin/categories page, including system-row protection and challenge-attached protection. tags: [guide, admin, categories, tester] -timestamp: 2026-07-22T18:37:00Z +timestamp: 2026-07-22T19:18:00Z --- # When this view is available @@ -45,6 +45,7 @@ side-nav (categories are also embedded inside the General settings page). | Form modal | `[data-testid="cat-form-modal"]` | Create/edit dialog (`cat-form-backdrop` is the click-outside dismiss layer). | | Delete modal | `[data-testid="cat-delete-modal"]` | Confirmation dialog (`cat-delete-backdrop` is the dismiss layer). | | Delete error | `[data-testid="cat-delete-error"]` | Inline error message after a failed delete. | +| Form error | `[data-testid="cf-error"]` | Inline error rendered inside the form modal after a failed create/update/upload. | # Form modal fields @@ -54,7 +55,7 @@ side-nav (categories are also embedded inside the General settings page). | Abbreviation (uppercase) | `cf-abbr` | Required, 2–6 chars; server upper-cases on save. `readonly` when editing a system row. | | Description | `cf-desc` | Optional, max 2000 chars. | | Icon (file picker) | `cf-icon` | Optional image; uploaded to `POST /api/v1/uploads/category-icon` and resized/normalized server-side. | -| Cancel / OK | `cf-cancel`, `cf-ok` | OK disabled while form invalid or already submitting. | +| Cancel / OK | `cf-cancel`, `cf-ok` | OK disabled while form invalid, already submitting, or while the parent is performing the async save (bound `saving` input). | # Expected behavior @@ -84,10 +85,23 @@ side-nav (categories are also embedded inside the General settings page). 1. Click `cat-edit-{ABBR}` → modal opens in edit mode, pre-filled with the row's values. -2. For system rows, the abbreviation field is `readonly`. -3. On save the component issues `updateCategory(id, {...})`. If an icon - file is selected, the new icon is uploaded first and the returned - `publicUrl` replaces `iconPath` in the same update. +2. For system rows, the abbreviation field is `readonly`. For user + (non-system) rows the abbreviation is editable and is uppercased + server-side on save (the existing category row is re-sorted + alphabetically by the defensive client sort after a successful + update). +3. On save the component issues `updateCategory(id, {...})` with + `name`, `abbreviation`, `description`, and (if an icon file was + chosen) `iconPath`. When an icon file is selected, the new icon is + uploaded first via `POST /api/v1/uploads/category-icon` and the + returned `publicUrl` replaces `iconPath` in the same update. +4. On either an upload rejection (HTTP 400) or an update rejection + (HTTP 400/404/409), the modal stays open, the OK button is disabled + while the request is in flight via the bound `saving` flag, and the + server message is rendered inside the modal at + `data-testid="cf-error"`. The list is not refreshed and any + previously-valid icon on disk is preserved (the upload endpoint + does not overwrite when Sharp decode fails). ### Edit prefill mechanics @@ -112,6 +126,29 @@ input. `syncCategoryForm` is exported from the modal file so it can be exercised in isolation by `tests/frontend/admin-categories-form-modal.spec.ts`. +### Save error and loading state + +`CategoryFormModalComponent` is purely presentational and does not own +HTTP state. It exposes two additional signal inputs: + +* `errorMessage: string | null` — when truthy, an inline error paragraph + is rendered at `data-testid="cf-error"` showing the supplied message. +* `saving: boolean` — when `true`, the OK button is disabled in addition + to the existing `form.invalid` and `submitting` guards. + +The parent `AdminCategoriesComponent` owns both signals: + +* `formError` — set from the server's `error.message` (or a generic + fallback) when create/update/upload fails, and cleared every time the + modal is opened or a submission begins. +* `saving` — set to `true` at the start of `onFormSubmit` and reset to + `false` in `finally`, so the modal can never get stuck in a + "submitting" state if the request rejects. + +This separation matters because `deleteError` (rendered by the delete +modal at `data-testid="cat-delete-error"`) is a different state machine +and must not be reused for create/update failures. + ## Delete behavior * **System rows:** the delete modal renders diff --git a/docs/index.md b/docs/index.md index 457b565..b68315a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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-22T18:37:00Z. +they need. Last regenerated 2026-07-22T19:18:00Z. # Architecture