feat(training+controls): interactive video pipeline, training blocks, control generator, CE libraries
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 37s
CI/CD / test-python-backend-compliance (push) Successful in 39s
CI/CD / test-python-document-crawler (push) Successful in 26s
CI/CD / test-python-dsms-gateway (push) Successful in 23s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Has been skipped

Interactive Training Videos (CP-TRAIN):
- DB migration 022: training_checkpoints + checkpoint_progress tables
- NarratorScript generation via Anthropic (AI Teacher persona, German)
- TTS batch synthesis + interactive video pipeline (slides + checkpoint slides + FFmpeg)
- 4 new API endpoints: generate-interactive, interactive-manifest, checkpoint submit, checkpoint progress
- InteractiveVideoPlayer component (HTML5 Video, quiz overlay, seek protection, progress tracking)
- Learner portal integration with automatic completion on all checkpoints passed
- 30 new tests (handler validation + grading logic + manifest/progress + seek protection)

Training Blocks:
- Block generator, block store, block config CRUD + preview/generate endpoints
- Migration 021: training_blocks schema

Control Generator + Canonical Library:
- Control generator routes + service enhancements
- Canonical control library helpers, sidebar entry
- Citation backfill service + tests
- CE libraries data (hazard, protection, evidence, lifecycle, components)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-16 21:41:48 +01:00
parent d2133dbfa2
commit 4f6bc8f6f6
50 changed files with 17299 additions and 198 deletions

View File

