Files

181 lines
10 KiB
Markdown
Raw Permalink 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.
# 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 -p`s 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 in `DatabaseInitService` rather than changes
to individual migrations.
- **Data Layer:** TypeORM + `better-sqlite3`. DB at `DATABASE_PATH`
(default `./data/db.sqlite`). Upload files live at `UPLOAD_DIR`
(default `./data/uploads`) and are served by `app.use('/uploads',
express.static(uploadDir))` in `backend/src/main.ts:79`. Migrations are
registered in `backend/src/database/database.module.ts:40`.
- **Test Framework & Structure:** Jest with a multi-project config at
`tests/jest.config.js`. All backend specs live in `tests/backend/**.spec.ts`,
run via `npm test` (or `npm run test:backend`). All frontend specs live in
`tests/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 a `backend` runtime dep at `^0.35.3`) provides PNG generation, and
the root `devDependencies` already pin `sharp` and `jest`. The existing
`admin-icon-normalize.spec.ts` test 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 after `runMigrations` so 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
exports `CANONICAL_SYSTEM_ICON_KEYS` (the six canonical keys in fixed order:
`['CRY','HW','MSC','PWN','REV','WEB']`) and an async
`seedSystemCategoryIcons(uploadDir)` function. Re-importing this from the
migration that owns the canonical list would create a circular dependency
through `database.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 using `sharp` (solid
color background derived from a stable hash of the key, plus the 26-letter
abbreviation rendered in white) and writes
`<uploadDir>/icons/<KEY>.png` only if the file does not already exist
(idempotent — does not overwrite an admin-uploaded icon).
- `tests/backend/system-category-icons.spec.ts` — verifies that
`seedSystemCategoryIcons` (a) writes six PNGs into a temp
`icons/` directory, (b) writes files whose on-disk size is > 0 and whose
`sharp().metadata()` reports `format === '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 checking
`stat.mtimeMs` is 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 of `sharp` (use a
real `os.tmpdir()`-based dir for the assertions).
## 3. Proposed Changes
1. **Helper module (`backend/src/database/system-category-icons.ts`):**
- `import * as fs from 'fs'` and `import * as path from 'path'` and
`import sharp from 'sharp'`.
- Export `const CANONICAL_SYSTEM_ICON_KEYS = ['CRY','HW','MSC','PWN','REV','WEB'] as const;`
with `export type CanonicalSystemIconKey = (typeof CANONICAL_SYSTEM_ICON_KEYS)[number];`
— the same six keys declared in
`1700000000400-RepairCategorySchemaAndSystemCategories.ts:13` as
`CANONICAL_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 = 128` matching the existing
`uploads.controller.ts:88` resize 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 `key` in `CANONICAL_SYSTEM_ICON_KEYS`, target file
`path.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 of `key` so each icon is visually
distinct but reproducible across machines/containers). Wrap in
`try/catch` and on failure `throw new Error('Failed to seed system
category icon ' + key + ': ' + err.message)`. On success `fs.writeFileSync`
the buffer to disk and push the absolute path to `written`.
- Return `{ written, skipped }`.
- Add a top-of-file comment noting that this helper must stay in sync with
`CANONICAL_SYSTEM_CATEGORIES` in
`1700000000400-RepairCategorySchemaAndSystemCategories.ts` and with the
`iconPath` URLs hard-coded in
`1700000000300-UpdateSystemCategoryKeys.ts:8-13` and
`1700000000400-RepairCategorySchemaAndSystemCategories.ts:19-54`.
2. **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 the `runMigrations` block and after `verifySeed()`,
call a new `await this.ensureSystemCategoryIcons()`. The call must run
on every startup (not gated on `pendingList.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 must `this.logger.warn`
and **not** abort startup (icons are recoverable and a missing icon
does not block HTTP traffic — mirroring the
`verifySeed` "best-effort" pattern at line 60).
3. **No changes to `setup.sh`.** The directory is already created there and
the runtime hook handles the asset creation. (Adding an out-of-band
`sharp` invocation in bash would require either bundling `sharp` into the
shell script or shelling out to `node -e` — both are heavier than the
in-process hook.)
4. **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, ~3050 lines, no test infrastructure beyond `os.tmpdir()`
and `fs.mkdtempSync`).
- **Mocking Strategy:** No mocks. Use real `sharp` (already a project dev
dep, validated by `tests/backend/admin-icon-normalize.spec.ts:1`) and a
fresh per-test temp directory under `os.tmpdir()` (e.g.
`fs.mkdtempSync(path.join(os.tmpdir(), 'hipctf-icons-'))`) that is removed
in `afterEach`. Asserting on real files via `fs.statSync` and
`sharp(buf).metadata()` is faster and more reliable than mocking the fs
module.
- **Cases (keep minimal, success-path only):**
1. `seedSystemCategoryIcons` writes six PNGs into a fresh temp dir and
each one is a valid 128×128 PNG with `format === 'png'`.
2. Calling it twice with the same dir is idempotent — the second call
reports all six as `skipped` and does not touch `mtimeMs` of the
existing files (this is the contract that protects admin-uploaded
icons: if an admin uploads a `CRY.png`, a later restart must not
overwrite it).
3. 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.ts` already proves the `sharp`
pipeline 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. `sharp` is sufficient.
- The `/data` persistent 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.