Files
HIPCTF2/docs/architecture/system-category-icons.md
T

7.0 KiB
Raw Blame History

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.
architecture
startup
categories
icons
seed
sharp
migrations
tester
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 26-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):

  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 <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_DIR value is read from ConfigService and defaults to ./data/uploads (see env.schema.ts 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/<KEY>.png to 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 same UPLOAD_DIR/icons/<KEY>.png path.
  • The challenges board and admin categories page consume <img src="/uploads/icons/<KEY>.png"> from each category.icon_path column — 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