--- 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.