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
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>
311 lines
9.6 KiB
TypeScript
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,
|
|
}
|
|
}
|