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

@@ -15,7 +15,7 @@ import {
isEnrollmentOverdue,
getDaysUntilDeadline
} from '@/lib/sdk/academy/types'
import { fetchSDKAcademyList } from '@/lib/sdk/academy/api'
import { fetchSDKAcademyList, generateAllCourses } from '@/lib/sdk/academy/api'
// =============================================================================
// TYPES
@@ -359,6 +359,8 @@ export default function AcademyPage() {
const [enrollments, setEnrollments] = useState<Enrollment[]>([])
const [statistics, setStatistics] = useState<AcademyStatistics | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [isGenerating, setIsGenerating] = useState(false)
const [generateResult, setGenerateResult] = useState<{ generated: number; skipped: number; errors: string[] } | null>(null)
// Filters
const [selectedCategory, setSelectedCategory] = useState<CourseCategory | 'all'>('all')
@@ -366,19 +368,6 @@ export default function AcademyPage() {
// Load data from SDK backend
useEffect(() => {
const loadData = async () => {
setIsLoading(true)
try {
const data = await fetchSDKAcademyList()
setCourses(data.courses)
setEnrollments(data.enrollments)
setStatistics(data.statistics)
} catch (error) {
console.error('Failed to load Academy data:', error)
} finally {
setIsLoading(false)
}
}
loadData()
}, [])
@@ -446,6 +435,36 @@ export default function AcademyPage() {
const stepInfo = STEP_EXPLANATIONS['academy']
const loadData = async () => {
setIsLoading(true)
try {
const data = await fetchSDKAcademyList()
setCourses(data.courses)
setEnrollments(data.enrollments)
setStatistics(data.statistics)
} catch (error) {
console.error('Failed to load Academy data:', error)
} finally {
setIsLoading(false)
}
}
const handleGenerateAll = async () => {
setIsGenerating(true)
setGenerateResult(null)
try {
const result = await generateAllCourses()
setGenerateResult({ generated: result.generated, skipped: result.skipped, errors: result.errors || [] })
// Reload data to show new courses
await loadData()
} catch (error) {
console.error('Failed to generate courses:', error)
setGenerateResult({ generated: 0, skipped: 0, errors: [error instanceof Error ? error.message : 'Fehler bei der Generierung'] })
} finally {
setIsGenerating(false)
}
}
const clearFilters = () => {
setSelectedCategory('all')
setSelectedStatus('all')
@@ -461,17 +480,54 @@ export default function AcademyPage() {
explanation={stepInfo?.explanation}
tips={stepInfo?.tips}
>
<Link
href="/sdk/academy/new"
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Kurs erstellen
</Link>
<div className="flex gap-2">
<button
onClick={handleGenerateAll}
disabled={isGenerating}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50"
>
{isGenerating ? (
<svg className="animate-spin w-5 h-5" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
)}
{isGenerating ? 'Generiere...' : 'Alle Kurse generieren'}
</button>
<Link
href="/sdk/academy/new"
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Kurs erstellen
</Link>
</div>
</StepHeader>
{/* Generation Result */}
{generateResult && (
<div className={`p-4 rounded-lg border ${generateResult.errors.length > 0 ? 'bg-yellow-50 border-yellow-200' : 'bg-green-50 border-green-200'}`}>
<div className="flex items-center gap-4 text-sm">
<span className="text-green-700 font-medium">{generateResult.generated} Kurse generiert</span>
<span className="text-gray-500">{generateResult.skipped} uebersprungen</span>
{generateResult.errors.length > 0 && (
<span className="text-red-600">{generateResult.errors.length} Fehler</span>
)}
</div>
{generateResult.errors.length > 0 && (
<div className="mt-2 text-xs text-red-600">
{generateResult.errors.map((err, i) => <div key={i}>{err}</div>)}
</div>
)}
</div>
)}
{/* Tab Navigation */}
<TabNavigation
tabs={tabs}

View File

@@ -132,7 +132,7 @@ export default function TrainingPage() {
setBulkResult(null)
try {
const result = await generateAllContent('de')
setBulkResult(result)
setBulkResult({ generated: result.generated ?? 0, skipped: result.skipped ?? 0, errors: result.errors ?? [] })
await loadData()
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler bei der Bulk-Generierung')
@@ -155,7 +155,7 @@ export default function TrainingPage() {
setBulkResult(null)
try {
const result = await generateAllQuizzes()
setBulkResult(result)
setBulkResult({ generated: result.generated ?? 0, skipped: result.skipped ?? 0, errors: result.errors ?? [] })
await loadData()
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler bei der Bulk-Quiz-Generierung')
@@ -475,11 +475,11 @@ export default function TrainingPage() {
<div className="flex gap-6">
<span className="text-green-700">Generiert: {bulkResult.generated}</span>
<span className="text-gray-500">Uebersprungen: {bulkResult.skipped}</span>
{bulkResult.errors.length > 0 && (
{bulkResult.errors?.length > 0 && (
<span className="text-red-600">Fehler: {bulkResult.errors.length}</span>
)}
</div>
{bulkResult.errors.length > 0 && (
{bulkResult.errors?.length > 0 && (
<div className="mt-2 text-xs text-red-600">
{bulkResult.errors.map((err, i) => <div key={i}>{err}</div>)}
</div>