Files
HIPCTF2/docs/architecture/system-category-icons.md
T
2026-07-23 16:14:03 +00:00

136 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 `<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`](/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/<KEY>.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/<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](/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.