Files
HIPCTF2/.kilo/plans/894.md
T
2026-07-22 17:34:53 +00:00

7.7 KiB

Implementation Plan: Admin Area General Settings and Categories 1.12 (Job 894)

1. Architectural Reconnaissance

  • Codebase style & conventions:
    • Backend: NestJS 10 with TypeORM + better-sqlite3, TypeScript, async/await throughout, zod-validated DTOs.
    • Frontend: Angular 17 standalone components, signals + reactive forms, OnPush change detection, jest-jsdom for tests.
    • Tests live under /repo/tests/backend and /repo/tests/frontend (a single root-level tests/ directory — never alongside source). They are executed with npm test from the repository root via tests/jest.config.js (multi-project Jest config, two projects: backend and frontend).
  • Data Layer: SQLite via TypeORM (/data/hipctf/db.sqlite by default). Theme catalog is file-based (backend/themes/*.json), loaded at module-init by ThemeLoaderService and filtered at request-time by AdminGeneralService.listThemes().
  • Test Framework & Structure: Jest 29 with ts-jest. Two projects under tests/jest.config.js. Backend project uses node environment, frontend uses jsdom with tests/frontend/jest.setup.ts. New tests must be placed under tests/backend/ (or tests/frontend/) and be discoverable by the existing globs (<rootDir>/backend/**/*.spec.ts / <rootDir>/frontend/**/*.spec.ts).
  • Required Tools & Dependencies: No new dependencies are required. The fix is a path-resolution change in the existing service. No setup.sh change is required (the canonical themes already ship in the repo at backend/themes/).

2. Impacted Files

  • To Modify:
    • backend/src/modules/admin/general.service.ts — change the THEMES_DIR resolution so the default ./themes resolves to the backend project's themes/ directory regardless of process CWD. This is the only behavioural change needed.
  • To Create:
    • tests/backend/admin-general-list-themes.spec.ts — minimal unit test that asserts listThemes() returns all 10 canonical themes for the default configuration (the current admin-general-service.spec.ts already has a listThemes suite but it uses process.env.THEMES_DIR and a temp dir, so we add a focused new spec that exercises the default path-resolution without any env override, which is the regression case).

3. Proposed Changes

Root-cause analysis

backend/src/modules/admin/general.service.ts:80-103 (listThemes) reads

const themesDir = path.resolve(this.config.get<string>('THEMES_DIR', './themes'));

path.resolve('./themes', ...) is resolved against process.cwd(). The backend is started with node /repo/backend/dist/main.js from /repo (per package.json start script), so ./themes resolves to /repo/themes, which does not exist. The 10 canonical JSON files live at /repo/backend/themes/. Because the directory does not exist, present stays empty and every theme is filtered out, so GET /api/v1/admin/general/themes returns [] and the Admin → General "Global theme" <select> renders with zero options.

1. Backend logic — fix default path resolution

In backend/src/modules/admin/general.service.ts:

  • Replace the inline resolution with a small private helper, e.g.:
    private resolveThemesDir(raw: string): string {
      if (path.isAbsolute(raw)) return raw;
      // Resolve relative to the backend project root (one level up from
      // backend/src/modules/admin at compile time, i.e. backend/), so the
      // default './themes' always points at backend/themes regardless of
      // the process CWD used to launch `node backend/dist/main.js`.
      const backendRoot = path.resolve(__dirname, '..', '..', '..');
      return path.resolve(backendRoot, raw);
    }
    
    and use it in listThemes():
    const themesDir = this.resolveThemesDir(this.config.get<string>('THEMES_DIR', './themes'));
    
  • Rationale: from backend/src/modules/admin/general.service.ts, three .. segments walk back to the backend package root (backend/). At runtime the compiled file lives at backend/dist/modules/admin/general.service.js, so __dirname is backend/dist/modules/admin and three .. segments still reach backend/. This makes ./themes always point at the canonical backend/themes/ directory shipped in the repo, without changing the public THEMES_DIR contract (an absolute path or a custom relative path still works).
  • Keep the rest of listThemes() unchanged: it still intersects the loader's catalog with on-disk JSON ids, so a custom THEMES_DIR pointing at a different folder still filters accordingly.

2. Frontend / signal hardening (defensive, small)

The job reports a browser console warning RuntimeError: NG0600: Writing to signals is not allowed in a computed or an effect by default when the Admin General page loads with the empty theme list. Once the backend returns 10 themes, this symptom goes away. To make the component robust if the network call fails, no structural change to the signal graph is needed — the existing pattern (signal<ThemeView[]>([]) written in ngOnInit via this.themes.set(themes)) is correct and not inside an effect()/computed(). We do not introduce new signal writes; we only verify nothing in frontend/src/app/features/admin/general.component.ts writes a signal from a computed/effect, and leave the file untouched unless the implementer finds an unrelated effect() that mutates state (none exists today — verified by grep). If the implementer wants to be extra defensive, a single optional one-liner fallback in general.pure.ts (e.g. a pure BUILTIN_THEME_VIEW_FALLBACK array used when the API returns []) is acceptable but not required.

3. setup.sh / data layer

No changes. /data is not touched by this job. The THEMES_DIR env var (and its default) continue to work the same way; only the default resolution now points at the correct directory.

4. Test Strategy

Target Unit Test File

  • tests/backend/admin-general-list-themes.spec.ts (new) — single focused regression spec for the path-resolution bug.

Mocking Strategy

  • Instantiate AdminGeneralService directly with hand-rolled fakes for the three injected collaborators (SettingsService, ThemeLoaderService, SseHubService) and a ConfigService whose .get('THEMES_DIR', './themes') returns the default './themes' (no process.env override) so the test exercises the same code path the real backend takes.
  • ThemeLoaderService.listThemes() is faked to return 10 entries with the canonical ids (classic, midnight, sunset, forest, cyber, paper, crimson, ocean, neon, monochrome).
  • Do NOT mock fs or path. The test relies on the real backend/themes/ directory shipped in the repo (this is exactly the regression case).

Cases (minimal, single-file)

  1. Regression: with no THEMES_DIR override, listThemes() returns exactly 10 entries whose ids are a permutation of the canonical THEME_IDS set, and each entry has id === key and a non-empty name. This is the single must-have test that locks in the fix.
  2. Robustness: when called twice in a row, the result is referentially equivalent (no I/O ordering surprises).

That is intentionally the entire scope. We do not add tests for: visual rendering of the <select> (frontend tests focus on logic), custom THEMES_DIR overrides (already covered by the existing admin-general-service.spec.ts "filter to on-disk themes" suite), PUT /settings happy-path (already covered), the unknown-themeKey 400 (already covered by the existing rejects unknown themeKey case in admin-general-service.spec.ts), and the runtime NG0600 warning (a downstream symptom that goes away once the list is non-empty — not a separate contract).

The single test must run with npm test -- tests/backend/admin-general-list-themes.spec.ts (or simply npm test) from the repository root.