122 lines
6.0 KiB
TypeScript
122 lines
6.0 KiB
TypeScript
import { Link } from '@tanstack/react-router';
|
|
import { usePipelineHealth, useIngestionSummary, useRecommendations, useCompanies, useCoverageGaps } from '../api/hooks';
|
|
import { LoadingSpinner, Card } from '../components/ui';
|
|
import {
|
|
Building2, FileText, TrendingUp, Lightbulb, ShoppingCart,
|
|
Wallet, ShieldCheck, Activity, Download, Cpu, Radar,
|
|
Terminal, LayoutDashboard, AlertTriangle,
|
|
} from 'lucide-react';
|
|
|
|
const quickNav = [
|
|
{ to: '/companies', label: 'Companies', icon: <Building2 size={20} />, color: 'text-blue-400' },
|
|
{ to: '/documents', label: 'Documents', icon: <FileText size={20} />, color: 'text-green-400' },
|
|
{ to: '/trends', label: 'Trends', icon: <TrendingUp size={20} />, color: 'text-purple-400' },
|
|
{ to: '/recommendations', label: 'Recommendations', icon: <Lightbulb size={20} />, color: 'text-yellow-400' },
|
|
{ to: '/orders', label: 'Orders', icon: <ShoppingCart size={20} />, color: 'text-orange-400' },
|
|
{ to: '/positions', label: 'Positions', icon: <Wallet size={20} />, color: 'text-cyan-400' },
|
|
{ to: '/trading', label: 'Trading', icon: <ShieldCheck size={20} />, color: 'text-red-400' },
|
|
{ to: '/ops/pipeline', label: 'Pipeline', icon: <Activity size={20} />, color: 'text-emerald-400' },
|
|
{ to: '/ops/ingestion', label: 'Ingestion', icon: <Download size={20} />, color: 'text-teal-400' },
|
|
{ to: '/ops/model', label: 'Model Perf', icon: <Cpu size={20} />, color: 'text-indigo-400' },
|
|
{ to: '/ops/coverage', label: 'Coverage', icon: <Radar size={20} />, color: 'text-pink-400' },
|
|
{ to: '/analytics/query', label: 'SQL Explorer', icon: <Terminal size={20} />, color: 'text-amber-400' },
|
|
{ to: '/analytics/dashboards', label: 'Dashboards', icon: <LayoutDashboard size={20} />, color: 'text-violet-400' },
|
|
];
|
|
|
|
export function HomePage() {
|
|
const { data: pipeline, isLoading: pLoading } = usePipelineHealth(24);
|
|
const { data: ingestion } = useIngestionSummary(24);
|
|
const { data: recs } = useRecommendations({ limit: 5 });
|
|
const { data: companies } = useCompanies();
|
|
const { data: gaps } = useCoverageGaps();
|
|
|
|
if (pLoading) return <LoadingSpinner />;
|
|
|
|
const ing = (ingestion ?? {}) as Record<string, unknown>;
|
|
const staleCount = (gaps?.stale_sources?.length ?? 0) + (gaps?.missing_source_types?.length ?? 0);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<h1 className="text-2xl font-bold text-gray-100">Stonks Oracle</h1>
|
|
|
|
{/* Alert banner */}
|
|
{staleCount > 0 && (
|
|
<div className="flex items-center gap-3 rounded-lg border border-yellow-700/50 bg-yellow-900/20 p-3" role="alert">
|
|
<AlertTriangle size={18} className="text-yellow-400" />
|
|
<span className="text-sm text-yellow-300">
|
|
{staleCount} coverage issue{staleCount > 1 ? 's' : ''} detected — check{' '}
|
|
<Link to="/ops/coverage" className="underline hover:text-yellow-200">Source Coverage</Link>
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Key metrics */}
|
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
<MetricCard label="Active Companies" value={companies?.length ?? 0} />
|
|
<MetricCard label="Ingestion Runs (24h)" value={ing.total_runs} />
|
|
<MetricCard label="Items Fetched (24h)" value={ing.total_items_fetched} />
|
|
<MetricCard label="Recommendations Today" value={recs?.length ?? 0} />
|
|
</div>
|
|
|
|
{/* Pipeline status */}
|
|
{pipeline && (
|
|
<Card>
|
|
<h2 className="mb-3 text-sm font-medium text-gray-400">Pipeline Status (24h)</h2>
|
|
<div className="grid grid-cols-2 gap-3 text-sm sm:grid-cols-4">
|
|
{((pipeline.document_stages ?? []) as Array<{ status: string; doc_count: number }>).map((s) => (
|
|
<div key={s.status} className="text-center">
|
|
<div className="text-lg font-bold text-gray-100">{s.doc_count}</div>
|
|
<div className="text-xs capitalize text-gray-500">{s.status}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Recent activity */}
|
|
{recs && recs.length > 0 && (
|
|
<Card>
|
|
<h2 className="mb-3 text-sm font-medium text-gray-400">Recent Recommendations</h2>
|
|
<div className="space-y-2">
|
|
{recs.map((r) => (
|
|
<Link key={r.id} to="/recommendations/$id" params={{ id: r.id }} className="flex items-center justify-between rounded border border-surface-700 bg-surface-950 p-2 hover:bg-surface-800/50">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-mono text-sm font-semibold text-brand-300">{r.ticker}</span>
|
|
<span className={`rounded-full px-2 py-0.5 text-xs font-medium ${r.action === 'buy' ? 'bg-green-900/40 text-green-400' : r.action === 'sell' ? 'bg-red-900/40 text-red-400' : 'bg-gray-800/40 text-gray-400'}`}>
|
|
{r.action}
|
|
</span>
|
|
<span className="text-xs text-gray-500 line-clamp-1 max-w-xs">{r.thesis}</span>
|
|
</div>
|
|
<span className="text-xs text-gray-500">{new Date(r.generated_at).toLocaleString()}</span>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Quick nav */}
|
|
<div className="grid grid-cols-3 gap-2 sm:grid-cols-4 lg:grid-cols-5">
|
|
{quickNav.map((item) => (
|
|
<Link
|
|
key={item.to}
|
|
to={item.to}
|
|
className="flex flex-col items-center gap-1.5 rounded-lg border border-surface-700 bg-surface-900 p-3 text-center transition-colors hover:border-brand-500/50 hover:bg-surface-800"
|
|
>
|
|
<span className={item.color}>{item.icon}</span>
|
|
<span className="text-xs text-gray-400">{item.label}</span>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function MetricCard({ label, value }: { label: string; value: unknown }) {
|
|
return (
|
|
<Card className="text-center">
|
|
<div className="text-2xl font-bold text-gray-100">{value != null ? String(value) : '—'}</div>
|
|
<div className="text-xs text-gray-500">{label}</div>
|
|
</Card>
|
|
);
|
|
}
|