Files
AfiS-Aktensystem/web/lib/health/check.ts
T
Harlekin ade977538d 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.
2026-09-01 00:01:41 +02:00

78 lines
2.5 KiB
TypeScript

import { db } from '@/lib/db'
import { getEnv } from '@/lib/env'
import { pingClamav } from '@/lib/health/clamav'
export type ComponentState = 'up' | 'degraded' | 'down'
export type HealthReport = {
status: ComponentState
checkedAt: string
components: Record<'database' | 'discordGateway' | 'clamav', {
status: ComponentState
detail?: string
}>
}
/** Der Worker gilt als abgehaengt, wenn sein Lebenszeichen aelter ist. */
const HEARTBEAT_MAX_AGE_MS = 3 * 60_000
async function checkDatabase(): Promise<{ status: ComponentState; detail?: string }> {
try {
await db.$queryRaw`SELECT 1`
return { status: 'up' }
} catch (error) {
return { status: 'down', detail: error instanceof Error ? error.message : 'unbekannt' }
}
}
async function checkDiscordGateway(): Promise<{ status: ComponentState; detail?: string }> {
try {
const beat = await db.serviceHeartbeat.findUnique({ where: { service: 'worker' } })
if (!beat) return { status: 'down', detail: 'kein Lebenszeichen' }
const ageMs = Date.now() - beat.at.getTime()
if (ageMs > HEARTBEAT_MAX_AGE_MS) {
return { status: 'down', detail: `Lebenszeichen ${Math.round(ageMs / 1000)} s alt` }
}
if (beat.status !== 'up') {
return { status: beat.status === 'degraded' ? 'degraded' : 'down', detail: beat.detail ?? undefined }
}
return { status: 'up' }
} catch {
return { status: 'down', detail: 'nicht abfragbar' }
}
}
async function checkClamav(): Promise<{ status: ComponentState; detail?: string }> {
const ok = await pingClamav(getEnv().CLAMAV_SOCKET)
return ok ? { status: 'up' } : { status: 'down', detail: 'clamd antwortet nicht' }
}
/**
* Sammelt den Zustand der drei Abhaengigkeiten. Die Gesamtbewertung faellt
* bewusst nur auf `down`, wenn die Datenbank weg ist: Worker, clamav und pdf
* haben eigene Healthchecks im Stack, und ein ausgefallener Virenscanner soll
* nicht den ganzen Web-Container als tot markieren. Sichtbar bleibt der Ausfall
* trotzdem, in dieser Antwort und in Dockge am jeweiligen Dienst.
*/
export async function collectHealth(): Promise<HealthReport> {
const [database, discordGateway, clamav] = await Promise.all([
checkDatabase(),
checkDiscordGateway(),
checkClamav(),
])
const status: ComponentState =
database.status === 'down'
? 'down'
: discordGateway.status === 'up' && clamav.status === 'up'
? 'up'
: 'degraded'
return {
status,
checkedAt: new Date().toISOString(),
components: { database, discordGateway, clamav },
}
}