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>
491 lines
19 KiB
TypeScript
491 lines
19 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import type { LLMRoutingOption } from '@/types/infrastructure-modules'
|
|
import type { FailedTest, BacklogItem, BacklogPriority } from '../types'
|
|
|
|
// ==============================================================================
|
|
// FailedTestCard
|
|
// ==============================================================================
|
|
|
|
function FailedTestCard({
|
|
test,
|
|
onStatusChange,
|
|
onPriorityChange,
|
|
priority = 'medium',
|
|
failureCount = 1,
|
|
}: {
|
|
test: FailedTest
|
|
onStatusChange: (testId: string, status: string) => void
|
|
onPriorityChange?: (testId: string, priority: string) => void
|
|
priority?: BacklogPriority
|
|
failureCount?: number
|
|
}) {
|
|
const errorTypeColors: Record<string, string> = {
|
|
assertion: 'bg-amber-100 text-amber-700',
|
|
nil_pointer: 'bg-red-100 text-red-700',
|
|
type_error: 'bg-purple-100 text-purple-700',
|
|
network: 'bg-blue-100 text-blue-700',
|
|
timeout: 'bg-orange-100 text-orange-700',
|
|
logic_error: 'bg-slate-100 text-slate-700',
|
|
unknown: 'bg-slate-100 text-slate-700',
|
|
}
|
|
|
|
const statusColors: Record<string, string> = {
|
|
open: 'bg-red-100 text-red-700',
|
|
in_progress: 'bg-blue-100 text-blue-700',
|
|
fixed: 'bg-emerald-100 text-emerald-700',
|
|
wont_fix: 'bg-slate-100 text-slate-700',
|
|
flaky: 'bg-purple-100 text-purple-700',
|
|
}
|
|
|
|
const priorityColors: Record<string, string> = {
|
|
critical: 'bg-red-500 text-white',
|
|
high: 'bg-orange-500 text-white',
|
|
medium: 'bg-yellow-500 text-white',
|
|
low: 'bg-slate-400 text-white',
|
|
}
|
|
|
|
const priorityLabels: Record<string, string> = {
|
|
critical: '!!! Kritisch',
|
|
high: '!! Hoch',
|
|
medium: '! Mittel',
|
|
low: 'Niedrig',
|
|
}
|
|
|
|
return (
|
|
<div className="bg-white rounded-lg border border-slate-200 p-4 hover:border-red-300 transition-colors">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
|
<span className={`px-2 py-0.5 rounded text-xs font-medium ${priorityColors[priority]}`}>
|
|
{priorityLabels[priority]}
|
|
</span>
|
|
<span className={`px-2 py-0.5 rounded text-xs font-medium ${errorTypeColors[test.error_type] || errorTypeColors.unknown}`}>
|
|
{test.error_type.replace('_', ' ')}
|
|
</span>
|
|
<span className="text-xs text-slate-400">{test.service}</span>
|
|
{failureCount > 1 && (
|
|
<span className="px-1.5 py-0.5 rounded bg-red-100 text-red-600 text-xs font-medium">
|
|
{failureCount}x fehlgeschlagen
|
|
</span>
|
|
)}
|
|
</div>
|
|
<h4 className="font-mono text-sm font-medium text-slate-900 truncate" title={test.name}>
|
|
{test.name}
|
|
</h4>
|
|
<p className="text-xs text-slate-500 truncate" title={test.file_path}>
|
|
{test.file_path}
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-col gap-1 ml-2">
|
|
<select
|
|
value={test.status}
|
|
onChange={(e) => onStatusChange(test.id, e.target.value)}
|
|
className={`px-2 py-1 rounded text-xs font-medium cursor-pointer border-0 ${statusColors[test.status]}`}
|
|
>
|
|
<option value="open">Offen</option>
|
|
<option value="in_progress">In Arbeit</option>
|
|
<option value="fixed">Behoben</option>
|
|
<option value="wont_fix">Ignoriert</option>
|
|
<option value="flaky">Flaky</option>
|
|
</select>
|
|
{onPriorityChange && (
|
|
<select
|
|
value={priority}
|
|
onChange={(e) => onPriorityChange(test.id, e.target.value)}
|
|
className="px-2 py-1 rounded text-xs font-medium cursor-pointer border border-slate-200"
|
|
>
|
|
<option value="critical">Kritisch</option>
|
|
<option value="high">Hoch</option>
|
|
<option value="medium">Mittel</option>
|
|
<option value="low">Niedrig</option>
|
|
</select>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-red-50 rounded-lg p-3 mb-3">
|
|
<p className="text-sm text-red-800 font-medium mb-1">Fehlermeldung:</p>
|
|
<p className="text-xs text-red-700 font-mono break-words">
|
|
{test.error_message || 'Keine Details verfuegbar'}
|
|
</p>
|
|
</div>
|
|
|
|
{test.suggestion && (
|
|
<div className="bg-emerald-50 rounded-lg p-3">
|
|
<p className="text-sm text-emerald-800 font-medium mb-1">Loesungsvorschlag:</p>
|
|
<p className="text-xs text-emerald-700">
|
|
{test.suggestion}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-3 pt-3 border-t border-slate-100 flex items-center justify-between text-xs text-slate-400">
|
|
<span>Zuletzt fehlgeschlagen: {test.last_failed ? new Date(test.last_failed).toLocaleString('de-DE') : 'Unbekannt'}</span>
|
|
<button
|
|
className="text-orange-600 hover:text-orange-700 font-medium"
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(test.id)
|
|
}}
|
|
>
|
|
ID kopieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// ==============================================================================
|
|
// BacklogTab
|
|
// ==============================================================================
|
|
|
|
export function BacklogTab({
|
|
failedTests,
|
|
onStatusChange,
|
|
onPriorityChange,
|
|
isLoading,
|
|
backlogItems,
|
|
usePostgres = false,
|
|
}: {
|
|
failedTests: FailedTest[]
|
|
onStatusChange: (testId: string, status: string) => void
|
|
onPriorityChange?: (testId: string, priority: string) => void
|
|
isLoading: boolean
|
|
backlogItems?: BacklogItem[]
|
|
usePostgres?: boolean
|
|
}) {
|
|
const [filterStatus, setFilterStatus] = useState<string>('open')
|
|
const [filterService, setFilterService] = useState<string>('all')
|
|
const [filterPriority, setFilterPriority] = useState<string>('all')
|
|
const [llmAutoAnalysis, setLlmAutoAnalysis] = useState<boolean>(true)
|
|
const [llmRouting, setLlmRouting] = useState<LLMRoutingOption>('smart_routing')
|
|
|
|
// Nutze PostgreSQL-Backlog wenn verfuegbar, sonst Legacy
|
|
const items = usePostgres && backlogItems ? backlogItems : failedTests
|
|
|
|
// Gruppiere nach Service
|
|
const services = [...new Set(items.map(t => 'service' in t ? t.service : (t as BacklogItem).service))]
|
|
|
|
// Filtere Items
|
|
const filteredItems = items.filter(item => {
|
|
const status = 'status' in item ? item.status : 'open'
|
|
const service = 'service' in item ? item.service : ''
|
|
const priority = 'priority' in item ? (item as BacklogItem).priority : 'medium'
|
|
|
|
if (filterStatus !== 'all' && status !== filterStatus) return false
|
|
if (filterService !== 'all' && service !== filterService) return false
|
|
if (filterPriority !== 'all' && priority !== filterPriority) return false
|
|
return true
|
|
})
|
|
|
|
// Zaehle nach Status
|
|
const openCount = items.filter(t => t.status === 'open').length
|
|
const inProgressCount = items.filter(t => t.status === 'in_progress').length
|
|
const fixedCount = items.filter(t => t.status === 'fixed').length
|
|
const flakyCount = items.filter(t => t.status === 'flaky').length
|
|
|
|
// Zaehle nach Prioritaet (nur bei PostgreSQL)
|
|
const criticalCount = backlogItems?.filter(t => t.priority === 'critical').length || 0
|
|
const highCount = backlogItems?.filter(t => t.priority === 'high').length || 0
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-orange-600"></div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Konvertiere BacklogItem zu FailedTest fuer die Anzeige
|
|
const convertToFailedTest = (item: BacklogItem): FailedTest => ({
|
|
id: String(item.id),
|
|
name: item.test_name,
|
|
service: item.service,
|
|
file_path: item.test_file || '',
|
|
error_message: item.error_message || '',
|
|
error_type: item.error_type || 'unknown',
|
|
suggestion: item.fix_suggestion || '',
|
|
run_id: '',
|
|
last_failed: item.last_failed_at,
|
|
status: item.status,
|
|
})
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
|
<div className="bg-red-50 border border-red-200 rounded-xl p-4">
|
|
<p className="text-2xl font-bold text-red-600">{openCount}</p>
|
|
<p className="text-sm text-red-700">Offene Fehler</p>
|
|
</div>
|
|
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
|
|
<p className="text-2xl font-bold text-blue-600">{inProgressCount}</p>
|
|
<p className="text-sm text-blue-700">In Arbeit</p>
|
|
</div>
|
|
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-4">
|
|
<p className="text-2xl font-bold text-emerald-600">{fixedCount}</p>
|
|
<p className="text-sm text-emerald-700">Behoben</p>
|
|
</div>
|
|
<div className="bg-purple-50 border border-purple-200 rounded-xl p-4">
|
|
<p className="text-2xl font-bold text-purple-600">{flakyCount}</p>
|
|
<p className="text-sm text-purple-700">Flaky</p>
|
|
</div>
|
|
{usePostgres && criticalCount + highCount > 0 && (
|
|
<div className="bg-orange-50 border border-orange-200 rounded-xl p-4">
|
|
<p className="text-2xl font-bold text-orange-600">{criticalCount + highCount}</p>
|
|
<p className="text-sm text-orange-700">Kritisch/Hoch</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* PostgreSQL Badge */}
|
|
{usePostgres && (
|
|
<div className="flex items-center gap-2 px-3 py-1.5 bg-emerald-50 border border-emerald-200 rounded-lg w-fit">
|
|
<svg className="w-4 h-4 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
<span className="text-xs text-emerald-700 font-medium">Persistente Speicherung aktiv (PostgreSQL)</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* LLM Analysis Toggle */}
|
|
<LLMAnalysisPanel
|
|
llmAutoAnalysis={llmAutoAnalysis}
|
|
setLlmAutoAnalysis={setLlmAutoAnalysis}
|
|
llmRouting={llmRouting}
|
|
setLlmRouting={setLlmRouting}
|
|
/>
|
|
|
|
{/* Filter */}
|
|
<div className="flex flex-wrap gap-4 items-center">
|
|
<div>
|
|
<label className="text-sm text-slate-600 mr-2">Status:</label>
|
|
<select
|
|
value={filterStatus}
|
|
onChange={(e) => setFilterStatus(e.target.value)}
|
|
className="px-3 py-1.5 rounded-lg border border-slate-200 text-sm"
|
|
>
|
|
<option value="all">Alle</option>
|
|
<option value="open">Offen ({openCount})</option>
|
|
<option value="in_progress">In Arbeit ({inProgressCount})</option>
|
|
<option value="fixed">Behoben ({fixedCount})</option>
|
|
<option value="flaky">Flaky ({flakyCount})</option>
|
|
<option value="wont_fix">Ignoriert</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm text-slate-600 mr-2">Service:</label>
|
|
<select
|
|
value={filterService}
|
|
onChange={(e) => setFilterService(e.target.value)}
|
|
className="px-3 py-1.5 rounded-lg border border-slate-200 text-sm"
|
|
>
|
|
<option value="all">Alle Services</option>
|
|
{services.map(s => (
|
|
<option key={s} value={s}>{s}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
{usePostgres && (
|
|
<div>
|
|
<label className="text-sm text-slate-600 mr-2">Prioritaet:</label>
|
|
<select
|
|
value={filterPriority}
|
|
onChange={(e) => setFilterPriority(e.target.value)}
|
|
className="px-3 py-1.5 rounded-lg border border-slate-200 text-sm"
|
|
>
|
|
<option value="all">Alle</option>
|
|
<option value="critical">Kritisch</option>
|
|
<option value="high">Hoch</option>
|
|
<option value="medium">Mittel</option>
|
|
<option value="low">Niedrig</option>
|
|
</select>
|
|
</div>
|
|
)}
|
|
<div className="ml-auto text-sm text-slate-500">
|
|
{filteredItems.length} von {items.length} Tests angezeigt
|
|
</div>
|
|
</div>
|
|
|
|
{/* Test-Liste */}
|
|
{filteredItems.length === 0 ? (
|
|
<div className="text-center py-12 bg-emerald-50 rounded-xl border border-emerald-200">
|
|
<svg className="w-12 h-12 mx-auto text-emerald-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<p className="text-emerald-700 font-medium">
|
|
{filterStatus === 'open' ? 'Keine offenen Fehler!' : 'Keine Tests mit diesem Filter gefunden.'}
|
|
</p>
|
|
{filterStatus === 'open' && (
|
|
<p className="text-sm text-emerald-600 mt-2">
|
|
Alle Tests bestanden. Bereit fuer Go-Live!
|
|
</p>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
{filteredItems.map((item) => {
|
|
const test = usePostgres && 'test_name' in item
|
|
? convertToFailedTest(item as BacklogItem)
|
|
: item as FailedTest
|
|
const priority = usePostgres && 'priority' in item
|
|
? (item as BacklogItem).priority
|
|
: 'medium'
|
|
const failureCount = usePostgres && 'failure_count' in item
|
|
? (item as BacklogItem).failure_count
|
|
: 1
|
|
|
|
return (
|
|
<FailedTestCard
|
|
key={test.id}
|
|
test={test}
|
|
onStatusChange={onStatusChange}
|
|
onPriorityChange={onPriorityChange}
|
|
priority={priority}
|
|
failureCount={failureCount}
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{/* Info */}
|
|
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
|
|
<div className="flex items-start gap-3">
|
|
<svg className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<div>
|
|
<p className="text-sm text-blue-800 font-medium">Workflow fuer fehlgeschlagene Tests:</p>
|
|
<ol className="text-xs text-blue-700 mt-2 space-y-1 list-decimal list-inside">
|
|
<li>Markiere den Test als "In Arbeit" wenn du daran arbeitest</li>
|
|
<li>Analysiere die Fehlermeldung und den Loesungsvorschlag</li>
|
|
<li>Behebe den Fehler im Code</li>
|
|
<li>Fuehre den Test erneut aus (Button im Service-Tab)</li>
|
|
<li>Markiere als "Behoben" wenn der Test besteht</li>
|
|
{usePostgres && <li>Setze "Flaky" fuer sporadisch fehlschlagende Tests</li>}
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// ==============================================================================
|
|
// LLM Analysis Panel (internal)
|
|
// ==============================================================================
|
|
|
|
function LLMAnalysisPanel({
|
|
llmAutoAnalysis,
|
|
setLlmAutoAnalysis,
|
|
llmRouting,
|
|
setLlmRouting,
|
|
}: {
|
|
llmAutoAnalysis: boolean
|
|
setLlmAutoAnalysis: (v: boolean) => void
|
|
llmRouting: LLMRoutingOption
|
|
setLlmRouting: (v: LLMRoutingOption) => void
|
|
}) {
|
|
return (
|
|
<div className="bg-gradient-to-r from-violet-50 to-purple-50 border border-violet-200 rounded-xl p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-violet-100 rounded-lg flex items-center justify-center">
|
|
<svg className="w-5 h-5 text-violet-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h4 className="font-medium text-slate-800">Automatische LLM-Analyse</h4>
|
|
<p className="text-xs text-slate-500">KI-gestuetzte Fix-Vorschlaege fuer Backlog-Eintraege</p>
|
|
</div>
|
|
</div>
|
|
<label className="relative inline-flex items-center cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={llmAutoAnalysis}
|
|
onChange={(e) => setLlmAutoAnalysis(e.target.checked)}
|
|
className="sr-only peer"
|
|
/>
|
|
<div className="w-11 h-6 bg-slate-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-violet-300 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-violet-600"></div>
|
|
</label>
|
|
</div>
|
|
|
|
{llmAutoAnalysis && (
|
|
<div className="mt-4 pt-4 border-t border-violet-200">
|
|
<p className="text-xs text-slate-600 mb-3">LLM-Routing Strategie:</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
<RoutingOption
|
|
value="local_only"
|
|
current={llmRouting}
|
|
onChange={setLlmRouting}
|
|
label="Nur lokales 32B LLM"
|
|
badge="DSGVO"
|
|
badgeColor="bg-emerald-100 text-emerald-700"
|
|
/>
|
|
<RoutingOption
|
|
value="claude_preferred"
|
|
current={llmRouting}
|
|
onChange={setLlmRouting}
|
|
label="Claude bevorzugt"
|
|
badge="Qualitaet"
|
|
badgeColor="bg-blue-100 text-blue-700"
|
|
/>
|
|
<RoutingOption
|
|
value="smart_routing"
|
|
current={llmRouting}
|
|
onChange={setLlmRouting}
|
|
label="Smart Routing"
|
|
badge="Empfohlen"
|
|
badgeColor="bg-amber-100 text-amber-700"
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-slate-500 mt-2">
|
|
{llmRouting === 'local_only' && 'Alle Analysen werden mit Qwen2.5-32B lokal durchgefuehrt. Keine Daten verlassen den Server.'}
|
|
{llmRouting === 'claude_preferred' && 'Verwendet Claude fuer beste Fix-Qualitaet. Nur Code-Snippets werden uebertragen.'}
|
|
{llmRouting === 'smart_routing' && 'Privacy Classifier entscheidet automatisch: Sensitive Daten → lokal, Code → Claude.'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function RoutingOption({
|
|
value,
|
|
current,
|
|
onChange,
|
|
label,
|
|
badge,
|
|
badgeColor,
|
|
}: {
|
|
value: LLMRoutingOption
|
|
current: LLMRoutingOption
|
|
onChange: (v: LLMRoutingOption) => void
|
|
label: string
|
|
badge: string
|
|
badgeColor: string
|
|
}) {
|
|
const isActive = current === value
|
|
return (
|
|
<label className={`flex items-center gap-2 px-3 py-2 rounded-lg border cursor-pointer transition-colors ${
|
|
isActive
|
|
? 'bg-violet-100 border-violet-300 text-violet-800'
|
|
: 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'
|
|
}`}>
|
|
<input
|
|
type="radio"
|
|
name="llm-routing"
|
|
value={value}
|
|
checked={isActive}
|
|
onChange={() => onChange(value)}
|
|
className="sr-only"
|
|
/>
|
|
<span className="text-sm font-medium">{label}</span>
|
|
<span className={`text-xs px-1.5 py-0.5 rounded ${badgeColor}`}>{badge}</span>
|
|
</label>
|
|
)
|
|
}
|