Quality.jsx
1 import { useQuery } from '@tanstack/react-query'; 2 import { fetchQuality } from '../api'; 3 import MetricCard from '../components/MetricCard'; 4 import DataTable from '../components/DataTable'; 5 import Tabs from '../components/Tabs'; 6 import { BarChart, LineChart, GaugeChart } from '../components/charts'; 7 8 const TASK_COLS = [ 9 { accessorKey: 'task_type', header: 'Type' }, 10 { 11 accessorKey: 'status', 12 header: 'Status', 13 cell: i => { 14 const v = i.getValue(); 15 const color = 16 { 17 completed: 'text-emerald-400', 18 pending: 'text-slate-400', 19 in_progress: 'text-sky-400', 20 failed: 'text-red-400', 21 blocked: 'text-amber-400', 22 cancelled: 'text-slate-500', 23 }[v] ?? 'text-slate-400'; 24 return <span className={color}>{v}</span>; 25 }, 26 }, 27 { accessorKey: 'assigned_agent', header: 'Agent' }, 28 { 29 accessorKey: 'created_at', 30 header: 'Created', 31 cell: i => i.getValue()?.slice(0, 16).replace('T', ' ') ?? '—', 32 }, 33 ]; 34 35 const LOG_COLS = [ 36 { accessorKey: 'agent_type', header: 'Agent' }, 37 { 38 accessorKey: 'level', 39 header: 'Level', 40 cell: i => { 41 const v = i.getValue(); 42 const color = 43 v === 'error' ? 'text-red-400' : v === 'warn' ? 'text-amber-400' : 'text-slate-400'; 44 return <span className={color}>{v}</span>; 45 }, 46 }, 47 { 48 accessorKey: 'message', 49 header: 'Message', 50 cell: i => ( 51 <span className="text-xs text-slate-300 truncate max-w-md block">{i.getValue()}</span> 52 ), 53 }, 54 { accessorKey: 'created_at', header: 'Time', cell: i => i.getValue()?.slice(11, 19) ?? '—' }, 55 ]; 56 57 const COVERAGE_COLS = [ 58 { accessorKey: 'file', header: 'File' }, 59 { 60 accessorKey: 'statements', 61 header: 'Stmts %', 62 cell: i => { 63 const v = i.getValue() ?? 0; 64 const color = v >= 85 ? 'text-emerald-400' : v >= 60 ? 'text-amber-400' : 'text-red-400'; 65 return <span className={color}>{v.toFixed(1)}%</span>; 66 }, 67 }, 68 { 69 accessorKey: 'branches', 70 header: 'Branch %', 71 cell: i => { 72 const v = i.getValue() ?? 0; 73 const color = v >= 85 ? 'text-emerald-400' : v >= 60 ? 'text-amber-400' : 'text-red-400'; 74 return <span className={color}>{v.toFixed(1)}%</span>; 75 }, 76 }, 77 { 78 accessorKey: 'functions', 79 header: 'Func %', 80 cell: i => { 81 const v = i.getValue() ?? 0; 82 const color = v >= 85 ? 'text-emerald-400' : v >= 60 ? 'text-amber-400' : 'text-red-400'; 83 return <span className={color}>{v.toFixed(1)}%</span>; 84 }, 85 }, 86 ]; 87 88 export default function Quality() { 89 const { data, isLoading, error } = useQuery({ queryKey: ['quality'], queryFn: fetchQuality }); 90 91 if (isLoading) return <div className="text-slate-400 p-8">Loading…</div>; 92 if (error) return <div className="text-red-400 p-8">Error: {error.message}</div>; 93 94 const d = data ?? {}; 95 const ag = d.agent_state ?? {}; 96 const tasks = d.agent_tasks ?? []; 97 const logs = d.agent_logs ?? []; 98 const perf = d.agent_performance_7d ?? []; 99 const cost24h = d.agent_cost_24h ?? {}; 100 const cov = d.coverage_data ?? {}; 101 const tests = d.test_results ?? {}; 102 103 const agentSummary = Object.entries(ag).map(([name, state]) => ({ 104 name, 105 status: state?.status ?? 'unknown', 106 invocations: state?.invocations_24h ?? 0, 107 failures: state?.failures_24h ?? 0, 108 })); 109 110 return ( 111 <div className="space-y-6"> 112 <Tabs tabs={['Agent System', 'Code Coverage']}> 113 {active => 114 active === 'Agent System' ? ( 115 <div className="space-y-6"> 116 {/* Agent metrics */} 117 <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> 118 <MetricCard 119 label="Active Agents" 120 value={agentSummary.filter(a => a.status === 'active').length} 121 /> 122 <MetricCard 123 label="Tasks Pending" 124 value={tasks.filter(t => t.status === 'pending').length} 125 variant="warning" 126 /> 127 <MetricCard 128 label="Tasks Blocked" 129 value={tasks.filter(t => t.status === 'blocked').length} 130 variant={ 131 tasks.filter(t => t.status === 'blocked').length > 0 ? 'error' : 'success' 132 } 133 /> 134 <MetricCard 135 label="Agent Cost (24h)" 136 value={`$${(cost24h.total_usd ?? 0).toFixed(4)}`} 137 variant={(cost24h.total_usd ?? 0) > 1 ? 'warning' : 'default'} 138 /> 139 </div> 140 141 {/* Agent status grid */} 142 {agentSummary.length > 0 && ( 143 <div className="grid grid-cols-2 md:grid-cols-3 gap-3"> 144 {agentSummary.map(a => ( 145 <div 146 key={a.name} 147 className="bg-slate-800 rounded-lg p-3 border border-slate-700" 148 > 149 <div className="flex items-center justify-between"> 150 <span className="text-sm font-medium text-slate-300 capitalize"> 151 {a.name} 152 </span> 153 <span 154 className={`text-xs px-1.5 py-0.5 rounded ${ 155 a.status === 'active' 156 ? 'bg-emerald-900 text-emerald-300' 157 : a.status === 'disabled' 158 ? 'bg-slate-700 text-slate-400' 159 : 'bg-amber-900 text-amber-300' 160 }`} 161 > 162 {a.status} 163 </span> 164 </div> 165 <div className="mt-1.5 text-xs text-slate-500"> 166 {a.invocations} runs · {a.failures} failed (24h) 167 </div> 168 </div> 169 ))} 170 </div> 171 )} 172 173 {/* Performance 7d */} 174 {perf.length > 0 && ( 175 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 176 <h3 className="text-sm font-medium text-slate-400 mb-3"> 177 Agent Performance (7 Days) 178 </h3> 179 <LineChart 180 data={perf} 181 xKey="date" 182 lines={[ 183 { key: 'success_rate', name: 'Success %', color: '#34d399' }, 184 { key: 'invocations', name: 'Invocations', color: '#60a5fa' }, 185 ]} 186 /> 187 </div> 188 )} 189 190 {/* Pending tasks */} 191 {tasks.length > 0 && ( 192 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 193 <h3 className="text-sm font-medium text-slate-400 mb-3">Recent Tasks</h3> 194 <DataTable columns={TASK_COLS} data={tasks} maxRows={20} /> 195 </div> 196 )} 197 198 {/* Agent logs */} 199 {logs.length > 0 && ( 200 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 201 <h3 className="text-sm font-medium text-slate-400 mb-3">Recent Agent Logs</h3> 202 <DataTable columns={LOG_COLS} data={logs} maxRows={30} /> 203 </div> 204 )} 205 </div> 206 ) : ( 207 <div className="space-y-6"> 208 {/* Coverage gauges */} 209 <div className="grid grid-cols-3 gap-4"> 210 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 211 <GaugeChart 212 value={cov.statements ?? 0} 213 label="Statements" 214 color={ 215 cov.statements >= 85 216 ? '#34d399' 217 : cov.statements >= 60 218 ? '#fbbf24' 219 : '#f87171' 220 } 221 /> 222 </div> 223 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 224 <GaugeChart 225 value={cov.branches ?? 0} 226 label="Branches" 227 color={ 228 cov.branches >= 85 ? '#34d399' : cov.branches >= 60 ? '#fbbf24' : '#f87171' 229 } 230 /> 231 </div> 232 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 233 <GaugeChart 234 value={cov.functions ?? 0} 235 label="Functions" 236 color={ 237 cov.functions >= 85 ? '#34d399' : cov.functions >= 60 ? '#fbbf24' : '#f87171' 238 } 239 /> 240 </div> 241 </div> 242 243 {/* Test results */} 244 {tests.total > 0 && ( 245 <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> 246 <MetricCard label="Total Tests" value={tests.total ?? 0} /> 247 <MetricCard label="Passed" value={tests.passed ?? 0} variant="success" /> 248 <MetricCard 249 label="Failed" 250 value={tests.failed ?? 0} 251 variant={(tests.failed ?? 0) > 0 ? 'error' : 'success'} 252 /> 253 <MetricCard label="Skipped" value={tests.skipped ?? 0} /> 254 </div> 255 )} 256 257 {/* Per-file coverage */} 258 {(cov.files ?? []).length > 0 && ( 259 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 260 <h3 className="text-sm font-medium text-slate-400 mb-3">Per-file Coverage</h3> 261 <DataTable columns={COVERAGE_COLS} data={cov.files ?? []} maxRows={30} /> 262 </div> 263 )} 264 265 {!cov.statements && ( 266 <div className="bg-slate-800 rounded-xl p-8 border border-slate-700 text-center"> 267 <p className="text-slate-500 text-sm"> 268 No coverage data found. Run{' '} 269 <code className="bg-slate-900 px-1 rounded">npm test</code> to generate. 270 </p> 271 </div> 272 )} 273 </div> 274 ) 275 } 276 </Tabs> 277 </div> 278 ); 279 }