Files
HIPCTF2/.kilo/plans/889.md
T
2026-07-22 14:52:59 +00:00

14 KiB

Implementation Plan: Job 889 — Admin Area General Settings and Categories 1.07

Status

NOT FULLY IMPLEMENTED. Two residual defects remain after Job 888:

  1. Categories SQLbackend/src/modules/admin/categories.service.ts:30-37 uses createQueryBuilder('c').orderBy('LOWER(c.abbreviation)', 'ASC').addOrderBy('c.abbreviation', 'ASC').getMany(). getMany() auto-selects every column mapped on CategoryEntity, including created_at and updated_at (added by migration 1700000000200-AddCategoryTimestampsAndUniqueAbbrev). The InitSchema1700000000000 migration (backend/src/database/migrations/1700000000000-InitSchema.ts:28-37) creates the category table without those columns and relies on the 200-migration to add them at startup. On any database where the 200-migration has not yet run (e.g. when an older pre-1.06 DB is hit before migrations resolve, or when tests / fresh clones initialise the schema out of order), SQLite raises SqliteError: no such column: c.created_at and the Categories section on /admin/general (which embeds AdminCategoriesComponent) renders the error to the admin. The fix is to make the category table include created_at and updated_at in its initial CREATE TABLE and keep the 200 migration as a no-op if (!names.has(...)) ALTER for pre-existing DBs.

  2. Event End equal-to-start field-level messagefrontend/src/app/features/admin/general.pure.ts:41-51 already returns { endBeforeStart: true } when e <= s, and frontend/src/app/features/admin/general.component.ts:123-128 renders the general-endBeforeStart element whenever eventEndCrossFieldError() is true. However the message lives in a separate data-testid="general-endBeforeStart" div instead of inside the per-field error region data-testid="general-eventEnd-error" that the Job (and docs/guides/admin-general-settings.md) describe as the canonical "field-level validation message" for the Event End input. The Job explicitly states: "no visible message inside the field-level error region (e.g., [data-testid=general-eventEnd-error])". The fix is to funnel the cross-field endBeforeStart message into general-eventEnd-error (and surface it via aria-invalid / aria-describedby / title on the input) so it is rendered as the Event End field's own validation message.

1. Architectural Reconnaissance

  • Codebase style & conventions:

    • Backend: NestJS 10, TypeScript strict, TypeORM 0.3.20 over better-sqlite3. Migrations live under backend/src/database/migrations/ and are wired through DatabaseModule.MIGRATIONS (backend/src/database/database.module.ts:31-36) and run on bootstrap by DatabaseInitService.init() (backend/src/database/database-init.service.ts:26-54). Entities are auto-selected from DatabaseModule.ENTITIES and must remain in sync with the SQL schema produced by the migrations.
    • Frontend: Angular 17+ standalone components with ChangeDetectionStrategy.OnPush, reactive forms, and the Value / Invalid / TouchedOrDirty signal trio for inline error messaging (see general.component.ts:204-321). Pure validation / message helpers live in frontend/src/app/features/admin/general.pure.ts and are unit-tested under tests/frontend/admin-general-pure.spec.ts.
    • Error envelopes: backend emits { code: 'VALIDATION_FAILED', message, details: [{path, message}] } via ZodValidationPipe (backend/src/common/pipes/zod-validation.pipe.ts:9-15) for 400s; ApiError helpers live in backend/src/common/errors/api-error.ts.
  • Data Layer: SQLite (better-sqlite3) database file at process.env.DATABASE_PATH (default /data/hipctf/db.sqlite). The category table is created by InitSchema1700000000000 and seeded / amended by SeedSystemData1700000000100, AddCategoryTimestampsAndUniqueAbbrev1700000000200, and UpdateSystemCategoryKeys1700000000300. The CategoryEntity (backend/src/database/entities/category.entity.ts) is the single source of truth for the column names that TypeORM will SELECT via getMany().

  • Test Framework & Structure: Jest (ts-jest) with two projects (backend and frontend) configured in tests/jest.config.js. Tests live in tests/backend/**/*.spec.ts and tests/frontend/**/*.spec.ts and run with a single command npm test from the repo root (already in package.json:scripts.test). Backend tests typically boot the real AppModule against :memory: SQLite via process.env.DATABASE_PATH = ':memory:'; frontend tests are pure function specs that never touch the DOM. New tests must follow the same layout.

  • Required Tools & Dependencies: No new packages. Node 20+, npm workspaces, jest, ts-jest, better-sqlite3, @nestjs/typeorm, and @nestjs/common are all already declared and bootstrapped by setup.sh (setup.sh:1-19). No setup.sh change is required for this Job.

