Files
breakpilot-lehrer/admin-lehrer/app/(admin)/infrastructure/tests/_hooks/useTestDashboard.ts
Benjamin Admin 9ba420fa91
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 42s
CI / test-go-edu-search (push) Successful in 34s
CI / test-python-klausur (push) Failing after 2m51s
CI / test-python-agent-core (push) Successful in 21s
CI / test-nodejs-website (push) Successful in 29s
Fix: Remove broken getKlausurApiUrl and clean up empty lines
sed replacement left orphaned hostname references in story page
and empty lines in getApiBase functions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 16:02:04 +02:00

311 lines
9.6 KiB
TypeScript

'use client'
import { useState, useEffect, useCallback, useRef } from 'react'
import type {
ServiceTestInfo,
TestRegistryStats,
TestRun,
CoverageData,
TabType,
Toast,
FailedTest,
BacklogItem,
} from '../types'
import { API_BASE, DEMO_SERVICES, DEMO_STATS } from '../_lib/constants'
import type { ServiceProgress } from '../_components/ServiceTestCard'
export function useTestDashboard() {
const [activeTab, setActiveTab] = useState<TabType>('overview')
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Toast state
const [toasts, setToasts] = useState<Toast[]>([])
const toastIdRef = useRef(0)
const addToast = useCallback((type: Toast['type'], message: string) => {
const id = ++toastIdRef.current
setToasts((prev) => [...prev, { id, type, message }])
if (type !== 'loading') {
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id))
}, 5000)
}
return id
}, [])
const removeToast = useCallback((id: number) => {
setToasts((prev) => prev.filter((t) => t.id !== id))
}, [])
const updateToast = useCallback((id: number, type: Toast['type'], message: string) => {
setToasts((prev) => prev.map((t) => (t.id === id ? { ...t, type, message } : t)))
if (type !== 'loading') {
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id))
}, 5000)
}
}, [])
// Data states
const [services, setServices] = useState<ServiceTestInfo[]>([])
const [stats, setStats] = useState<TestRegistryStats | null>(null)
const [coverage, setCoverage] = useState<CoverageData[]>([])
const [testRuns, setTestRuns] = useState<TestRun[]>([])
const [failedTests, setFailedTests] = useState<FailedTest[]>([])
const [backlogItems, setBacklogItems] = useState<BacklogItem[]>([])
const [usePostgres, setUsePostgres] = useState(false)
// Running states
const [runningServices, setRunningServices] = useState<Set<string>>(new Set())
// Progress states fuer laufende Tests
const [serviceProgress, setServiceProgress] = useState<Record<string, ServiceProgress>>({})
// Fetch data
const fetchData = useCallback(async () => {
setIsLoading(true)
setError(null)
try {
const registryResponse = await fetch(`${API_BASE}/registry`)
if (registryResponse.ok) {
const data = await registryResponse.json()
setServices(data.services || DEMO_SERVICES)
setStats(data.stats || DEMO_STATS)
} else {
setServices(DEMO_SERVICES)
setStats(DEMO_STATS)
}
const coverageResponse = await fetch(`${API_BASE}/coverage`)
if (coverageResponse.ok) {
const data = await coverageResponse.json()
setCoverage(data.services || [])
} else {
setCoverage(DEMO_SERVICES.filter(s => s.coverage_percent).map(s => ({
service: s.service,
display_name: s.display_name,
coverage_percent: s.coverage_percent!,
language: s.language,
})))
}
const runsResponse = await fetch(`${API_BASE}/runs`)
if (runsResponse.ok) {
const data = await runsResponse.json()
setTestRuns(data.runs || [])
}
// Lade fehlgeschlagene Tests fuer Backlog
const failedResponse = await fetch(`${API_BASE}/failed`)
if (failedResponse.ok) {
const data = await failedResponse.json()
setFailedTests(data.tests || [])
}
// Versuche PostgreSQL-Backlog zu laden (neue API)
try {
const backlogResponse = await fetch(`${API_BASE}/backlog`)
if (backlogResponse.ok) {
const data = await backlogResponse.json()
if (data.items && data.items.length > 0) {
setBacklogItems(data.items)
setUsePostgres(true)
}
}
} catch {
// PostgreSQL nicht verfuegbar, nutze Legacy
setUsePostgres(false)
}
} catch (err) {
console.error('Failed to fetch test registry data:', err)
setServices(DEMO_SERVICES)
setStats(DEMO_STATS)
setCoverage(DEMO_SERVICES.filter(s => s.coverage_percent).map(s => ({
service: s.service,
display_name: s.display_name,
coverage_percent: s.coverage_percent!,
language: s.language,
})))
} finally {
setIsLoading(false)
}
}, [])
useEffect(() => {
fetchData()
}, [fetchData])
// Update failed test status
const updateTestStatus = async (testId: string, status: string) => {
try {
// Nutze PostgreSQL-Endpoint wenn verfuegbar
const endpoint = usePostgres
? `${API_BASE}/backlog/${testId}/status`
: `${API_BASE}/failed/${encodeURIComponent(testId)}/status?status=${status}`
const response = await fetch(endpoint, {
method: 'POST',
headers: usePostgres ? { 'Content-Type': 'application/json' } : undefined,
body: usePostgres ? JSON.stringify({ status }) : undefined,
})
if (response.ok) {
// Aktualisiere lokalen State
if (usePostgres) {
setBacklogItems(prev =>
prev.map(t => String(t.id) === testId ? { ...t, status: status as any } : t)
)
}
setFailedTests(prev =>
prev.map(t => t.id === testId ? { ...t, status: status as any } : t)
)
addToast('success', `Test-Status auf "${status}" gesetzt`)
}
} catch (err) {
console.error('Failed to update test status:', err)
// Trotzdem lokal aktualisieren fuer bessere UX
setFailedTests(prev =>
prev.map(t => t.id === testId ? { ...t, status: status as any } : t)
)
if (usePostgres) {
setBacklogItems(prev =>
prev.map(t => String(t.id) === testId ? { ...t, status: status as any } : t)
)
}
}
}
// Update failed test priority (nur PostgreSQL)
const updateTestPriority = async (testId: string, priority: string) => {
if (!usePostgres) return
try {
const response = await fetch(`${API_BASE}/backlog/${testId}/priority`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ priority }),
})
if (response.ok) {
setBacklogItems(prev =>
prev.map(t => String(t.id) === testId ? { ...t, priority: priority as any } : t)
)
addToast('success', `Prioritaet auf "${priority}" gesetzt`)
}
} catch (err) {
console.error('Failed to update test priority:', err)
// Trotzdem lokal aktualisieren
setBacklogItems(prev =>
prev.map(t => String(t.id) === testId ? { ...t, priority: priority as any } : t)
)
}
}
// Run tests mit Progress-Polling
const runTests = async (service: string) => {
setRunningServices((prev) => new Set(prev).add(service))
const loadingToast = addToast('loading', `Tests fuer ${service} werden gestartet...`)
// Progress-Polling starten
let pollInterval: NodeJS.Timeout | null = null
const pollProgress = async () => {
try {
const progressResponse = await fetch(`${API_BASE}/progress/${service}`)
if (progressResponse.ok) {
const progress = await progressResponse.json()
setServiceProgress((prev) => ({
...prev,
[service]: progress,
}))
// Toast-Message mit aktuellem Fortschritt aktualisieren
if (progress.status === 'running' && progress.files_total > 0) {
const toastMsg = `${service}: ${progress.current_file} (${progress.passed} bestanden, ${progress.failed} fehler)`
updateToast(loadingToast, 'loading', toastMsg)
}
}
} catch {
// Ignore polling errors
}
}
// Start polling (alle 1 Sekunde)
pollInterval = setInterval(pollProgress, 1000)
try {
const response = await fetch(`${API_BASE}/run/${service}`, {
method: 'POST',
})
if (response.ok) {
// Warte kurz und pruefe finalen Progress
await new Promise(resolve => setTimeout(resolve, 500))
await pollProgress()
const finalProgress = serviceProgress[service]
const passedMsg = finalProgress ? `${finalProgress.passed} bestanden, ${finalProgress.failed} fehler` : 'abgeschlossen'
updateToast(loadingToast, 'success', `${service}: Tests ${passedMsg}`)
await fetchData()
} else {
updateToast(loadingToast, 'info', `${service}: Demo-Modus (API nicht verfuegbar)`)
}
} catch (err) {
console.error('Failed to run tests:', err)
updateToast(loadingToast, 'info', `${service}: Demo-Modus (API nicht verfuegbar)`)
} finally {
// Polling stoppen
if (pollInterval) {
clearInterval(pollInterval)
}
setRunningServices((prev) => {
const next = new Set(prev)
next.delete(service)
return next
})
// Progress-Daten entfernen nach Abschluss
setServiceProgress((prev) => {
const next = { ...prev }
delete next[service]
return next
})
}
}
// Filter services by category
const unitServices = services.filter(s => !s.service.startsWith('bqas-'))
const bqasServices = services.filter(s => s.service.startsWith('bqas-'))
return {
// Tab
activeTab,
setActiveTab,
// Loading / Error
isLoading,
error,
fetchData,
// Toast
toasts,
removeToast,
// Data
services,
stats,
coverage,
testRuns,
failedTests,
backlogItems,
usePostgres,
// Running
runningServices,
serviceProgress,
// Actions
updateTestStatus,
updateTestPriority,
runTests,
// Derived
unitServices,
bqasServices,
}
}