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:
-
Categories SQL —
backend/src/modules/admin/categories.service.ts:30-37usescreateQueryBuilder('c').orderBy('LOWER(c.abbreviation)', 'ASC').addOrderBy('c.abbreviation', 'ASC').getMany().getMany()auto-selects every column mapped onCategoryEntity, includingcreated_atandupdated_at(added by migration1700000000200-AddCategoryTimestampsAndUniqueAbbrev). TheInitSchema1700000000000migration (backend/src/database/migrations/1700000000000-InitSchema.ts:28-37) creates thecategorytable 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 raisesSqliteError: no such column: c.created_atand the Categories section on/admin/general(which embedsAdminCategoriesComponent) renders the error to the admin. The fix is to make thecategorytable includecreated_atandupdated_atin its initialCREATE TABLEand keep the 200 migration as a no-opif (!names.has(...))ALTER for pre-existing DBs. -
Event End equal-to-start field-level message —
frontend/src/app/features/admin/general.pure.ts:41-51already returns{ endBeforeStart: true }whene <= s, andfrontend/src/app/features/admin/general.component.ts:123-128renders thegeneral-endBeforeStartelement whenevereventEndCrossFieldError()is true. However the message lives in a separatedata-testid="general-endBeforeStart"div instead of inside the per-field error regiondata-testid="general-eventEnd-error"that the Job (anddocs/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-fieldendBeforeStartmessage intogeneral-eventEnd-error(and surface it viaaria-invalid/aria-describedby/titleon 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 underbackend/src/database/migrations/and are wired throughDatabaseModule.MIGRATIONS(backend/src/database/database.module.ts:31-36) and run on bootstrap byDatabaseInitService.init()(backend/src/database/database-init.service.ts:26-54). Entities are auto-selected fromDatabaseModule.ENTITIESand must remain in sync with the SQL schema produced by the migrations. - Frontend: Angular 17+ standalone components with
ChangeDetectionStrategy.OnPush, reactive forms, and theValue/Invalid/TouchedOrDirtysignal trio for inline error messaging (seegeneral.component.ts:204-321). Pure validation / message helpers live infrontend/src/app/features/admin/general.pure.tsand are unit-tested undertests/frontend/admin-general-pure.spec.ts. - Error envelopes: backend emits
{ code: 'VALIDATION_FAILED', message, details: [{path, message}] }viaZodValidationPipe(backend/src/common/pipes/zod-validation.pipe.ts:9-15) for400s;ApiErrorhelpers live inbackend/src/common/errors/api-error.ts.
- Backend: NestJS 10, TypeScript strict, TypeORM 0.3.20 over
-
Data Layer: SQLite (
better-sqlite3) database file atprocess.env.DATABASE_PATH(default/data/hipctf/db.sqlite). Thecategorytable is created byInitSchema1700000000000and seeded / amended bySeedSystemData1700000000100,AddCategoryTimestampsAndUniqueAbbrev1700000000200, andUpdateSystemCategoryKeys1700000000300. TheCategoryEntity(backend/src/database/entities/category.entity.ts) is the single source of truth for the column names that TypeORM will SELECT viagetMany(). -
Test Framework & Structure: Jest (
ts-jest) with two projects (backendandfrontend) configured intests/jest.config.js. Tests live intests/backend/**/*.spec.tsandtests/frontend/**/*.spec.tsand run with a single commandnpm testfrom the repo root (already inpackage.json:scripts.test). Backend tests typically boot the realAppModuleagainst:memory:SQLite viaprocess.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/commonare all already declared and bootstrapped bysetup.sh(setup.sh:1-19). Nosetup.shchange 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 initialCREATE TABLE "category"so the columns exist from the very first migration; preserve thedescriptionandicon_pathdefaults.frontend/src/app/features/admin/general.component.ts— extend the Event Endgeneral-eventEnd-errorelement so it also renders the cross-fieldendBeforeStartmessage when no per-fieldinvalidDatetimeerror is present; keepgeneral-endBeforeStartas 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 runningInitSchema1700000000000against an empty database produces acategorytable that contains acreated_atcolumn (andupdated_at). This locks the SQL schema in place so the Categories endpoint can never regress to theno such column: c.created_atstate.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
-
Update
InitSchema1700000000000atbackend/src/database/migrations/1700000000000-InitSchema.ts:28-37so thecategoryCREATE TABLEincludescreated_atandupdated_atcolumns 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 foruser.created_at(line 17) andchallenge.created_at(line 54) so newly-inserted rows get a sane timestamp without a follow-up UPDATE-pass. -
Keep
AddCategoryTimestampsAndUniqueAbbrev1700000000200unchanged. Itsif (!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 todown()either — the down() drops the columns, which is the correct mirror ofup(). -
Keep
UpdateSystemCategoryKeys1700000000300unchanged. ItsINSERTalready listscreated_atandupdated_at(backend/src/database/migrations/1700000000300-UpdateSystemCategoryKeys.ts:51). -
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
-
Update
frontend/src/app/features/admin/general.pure.tsto expose a smalleventEndFieldMessage(controlErrors, crossField)pure helper that returns the canonical Event End field error string:controlErrors?.['invalidDatetime']form.errors?.['endBeforeStart']Returned message trueany "Event end must be a valid ISO-8601 datetime."falsy true"Event end must be after event start."falsy falsy nullThis mirrors the existing
datetimeMessagepattern and keeps the template a thin consumer. -
Update
frontend/src/app/features/admin/general.component.ts:- Replace the current
@if (showEventEndError())/@if (eventEndCrossFieldError())pair (lines 123-128) with a singleeventEndFieldMessageconsumer that always renders inside thedata-testid="general-eventEnd-error"element. When both validators are off, the element is absent. - Bind
[attr.aria-invalid]totruewheneventEndFieldMessage(...)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-endBeforeStartblock only after confirming no existing test asserts on it (a quick search oftests/frontendshows it is only mentioned intests/frontend/admin-general-pure.spec.tsas a value assertion on the constantendBeforeStartMessage, not as a DOM query). If a test asserts on the element, keep it as a redundant duplicate or update the test.
- Replace the current
-
Do not change the form-level validator at
general.pure.ts:41-51— it already returns{ endBeforeStart: true }for bothe < sande === 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 focuseddescribe('InitSchema creates category with created_at + updated_at columns')that:- Boots
AppModuleagainst:memory:SQLite. - Calls
dataSource.runMigrations({ transaction: 'each' }). - Queries
PRAGMA table_info("category")and asserts the returned names contain bothcreated_atandupdated_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-visibleno such column: c.created_aterror from coming back.
- Boots
-
Target backend test file:
tests/backend/admin-categories-service.spec.ts(extend). Add oneit('list() succeeds against the migrated schema and returns seeded rows')that runs after the existing CRUD specs. The existing tests already usesynchronize: truewhich 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 adescribe('eventEndFieldMessage')block with three focused cases:- Returns the ISO message when
invalidDatetimeis set, even ifendBeforeStartis also set. - Returns the end-before-start message when
invalidDatetimeis absent andendBeforeStartis set. - Returns
nullwhen both are absent.
- Returns the ISO message when
-
Mocking Strategy: No new mocks. Backend spec uses the real
AppModuleagainst:memory:SQLite (already the pattern intests/backend/admin-validation.spec.tsandtests/backend/admin-general-service.spec.ts). Frontend spec continues to assert on the pure helper return values only. -
Single command:
npm testfrom/reporuns the full suite (already inpackage.json:14). Frontend and backend can also be run separately vianpm 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.