@@ -320,3 +320,158 @@ export async function generateVideo(moduleId: string): Promise<TrainingMedia> {
export async function previewVideoScript(moduleId: string): Promise<{ title: string; sections: Array<{ heading: string; text: string; bullet_points: string[] }> }> {
return apiFetch(`/content/${moduleId}/preview-script`, { method: 'POST' })
}
// =============================================================================
// TRAINING BLOCKS (Controls → Schulungsmodule)
// =============================================================================
import type {
TrainingBlockConfig,
CanonicalControlSummary,
CanonicalControlMeta,
BlockPreview,
BlockGenerateResult,
TrainingBlockControlLink,
} from './types'
export async function listBlockConfigs(): Promise<{ blocks: TrainingBlockConfig[]; total: number }> {
return apiFetch('/blocks')
}
export async function createBlockConfig(data: {
name: string
description?: string
domain_filter?: string
category_filter?: string
severity_filter?: string
target_audience_filter?: string
regulation_area: string
module_code_prefix: string
frequency_type?: string
duration_minutes?: number
pass_threshold?: number
max_controls_per_module?: number
}): Promise<TrainingBlockConfig> {
return apiFetch<TrainingBlockConfig>('/blocks', {
method: 'POST',
body: JSON.stringify(data),
})
}
export async function getBlockConfig(id: string): Promise<TrainingBlockConfig> {
return apiFetch<TrainingBlockConfig>(`/blocks/${id}`)
}
export async function updateBlockConfig(id: string, data: Record<string, unknown>): Promise<TrainingBlockConfig> {
return apiFetch<TrainingBlockConfig>(`/blocks/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
})
}
export async function deleteBlockConfig(id: string): Promise<void> {
return apiFetch(`/blocks/${id}`, { method: 'DELETE' })
}
export async function previewBlock(id: string): Promise<BlockPreview> {
return apiFetch<BlockPreview>(`/blocks/${id}/preview`, { method: 'POST' })
}
export async function generateBlock(id: string, data?: {
language?: string
auto_matrix?: boolean
}): Promise<BlockGenerateResult> {
return apiFetch<BlockGenerateResult>(`/blocks/${id}/generate`, {
method: 'POST',
body: JSON.stringify(data || { language: 'de', auto_matrix: true }),
})
}
export async function getBlockControls(id: string): Promise<{ controls: TrainingBlockControlLink[]; total: number }> {
return apiFetch(`/blocks/${id}/controls`)
}
export async function listCanonicalControls(filters?: {
domain?: string
category?: string
severity?: string
target_audience?: string
}): Promise<{ controls: CanonicalControlSummary[]; total: number }> {
const params = new URLSearchParams()
if (filters?.domain) params.set('domain', filters.domain)
if (filters?.category) params.set('category', filters.category)
if (filters?.severity) params.set('severity', filters.severity)
if (filters?.target_audience) params.set('target_audience', filters.target_audience)
const qs = params.toString()
return apiFetch(`/canonical/controls${qs ? `?${qs}` : ''}`)
}
export async function getCanonicalMeta(): Promise<CanonicalControlMeta> {
return apiFetch<CanonicalControlMeta>('/canonical/meta')
}
// =============================================================================
// CERTIFICATES
// =============================================================================
export async function generateCertificate(assignmentId: string): Promise<{ certificate_id: string; assignment: TrainingAssignment }> {
return apiFetch(`/certificates/generate/${assignmentId}`, { method: 'POST' })
}
export async function listCertificates(): Promise<{ certificates: TrainingAssignment[]; total: number }> {
return apiFetch('/certificates')
}
export async function downloadCertificatePDF(certificateId: string): Promise<Blob> {
const res = await fetch(`${BASE_URL}/certificates/${certificateId}/pdf`, {
headers: {
'X-Tenant-ID': typeof window !== 'undefined'
? (localStorage.getItem('bp-tenant-id') || 'default')
: 'default',
},
})
if (!res.ok) throw new Error(`PDF download failed: ${res.status}`)
return res.blob()
}
export async function verifyCertificate(certificateId: string): Promise<{ valid: boolean; assignment: TrainingAssignment }> {
return apiFetch(`/certificates/${certificateId}/verify`)
}
// =============================================================================
// MEDIA STREAMING
// =============================================================================
export function getMediaStreamURL(mediaId: string): string {
return `${BASE_URL}/media/${mediaId}/stream`
}
// =============================================================================
// INTERACTIVE VIDEO
// =============================================================================
import type {
InteractiveVideoManifest,
CheckpointQuizResult,
CheckpointProgress,
} from './types'
export async function generateInteractiveVideo(moduleId: string): Promise<TrainingMedia> {
return apiFetch<TrainingMedia>(`/content/${moduleId}/generate-interactive`, { method: 'POST' })
}
export async function getInteractiveManifest(moduleId: string, assignmentId?: string): Promise<InteractiveVideoManifest> {
const qs = assignmentId ? `?assignment_id=${assignmentId}` : ''
return apiFetch<InteractiveVideoManifest>(`/content/${moduleId}/interactive-manifest${qs}`)
}
export async function submitCheckpointQuiz(checkpointId: string, assignmentId: string, answers: number[]): Promise<CheckpointQuizResult> {
return apiFetch<CheckpointQuizResult>(`/checkpoints/${checkpointId}/submit`, {
method: 'POST',
body: JSON.stringify({ assignment_id: assignmentId, answers }),
})
}
export async function getCheckpointProgress(assignmentId: string): Promise<{ progress: CheckpointProgress[]; total: number }> {
return apiFetch(`/checkpoints/progress/${assignmentId}`)
}

View File

@@ -65,9 +65,17 @@ export const ROLE_LABELS: Record<string, string> = {
R7: 'Fachabteilung',
R8: 'IT-Administration',
R9: 'Alle Mitarbeiter',
R10: 'Behoerden / Oeffentlicher Dienst',
}
export const ALL_ROLES = ['R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8', 'R9'] as const
export const ALL_ROLES = ['R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8', 'R9', 'R10'] as const
export const TARGET_AUDIENCE_LABELS: Record<string, string> = {
enterprise: 'Unternehmen',
authority: 'Behoerden',
provider: 'IT-Dienstleister',
all: 'Alle',
}
// =============================================================================
// MAIN ENTITIES
@@ -273,7 +281,7 @@ export interface QuizSubmitResponse {
// MEDIA (Audio/Video)
// =============================================================================
export type MediaType = 'audio' | 'video'
export type MediaType = 'audio' | 'video' | 'interactive_video'
export type MediaStatus = 'processing' | 'completed' | 'failed'
export interface TrainingMedia {
@@ -307,3 +315,121 @@ export interface VideoScriptSection {
text: string
bullet_points: string[]
}
// =============================================================================
// TRAINING BLOCKS (Controls → Schulungsmodule)
// =============================================================================
export interface TrainingBlockConfig {
id: string
tenant_id: string
name: string
description?: string
domain_filter?: string
category_filter?: string
severity_filter?: string
target_audience_filter?: string
regulation_area: RegulationArea
module_code_prefix: string
frequency_type: FrequencyType
duration_minutes: number
pass_threshold: number
max_controls_per_module: number
is_active: boolean
last_generated_at?: string
created_at: string
updated_at: string
}
export interface CanonicalControlSummary {
control_id: string
title: string
objective: string
rationale: string
requirements: string[]
severity: string
category: string
target_audience: string
tags: string[]
}
export interface CanonicalControlMeta {
domains: { domain: string; count: number }[]
categories: { category: string; count: number }[]
audiences: { audience: string; count: number }[]
total: number
}
export interface BlockPreview {
control_count: number
module_count: number
controls: CanonicalControlSummary[]
proposed_roles: string[]
}
export interface BlockGenerateResult {
modules_created: number
controls_linked: number
matrix_entries_created: number
content_generated: number
errors?: string[]
}
export interface TrainingBlockControlLink {
id: string
block_config_id: string
module_id: string
control_id: string
control_title: string
control_objective: string
control_requirements: string[]
sort_order: number
created_at: string
}
// =============================================================================
// INTERACTIVE VIDEO / CHECKPOINTS
// =============================================================================
export interface InteractiveVideoManifest {
media_id: string
stream_url: string
checkpoints: CheckpointEntry[]
}
export interface CheckpointEntry {
checkpoint_id: string
index: number
title: string
timestamp_seconds: number
questions: CheckpointQuestion[]
progress?: CheckpointProgress
}
export interface CheckpointQuestion {
question: string
options: string[]
correct_index: number
explanation: string
}
export interface CheckpointProgress {
id: string
assignment_id: string
checkpoint_id: string
passed: boolean
attempts: number
last_attempt_at?: string
}
export interface CheckpointQuizResult {
passed: boolean
score: number
feedback: CheckpointQuizFeedback[]
}
export interface CheckpointQuizFeedback {
question: string
correct: boolean
explanation: string
}