7.0 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| architecture | System Category Icon Seed | 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. |
|
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 -ps 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 <uploadDir>/icons/ recursively, generates a sharp 128×128 PNG (palette-coloured background + white 2–6-letter abbreviation overlay) for every key whose <KEY>.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):
- Resolves
iconsDir = path.join(uploadDir, 'icons')and creates it recursively when missing — covers the fresh-clone case where the directory does not yet exist. - For each
keyinCANONICAL_SYSTEM_ICON_KEYS:target = path.join(iconsDir,${key}.png).- If the file exists and its size is
> 0, pushtargetontoskippedand 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 pushtargetontowritten.
- 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 <uploadDir>/icons/<KEY>.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=<n> skipped=<n>
A failure logs ensureSystemCategoryIcons skipped: <message> 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_DIRvalue is read fromConfigServiceand defaults to./data/uploads(seeenv.schema.tsfor the canonical defaults). - The icons directory is served by
app.use('/uploads', express.static(uploadDir))inbackend/src/main.ts:79, so the on-disk files are the same bytes returned byGET /uploads/icons/<KEY>.pngto the SPA. - Admin overrides go through
POST /api/v1/uploads/category-icon, which normalizes uploaded images to the same 128×128 PNG size and writes to the sameUPLOAD_DIR/icons/<KEY>.pngpath. - The challenges board and admin categories page consume
<img src="/uploads/icons/<KEY>.png">from eachcategory.icon_pathcolumn — see Challenges Board and Admin — Categories.
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 — canonical six system categories, DB-side.
- Uploads Endpoints — admin override path for the same on-disk files.
- Challenges Board — tester-visible consumption of the icons.
- Admin — Categories — admin override UI.
- Backend Module Map — startup chain.
- Key Files Index — file-by-file responsibilities.