UI-Improvements

This commit is contained in:
m0rph3us1987
2026-07-24 12:58:34 +02:00
parent 143ed3c538
commit b99577e76a
40 changed files with 2202 additions and 821 deletions
+1
View File
@@ -1,4 +1,5 @@
export const THEME_IDS = [
'hip',
'classic',
'midnight',
'sunset',
@@ -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<string, Theme> = 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<void> {
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<string>(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',
+39 -69
View File
@@ -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/<KEY>.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 = '<polyline points="4 17 10 11 4 5" /><line x1="12" x2="20" y1="19" y2="19" />';
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 = '<path d="M19.07 4.93A10 10 0 0 0 6.99 3.34" /><path d="M4.93 19.07A10 10 0 0 0 20.66 17" /><path d="M2 12h20" /><path d="M12 2v20" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" />';
break;
case 'PWN':
strokeColor = '#ffaa00';
pathSvg = '<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />';
break;
case 'REV':
strokeColor = '#00f0ff';
pathSvg = '<path d="M3 12a9 9 0 0 1 15-6.7L21 8" /><path d="M21 3v5h-5" /><path d="M21 12a9 9 0 0 1-15 6.7L3 16" /><path d="M3 21v-5h5" />';
break;
case 'CRY':
strokeColor = '#b000ff';
pathSvg = '<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" /><path d="m3.3 7 8.7 5 8.7-5" /><path d="M12 22V12" />';
break;
case 'HW':
strokeColor = '#00ff66';
pathSvg = '<rect x="4" y="4" width="16" height="16" rx="2" /><rect x="9" y="9" width="6" height="6" /><path d="M15 2v2M9 2v2M15 20v2M9 20v2M2 15h2M2 9h2M20 15h2M20 9h2" />';
break;
default:
strokeColor = '#b000ff';
pathSvg = '<polyline points="4 17 10 11 4 5" /><line x1="12" x2="20" y1="19" y2="19" />';
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 = `<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<rect width="128" height="128" rx="8" fill="#080808" stroke="#33333d" stroke-width="4" />
<g transform="translate(24, 24) scale(3.33)" fill="none" stroke="${strokeColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
${pathSvg}
</g>
</svg>`;
return Buffer.from(svg);
}
export interface SystemCategoryIconSeedResult {
@@ -95,20 +77,8 @@ export async function seedSystemCategoryIcons(uploadDir: string): Promise<System
/* fall through and write */
}
const bg = colorForKey(key);
const overlay = renderLabel(key);
const buf = await sharp({
create: {
width: SYSTEM_ICON_SIZE,
height: SYSTEM_ICON_SIZE,
channels: 4,
background: { r: bg.r, g: bg.g, b: bg.b, alpha: 1 },
},
})
.composite([{ input: overlay, raw: { width: SYSTEM_ICON_SIZE, height: SYSTEM_ICON_SIZE, channels: 4 } }])
.png()
.toBuffer();
const svgBuf = renderCategoryIconSvg(key);
const buf = await sharp(svgBuf).png().toBuffer();
fs.writeFileSync(target, buf);
result.written.push(target);
}
+17
View File
@@ -0,0 +1,17 @@
{
"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]
}
}