2. Impacted Files

  • To Modify:

    • backend/src/database/migrations/1700000000000-InitSchema.ts — add "created_at" and "updated_at" columns to the initial CREATE TABLE "category" so the columns exist from the very first migration; preserve the description and icon_path defaults.
    • frontend/src/app/features/admin/general.component.ts — extend the Event End general-eventEnd-error element so it also renders the cross-field endBeforeStart message when no per-field invalidDatetime error is present; keep general-endBeforeStart as a redundant/legacy visual element only if it is already asserted in tests (see Test Strategy); bind [attr.aria-invalid] / [attr.aria-describedby] / [attr.title] on the Event End <input> to the merged state.
    • frontend/src/app/features/admin/general.pure.ts — add a tiny helper (e.g. eventEndFieldError(fieldErrors, crossField)) that returns the right human-readable string for the merged Event End field error, so the template can read it from a single source.
    • tests/frontend/admin-general-pure.spec.ts — add focused unit assertions for the new helper (equal-to-start and end-before-start cases).
    • tests/backend/migrations.spec.ts — add a focused assertion that running InitSchema1700000000000 against an empty database produces a category table that contains a created_at column (and updated_at). This locks the SQL schema in place so the Categories endpoint can never regress to the no such column: c.created_at state.
    • docs/guides/admin-general-settings.md — clarify that the Event End inline error region (general-eventEnd-error) is the canonical place where the end-before-start message surfaces.
  • To Create:

    • None required. Existing test files are sufficient.

3. Proposed Changes

1. Database / Schema Migration

  1. Update InitSchema1700000000000 at backend/src/database/migrations/1700000000000-InitSchema.ts:28-37 so the category CREATE TABLE includes created_at and updated_at columns from the very first migration:

    CREATE TABLE IF NOT EXISTS "category" (
      "id" TEXT PRIMARY KEY,
      "system_key" TEXT,
      "name" TEXT NOT NULL,
      "abbreviation" TEXT NOT NULL,
      "description" TEXT NOT NULL DEFAULT '',
      "icon_path" TEXT NOT NULL DEFAULT '',
      "created_at" TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
      "updated_at" TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
    );
    

    Mirror the strftime('%Y-%m-%dT%H:%M:%fZ','now') default already used for user.created_at (line 17) and challenge.created_at (line 54) so newly-inserted rows get a sane timestamp without a follow-up UPDATE-pass.

  2. Keep AddCategoryTimestampsAndUniqueAbbrev1700000000200 unchanged. Its if (!names.has('created_at')) / if (!names.has('updated_at')) guards (backend/src/database/migrations/1700000000200-AddCategoryTimestampsAndUniqueAbbrev.ts:8-20) make the ALTER a no-op for fresh databases and still patch older pre-1.06 databases that lack the columns. No changes needed to down() either — the down() drops the columns, which is the correct mirror of up().

  3. Keep UpdateSystemCategoryKeys1700000000300 unchanged. Its INSERT already lists created_at and updated_at (backend/src/database/migrations/1700000000300-UpdateSystemCategoryKeys.ts:51).

  4. No need to touch CategoryEntity (backend/src/database/entities/category.entity.ts) — its current @Column('text', { name: 'created_at', default: '' }) and @Column('text', { name: 'updated_at', default: '' }) annotations match the columns we are guaranteeing.

