Web-Anwendung: Design-Tokens, Texte, Gesundheitsendpunkt

Design-Tokens aus docs/10-design-system.md als CSS-Variablen: Grundpalette,
die sechs Klassifizierungsfarben, Eckenradius durchgehend 2 px, keine Schatten
ausser dem Fokusring. Der Schwärzungsbalken mit dem einmaligen Durchwischen
respektiert prefers-reduced-motion. Vier Tests halten die Tokens gegen das
Dokument.

IBM Plex selbst gehostet als woff2, latin und latin-ext getrennt über
unicode-range, font-display: swap, immutable-Cache.

Alle Oberflächentexte in lib/i18n/de.ts. Ein Test durchsucht app/ und
components/ nach Umlauten im JSX und schlägt fehl, wenn dort deutscher Text
steht. Er hat beim ersten Lauf gleich einen Verstoss gefunden.

/api/health prüft Datenbank, Discord-Gateway und ClamAV. Die Gesamtbewertung
fällt nur auf "down", wenn die Datenbank weg ist: die anderen Dienste haben
eigene Healthchecks im Stack, und ein ausgefallener Virenscanner soll nicht
den Web-Container als tot markieren.

Strikte CSP mit Nonce, kein unsafe-inline für Skripte. Die erste Route liegt
bei 100 kB JavaScript nach Kompression, Budget sind 120 kB.
This commit is contained in:
Harlekin
2026-09-01 00:01:41 +02:00
parent 8f0644e053
commit ade977538d
38 changed files with 1343 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
import { describe, expect, it } from 'vitest'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
const css = readFileSync(join(process.cwd(), 'app/globals.css'), 'utf8')
describe('Design-Tokens', () => {
it('enthaelt die Grundpalette aus dem Design-System', () => {
for (const hex of ['#15181a', '#1d2124', '#262b2f', '#3a4147', '#e4e7e4', '#939c99', '#7a97a6']) {
expect(css).toContain(hex)
}
})
it('enthaelt alle sechs Klassifizierungsfarben', () => {
for (const hex of ['#8a918d', '#6d8a9c', '#4e8f72', '#b08637', '#b4543c', '#7e5e96']) {
expect(css).toContain(hex)
}
})
it('setzt den Eckenradius durchgehend auf 2 px', () => {
const radii = [...css.matchAll(/--radius-[\w-]+:\s*([^;]+);/g)].map((m) => m[1]!.trim())
expect(radii.length).toBeGreaterThan(0)
for (const value of radii) expect(['0px', '2px']).toContain(value)
})
it('kennt keine Schatten ausser dem Fokusring', () => {
const shadows = [...css.matchAll(/--shadow-[\w-]+:\s*([^;]+);/g)].map((m) => m[1]!.trim())
expect(shadows.length).toBeGreaterThan(0)
for (const value of shadows) expect(value).toBe('none')
})
it('respektiert prefers-reduced-motion bei der Schwaerzung', () => {
expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)/)
})
})
+33
View File
@@ -0,0 +1,33 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('@/lib/db', () => ({
db: {
$queryRaw: vi.fn(async () => [{ '?column?': 1 }]),
serviceHeartbeat: { findUnique: vi.fn(async () => ({ service: 'worker', status: 'up', detail: null, at: new Date() })) },
},
}))
vi.mock('@/lib/env', () => ({
getEnv: () => ({ CLAMAV_SOCKET: '/var/run/clamav/clamd.sock' }),
}))
vi.mock('@/lib/health/clamav', () => ({ pingClamav: vi.fn(async () => true) }))
describe('collectHealth', () => {
it('meldet betriebsbereit, wenn alle drei Abhaengigkeiten antworten', async () => {
const { collectHealth } = await import('@/lib/health/check')
const report = await collectHealth()
expect(report.status).toBe('up')
expect(report.components.database.status).toBe('up')
expect(report.components.discordGateway.status).toBe('up')
expect(report.components.clamav.status).toBe('up')
})
it('bleibt nur eingeschraenkt, wenn allein die Virenpruefung ausfaellt', async () => {
const clamav = await import('@/lib/health/clamav')
vi.mocked(clamav.pingClamav).mockResolvedValueOnce(false)
const { collectHealth } = await import('@/lib/health/check')
const report = await collectHealth()
expect(report.status).toBe('degraded')
})
})
+47
View File
@@ -0,0 +1,47 @@
import { describe, expect, it } from 'vitest'
import { readFileSync, readdirSync, statSync } from 'node:fs'
import { join } from 'node:path'
import { de } from '@/lib/i18n/de'
describe('Sprachkonvention', () => {
it('kennt alle sechs Freigabestufen', () => {
for (let level = 0; level <= 5; level++) {
expect(de.clearance.names[level]).toBeTypeOf('string')
}
expect(de.clearance.names[3]).toBe('Geheim')
})
it('verzichtet auf Ausrufezeichen und Entschuldigungen', () => {
const flat = JSON.stringify(de)
expect(flat).not.toMatch(/!/)
expect(flat.toLowerCase()).not.toMatch(/erfolgreich|entschuldigung/)
})
it('haelt deutsche Oberflaechentexte aus dem JSX heraus', () => {
// Umlaute und Eszett sind der billigste verlaessliche Marker fuer einen
// deutschen Text. Alles Sichtbare gehoert nach lib/i18n/de.ts, siehe CLAUDE.md.
const offenders: string[] = []
const roots = ['app', 'components']
function walk(dir: string) {
for (const name of readdirSync(dir)) {
const path = join(dir, name)
if (statSync(path).isDirectory()) {
walk(path)
continue
}
if (!name.endsWith('.tsx')) continue
const source = readFileSync(path, 'utf8')
for (const [index, line] of source.split('\n').entries()) {
const withoutComments = line.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '')
if (/[äöüÄÖÜß]/.test(withoutComments)) {
offenders.push(`${path}:${index + 1}`)
}
}
}
}
for (const root of roots) walk(join(process.cwd(), root))
expect(offenders).toEqual([])
})
})
+3
View File
@@ -0,0 +1,3 @@
import '@testing-library/dom'
process.env.TZ = 'Europe/Berlin'