10 KiB
Implementation Plan: Job 917 — Category Icons
0. Status
NOT IMPLEMENTED. The repository contains zero PNG asset files anywhere under
/repo (confirmed via find /repo -name "*.png"). The database rows created by
UpdateSystemCategoryKeys1700000000300 and
RepairCategorySchemaAndSystemCategories1700000000400 reference
/uploads/icons/{CRY,HW,MSC,PWN,REV,WEB}.png, but nothing on bootstrap writes
those PNGs to UPLOAD_DIR/icons. setup.sh only mkdir -ps the directory; it
does not populate it. As a result GET /uploads/icons/CRY.png returns 404 and
the challenges board shows broken images.
The job must: generate the canonical six system-category PNG icons at
UPLOAD_DIR/icons/{KEY}.png on every startup, idempotently, so a fresh clone
that runs setup.sh and npm start has working category icons immediately.
1. Architectural Reconnaissance
- Codebase style & conventions: TypeScript strict, NestJS 10 modules, ESM
CommonJS hybrid, async/await everywhere. Migrations are forward-only classes
implementing
MigrationInterface; service-layer logic in*.service.ts. A recent precedent (Job 919 filesystem recovery) shows the codebase prefers small, idempotent startup hooks inDatabaseInitServicerather than changes to individual migrations. - Data Layer: TypeORM +
better-sqlite3. DB atDATABASE_PATH(default./data/db.sqlite). Upload files live atUPLOAD_DIR(default./data/uploads) and are served byapp.use('/uploads', express.static(uploadDir))inbackend/src/main.ts:79. Migrations are registered inbackend/src/database/database.module.ts:40. - Test Framework & Structure: Jest with a multi-project config at
tests/jest.config.js. All backend specs live intests/backend/**.spec.ts, run vianpm test(ornpm run test:backend). All frontend specs live intests/frontend/**.spec.ts. New tests must follow the same layout — no source-side spec files. - Required Tools & Dependencies: No new dependencies are needed.
sharp(already abackendruntime dep at^0.35.3) provides PNG generation, and the rootdevDependenciesalready pinsharpandjest. The existingadmin-icon-normalize.spec.tstest already verifies that the same sharp pipeline produces a valid 128×128 PNG.
2. Impacted Files
-
To Modify:
backend/src/database/database-init.service.ts— call the new icon-seed step afterrunMigrationsso freshly migrated databases (and warm databases that pre-date this job) both end up with canonical icons on disk.setup.sh— leave as-is (the directory is already created); no shell-side asset copy is required because the generation will happen at startup.
-
To Create:
backend/src/database/system-category-icons.ts— small pure helper that exportsCANONICAL_SYSTEM_ICON_KEYS(the six canonical keys in fixed order:['CRY','HW','MSC','PWN','REV','WEB']) and an asyncseedSystemCategoryIcons(uploadDir)function. Re-importing this from the migration that owns the canonical list would create a circular dependency throughdatabase.module.ts, so the helper mirrors the canonical set directly and references the migration file via comment for traceability. It generates a deterministic 128×128 PNG for each key usingsharp(solid color background derived from a stable hash of the key, plus the 2–6-letter abbreviation rendered in white) and writes<uploadDir>/icons/<KEY>.pngonly if the file does not already exist (idempotent — does not overwrite an admin-uploaded icon).tests/backend/system-category-icons.spec.ts— verifies thatseedSystemCategoryIcons(a) writes six PNGs into a tempicons/directory, (b) writes files whose on-disk size is > 0 and whosesharp().metadata()reportsformat === 'png',width === 128,height === 128, (c) is idempotent — running it twice does not change file contents when the first run's bytes are preserved (assert by checkingstat.mtimeMsis unchanged or by capturing the buffer once and comparing), (d) does not throw when the target directory is missing (it creates it), and (e) covers exactly the canonical six keys (CRY, HW, MSC, PWN, REV, WEB). One focused test per behavior above; no mocks ofsharp(use a realos.tmpdir()-based dir for the assertions).
3. Proposed Changes
-
Helper module (
backend/src/database/system-category-icons.ts):import * as fs from 'fs'andimport * as path from 'path'andimport sharp from 'sharp'.- Export
const CANONICAL_SYSTEM_ICON_KEYS = ['CRY','HW','MSC','PWN','REV','WEB'] as const;withexport type CanonicalSystemIconKey = (typeof CANONICAL_SYSTEM_ICON_KEYS)[number];— the same six keys declared in1700000000400-RepairCategorySchemaAndSystemCategories.ts:13asCANONICAL_SYSTEM_CATEGORIES. The duplication is intentional to avoid a migration → runtime import cycle; a code comment must point at the migration file as the canonical source of truth so future jobs keep them in sync. - Define
export const SYSTEM_ICON_SIZE = 128matching the existinguploads.controller.ts:88resize target so generated icons look consistent with admin-uploaded ones. - Implement
export async function seedSystemCategoryIcons(uploadDir: string): Promise<{ written: string[]; skipped: string[] }>:- Resolve
iconsDir = path.join(uploadDir, 'icons'), create it recursively if missing. - For each
keyinCANONICAL_SYSTEM_ICON_KEYS, target filepath.join(iconsDir,${key}.png). - If the file already exists and is non-empty, push to
skipped. - Otherwise generate a 128×128 PNG via
sharp({ create: { width, height, channels: 4, background: <color for key> } }).png().toBuffer()(color picked by a deterministic hash ofkeyso each icon is visually distinct but reproducible across machines/containers). Wrap intry/catchand on failurethrow new Error('Failed to seed system category icon ' + key + ': ' + err.message). On successfs.writeFileSyncthe buffer to disk and push the absolute path towritten. - Return
{ written, skipped }.
- Resolve
- Add a top-of-file comment noting that this helper must stay in sync with
CANONICAL_SYSTEM_CATEGORIESin1700000000400-RepairCategorySchemaAndSystemCategories.tsand with theiconPathURLs hard-coded in1700000000300-UpdateSystemCategoryKeys.ts:8-13and1700000000400-RepairCategorySchemaAndSystemCategories.ts:19-54.
-
Bootstrap hook (
backend/src/database/database-init.service.ts):- Add
import { seedSystemCategoryIcons } from './system-category-icons';near the existing top-of-file imports. - In
init(), after therunMigrationsblock and afterverifySeed(), call a newawait this.ensureSystemCategoryIcons(). The call must run on every startup (not gated onpendingList.length > 0) so warm databases that pre-date this job also get the icons on next boot. - Implement
private async ensureSystemCategoryIcons(): Promise<void>:const uploadDir = path.resolve(this.config.get<string>('UPLOAD_DIR', './data/uploads'));- If
uploadDir === ':memory:'or contains:memory:skip with a debug log (matches how the module already short-circuits in-memory test DBs in some places and avoids touching whatever a test has wired up). - Otherwise
const result = await seedSystemCategoryIcons(uploadDir);and log:System category icons: written=${result.written.length} skipped=${result.skipped.length}. A failure mustthis.logger.warnand not abort startup (icons are recoverable and a missing icon does not block HTTP traffic — mirroring theverifySeed"best-effort" pattern at line 60).
- Add
-
No changes to
setup.sh. The directory is already created there and the runtime hook handles the asset creation. (Adding an out-of-bandsharpinvocation in bash would require either bundlingsharpinto the shell script or shelling out tonode -e— both are heavier than the in-process hook.) -
No changes to existing migrations. The DB rows already reference the right URLs; we only need the files to exist on disk.
4. Test Strategy
- Target Unit Test File:
tests/backend/system-category-icons.spec.ts(single file, ~30–50 lines, no test infrastructure beyondos.tmpdir()andfs.mkdtempSync). - Mocking Strategy: No mocks. Use real
sharp(already a project dev dep, validated bytests/backend/admin-icon-normalize.spec.ts:1) and a fresh per-test temp directory underos.tmpdir()(e.g.fs.mkdtempSync(path.join(os.tmpdir(), 'hipctf-icons-'))) that is removed inafterEach. Asserting on real files viafs.statSyncandsharp(buf).metadata()is faster and more reliable than mocking the fs module. - Cases (keep minimal, success-path only):
seedSystemCategoryIconswrites six PNGs into a fresh temp dir and each one is a valid 128×128 PNG withformat === 'png'.- Calling it twice with the same dir is idempotent — the second call
reports all six as
skippedand does not touchmtimeMsof the existing files (this is the contract that protects admin-uploaded icons: if an admin uploads aCRY.png, a later restart must not overwrite it). - The helper creates the
icons/subdirectory when the parent dir is empty (covers the fresh-clone case the job explicitly calls out).
- All tests must run under
npm test(single command from repo root) and complete in well under a second on a warm cache. No UI, no visual verification, no complex infrastructure.
5. Cross-cutting Notes
- The existing
admin-icon-normalize.spec.tsalready proves thesharppipeline at the size we need (128×128 PNG) and is the closest precedent for the new spec — the new test should follow its style (real sharp, real buffers, no jest mocks). - Do not modify the canonical migration files. They are forward-only and already correct; the bug is purely an asset-on-disk problem.
- Do not add new npm dependencies.
sharpis sufficient. - The
/datapersistent volume rule applies only if any test wants to share state across runs — this job does not need to; per-test temp dirs are fine and isolated.