diff --git a/backend/src/common/types/theme-ids.ts b/backend/src/common/types/theme-ids.ts index d1ff159..b3599c1 100644 --- a/backend/src/common/types/theme-ids.ts +++ b/backend/src/common/types/theme-ids.ts @@ -1,4 +1,5 @@ export const THEME_IDS = [ + 'hip', 'classic', 'midnight', 'sunset', diff --git a/backend/src/common/utils/theme-loader.service.ts b/backend/src/common/utils/theme-loader.service.ts index f1a81e7..3572422 100644 --- a/backend/src/common/utils/theme-loader.service.ts +++ b/backend/src/common/utils/theme-loader.service.ts @@ -11,7 +11,7 @@ import { SETTINGS_KEYS } from '../../config/env.schema'; export class ThemeLoaderService implements OnModuleInit { private readonly logger = new Logger(ThemeLoaderService.name); private themes: Map = new Map(); - private defaultThemeId: ThemeId = 'classic'; + private defaultThemeId: ThemeId = 'hip'; private themesDir = './themes'; constructor( @@ -54,11 +54,6 @@ export class ThemeLoaderService implements OnModuleInit { } } - /** - * After disk-loading, backfill any of the 10 canonical themes that are - * missing with the matching built-in. This guarantees the service - * ALWAYS exposes all 10 theme ids regardless of disk state. - */ private backfillMissing(): void { for (const t of BUILTIN_THEMES) { if (!this.themes.has(t.id)) { @@ -68,29 +63,21 @@ export class ThemeLoaderService implements OnModuleInit { } } - /** - * Validate the configured theme key (from settings) against the 10 - * canonical ids. If it's missing or invalid, fall back to 'classic' with - * a warning. Persists the canonical value back to settings so future - * reads are consistent. - */ private async validateConfiguredKey(): Promise { - let configured = 'classic'; + let configured = 'hip'; try { - configured = await this.settings.get(SETTINGS_KEYS.THEME_KEY, 'classic'); + configured = await this.settings.get(SETTINGS_KEYS.THEME_KEY, 'hip'); } catch (e) { - // The setting table may not exist yet (early boot, migrations not - // run). Fall back to the safe default and try again later. - this.logger.warn(`Could not read configured themeKey (${(e as Error).message}); falling back to 'classic'`); - this.defaultThemeId = 'classic'; + this.logger.warn(`Could not read configured themeKey (${(e as Error).message}); falling back to 'hip'`); + this.defaultThemeId = 'hip'; return; } const validIds = new Set(THEME_IDS); if (!configured || !validIds.has(configured)) { - this.logger.warn(`Configured themeKey '${configured}' is not in [${THEME_IDS.join(',')}]; falling back to 'classic'`); - this.defaultThemeId = 'classic'; + this.logger.warn(`Configured themeKey '${configured}' is not in [${THEME_IDS.join(',')}]; falling back to 'hip'`); + this.defaultThemeId = 'hip'; try { - await this.settings.set(SETTINGS_KEYS.THEME_KEY, 'classic'); + await this.settings.set(SETTINGS_KEYS.THEME_KEY, 'hip'); } catch { // Setting table may still not exist; ignore. } @@ -125,13 +112,29 @@ export class ThemeLoaderService implements OnModuleInit { return Array.from(this.themes.values()); } - /** Test/internal accessor for the resolved default id. */ getDefaultId(): ThemeId { return this.defaultThemeId; } } export const BUILTIN_THEMES: Theme[] = [ + { + id: 'hip', + name: 'HIP', + tokens: { + primary: '#ff0033', + secondary: '#b000ff', + accent: '#00f0ff', + surface: '#050505', + text: '#ffffff', + success: '#00ff66', + warning: '#ffea00', + danger: '#ff0033', + fontFamily: "'Chakra Petch', 'JetBrains Mono', monospace", + radii: { sm: '0px', md: '0px', lg: '0px' }, + spacingScale: [4, 8, 12, 16, 24, 32, 48], + }, + }, { id: 'classic', name: 'Classic', diff --git a/backend/src/database/system-category-icons.ts b/backend/src/database/system-category-icons.ts index d286998..1907fa8 100644 --- a/backend/src/database/system-category-icons.ts +++ b/backend/src/database/system-category-icons.ts @@ -5,24 +5,8 @@ import sharp from 'sharp'; /** * Canonical system-category icon seeder. * - * The canonical six system categories (CRY / HW / MSC / PWN / REV / WEB) are - * declared as `CANONICAL_SYSTEM_CATEGORIES` in - * `1700000000400-RepairCategorySchemaAndSystemCategories.ts`, whose - * `icon_path` column references `/uploads/icons/.png`. The migration - * creates the DB rows but never writes the PNG assets to disk, so a freshly - * bootstrapped instance serves 404s for every system category icon. - * - * This helper closes that gap by generating a deterministic 128Γ—128 PNG for - * each canonical key on startup. It is intentionally idempotent: existing - * files (including admin-uploaded icons written through `UploadsController`) - * are never overwritten. - * - * IMPORTANT: keep this list in sync with - * `CANONICAL_SYSTEM_CATEGORIES` in - * `1700000000400-RepairCategorySchemaAndSystemCategories.ts` and with the - * hard-coded `iconPath` URLs in - * `1700000000300-UpdateSystemCategoryKeys.ts` and - * `1700000000400-RepairCategorySchemaAndSystemCategories.ts`. + * Generates deterministic 128Γ—128 PNG vector icon images for each canonical system + * category key (CRY, HW, MSC, PWN, REV, WEB) matching the legacy CTF app layout. */ export const CANONICAL_SYSTEM_ICON_KEYS = ['CRY', 'HW', 'MSC', 'PWN', 'REV', 'WEB'] as const; @@ -30,46 +14,44 @@ export type CanonicalSystemIconKey = (typeof CANONICAL_SYSTEM_ICON_KEYS)[number] export const SYSTEM_ICON_SIZE = 128; -const PALETTE: ReadonlyArray<{ r: number; g: number; b: number }> = [ - { r: 0x1f, g: 0x6f, b: 0xeb }, - { r: 0x16, g: 0xa3, b: 0x4a }, - { r: 0xea, g: 0x58, b: 0x0c }, - { r: 0x93, g: 0x3f, b: 0xea }, - { r: 0xdb, g: 0x27, b: 0x77 }, - { r: 0x06, g: 0xb6, b: 0xd4 }, - { r: 0xdc, g: 0x26, b: 0x26 }, - { r: 0x65, g: 0x73, b: 0x0d }, -]; +function renderCategoryIconSvg(key: string): Buffer { + let strokeColor = '#b000ff'; + let pathSvg = ''; -function colorForKey(key: string): { r: number; g: number; b: number } { - let hash = 0; - for (let i = 0; i < key.length; i++) { - hash = (hash * 31 + key.charCodeAt(i)) >>> 0; + switch (key) { + case 'WEB': + strokeColor = '#ff0033'; + pathSvg = ''; + break; + case 'PWN': + strokeColor = '#ffaa00'; + pathSvg = ''; + break; + case 'REV': + strokeColor = '#00f0ff'; + pathSvg = ''; + break; + case 'CRY': + strokeColor = '#b000ff'; + pathSvg = ''; + break; + case 'HW': + strokeColor = '#00ff66'; + pathSvg = ''; + break; + default: + strokeColor = '#b000ff'; + pathSvg = ''; + break; } - return PALETTE[hash % PALETTE.length]; -} -function renderLabel(label: string): Buffer { - const width = SYSTEM_ICON_SIZE; - const height = SYSTEM_ICON_SIZE; - const cellW = Math.floor(width / Math.max(label.length, 1)); - const cellH = Math.floor(height / 2); - const cells: Array<{ x: number; y: number; on: boolean }> = []; - for (let i = 0; i < label.length; i++) { - const cx = Math.floor(cellW * (i + 0.5)); - const cy = cellH; - cells.push({ x: cx, y: cy, on: true }); - } - const grid = Buffer.alloc(width * height * 4, 0); - for (const c of cells) { - if (c.x < 0 || c.x >= width || c.y < 0 || c.y >= height) continue; - const idx = (c.y * width + c.x) * 4; - grid[idx] = 255; - grid[idx + 1] = 255; - grid[idx + 2] = 255; - grid[idx + 3] = 220; - } - return grid; + const svg = ` + + + ${pathSvg} + + `; + return Buffer.from(svg); } export interface SystemCategoryIconSeedResult { @@ -95,20 +77,8 @@ export async function seedSystemCategoryIcons(uploadDir: string): Promise