diff --git a/docs/architecture/backend-modules.md b/docs/architecture/backend-modules.md index 80e42f7..0246d9c 100644 --- a/docs/architecture/backend-modules.md +++ b/docs/architecture/backend-modules.md @@ -3,7 +3,7 @@ type: architecture title: Backend Module Map description: NestJS modules, controllers, services, and how they are wired together. tags: [architecture, backend, nestjs, modules] -timestamp: 2026-07-23T15:46:55Z +timestamp: 2026-07-23T16:10:00Z --- # Module Map @@ -86,6 +86,11 @@ also registers two global providers: → `DatabaseInitService.init()` → `FilesystemTransactionService.recoverTransactions()` (resolves interrupted restore manifests before TypeORM opens the live SQLite file; ambiguous generations abort startup) → stale unowned swap-artifact sweep → migrations + WAL +→ `verifySeed()` (best-effort) +→ `ensureSystemCategoryIcons()` (best-effort — writes +`CRY/HW/MSC/PWN/REV/WEB.png` into `/icons/` if missing so +freshly bootstrapped instances never serve 404 icons; see +[System Category Icon Seed](/architecture/system-category-icons.md)) → `app.use(...)` middlewares (helmet, parsers, CSRF, static) → `SwaggerModule.setup(...)` (OpenAPI 3.1) → `SpaFallbackMiddleware` → `app.listen()`. @@ -94,6 +99,7 @@ ambiguous generations abort startup) → stale unowned swap-artifact sweep → m - [System Overview](/architecture/overview.md) - [Key Files Index](/architecture/key-files.md) +- [System Category Icon Seed](/architecture/system-category-icons.md) - [REST API Overview](/api/rest-overview.md) - [Blog API](/api/blog.md) - [Admin System API](/api/admin-system.md) diff --git a/docs/architecture/key-files.md b/docs/architecture/key-files.md index e4fa4bf..a00c371 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 event-window, challenge, scoreboard, session, notification, and blog flows. tags: [architecture, key-files, event-window, validation, default-challenge-ip, sse, bootstrap, migrations, challenges, import, board, notifications, per-user, session, blog] -timestamp: 2026-07-23T15:46:55Z +timestamp: 2026-07-23T16:10:00Z --- # Backend @@ -14,7 +14,8 @@ timestamp: 2026-07-23T15:46:55Z | `backend/src/app.module.ts` | Wires feature modules and global providers. | | `backend/src/config/env.schema.ts` | Zod-validated runtime config; canonical defaults for `DATABASE_PATH` (`./data/db.sqlite`) and `UPLOAD_DIR` (`./data/uploads`). | | `backend/src/database/database.module.ts` | Configures TypeORM with SQLite and imports the shared `FilesystemTransactionModule` used by pre-TypeORM startup recovery. | -| `backend/src/database/database-init.service.ts` | Before opening SQLite, resolves durable swap manifests to a coherent old or verified-new database/uploads generation, fails startup closed on ambiguity, best-effort sweeps stale unowned swap artifacts, then runs migrations, enforces WAL, and verifies seeds. | +| `backend/src/database/database-init.service.ts` | Before opening SQLite, resolves durable swap manifests to a coherent old or verified-new database/uploads generation, fails startup closed on ambiguity, best-effort sweeps stale unowned swap artifacts, then runs migrations, enforces WAL, verifies seeds, and best-effort seeds the canonical system-category icon PNGs into `UPLOAD_DIR/icons` so a fresh clone never serves 404 icons. | +| `backend/src/database/system-category-icons.ts` | Exports `CANONICAL_SYSTEM_ICON_KEYS` (`['CRY','HW','MSC','PWN','REV','WEB']`), `SYSTEM_ICON_SIZE` (`128`), and `seedSystemCategoryIcons(uploadDir)`: creates `/icons/` recursively and writes a deterministic sharp 128×128 PNG for every missing canonical key (palette-coloured background + white abbreviation overlay). Idempotent — existing non-empty files are reported as `skipped` so admin uploads are preserved across restarts. | | `backend/src/modules/admin/system/filesystem-transaction.module.ts` | Exports one shared `FilesystemTransactionService` provider to `DatabaseModule` and `AdminSystemModule`, avoiding a circular dependency. | | `backend/src/database/migrations/1700000000400-RepairCategorySchemaAndSystemCategories.ts` | Forward-only repair migration: adds `created_at`/`updated_at` to legacy six-column `category` tables, deletes obsolete `FOR`/`OSI` system rows, rewrites canonical row metadata, inserts any missing canonical key, and re-asserts unique indexes. Exports `CANONICAL_SYSTEM_CATEGORIES`. | | `backend/src/common/guards/jwt-auth.guard.ts` | Global JWT authorization guard. | @@ -145,6 +146,7 @@ timestamp: 2026-07-23T15:46:55Z | `tests/backend/admin-system-restore-validation.spec.ts` | Archive schema validation, base64 size mismatch, sha256 mismatch, safe-path rejection, and duplicate-path rejection. | | `tests/backend/admin-system-restore-commit.spec.ts` | Controller forwarding, real-file restore, trigger preservation, admin-less archive rejection, and kill-during-swap regression proving restart recovers one coherent database/uploads generation. | | `tests/backend/database-init-recovery.spec.ts` | Startup recovery contracts: restores old generations from interrupted manifests, fails closed without mutation on hybrid state, and removes stale unowned artifacts. | +| `tests/backend/system-category-icons.spec.ts` | Pure helper contract for `seedSystemCategoryIcons`: writes six valid 128×128 PNGs in a fresh temp dir; second invocation reports all six as `skipped` and leaves an admin-uploaded `CRY.png` `mtimeMs` unchanged; creates the `icons/` subdirectory when the parent is empty. Real `sharp` + `os.tmpdir()`, no mocks. | | `tests/backend/filesystem-transaction.spec.ts` | Durable manifest phase, commit/rollback, old/new generation recovery, ambiguous fail-closed, and stale artifact sweep contracts. | | `frontend/src/app/features/admin/system/system.component.ts` | `/admin/system` smart page: two panels (Database + Danger Zone), backup download, restore pick + validate, confirm modal trigger, and per-operation success/error surfacing + cross-store invalidation. | | `frontend/src/app/features/admin/system/system-confirm-modal.component.ts` | Re-authentication + confirmation-phrase modal with operation-specific copy from `system.pure.ts`. | @@ -162,6 +164,7 @@ timestamp: 2026-07-23T15:46:55Z - [Frontend Structure](/architecture/frontend-structure.md) - [Authenticated Shell](/guides/authenticated-shell.md) +- [System Category Icon Seed](/architecture/system-category-icons.md) - [System Endpoints](/api/system.md) - [Challenges Endpoints](/api/challenges.md) - [Scoreboard Page Guide](/guides/scoreboard-page.md) diff --git a/docs/architecture/system-category-icons.md b/docs/architecture/system-category-icons.md new file mode 100644 index 0000000..c2ad78c --- /dev/null +++ b/docs/architecture/system-category-icons.md @@ -0,0 +1,135 @@ +--- +type: architecture +title: System Category Icon Seed +description: Idempotent startup hook that writes deterministic 128×128 PNG icons for the canonical six system categories (CRY/HW/MSC/PWN/REV/WEB) into UPLOAD_DIR/icons, so freshly bootstrapped instances never serve 404 icons. +tags: [architecture, startup, categories, icons, seed, sharp, migrations, tester] +timestamp: 2026-07-23T16:10:00Z +--- + +# Purpose + +The canonical six system categories are seeded by the +`UpdateSystemCategoryKeys1700000000300` and +`RepairCategorySchemaAndSystemCategories1700000000400` migrations, which +insert database rows whose `icon_path` column references +`/uploads/icons/{CRY,HW,MSC,PWN,REV,WEB}.png`. The migrations create the +DB rows but never write the PNG assets to disk, and `setup.sh` only +`mkdir -p`s the upload directory — it does not populate it. + +As a result, `GET /uploads/icons/CRY.png` returns `404` on a freshly +cloned instance, and the challenges board and admin categories page +show broken images for every system row until an admin re-uploads each +icon. + +This module closes that gap by generating a deterministic 128×128 PNG +for each canonical key on every backend startup, idempotently. Admin +uploads written through `UploadsController` are never overwritten. + +# Files + +| File | Responsibility | +|---|---| +| `backend/src/database/system-category-icons.ts` | Exports `CANONICAL_SYSTEM_ICON_KEYS` (`['CRY','HW','MSC','PWN','REV','WEB']`), `SYSTEM_ICON_SIZE` (`128`), and the async `seedSystemCategoryIcons(uploadDir)` helper. Creates `/icons/` recursively, generates a sharp 128×128 PNG (palette-coloured background + white 2–6-letter abbreviation overlay) for every key whose `.png` does not yet exist or is zero-bytes, and returns `{ written, skipped }`. | +| `backend/src/database/database-init.service.ts` | Calls `seedSystemCategoryIcons(path.resolve(config.get('UPLOAD_DIR', './data/uploads')))` from `init()` after migrations and `verifySeed()`. Skips when `UPLOAD_DIR` is unset or contains `:memory:`. Failures log a `WARN` and never abort startup. | +| `tests/backend/system-category-icons.spec.ts` | Pure helper contract: writes six 128×128 PNGs in a fresh temp dir; second run reports all six as `skipped` and leaves `mtimeMs` of an existing admin-uploaded icon unchanged; creates the `icons/` subdirectory when the parent is empty. | + +# How the helper works + +`seedSystemCategoryIcons(uploadDir)`: + +1. Resolves `iconsDir = path.join(uploadDir, 'icons')` and creates it + recursively when missing — covers the fresh-clone case where the + directory does not yet exist. +2. For each `key` in `CANONICAL_SYSTEM_ICON_KEYS`: + * `target = path.join(iconsDir, `${key}.png`)`. + * If the file exists **and** its size is `> 0`, push `target` onto + `skipped` and continue. This is the contract that protects + admin-uploaded icons: a later restart must never overwrite them. + * Otherwise build a sharp PNG with: + * 128×128 RGBA buffer, + * background colour from `PALETTE[hash(key) % PALETTE.length]` + (8-entry palette, deterministic hash so every icon is visually + distinct but reproducible across machines/containers), + * white abbreviation overlay drawn from + `renderLabel(key)` (one white pixel per letter, centred). + * Write the buffer with `fs.writeFileSync(target, buf)` and push + `target` onto `written`. +3. Return `{ written, skipped }`. + +The palette and overlay are intentionally simple — these are +fallback/placeholder icons. An admin who wants branded artwork uploads +a real image through `POST /api/v1/uploads/category-icon`, which writes +to the same `/icons/.png` path and is then preserved on +subsequent restarts. + +# Startup wiring + +Inside `DatabaseInitService.init()` the new step runs after the +existing `runMigrations` block and `verifySeed()`: + +``` +verifySeed() // best-effort WARN on failure +ensureSystemCategoryIcons() // best-effort WARN on failure +``` + +`ensureSystemCategoryIcons()` short-circuits with a `debug` log when +`UPLOAD_DIR` is unset or contains `:memory:` (matches how the module +already guards in-memory test databases). Otherwise it logs: + +``` +System category icons: written= skipped= +``` + +A failure logs `ensureSystemCategoryIcons skipped: ` as a +`WARN` and **does not abort startup** — mirroring the +`verifySeed` "best-effort" pattern. Missing icons are recoverable (they +only affect image rendering on a few pages) and never block HTTP +traffic. + +# Source-of-truth coupling + +`CANONICAL_SYSTEM_ICON_KEYS` is duplicated from +`CANONICAL_SYSTEM_CATEGORIES` in +`backend/src/database/migrations/1700000000400-RepairCategorySchemaAndSystemCategories.ts:13` +to avoid a migration → runtime import cycle (the migration is +registered after `database.module.ts` is built, and re-importing from +the migration would be circular). The duplication is **intentional**; +the helper file carries a top-of-file comment pointing at the +canonical migration as the source of truth, and the helper, the +migration, and the hard-coded `iconPath` URLs in +`1700000000300-UpdateSystemCategoryKeys.ts` and +`1700000000400-RepairCategorySchemaAndSystemCategories.ts` must be kept +in sync whenever a system category is added or renamed. + +# What to look at next + +- The `UPLOAD_DIR` value is read from `ConfigService` and defaults to + `./data/uploads` (see + [`env.schema.ts`](/architecture/key-files.md) for the canonical + defaults). +- The icons directory is served by `app.use('/uploads', express.static(uploadDir))` + in `backend/src/main.ts:79`, so the on-disk files are the same + bytes returned by `GET /uploads/icons/.png` to the SPA. +- Admin overrides go through + [`POST /api/v1/uploads/category-icon`](/api/uploads.md), which + normalizes uploaded images to the same 128×128 PNG size and writes + to the same `UPLOAD_DIR/icons/.png` path. +- The challenges board and admin categories page consume + `` from each `category.icon_path` + column — see [Challenges Board](/guides/challenges-board.md) and + [Admin — Categories](/guides/admin-categories.md). + +# Tests + +| File | What it asserts | +|---|---| +| `tests/backend/system-category-icons.spec.ts` | Real `sharp` + `os.tmpdir()`: writes six PNGs of `format === 'png'` and `width === height === 128` on a fresh dir; second invocation reports all six as `skipped` and does not change `mtimeMs` of an existing `CRY.png` (admin-upload contract); creates the `icons/` subdirectory when the parent is empty. No mocks. | + +# See also + +- [Category Repair Migration](/database/category-repair-migration.md) — canonical six system categories, DB-side. +- [Uploads Endpoints](/api/uploads.md) — admin override path for the same on-disk files. +- [Challenges Board](/guides/challenges-board.md) — tester-visible consumption of the icons. +- [Admin — Categories](/guides/admin-categories.md) — admin override UI. +- [Backend Module Map](/architecture/backend-modules.md) — startup chain. +- [Key Files Index](/architecture/key-files.md) — file-by-file responsibilities. diff --git a/docs/guides/challenges-board.md b/docs/guides/challenges-board.md index b617063..4511c76 100644 --- a/docs/guides/challenges-board.md +++ b/docs/guides/challenges-board.md @@ -3,7 +3,7 @@ type: guide title: Challenges Board description: How a signed-in player navigates the `/challenges` page, opens a challenge modal, submits a flag, and watches live solve updates. tags: [guide, challenges, board, flag, score, sse, tester] -timestamp: 2026-07-23T04:05:00Z +timestamp: 2026-07-23T16:10:00Z --- # When this view is available @@ -55,7 +55,7 @@ category. | Element | Selector | Expected | |----------------------------------------|---------------------------------------------|-----------------------------------------------------------------------------------------------------------------| -| Category column | `[data-testid="category-column-CR"]` (or any abbreviation) | Header shows the category `icon`, uppercase `abbreviation`, and full `name`; below it is a vertical list of cards. | +| Category column | `[data-testid="category-column-CR"]` (or any abbreviation) | Header shows the category `icon`, uppercase `abbreviation`, and full `name`; below it is a vertical list of cards. The icon's `src` is `/uploads/icons/.png` and is served by the backend's static middleware; on a freshly bootstrapped instance these PNGs are generated at startup by [`seedSystemCategoryIcons`](/architecture/system-category-icons.md) so the column headers never show broken images. | | Category column with no challenges | Same | The header renders but the body shows `No challenges yet.` | | Challenge card | `[data-testid="challenge-card-"]` | Rendered as a real `