/ dashboard-v2 / frontend / src / pages / Operations.jsx
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  }