AI Implementation feature(903): Admin Area Challenges: List, Import/Export and Add/Edit Modal 1.03 (#44)
This commit was merged in pull request #44.
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
* Importing the same document twice is idempotent: every existing challenge (matching or not) is replaced by the archive contents.
|
||||
Reference in New Issue
Block a user