Operations.jsx
1 import { useQuery } from '@tanstack/react-query'; 2 import { fetchOperations } from '../api'; 3 import MetricCard from '../components/MetricCard'; 4 import DataTable from '../components/DataTable'; 5 import Tabs from '../components/Tabs'; 6 import { BarChart, LineChart, StackedBarChart } from '../components/charts'; 7 8 const CRON_COLS = [ 9 { accessorKey: 'name', header: 'Job' }, 10 { accessorKey: 'schedule', header: 'Schedule' }, 11 { 12 accessorKey: 'last_run', 13 header: 'Last Run', 14 cell: i => i.getValue()?.slice(0, 16).replace('T', ' ') ?? 'Never', 15 }, 16 { 17 accessorKey: 'last_status', 18 header: 'Status', 19 cell: i => { 20 const v = i.getValue(); 21 const color = 22 v === 'success' 23 ? 'text-emerald-400' 24 : v === 'running' 25 ? 'text-sky-400' 26 : v === 'failed' 27 ? 'text-red-400' 28 : 'text-slate-400'; 29 return <span className={color}>{v ?? '—'}</span>; 30 }, 31 }, 32 { 33 accessorKey: 'last_duration_ms', 34 header: 'Duration', 35 cell: i => { 36 const ms = i.getValue(); 37 if (!ms) return '—'; 38 return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`; 39 }, 40 }, 41 { 42 accessorKey: 'error_message', 43 header: 'Error', 44 cell: i => ( 45 <span className="text-red-400 text-xs" title={i.getValue() ?? ''}> 46 {i.getValue()?.slice(0, 60) ?? ''} 47 </span> 48 ), 49 }, 50 ]; 51 52 const HTTP_ERR_COLS = [ 53 { accessorKey: 'domain', header: 'Domain' }, 54 { accessorKey: 'status_code', header: 'Status' }, 55 { accessorKey: 'error_count', header: 'Errors' }, 56 { 57 accessorKey: 'last_seen', 58 header: 'Last Seen', 59 cell: i => i.getValue()?.slice(0, 16).replace('T', ' ') ?? '—', 60 }, 61 ]; 62 63 const RATE_LIMIT_COLS = [ 64 { accessorKey: 'api', header: 'API' }, 65 { 66 accessorKey: 'status', 67 header: 'Status', 68 cell: i => { 69 const v = i.getValue(); 70 return <span className={v === 'limited' ? 'text-red-400' : 'text-emerald-400'}>{v}</span>; 71 }, 72 }, 73 { 74 accessorKey: 'resets_at', 75 header: 'Resets At', 76 cell: i => i.getValue()?.slice(0, 16).replace('T', ' ') ?? '—', 77 }, 78 { accessorKey: 'stages_paused', header: 'Paused Stages' }, 79 ]; 80 81 export default function Operations() { 82 const { data, isLoading, error } = useQuery({ 83 queryKey: ['operations'], 84 queryFn: fetchOperations, 85 }); 86 87 if (isLoading) return <div className="text-slate-400 p-8">Loading…</div>; 88 if (error) return <div className="text-red-400 p-8">Error: {error.message}</div>; 89 90 const d = data ?? {}; 91 const cs = d.cron_summary ?? {}; 92 const cj = d.cron_jobs ?? []; 93 const tl = d.cron_timeline_24h ?? []; 94 const ch = d.cron_daily_history_7d ?? []; 95 const he = d.http_error_history_30d ?? []; 96 const dh = d.database_health ?? {}; 97 const rl = d.api_rate_limits ?? []; 98 99 return ( 100 <div className="space-y-6"> 101 <Tabs tabs={['Cron Jobs', 'System Health']}> 102 {active => 103 active === 'Cron Jobs' ? ( 104 <div className="space-y-6"> 105 {/* Cron summary */} 106 <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> 107 <MetricCard label="Total Jobs" value={cs.total ?? 0} /> 108 <MetricCard 109 label="Succeeded (24h)" 110 value={cs.succeeded_24h ?? 0} 111 variant="success" 112 /> 113 <MetricCard 114 label="Failed (24h)" 115 value={cs.failed_24h ?? 0} 116 variant={(cs.failed_24h ?? 0) > 0 ? 'error' : 'success'} 117 /> 118 <MetricCard label="Currently Running" value={cs.running ?? 0} variant="default" /> 119 </div> 120 121 {/* Cron jobs table */} 122 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 123 <h3 className="text-sm font-medium text-slate-400 mb-3">All Cron Jobs</h3> 124 <DataTable columns={CRON_COLS} data={cj} /> 125 </div> 126 127 {/* Timeline 24h */} 128 {tl.length > 0 && ( 129 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 130 <h3 className="text-sm font-medium text-slate-400 mb-3">Job Runs (Last 24h)</h3> 131 <StackedBarChart 132 data={tl} 133 xKey="hour" 134 bars={[ 135 { key: 'success', name: 'Success', color: '#34d399' }, 136 { key: 'failed', name: 'Failed', color: '#f87171' }, 137 ]} 138 /> 139 </div> 140 )} 141 142 {/* 7-day history */} 143 {ch.length > 0 && ( 144 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 145 <h3 className="text-sm font-medium text-slate-400 mb-3"> 146 Daily History (7 Days) 147 </h3> 148 <BarChart 149 data={ch} 150 xKey="date" 151 bars={[ 152 { key: 'success', name: 'Success', color: '#34d399' }, 153 { key: 'failed', name: 'Failed', color: '#f87171' }, 154 ]} 155 /> 156 </div> 157 )} 158 </div> 159 ) : ( 160 <div className="space-y-6"> 161 {/* DB health */} 162 <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> 163 <MetricCard 164 label="DB Size" 165 value={dh.size_mb ? `${dh.size_mb.toFixed(1)} MB` : '—'} 166 /> 167 <MetricCard label="Total Sites" value={(dh.total_sites ?? 0).toLocaleString()} /> 168 <MetricCard 169 label="Outreaches" 170 value={(dh.total_outreaches ?? 0).toLocaleString()} 171 /> 172 <MetricCard 173 label="DB Status" 174 value={dh.integrity ?? '—'} 175 variant={dh.integrity === 'ok' ? 'success' : 'error'} 176 /> 177 </div> 178 179 {/* Rate limits */} 180 {rl.length > 0 && ( 181 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 182 <h3 className="text-sm font-medium text-slate-400 mb-3">API Rate Limits</h3> 183 <DataTable columns={RATE_LIMIT_COLS} data={rl} /> 184 </div> 185 )} 186 187 {/* HTTP errors */} 188 {he.length > 0 && ( 189 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 190 <h3 className="text-sm font-medium text-slate-400 mb-3"> 191 HTTP Errors (Last 30 Days) 192 </h3> 193 <DataTable columns={HTTP_ERR_COLS} data={he} maxRows={20} /> 194 </div> 195 )} 196 197 {rl.length === 0 && he.length === 0 && ( 198 <div className="bg-slate-800 rounded-xl p-8 border border-slate-700 text-center"> 199 <p className="text-emerald-400">No active rate limits or HTTP errors.</p> 200 </div> 201 )} 202 </div> 203 ) 204 } 205 </Tabs> 206 </div> 207 ); 208 }