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
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:
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user