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>
184 lines
4.8 KiB
Go
184 lines
4.8 KiB
Go
package training
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// timeNow is a package-level function for testing
|
|
var timeNow = time.Now
|
|
|
|
// ComputeAssignments calculates all necessary assignments for a user based on
|
|
// their roles, existing assignments, and deadlines. Returns new assignments
|
|
// that need to be created.
|
|
func ComputeAssignments(ctx context.Context, store *Store, tenantID uuid.UUID,
|
|
userID uuid.UUID, userName, userEmail string, roleCodes []string, trigger string) ([]TrainingAssignment, error) {
|
|
|
|
if trigger == "" {
|
|
trigger = string(TriggerManual)
|
|
}
|
|
|
|
// Get all required modules for the user's roles
|
|
requiredModules, err := ComputeRequiredModules(ctx, store, tenantID, roleCodes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get existing active assignments for this user
|
|
existingAssignments, _, err := store.ListAssignments(ctx, tenantID, &AssignmentFilters{
|
|
UserID: &userID,
|
|
Limit: 1000,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Build a map of existing assignments by module_id for quick lookup
|
|
existingByModule := make(map[uuid.UUID]*TrainingAssignment)
|
|
for i := range existingAssignments {
|
|
a := &existingAssignments[i]
|
|
// Only consider non-expired, non-completed-and-expired assignments
|
|
if a.Status != AssignmentStatusExpired {
|
|
existingByModule[a.ModuleID] = a
|
|
}
|
|
}
|
|
|
|
var newAssignments []TrainingAssignment
|
|
now := timeNow().UTC()
|
|
|
|
for _, module := range requiredModules {
|
|
existing, hasExisting := existingByModule[module.ID]
|
|
|
|
// Skip if there's an active, valid assignment
|
|
if hasExisting {
|
|
switch existing.Status {
|
|
case AssignmentStatusCompleted:
|
|
// Check if the completed assignment is still valid
|
|
if existing.CompletedAt != nil {
|
|
validUntil := existing.CompletedAt.AddDate(0, 0, module.ValidityDays)
|
|
if validUntil.After(now) {
|
|
continue // Still valid, skip
|
|
}
|
|
}
|
|
case AssignmentStatusPending, AssignmentStatusInProgress:
|
|
continue // Assignment exists and is active
|
|
case AssignmentStatusOverdue:
|
|
continue // Already tracked as overdue
|
|
}
|
|
}
|
|
|
|
// Determine the role code for this assignment
|
|
roleCode := ""
|
|
for _, role := range roleCodes {
|
|
entries, err := store.GetMatrixForRole(ctx, tenantID, role)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.ModuleID == module.ID {
|
|
roleCode = role
|
|
break
|
|
}
|
|
}
|
|
if roleCode != "" {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Calculate deadline based on frequency
|
|
var deadline time.Time
|
|
switch module.FrequencyType {
|
|
case FrequencyOnboarding:
|
|
deadline = now.AddDate(0, 0, 30) // 30 days for onboarding
|
|
case FrequencyMicro:
|
|
deadline = now.AddDate(0, 0, 14) // 14 days for micro
|
|
default:
|
|
deadline = now.AddDate(0, 0, 90) // 90 days default
|
|
}
|
|
|
|
assignment := TrainingAssignment{
|
|
TenantID: tenantID,
|
|
ModuleID: module.ID,
|
|
UserID: userID,
|
|
UserName: userName,
|
|
UserEmail: userEmail,
|
|
RoleCode: roleCode,
|
|
TriggerType: TriggerType(trigger),
|
|
Status: AssignmentStatusPending,
|
|
Deadline: deadline,
|
|
ModuleCode: module.ModuleCode,
|
|
ModuleTitle: module.Title,
|
|
}
|
|
|
|
// Create the assignment in the store
|
|
if err := store.CreateAssignment(ctx, &assignment); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Log the assignment
|
|
store.LogAction(ctx, &AuditLogEntry{
|
|
TenantID: tenantID,
|
|
UserID: &userID,
|
|
Action: AuditActionAssigned,
|
|
EntityType: AuditEntityAssignment,
|
|
EntityID: &assignment.ID,
|
|
Details: map[string]interface{}{
|
|
"module_code": module.ModuleCode,
|
|
"trigger": trigger,
|
|
"role_code": roleCode,
|
|
"deadline": deadline.Format(time.RFC3339),
|
|
},
|
|
})
|
|
|
|
newAssignments = append(newAssignments, assignment)
|
|
}
|
|
|
|
if newAssignments == nil {
|
|
newAssignments = []TrainingAssignment{}
|
|
}
|
|
|
|
return newAssignments, nil
|
|
}
|
|
|
|
// BulkAssign assigns a module to all users with specific roles
|
|
// Returns the number of assignments created
|
|
func BulkAssign(ctx context.Context, store *Store, tenantID uuid.UUID,
|
|
moduleID uuid.UUID, users []UserInfo, trigger string, deadline time.Time) (int, error) {
|
|
|
|
if trigger == "" {
|
|
trigger = string(TriggerManual)
|
|
}
|
|
|
|
count := 0
|
|
for _, user := range users {
|
|
assignment := TrainingAssignment{
|
|
TenantID: tenantID,
|
|
ModuleID: moduleID,
|
|
UserID: user.UserID,
|
|
UserName: user.UserName,
|
|
UserEmail: user.UserEmail,
|
|
RoleCode: user.RoleCode,
|
|
TriggerType: TriggerType(trigger),
|
|
Status: AssignmentStatusPending,
|
|
Deadline: deadline,
|
|
}
|
|
|
|
if err := store.CreateAssignment(ctx, &assignment); err != nil {
|
|
return count, err
|
|
}
|
|
count++
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
// UserInfo contains basic user information for bulk operations
|
|
type UserInfo struct {
|
|
UserID uuid.UUID `json:"user_id"`
|
|
UserName string `json:"user_name"`
|
|
UserEmail string `json:"user_email"`
|
|
RoleCode string `json:"role_code"`
|
|
}
|