All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 48s
CI / test-python-backend-compliance (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 20s
Phase A: 8 new IT-Security training modules (SEC-PWD, SEC-DESK, SEC-KIAI, SEC-BYOD, SEC-VIDEO, SEC-USB, SEC-INC, SEC-HOME) with CTM entries. Bulk content and quiz generation endpoints for all 28 modules. Phase B: Piper TTS service (Python/FastAPI) for local German speech synthesis. training_media table, TTSClient in Go backend, audio generation endpoints, AudioPlayer component in frontend. MinIO storage integration. Phase C: FFmpeg presentation video pipeline — LLM generates slide scripts, ImageMagick renders 1920x1080 slides, FFmpeg combines with audio to MP4. VideoPlayer and ScriptPreview components in frontend. New files: 15 created, 9 modified - compliance-tts-service/ (Dockerfile, main.py, tts_engine.py, storage.py, slide_renderer.py, video_generator.py) - migrations 014-016 (training engine, IT-security modules, media table) - training package (models, store, content_generator, media, handlers) - frontend (AudioPlayer, VideoPlayer, ScriptPreview, api, types, page) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
128 lines
3.0 KiB
Go
128 lines
3.0 KiB
Go
package training
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ComputeRequiredModules returns all required training modules for a user
|
|
// based on their assigned roles. Deduplicates modules across roles.
|
|
func ComputeRequiredModules(ctx context.Context, store *Store, tenantID uuid.UUID, roleCodes []string) ([]TrainingModule, error) {
|
|
seen := make(map[uuid.UUID]bool)
|
|
var modules []TrainingModule
|
|
|
|
for _, role := range roleCodes {
|
|
entries, err := store.GetMatrixForRole(ctx, tenantID, role)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if seen[entry.ModuleID] {
|
|
continue
|
|
}
|
|
seen[entry.ModuleID] = true
|
|
|
|
module, err := store.GetModule(ctx, entry.ModuleID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if module != nil && module.IsActive {
|
|
modules = append(modules, *module)
|
|
}
|
|
}
|
|
}
|
|
|
|
if modules == nil {
|
|
modules = []TrainingModule{}
|
|
}
|
|
|
|
return modules, nil
|
|
}
|
|
|
|
// GetComplianceGaps finds modules that are required but not completed for a user
|
|
func GetComplianceGaps(ctx context.Context, store *Store, tenantID uuid.UUID, userID uuid.UUID, roleCodes []string) ([]ComplianceGap, error) {
|
|
var gaps []ComplianceGap
|
|
|
|
for _, role := range roleCodes {
|
|
entries, err := store.GetMatrixForRole(ctx, tenantID, role)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
// Check if there's an active, completed assignment for this module
|
|
assignments, _, err := store.ListAssignments(ctx, tenantID, &AssignmentFilters{
|
|
ModuleID: &entry.ModuleID,
|
|
UserID: &userID,
|
|
Limit: 1,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
gap := ComplianceGap{
|
|
ModuleID: entry.ModuleID,
|
|
ModuleCode: entry.ModuleCode,
|
|
ModuleTitle: entry.ModuleTitle,
|
|
RoleCode: role,
|
|
IsMandatory: entry.IsMandatory,
|
|
}
|
|
|
|
// Determine regulation area from module
|
|
module, err := store.GetModule(ctx, entry.ModuleID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if module != nil {
|
|
gap.RegulationArea = module.RegulationArea
|
|
}
|
|
|
|
if len(assignments) == 0 {
|
|
gap.Status = "missing"
|
|
gaps = append(gaps, gap)
|
|
} else {
|
|
a := assignments[0]
|
|
gap.AssignmentID = &a.ID
|
|
gap.Deadline = &a.Deadline
|
|
|
|
switch a.Status {
|
|
case AssignmentStatusCompleted:
|
|
// No gap
|
|
continue
|
|
case AssignmentStatusOverdue, AssignmentStatusExpired:
|
|
gap.Status = string(a.Status)
|
|
gaps = append(gaps, gap)
|
|
default:
|
|
// Check if overdue
|
|
if a.Deadline.Before(timeNow()) {
|
|
gap.Status = "overdue"
|
|
gaps = append(gaps, gap)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if gaps == nil {
|
|
gaps = []ComplianceGap{}
|
|
}
|
|
|
|
return gaps, nil
|
|
}
|
|
|
|
// BuildMatrixResponse builds the full CTM response grouped by role
|
|
func BuildMatrixResponse(entries []TrainingMatrixEntry) *MatrixResponse {
|
|
resp := &MatrixResponse{
|
|
Entries: make(map[string][]TrainingMatrixEntry),
|
|
Roles: RoleLabels,
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
resp.Entries[entry.RoleCode] = append(resp.Entries[entry.RoleCode], entry)
|
|
}
|
|
|
|
return resp
|
|
}
|