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

@@ -44,7 +44,7 @@ export interface CanonicalControl {
customer_visible?: boolean
verification_method: string | null
category: string | null
target_audience: string | null
target_audience: string | string[] | null
generation_metadata?: Record<string, unknown> | null
generation_strategy?: string | null
created_at: string
@@ -142,10 +142,27 @@ export const CATEGORY_OPTIONS = [
]
export const TARGET_AUDIENCE_OPTIONS: Record<string, { bg: string; label: string }> = {
// Legacy English keys
enterprise: { bg: 'bg-cyan-100 text-cyan-700', label: 'Unternehmen' },
authority: { bg: 'bg-rose-100 text-rose-700', label: 'Behoerden' },
provider: { bg: 'bg-violet-100 text-violet-700', label: 'Anbieter' },
all: { bg: 'bg-gray-100 text-gray-700', label: 'Alle' },
// German keys from LLM generation
unternehmen: { bg: 'bg-cyan-100 text-cyan-700', label: 'Unternehmen' },
behoerden: { bg: 'bg-rose-100 text-rose-700', label: 'Behoerden' },
entwickler: { bg: 'bg-sky-100 text-sky-700', label: 'Entwickler' },
datenschutzbeauftragte: { bg: 'bg-purple-100 text-purple-700', label: 'DSB' },
geschaeftsfuehrung: { bg: 'bg-amber-100 text-amber-700', label: 'GF' },
'it-abteilung': { bg: 'bg-blue-100 text-blue-700', label: 'IT' },
rechtsabteilung: { bg: 'bg-fuchsia-100 text-fuchsia-700', label: 'Recht' },
'compliance-officer': { bg: 'bg-indigo-100 text-indigo-700', label: 'Compliance' },
personalwesen: { bg: 'bg-pink-100 text-pink-700', label: 'Personal' },
einkauf: { bg: 'bg-lime-100 text-lime-700', label: 'Einkauf' },
produktion: { bg: 'bg-orange-100 text-orange-700', label: 'Produktion' },
vertrieb: { bg: 'bg-teal-100 text-teal-700', label: 'Vertrieb' },
gesundheitswesen: { bg: 'bg-red-100 text-red-700', label: 'Gesundheit' },
finanzwesen: { bg: 'bg-emerald-100 text-emerald-700', label: 'Finanzen' },
oeffentlicher_dienst: { bg: 'bg-rose-100 text-rose-700', label: 'Oeffentl. Dienst' },
}
export const COLLECTION_OPTIONS = [
@@ -223,11 +240,32 @@ export function CategoryBadge({ category }: { category: string | null }) {
)
}
export function TargetAudienceBadge({ audience }: { audience: string | null }) {
export function TargetAudienceBadge({ audience }: { audience: string | string[] | null }) {
if (!audience) return null
const config = TARGET_AUDIENCE_OPTIONS[audience]
if (!config) return null
return <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${config.bg}`}>{config.label}</span>
// Parse JSON array string from DB (e.g. '["unternehmen", "einkauf"]')
let items: string[] = []
if (typeof audience === 'string') {
if (audience.startsWith('[')) {
try { items = JSON.parse(audience) } catch { items = [audience] }
} else {
items = [audience]
}
} else if (Array.isArray(audience)) {
items = audience
}
if (items.length === 0) return null
return (
<span className="inline-flex items-center gap-1 flex-wrap">
{items.map((item, i) => {
const config = TARGET_AUDIENCE_OPTIONS[item]
if (!config) return <span key={i} className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600">{item}</span>
return <span key={i} className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${config.bg}`}>{config.label}</span>
})}
</span>
)
}
export function GenerationStrategyBadge({ strategy }: { strategy: string | null | undefined }) {