Files
stonks-oracle/frontend/src/pages/OpsIngestion.tsx
T
Celes Renata b5c0c6d7c9 fix: aggregate ingestion throughput chart by time bucket
The throughput API returns one row per source_type per time bucket,
but the chart was mapping each row as a separate bar. With 5 source
types × 24 hours, the bars were tiny and overlapping. Now aggregates
completed/failed/items across source types per time bucket so the
chart shows meaningful totals.
2026-04-16 00:52:29 +00:00

119 lines
5.2 KiB
TypeScript

import { useState } from 'react';
import { useIngestionThroughput, useIngestionSummary } from '../api/hooks';
import { LoadingSpinner, DateRangeSelector, Card } from '../components/ui';
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts';
export function OpsIngestionPage() {
const [hours, setHours] = useState(24);
const [bucket, setBucket] = useState('1h');
const { data: throughput, isLoading: tpLoading } = useIngestionThroughput(hours, bucket);
const { data: summary } = useIngestionSummary(hours);
if (tpLoading) return <LoadingSpinner />;
const chartData = (() => {
const buckets = new Map<string, { time: string; completed: number; failed: number; items: number }>();
for (const row of throughput ?? []) {
const r = row as Record<string, unknown>;
const time = r.bucket_start
? new Date(r.bucket_start as string).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
: '';
if (!time) continue;
const existing = buckets.get(time) ?? { time, completed: 0, failed: 0, items: 0 };
existing.completed += Number(r.completed ?? 0);
existing.failed += Number(r.failed ?? 0);
existing.items += Number(r.items_fetched ?? 0);
buckets.set(time, existing);
}
return Array.from(buckets.values());
})();
const s = (summary ?? {}) as Record<string, unknown>;
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-xl font-semibold text-gray-100">Ingestion Monitor</h1>
<div className="flex items-center gap-3">
<DateRangeSelector value={hours} onChange={setHours} />
<div className="inline-flex rounded-md border border-surface-700" role="group">
{['15m', '1h', '6h', '1d'].map((b) => (
<button
key={b}
onClick={() => setBucket(b)}
className={`px-2 py-1 text-xs font-medium first:rounded-l-md last:rounded-r-md ${bucket === b ? 'bg-brand-600 text-white' : 'bg-surface-900 text-gray-400 hover:bg-surface-800'}`}
>
{b}
</button>
))}
</div>
</div>
</div>
{/* Summary stats */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-5">
<StatCard label="Total Runs" value={String(s.total_runs ?? '—')} />
<StatCard label="Completed" value={String(s.completed ?? '—')} color="text-green-400" />
<StatCard label="Failed" value={String(s.failed ?? '—')} color="text-red-400" />
<StatCard label="Items Fetched" value={String(s.total_items_fetched ?? '—')} />
<StatCard label="New Items" value={String(s.total_items_new ?? '—')} />
</div>
{/* Throughput chart */}
<Card>
<h2 className="mb-3 text-sm font-medium text-gray-400">Throughput</h2>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={chartData}>
<XAxis dataKey="time" tick={{ fill: '#6b7280', fontSize: 11 }} />
<YAxis tick={{ fill: '#6b7280', fontSize: 11 }} />
<Tooltip contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: 8 }} labelStyle={{ color: '#9ca3af' }} />
<Legend />
<Bar dataKey="completed" fill="#22c55e" name="Completed" />
<Bar dataKey="failed" fill="#ef4444" name="Failed" />
</BarChart>
</ResponsiveContainer>
</Card>
{/* By source type */}
{s.by_source_type ? (
<Card>
<h2 className="mb-3 text-sm font-medium text-gray-400">By Source Type</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-surface-700 text-left text-gray-500">
<th className="px-3 py-2">Type</th>
<th className="px-3 py-2">Runs</th>
<th className="px-3 py-2">Completed</th>
<th className="px-3 py-2">Failed</th>
<th className="px-3 py-2">Items</th>
</tr>
</thead>
<tbody>
{(s.by_source_type as Array<Record<string, unknown>>).map((row, i) => (
<tr key={i} className="border-b border-surface-700/50">
<td className="px-3 py-2 text-gray-300">{String(row.source_type)}</td>
<td className="px-3 py-2 text-gray-300">{String(row.runs)}</td>
<td className="px-3 py-2 text-green-400">{String(row.completed)}</td>
<td className="px-3 py-2 text-red-400">{String(row.failed)}</td>
<td className="px-3 py-2 text-gray-300">{String(row.items_fetched)}</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
) : null}
</div>
);
}
function StatCard({ label, value, color = 'text-gray-100' }: { label: string; value: string; color?: string }) {
return (
<Card className="text-center">
<div className={`text-xl font-bold ${color}`}>{value}</div>
<div className="text-xs text-gray-500">{label}</div>
</Card>
);
}