153 lines
6.2 KiB
TypeScript
153 lines
6.2 KiB
TypeScript
import { useQuery } from "@tanstack/react-query";
|
|
import { Link } from "wouter";
|
|
import type { RunDetailResponse } from "./types";
|
|
import { fetchAdminJson, getAdminErrorMessage } from "./api";
|
|
|
|
export default function AdminRunDetail({ runId }: { runId: string }) {
|
|
const detailQuery = useQuery<RunDetailResponse | null>({
|
|
queryKey: ["admin/runs", runId],
|
|
queryFn: async () => {
|
|
const data = await fetchAdminJson<RunDetailResponse>(`admin/runs/${runId}`, {
|
|
treat404AsNull: true,
|
|
});
|
|
return data;
|
|
},
|
|
});
|
|
|
|
if (detailQuery.isLoading) {
|
|
return <div className="text-sm text-muted-foreground">Loading run...</div>;
|
|
}
|
|
|
|
if (detailQuery.isError) {
|
|
return (
|
|
<div className="rounded-2xl border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">
|
|
{getAdminErrorMessage(detailQuery.error, "Failed to load run.")}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!detailQuery.data) {
|
|
return <div className="text-sm text-muted-foreground">Run not found.</div>;
|
|
}
|
|
|
|
const { run, config, engine_status, state_snapshot, ledger_events, orders, trades, invariants } =
|
|
detailQuery.data;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
|
<Link href="/admin/runs">
|
|
<a className="text-xs text-primary hover:underline"><- Back to runs</a>
|
|
</Link>
|
|
<h2 className="mt-2 text-2xl font-semibold">{run.run_id}</h2>
|
|
<p className="text-xs text-muted-foreground">User: {run.user_id}</p>
|
|
</div>
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
|
<p className="text-sm font-semibold">Metadata</p>
|
|
<div className="mt-3 space-y-1 text-xs text-muted-foreground">
|
|
<p>Status: {run.status}</p>
|
|
<p>Mode: {run.mode ?? "-"}</p>
|
|
<p>Strategy: {run.strategy ?? "-"}</p>
|
|
<p>Started: {run.started_at ?? "-"}</p>
|
|
<p>Last event: {run.last_event_time ?? "-"}</p>
|
|
</div>
|
|
</div>
|
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
|
<p className="text-sm font-semibold">Engine Status</p>
|
|
<div className="mt-3 space-y-1 text-xs text-muted-foreground">
|
|
<p>Status: {engine_status?.status ?? "-"}</p>
|
|
<p>Updated: {engine_status?.last_updated ?? "-"}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
|
<p className="text-sm font-semibold">Config</p>
|
|
<pre className="mt-3 whitespace-pre-wrap text-xs text-muted-foreground">
|
|
{JSON.stringify(config, null, 2)}
|
|
</pre>
|
|
</div>
|
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
|
<p className="text-sm font-semibold">State Snapshot</p>
|
|
<pre className="mt-3 whitespace-pre-wrap text-xs text-muted-foreground">
|
|
{JSON.stringify(state_snapshot, null, 2)}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
|
<p className="text-sm font-semibold">Invariants</p>
|
|
<div className="mt-3 grid gap-2 text-xs text-muted-foreground md:grid-cols-2">
|
|
{Object.entries(invariants).map(([key, value]) => (
|
|
<div key={key} className="flex items-center justify-between">
|
|
<span>{key}</span>
|
|
<span className={value ? "text-red-400" : "text-emerald-400"}>{String(value)}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
|
<p className="text-sm font-semibold">Ledger Events</p>
|
|
<div className="mt-3 space-y-2 text-xs text-muted-foreground">
|
|
{ledger_events.map((evt: RunDetailResponse["ledger_events"][number], idx: number) => (
|
|
<div key={idx}>
|
|
<span className="text-foreground">{String(evt.event)}</span>{" "}
|
|
<span>{String(evt.timestamp ?? "")}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm overflow-x-auto">
|
|
<p className="text-sm font-semibold">Orders</p>
|
|
<table className="mt-3 min-w-full text-xs">
|
|
<thead className="text-muted-foreground">
|
|
<tr>
|
|
<th className="px-2 py-1 text-left">ID</th>
|
|
<th className="px-2 py-1 text-left">Symbol</th>
|
|
<th className="px-2 py-1 text-left">Side</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-border/60">
|
|
{orders.map((order: RunDetailResponse["orders"][number], idx: number) => (
|
|
<tr key={idx}>
|
|
<td className="px-2 py-1">{String(order.id)}</td>
|
|
<td className="px-2 py-1">{String(order.symbol)}</td>
|
|
<td className="px-2 py-1">{String(order.side)}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm overflow-x-auto">
|
|
<p className="text-sm font-semibold">Trades</p>
|
|
<table className="mt-3 min-w-full text-xs">
|
|
<thead className="text-muted-foreground">
|
|
<tr>
|
|
<th className="px-2 py-1 text-left">ID</th>
|
|
<th className="px-2 py-1 text-left">Symbol</th>
|
|
<th className="px-2 py-1 text-left">Side</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-border/60">
|
|
{trades.map((trade: RunDetailResponse["trades"][number], idx: number) => (
|
|
<tr key={idx}>
|
|
<td className="px-2 py-1">{String(trade.id)}</td>
|
|
<td className="px-2 py-1">{String(trade.symbol)}</td>
|
|
<td className="px-2 py-1">{String(trade.side)}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|