feat(academy): bridge Academy with Training Engine for course generation
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 46s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 29s

- Add POST /academy/courses/generate endpoint that creates an academy
  course from a training module (with content + quiz as lessons)
- Add POST /academy/courses/generate-all to bulk-generate all courses
- Fix academy API response mapping (snake_case → camelCase)
- Fix fetchCourses/fetchCourse/fetchEnrollments/fetchStats to unwrap
  backend response wrappers ({courses:[...]}, {course:{...}})
- Add "Alle Kurse generieren" button to academy overview page
- Fix bulkResult.errors crash in training page (optional chaining)
- Add SetAcademyCourseID to training store for bidirectional linking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-02-26 11:57:13 +01:00
parent 305a068354
commit 66988d1304
7 changed files with 488 additions and 47 deletions

View File

@@ -5,7 +5,7 @@
* Connects to the ai-compliance-sdk backend via Next.js proxy
*/
import {
import type {
Course,
CourseCategory,
CourseCreateRequest,
@@ -18,10 +18,11 @@ import {
UpdateProgressRequest,
Certificate,
AcademyStatistics,
LessonType,
SubmitQuizRequest,
SubmitQuizResponse,
isEnrollmentOverdue
} from './types'
import { isEnrollmentOverdue } from './types'
// =============================================================================
// CONFIGURATION
@@ -111,18 +112,72 @@ async function fetchWithTimeout<T>(
* Alle Kurse abrufen
*/
export async function fetchCourses(): Promise<Course[]> {
return fetchWithTimeout<Course[]>(
const res = await fetchWithTimeout<{ courses: Course[]; total: number }>(
`${ACADEMY_API_BASE}/courses`
)
return mapCoursesFromBackend(res.courses || [])
}
/**
* Einzelnen Kurs abrufen
*/
export async function fetchCourse(id: string): Promise<Course> {
return fetchWithTimeout<Course>(
const res = await fetchWithTimeout<{ course: BackendCourse }>(
`${ACADEMY_API_BASE}/courses/${id}`
)
return mapCourseFromBackend(res.course)
}
// Backend returns snake_case, frontend uses camelCase
interface BackendCourse {
id: string
title: string
description: string
category: CourseCategory
duration_minutes: number
required_for_roles: string[]
is_active: boolean
lessons?: BackendLesson[]
created_at: string
updated_at: string
}
interface BackendLesson {
id: string
course_id: string
title: string
description?: string
lesson_type: LessonType
content_url?: string
duration_minutes: number
order_index: number
quiz_questions?: Array<{ question: string; options: string[]; correct_index: number; explanation: string }>
}
function mapCourseFromBackend(bc: BackendCourse): Course {
return {
id: bc.id,
title: bc.title,
description: bc.description || '',
category: bc.category,
durationMinutes: bc.duration_minutes || 0,
requiredForRoles: bc.required_for_roles || [],
lessons: (bc.lessons || []).map(l => ({
id: l.id,
courseId: l.course_id,
title: l.title,
type: l.lesson_type,
contentMarkdown: l.content_url || '',
durationMinutes: l.duration_minutes || 0,
order: l.order_index,
})),
createdAt: bc.created_at,
updatedAt: bc.updated_at,
}
}
function mapCoursesFromBackend(courses: BackendCourse[]): Course[] {
return courses.map(mapCourseFromBackend)
}
/**
@@ -173,12 +228,13 @@ export async function deleteCourse(id: string): Promise<void> {
export async function fetchEnrollments(courseId?: string): Promise<Enrollment[]> {
const params = new URLSearchParams()
if (courseId) {
params.set('courseId', courseId)
params.set('course_id', courseId)
}
const queryString = params.toString()
const url = `${ACADEMY_API_BASE}/enrollments${queryString ? `?${queryString}` : ''}`
return fetchWithTimeout<Enrollment[]>(url)
const res = await fetchWithTimeout<{ enrollments: Enrollment[]; total: number }>(url)
return res.enrollments || []
}
/**
@@ -269,9 +325,22 @@ export async function submitQuiz(lessonId: string, answers: SubmitQuizRequest):
* Academy-Statistiken abrufen
*/
export async function fetchAcademyStatistics(): Promise<AcademyStatistics> {
return fetchWithTimeout<AcademyStatistics>(
`${ACADEMY_API_BASE}/statistics`
)
const res = await fetchWithTimeout<{
total_courses: number
total_enrollments: number
completion_rate: number
overdue_count: number
avg_completion_days: number
}>(`${ACADEMY_API_BASE}/stats`)
return {
totalCourses: res.total_courses || 0,
totalEnrollments: res.total_enrollments || 0,
completionRate: res.completion_rate || 0,
overdueCount: res.overdue_count || 0,
byCategory: {} as Record<CourseCategory, number>,
byStatus: {} as Record<EnrollmentStatus, number>,
}
}
// =============================================================================
@@ -279,16 +348,27 @@ export async function fetchAcademyStatistics(): Promise<AcademyStatistics> {
// =============================================================================
/**
* KI-gestuetzten Kurs generieren (nutzt intern Training Content Pipeline)
* Academy-Kurs aus einem Training-Modul generieren
*/
export async function generateCourse(request: GenerateCourseRequest): Promise<Course> {
return fetchWithTimeout<Course>(
export async function generateCourse(request: GenerateCourseRequest): Promise<{ course: Course }> {
return fetchWithTimeout<{ course: Course }>(
`${ACADEMY_API_BASE}/courses/generate`,
{
method: 'POST',
body: JSON.stringify(request)
body: JSON.stringify({ module_id: request.moduleId || request.title })
},
120000 // 2 min timeout for LLM generation
120000 // 2 min timeout
)
}
/**
* Alle Academy-Kurse aus Training-Modulen generieren
*/
export async function generateAllCourses(): Promise<{ generated: number; skipped: number; errors: string[]; total: number }> {
return fetchWithTimeout(
`${ACADEMY_API_BASE}/courses/generate-all`,
{ method: 'POST' },
300000 // 5 min timeout
)
}

View File

@@ -216,8 +216,9 @@ export interface CourseUpdateRequest {
}
export interface GenerateCourseRequest {
title: string
category: CourseCategory
moduleId: string
title?: string
category?: CourseCategory
description?: string
regulationArea?: string
language?: string