← back to Omega Watches 2
src/app/(dashboard)/sources/page.tsx
111 lines
"use client";
import { useEffect, useState } from "react";
import { api } from "@/lib/api-client";
import { SourceReliabilityChart } from "@/components/charts";
export default function SourceCatalogPage() {
const [sources, setSources] = useState<any[]>([]);
const [selected, setSelected] = useState<any>(null);
const [detail, setDetail] = useState<any>(null);
const [error, setError] = useState("");
useEffect(() => {
api.getSources().then(setSources).catch((e) => setError(e.message));
}, []);
async function loadDetail(name: string) {
try {
const d = await api.getSourceStatus(name);
setDetail(d);
} catch (e: any) {
setError(e.message);
}
}
return (
<div className="space-y-6">
<h1 className="text-xl font-bold text-gray-100">Source Catalog</h1>
{error && <div className="rounded bg-red-900/30 p-3 text-red-300">{error}</div>}
{sources.length > 0 && (
<SourceReliabilityChart
data={sources
.filter((s: any) => s.total_runs > 0)
.map((s: any) => ({
name: s.display_name,
success_rate: parseFloat(s.success_rate_pct) || 0,
total_runs: parseInt(s.total_runs) || 0,
records: parseInt(s.total_records_inserted) || 0,
}))
.sort((a: any, b: any) => b.success_rate - a.success_rate)}
title="Source Reliability Comparison"
/>
)}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{sources.map((s) => (
<div
key={s.id}
onClick={() => { setSelected(s); loadDetail(s.name); }}
className={`card cursor-pointer border-navy-700 transition-colors hover:border-omega-500/50 ${selected?.id === s.id ? "ring-1 ring-omega-500" : ""}`}
>
<div className="flex items-start justify-between">
<div>
<div className="font-medium text-gray-200">{s.display_name}</div>
<div className="text-xs text-gray-500">{s.source_type} | {s.access_method}</div>
</div>
<span className={`badge ${s.is_active ? "badge-success" : "badge-error"}`}>
{s.is_active ? "Active" : "Off"}
</span>
</div>
<div className="mt-3 flex items-center gap-1">
{[1, 2, 3, 4, 5].map((n) => (
<span key={n} className={`inline-block h-1.5 w-4 rounded-full ${n <= s.reliability_score ? "bg-omega-400" : "bg-navy-700"}`} />
))}
<span className="ml-2 text-xs text-gray-500">{s.reliability_score}/5</span>
</div>
{s.tos_notes && (
<div className="mt-2 text-xs text-gray-500 line-clamp-2">{s.tos_notes}</div>
)}
<div className="mt-2 text-xs text-gray-600">
Coverage: {s.coverage_years_from || "?"} - {s.coverage_years_to || "?"}
</div>
</div>
))}
</div>
{detail && (
<div className="card border-navy-700">
<h2 className="mb-3 text-lg font-semibold text-gray-200">
{detail.source.display_name} — Detail
</h2>
<div className="grid gap-4 md:grid-cols-3">
<div>
<div className="text-xs text-gray-500">Base URL</div>
<div className="text-sm text-gray-300">{detail.source.base_url || "N/A"}</div>
</div>
<div>
<div className="text-xs text-gray-500">Rate Limit</div>
<div className="text-sm text-gray-300">{detail.source.rate_limit_rpm || "None"} rpm</div>
</div>
<div>
<div className="text-xs text-gray-500">Current Parser</div>
<div className="text-sm text-gray-300">{detail.current_parser?.version || "None"}</div>
</div>
</div>
<div className="mt-4">
<div className="text-xs text-gray-500">Event Counts</div>
<div className="mt-1 flex gap-4 text-sm">
<span className="text-gray-300">Total: {detail.event_counts?.total_events || 0}</span>
<span className="text-green-400">Auctions: {detail.event_counts?.auction_count || 0}</span>
<span className="text-blue-400">Marketplace: {detail.event_counts?.marketplace_count || 0}</span>
<span className="text-yellow-400">Quarantined: {detail.event_counts?.quarantined_count || 0}</span>
</div>
</div>
</div>
)}
</div>
);
}