From edb3feb457fab679f07d824ed2445513777116dc Mon Sep 17 00:00:00 2001 From: OpenVelo Agent Date: Wed, 22 Jul 2026 22:18:07 +0000 Subject: [PATCH] docs: update documentation to OKF v0.1 format --- docs/architecture/key-files.md | 8 ++- docs/guides/admin-challenges-import.md | 95 ++++++++++++++++++++++++++ docs/guides/admin-challenges.md | 12 ++-- docs/index.md | 5 +- 4 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 docs/guides/admin-challenges-import.md diff --git a/docs/architecture/key-files.md b/docs/architecture/key-files.md index e7e8839..f3c9db3 100644 --- a/docs/architecture/key-files.md +++ b/docs/architecture/key-files.md @@ -1,9 +1,9 @@ --- 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-22T19:18:00Z +description: One-line responsibility for important source and contract-test files, including strict event-window validation, the public bootstrap SSE listener, and the challenges full-replace import flow. +tags: [architecture, key-files, event-window, validation, default-challenge-ip, sse, bootstrap, migrations, challenges, import] +timestamp: 2026-07-22T22:17:11Z --- # Backend @@ -30,6 +30,7 @@ timestamp: 2026-07-22T19:18:00Z | `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. | +| `tests/backend/admin-challenges-import-replace.spec.ts` | Contract tests for the confirmed-import full-replace: wipes unrelated pre-existing challenges, unlinks wiped disk files, empty archive, unknown-category skip-then-wipe. | # Frontend @@ -63,6 +64,7 @@ timestamp: 2026-07-22T19:18:00Z | `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. | | `tests/frontend/authenticated-event-source.spec.ts` | Tests SSE authorization, frame transport behavior, and the `401`/`403` unauthorized path. | | `tests/frontend/auth-session-events.spec.ts` | Pure tests for cross-tab invalidation message encoding, payload validation, and `storage`-event filtering. | +| `tests/frontend/admin-challenges-import-autoclose.spec.ts` | Pure helpers (`summarizeImportResult`, `importErrorMessage`) and modal auto-close branch for the confirmed-import handler. | # See also diff --git a/docs/guides/admin-challenges-import.md b/docs/guides/admin-challenges-import.md new file mode 100644 index 0000000..df83d4a --- /dev/null +++ b/docs/guides/admin-challenges-import.md @@ -0,0 +1,95 @@ +--- +type: guide +title: Admin — Challenges Full-Replace Import +description: How the confirmed challenges import wipes every existing challenge (with cascading files/solves and disk unlink) and inserts the archive contents in one transaction. +tags: [guide, admin, challenges, import, full-replace, tester] +timestamp: 2026-07-22T22:17:11Z +--- + +# What it is + +The **confirmed challenges import** is the second step of the import flow +on `/admin/challenges` (after the preview). When the admin clicks +**Overwrite & Import** the client POSTs the already-validated archive to +`POST /api/v1/admin/challenges/import?confirm=overwrite`. The server +performs a **full replace** of the challenge set inside one DB +transaction, then best-effort unlinks the disk files owned by the +wiped challenges. + +# When to use it + +Use this when an admin wants to restore the platform challenge set to +the exact contents of a previously exported JSON archive. The user +guide for the surrounding flow lives at [Admin — +Challenges](/guides/admin-challenges.md); this concept focuses on the +import-specific behavior and its consequences. + +# Behavior contract + +| Phase | What happens | +|-------|--------------| +| Pre-flight | The client already validated the archive against the `IMPORT_VALIDATION_FAILED` Zod schema in [Admin Challenges DTOs](/api/admin.md#challenges). The server re-validates; preview counts (`total`, `conflicts`, `unknownCategories`, `warnings`) are surfaced by the import modal. | +| Staging | Every `dataBase64` file in the archive is decoded and staged to disk via `ChallengeFilesService.stage(...)`. If any stage fails the server rolls back every previously staged file and re-throws (mirrors the existing `rollbackStaged` safety net). | +| Transaction (DB) | The server begins a single TypeORM transaction. It loads every existing challenge, captures the disk-bound `stored_filename`s of every existing `ChallengeFile`, then `DELETE`s all existing `ChallengeEntity` rows by id. `ChallengeFileEntity` and `SolveEntity` cascade-delete via FK, so dependent rows disappear too. The archive's challenges are then inserted under their original names. | +| Post-commit (FS) | For every captured `storedFilename` the server calls `ChallengeFilesService.removeFinal(storedFilename)` (best-effort; failures are logged but do not abort). Each staged file for the imported set is committed to its final filename. | +| Response | Returns `{ imported, skipped, warnings }`. Known unknown `categorySystemKey`s are reported in `skipped[]` (still wipe everything else). | + +# Side effects to expect as a tester + +- **Every existing challenge disappears** from the list immediately + after the success banner — including rows whose names did not appear + in the archive. Use the Players page to confirm that pre-existing + `solve` rows for wiped challenges also vanish (they cascade). +- **Uploaded challenge files** owned by wiped rows are removed from + `./data/uploads/challenges/` post-commit. The imported archive's + files appear under the same directory using their original + filenames encoded onto the wipe. +- An empty archive (`{ challenges: [] }`) is valid and produces + `imported: 0`, an empty list, and zero on-disk challenge files. +- The **import modal auto-closes on success**; the page-level status + banner (`[data-testid="challenges-status"]`) shows the + imported/skipped/warning summary. Failures keep the modal open and + show the error inline. + +# Idempotency + +- Re-importing the same archive twice yields the same final DB state. +- The previous upsert-on-name semantics are gone: even challenges + whose names did not appear in the archive are removed. + +# Error codes + +See [Admin Endpoints](/api/admin.md) and the [Admin — +Challenges](/guides/admin-challenges.md) guide: + +- `IMPORT_VALIDATION_FAILED` (400) — schema failure on preview or + confirm. +- `IMPORT_PARTIAL_FAILURE` is no longer emitted: the response is + always 200 with `{ imported, skipped, warnings }` (consistent with + partial success being the normal case when unknown categories are + present). +- Any DB transaction failure rolls back every staged file and + re-throws the original error, leaving the DB untouched. + +# Wiring + +| Concern | File | +|---------|------| +| Endpoint | `backend/src/modules/admin/admin-challenges.controller.ts` (`POST /api/v1/admin/challenges/import?confirm=overwrite`). | +| Orchestration | `backend/src/modules/admin/challenges.service.ts` (`importConfirmed`) — full-replace transaction + post-commit unlink. | +| File staging and disk ops | `backend/src/modules/admin/challenge-files.service.ts` (`stage`, `commit`, `removeFinal`). | +| Schema | `backend/src/modules/admin/dto/challenges.dto.ts` (`CHALLENGE_FORMAT`, `CHALLENGE_FORMAT_VERSION`, import document schema). | +| Client UI | `frontend/src/app/features/admin/challenges/challenges.component.ts` (`onImportConfirm`) — refreshes list, sets status, calls `closeImport()`. | +| Pure helpers | `frontend/src/app/features/admin/challenges/challenges.pure.ts` (`summarizeImportResult`, `importErrorMessage`). | +| Backend contract tests | `tests/backend/admin-challenges-import-replace.spec.ts` — full-replace, disk-file unlink, empty archive, unknown-category wipe. | +| Frontend contract tests | `tests/frontend/admin-challenges-import-autoclose.spec.ts` — pure helpers + modal auto-close branch. | + +# Notes + +- The flag is not in the archive's per-challenge `data`; only admins + can read the plaintext, and the export/import preserves the same + restriction. +- Wiping cascades remove `solve` rows for wiped challenges — the + scoreboard will recompute on the next render without those entries. +- The destroy-then-insert happens in one transaction, so a partial + failure cannot leave the platform with a mix of old and new rows. diff --git a/docs/guides/admin-challenges.md b/docs/guides/admin-challenges.md index ba35b85..f155cc6 100644 --- a/docs/guides/admin-challenges.md +++ b/docs/guides/admin-challenges.md @@ -3,7 +3,7 @@ type: guide title: Admin — Challenges description: How an admin lists, searches, creates, edits, deletes, imports, and exports challenges from the /admin/challenges page. tags: [guide, admin, challenges, import, export, files, tester] -timestamp: 2026-07-22T21:25:29Z +timestamp: 2026-07-22T22:17:11Z --- # When this view is available @@ -104,8 +104,10 @@ The form modal has three sections (tabs): General, Connection, Files. Fields mar 1. Click **Import challenges** → a file picker accepts `.json`. The client reads it and posts it to `POST /api/v1/admin/challenges/import`. 2. On validation success the import modal opens with the preview (`total`, `conflicts`, `unknownCategories`, `warnings`) and the standard copy: *"Import N challenges. Existing challenges with matching names will be overwritten. New categories referenced but missing locally will be skipped with a warning. Continue?"* -3. Click **Overwrite & Import** → the client posts the same document with `?confirm=overwrite`. The server stages every file up front (rolling back any FS failure), then upserts challenges inside one transaction by case-insensitive name. -4. The response renders imported/skipped/warning counts; the list refreshes automatically. +3. Click **Overwrite & Import** → the client posts the same document with `?confirm=overwrite`. The server stages every file up front (rolling back any FS failure), then **wipes every existing challenge (cascade-deleting their `challenge_file` and `solve` rows and best-effort unlinking their disk files) and inserts the archive's challenges** inside one transaction. +4. The modal auto-closes on success and the list refreshes automatically; the page-level status banner shows imported/skipped/warning counts. + +See [Admin — Challenges Full-Replace Import](/guides/admin-challenges-import.md) for the full transactional semantics, side effects, and the auto-close contract. # Validation & error codes (server) @@ -127,7 +129,7 @@ The form modal has three sections (tabs): General, Connection, Files. Fields mar | 3 | `frontend/src/app/core/services/admin.service.ts` | Typed `listChallenges`, `getChallengeDetail`, `createChallenge`, `updateChallenge`, `deleteChallenge`, `exportChallenges`, `previewChallengeImport`, `confirmChallengeImport`, `stageChallengeFile`, `discardChallengeFile`. | | 4 | `frontend/src/app/features/admin/challenges/challenge-form.pure.ts` | Defaults/prefill, points/port validation, first-invalid-tab selection, file-size partitioning, payload mapping. | | 5 | `backend/src/modules/admin/admin-challenges.controller.ts` | `AdminGuard` + `@Roles('admin')` on every handler. | -| 6 | `backend/src/modules/admin/challenges.service.ts` | CRUD, list aggregates, transactional deletion, export, import orchestration. | +| 6 | `backend/src/modules/admin/challenges.service.ts` | CRUD, list aggregates, transactional deletion, export, full-replace import orchestration. | | 7 | `backend/src/modules/admin/challenge-files.service.ts` | Staging, commit, discard, read for export, limits. | | 8 | `backend/src/modules/admin/dto/challenges.dto.ts` | Zod schemas for IDs, list query, create/update, staged-file manifest, import document with field paths. | @@ -136,4 +138,4 @@ The form modal has three sections (tabs): General, Connection, Files. Fields mar * The flag is never present in any non-admin DTO. The player-facing challenges route and scoreboard remain unchanged; only admin endpoints can fetch the plaintext. * All mutating endpoints require the standard CSRF cookie + header pair; the existing `csrfInterceptor` handles the headers automatically. * The list query enforces a deterministic `LOWER(name), name` order so the UI sort is reproducible. -* Importing the same document twice is idempotent: every incoming challenge either replaces an existing row by lowercased name or inserts a new one. \ No newline at end of file +* Importing the same document twice is idempotent: every existing challenge (matching or not) is replaced by the archive contents. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index fd3ec16..e97932b 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-22T21:25:29Z. +they need. Last regenerated 2026-07-22T22:17:11Z. # Architecture @@ -74,6 +74,9 @@ they need. Last regenerated 2026-07-22T21:25:29Z. * [Admin — Challenges](/guides/admin-challenges.md) - How an admin lists, searches, creates, edits, deletes, imports, and exports challenges at `/admin/challenges`, including file staging and overwrite confirm. +* [Admin — Challenges Full-Replace Import](/guides/admin-challenges-import.md) - + Transactional full-replace semantics, side effects, and the auto-close + contract of the confirmed challenges import on `/admin/challenges`. * [Authenticated Shell](/guides/authenticated-shell.md) - The header, LED + countdown, quick-jump tabs, user menu, and SSE lifecycle that frame every signed-in page.