2. Backend Logic & APIs

No controller / service logic changes are required. With the schema fix, AdminCategoriesService.list() at backend/src/modules/admin/categories.service.ts:30-37 will resolve c.created_at and c.updated_at and return rows that satisfy the existing CategoryView shape used by the front-end.

If desired, the controller surface can stay untouched (backend/src/modules/admin/admin-categories.controller.ts:30-34) — no new endpoint is needed.

3. Frontend UI Integration

  1. Update frontend/src/app/features/admin/general.pure.ts to expose a small eventEndFieldMessage(controlErrors, crossField) pure helper that returns the canonical Event End field error string:

    controlErrors?.['invalidDatetime'] form.errors?.['endBeforeStart'] Returned message
    true any "Event end must be a valid ISO-8601 datetime."
    falsy true "Event end must be after event start."
    falsy falsy null

    This mirrors the existing datetimeMessage pattern and keeps the template a thin consumer.

  2. Update frontend/src/app/features/admin/general.component.ts:

    • Replace the current @if (showEventEndError()) / @if (eventEndCrossFieldError()) pair (lines 123-128) with a single eventEndFieldMessage consumer that always renders inside the data-testid="general-eventEnd-error" element. When both validators are off, the element is absent.
    • Bind [attr.aria-invalid] to true when eventEndFieldMessage(...) is non-null, otherwise null.
    • Bind [attr.aria-describedby] to "general-eventEnd-error" and bind [attr.title] to the resolved message string in that same case.
    • Drop the separate general-endBeforeStart block only after confirming no existing test asserts on it (a quick search of tests/frontend shows it is only mentioned in tests/frontend/admin-general-pure.spec.ts as a value assertion on the constant endBeforeStartMessage, not as a DOM query). If a test asserts on the element, keep it as a redundant duplicate or update the test.
  3. Do not change the form-level validator at general.pure.ts:41-51 — it already returns { endBeforeStart: true } for both e < s and e === s. The Job requirement is purely a rendering fix.

4. Test Strategy

  • Target backend test file: tests/backend/migrations.spec.ts (new spec). Add a focused describe('InitSchema creates category with created_at + updated_at columns') that:

    • Boots AppModule against :memory: SQLite.
    • Calls dataSource.runMigrations({ transaction: 'each' }).
    • Queries PRAGMA table_info("category") and asserts the returned names contain both created_at and updated_at.
    • Sanity-checks that AdminCategoriesService.list() returns an array (length ≥ 6 seeded system rows) without throwing — this is the regression test that prevents the user-visible no such column: c.created_at error from coming back.
  • Target backend test file: tests/backend/admin-categories-service.spec.ts (extend). Add one it('list() succeeds against the migrated schema and returns seeded rows') that runs after the existing CRUD specs. The existing tests already use synchronize: true which would have masked the bug; the new test should rely on the migrations path so it actually exercises the schema the production app uses.

  • Target frontend test file: tests/frontend/admin-general-pure.spec.ts (extend). Add a describe('eventEndFieldMessage') block with three focused cases:

    1. Returns the ISO message when invalidDatetime is set, even if endBeforeStart is also set.
    2. Returns the end-before-start message when invalidDatetime is absent and endBeforeStart is set.
    3. Returns null when both are absent.
  • Mocking Strategy: No new mocks. Backend spec uses the real AppModule against :memory: SQLite (already the pattern in tests/backend/admin-validation.spec.ts and tests/backend/admin-general-service.spec.ts). Frontend spec continues to assert on the pure helper return values only.

  • Single command: npm test from /repo runs the full suite (already in package.json:14). Frontend and backend can also be run separately via npm run test:backend / npm run test:frontend.

5. Persistent Project Data (/data)

Not applicable to this Job. The DB schema change is a migration; tests use :memory: SQLite and never write to /data. The Categories endpoint reads from the existing category table without touching uploaded files or any other shared asset.