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/backendand/repo/tests/frontend(a single root-leveltests/directory — never alongside source). They are executed withnpm testfrom the repository root viatests/jest.config.js(multi-project Jest config, two projects:backendandfrontend).
- Data Layer: SQLite via TypeORM (
/data/hipctf/db.sqliteby default). Theme catalog is file-based (backend/themes/*.json), loaded at module-init byThemeLoaderServiceand filtered at request-time byAdminGeneralService.listThemes(). - Test Framework & Structure: Jest 29 with
ts-jest. Two projects undertests/jest.config.js. Backend project usesnodeenvironment, frontend usesjsdomwithtests/frontend/jest.setup.ts. New tests must be placed undertests/backend/(ortests/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.shchange is required (the canonical themes already ship in the repo atbackend/themes/).
2. Impacted Files
- To Modify:
backend/src/modules/admin/general.service.ts— change theTHEMES_DIRresolution so the default./themesresolves to the backend project'sthemes/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 assertslistThemes()returns all 10 canonical themes for the default configuration (the currentadmin-general-service.spec.tsalready has alistThemessuite but it usesprocess.env.THEMES_DIRand 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.:
and use it in
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); }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 atbackend/dist/modules/admin/general.service.js, so__dirnameisbackend/dist/modules/adminand three..segments still reachbackend/. This makes./themesalways point at the canonicalbackend/themes/directory shipped in the repo, without changing the publicTHEMES_DIRcontract (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 JSONids, so a customTHEMES_DIRpointing 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
AdminGeneralServicedirectly with hand-rolled fakes for the three injected collaborators (SettingsService,ThemeLoaderService,SseHubService) and aConfigServicewhose.get('THEMES_DIR', './themes')returns the default'./themes'(noprocess.envoverride) 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
fsorpath. The test relies on the realbackend/themes/directory shipped in the repo (this is exactly the regression case).
Cases (minimal, single-file)
- Regression: with no
THEMES_DIRoverride,listThemes()returns exactly 10 entries whose ids are a permutation of the canonicalTHEME_IDSset, and each entry hasid === keyand a non-emptyname. This is the single must-have test that locks in the fix. - 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.