From b99577e76a577fd34ee88830259a183ba2f1dc6b Mon Sep 17 00:00:00 2001 From: m0rph3us1987 Date: Fri, 24 Jul 2026 12:58:34 +0200 Subject: [PATCH] UI-Improvements --- backend/src/common/types/theme-ids.ts | 1 + .../src/common/utils/theme-loader.service.ts | 47 +-- backend/src/database/system-category-icons.ts | 108 +++--- backend/themes/00-hip.json | 17 + frontend/angular.json | 3 + .../src/app/core/services/bootstrap.types.ts | 16 + .../features/admin/admin-shell.component.ts | 68 +++- .../admin/blog/blog-delete-modal.component.ts | 42 ++- .../admin/blog/blog-form-modal.component.ts | 83 +++-- .../admin/categories/categories.component.ts | 57 +++- .../category-delete-modal.component.ts | 41 ++- .../category-form-modal.component.ts | 67 +++- .../challenge-delete-modal.component.ts | 45 ++- .../challenge-form-modal.component.ts | 231 ++++++++----- .../challenge-import-modal.component.ts | 57 +++- .../admin/player-delete-modal.component.ts | 45 ++- .../system/system-confirm-modal.component.ts | 86 +++-- .../features/blog/blog-presenter.component.ts | 63 +++- .../challenges/category-column.component.ts | 33 +- .../challenges/challenge-card.component.ts | 86 +++-- .../challenges/challenge-modal.component.ts | 213 ++++++++---- .../src/app/features/home/home.component.ts | 1 + .../features/landing/landing.component.css | 253 ++++++++------ .../features/landing/landing.component.html | 99 ++++-- .../app/features/landing/landing.component.ts | 13 +- .../scoreboard/event-log.component.ts | 76 +++-- .../features/scoreboard/matrix.component.ts | 104 +++--- .../features/scoreboard/ranking.component.ts | 138 +++++--- .../scoreboard/score-graph.component.ts | 85 ++--- .../app/features/scoreboard/tabs.component.ts | 57 ++-- .../change-password-modal.component.ts | 79 ++++- .../shell/header/shell-header.component.ts | 312 +++++++++++++++--- .../category-icon/category-icon.component.ts | 99 ++++++ .../sticker-logo/sticker-logo.component.ts | 49 +++ frontend/src/index.html | 3 + frontend/src/styles.css | 196 +++++++++-- .../backend/admin-general-list-themes.spec.ts | 13 +- tests/backend/admin-general-service.spec.ts | 6 +- tests/backend/theme-required.spec.ts | 23 +- tests/frontend/theme.spec.ts | 8 +- 40 files changed, 2202 insertions(+), 821 deletions(-) create mode 100644 backend/themes/00-hip.json create mode 100644 frontend/src/app/shared/components/category-icon/category-icon.component.ts create mode 100644 frontend/src/app/shared/components/sticker-logo/sticker-logo.component.ts 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