admin_page
This commit is contained in:
parent
6a653b55df
commit
f5d4206127
@ -9,6 +9,14 @@ const API_BASE_URL = (ENV_API_BASE_URL || (IS_LOCALHOST ? "http://localhost:8000
|
|||||||
.replace(/\/api$/, "");
|
.replace(/\/api$/, "");
|
||||||
const REQUEST_TIMEOUT_MS = 12000;
|
const REQUEST_TIMEOUT_MS = 12000;
|
||||||
|
|
||||||
|
function truncateText(value: string, maxLength = 180) {
|
||||||
|
return value.length > maxLength ? `${value.slice(0, maxLength - 3)}...` : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeHtml(value: string) {
|
||||||
|
return /<!doctype html|<html[\s>]|<head[\s>]|<body[\s>]/i.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeApiPath(url: string) {
|
function normalizeApiPath(url: string) {
|
||||||
const parsed = new URL(url, "https://frontend.local");
|
const parsed = new URL(url, "https://frontend.local");
|
||||||
const pathname = parsed.pathname.startsWith("/api")
|
const pathname = parsed.pathname.startsWith("/api")
|
||||||
@ -29,10 +37,60 @@ export function resolveApiUrl(url: string) {
|
|||||||
return `${API_BASE_URL}${normalizedPath}`;
|
return `${API_BASE_URL}${normalizedPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getResponseErrorMessage(res: Response) {
|
||||||
|
const contentType = res.headers.get("content-type") || "";
|
||||||
|
|
||||||
|
if (contentType.includes("application/json")) {
|
||||||
|
try {
|
||||||
|
const data = await res.json();
|
||||||
|
const detail =
|
||||||
|
(typeof data?.detail === "string" && data.detail) ||
|
||||||
|
(typeof data?.message === "string" && data.message) ||
|
||||||
|
(typeof data?.error === "string" && data.error);
|
||||||
|
if (detail) {
|
||||||
|
return `${res.status}: ${detail}`;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall back to response text below.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = ((await res.text()) || res.statusText).trim();
|
||||||
|
if (!text) {
|
||||||
|
return `${res.status}: ${res.statusText}`;
|
||||||
|
}
|
||||||
|
if (looksLikeHtml(text)) {
|
||||||
|
return `${res.status}: Unexpected HTML response from the API. Check backend routing or deployment config.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${res.status}: ${truncateText(text.replace(/\s+/g, " "))}`;
|
||||||
|
}
|
||||||
|
|
||||||
async function throwIfResNotOk(res: Response) {
|
async function throwIfResNotOk(res: Response) {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = (await res.text()) || res.statusText;
|
throw new Error(await getResponseErrorMessage(res));
|
||||||
throw new Error(`${res.status}: ${text}`);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function throwIfUnexpectedApiPayload(res: Response) {
|
||||||
|
if (res.status === 204) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = (res.headers.get("content-type") || "").toLowerCase();
|
||||||
|
if (contentType.includes("application/json")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = (await res.clone().text()).trim();
|
||||||
|
if (!body) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (body.startsWith("{") || body.startsWith("[")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (looksLikeHtml(body)) {
|
||||||
|
throw new Error("Unexpected HTML response from the API. Check backend routing or deployment config.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,12 +120,16 @@ export async function apiRequest(
|
|||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const res = await fetchWithTimeout(resolveApiUrl(url), {
|
const res = await fetchWithTimeout(resolveApiUrl(url), {
|
||||||
method,
|
method,
|
||||||
headers: data ? { "Content-Type": "application/json" } : {},
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
...(data ? { "Content-Type": "application/json" } : {}),
|
||||||
|
},
|
||||||
body: data ? JSON.stringify(data) : undefined,
|
body: data ? JSON.stringify(data) : undefined,
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
});
|
});
|
||||||
|
|
||||||
await throwIfResNotOk(res);
|
await throwIfResNotOk(res);
|
||||||
|
await throwIfUnexpectedApiPayload(res);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,6 +140,9 @@ export const getQueryFn: <T>(options: {
|
|||||||
({ on401: unauthorizedBehavior }) =>
|
({ on401: unauthorizedBehavior }) =>
|
||||||
async ({ queryKey }) => {
|
async ({ queryKey }) => {
|
||||||
const res = await fetchWithTimeout(resolveApiUrl(queryKey.join("/") as string), {
|
const res = await fetchWithTimeout(resolveApiUrl(queryKey.join("/") as string), {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -86,6 +151,7 @@ export const getQueryFn: <T>(options: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await throwIfResNotOk(res);
|
await throwIfResNotOk(res);
|
||||||
|
await throwIfUnexpectedApiPayload(res);
|
||||||
return await res.json();
|
return await res.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,17 @@ export default function AdminInvariants() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (invariantsQuery.isLoading) {
|
if (invariantsQuery.isLoading) {
|
||||||
return <div className="text-sm text-muted-foreground">Loading invariants…</div>;
|
return <div className="text-sm text-muted-foreground">Loading invariants...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invariantsQuery.isError) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">
|
||||||
|
{invariantsQuery.error instanceof Error
|
||||||
|
? invariantsQuery.error.message
|
||||||
|
: "Failed to load invariant data."}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!invariantsQuery.data) {
|
if (!invariantsQuery.data) {
|
||||||
@ -36,7 +46,7 @@ export default function AdminInvariants() {
|
|||||||
>
|
>
|
||||||
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">{key}</p>
|
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">{key}</p>
|
||||||
<p className={`mt-2 text-2xl font-semibold ${value ? "text-red-400" : "text-emerald-400"}`}>
|
<p className={`mt-2 text-2xl font-semibold ${value ? "text-red-400" : "text-emerald-400"}`}>
|
||||||
{value}
|
{String(value)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -21,7 +21,17 @@ export default function AdminOverview() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (overviewQuery.isLoading) {
|
if (overviewQuery.isLoading) {
|
||||||
return <div className="text-sm text-muted-foreground">Loading overview…</div>;
|
return <div className="text-sm text-muted-foreground">Loading overview...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overviewQuery.isError) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">
|
||||||
|
{overviewQuery.error instanceof Error
|
||||||
|
? overviewQuery.error.message
|
||||||
|
: "Failed to load admin overview."}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!overviewQuery.data) {
|
if (!overviewQuery.data) {
|
||||||
@ -66,7 +76,7 @@ export default function AdminOverview() {
|
|||||||
<div className="grid gap-4 lg:grid-cols-2">
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
||||||
<p className="text-sm font-semibold">Run Status</p>
|
<p className="text-sm font-semibold">Run Status</p>
|
||||||
<div className="h-64 mt-4">
|
<div className="mt-4 h-64">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={runStatusData}>
|
<BarChart data={runStatusData}>
|
||||||
<XAxis dataKey="name" tickLine={false} axisLine={false} />
|
<XAxis dataKey="name" tickLine={false} axisLine={false} />
|
||||||
@ -79,7 +89,7 @@ export default function AdminOverview() {
|
|||||||
|
|
||||||
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
||||||
<p className="text-sm font-semibold">Activity (last 24h)</p>
|
<p className="text-sm font-semibold">Activity (last 24h)</p>
|
||||||
<div className="h-64 mt-4">
|
<div className="mt-4 h-64">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={activityData}>
|
<BarChart data={activityData}>
|
||||||
<XAxis dataKey="name" tickLine={false} axisLine={false} />
|
<XAxis dataKey="name" tickLine={false} axisLine={false} />
|
||||||
@ -94,7 +104,7 @@ export default function AdminOverview() {
|
|||||||
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
||||||
<p className="text-sm font-semibold">Latest Errors</p>
|
<p className="text-sm font-semibold">Latest Errors</p>
|
||||||
{data.top_errors.length === 0 ? (
|
{data.top_errors.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground mt-4">No recent errors.</p>
|
<p className="mt-4 text-sm text-muted-foreground">No recent errors.</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-4 space-y-3">
|
<div className="mt-4 space-y-3">
|
||||||
{data.top_errors.map((err, idx) => (
|
{data.top_errors.map((err, idx) => (
|
||||||
@ -107,7 +117,7 @@ export default function AdminOverview() {
|
|||||||
<span className="text-xs text-muted-foreground">{err.ts}</span>
|
<span className="text-xs text-muted-foreground">{err.ts}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{err.source} • {err.user_id ?? "unknown"} • {err.run_id ?? "n/a"}
|
{err.source} | {err.user_id ?? "unknown"} | {err.run_id ?? "n/a"}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1">{err.message ?? "No message"}</p>
|
<p className="mt-1">{err.message ?? "No message"}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -31,7 +31,12 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
const checkAccess = useCallback(async () => {
|
const checkAccess = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(resolveApiUrl("admin/overview"), { credentials: "include" });
|
const res = await fetch(resolveApiUrl("admin/overview"), {
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
setAccessState("unauthenticated");
|
setAccessState("unauthenticated");
|
||||||
return;
|
return;
|
||||||
@ -40,6 +45,11 @@ export default function AdminPage() {
|
|||||||
setAccessState("forbidden");
|
setAccessState("forbidden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const contentType = (res.headers.get("content-type") || "").toLowerCase();
|
||||||
|
if (res.ok && contentType.includes("text/html")) {
|
||||||
|
setAccessState("forbidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
setAccessState("forbidden");
|
setAccessState("forbidden");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -13,7 +13,15 @@ export default function AdminRunDetail({ runId }: { runId: string }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (detailQuery.isLoading) {
|
if (detailQuery.isLoading) {
|
||||||
return <div className="text-sm text-muted-foreground">Loading run…</div>;
|
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">
|
||||||
|
{detailQuery.error instanceof Error ? detailQuery.error.message : "Failed to load run."}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!detailQuery.data) {
|
if (!detailQuery.data) {
|
||||||
@ -27,7 +35,7 @@ export default function AdminRunDetail({ runId }: { runId: string }) {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
||||||
<Link href="/admin/runs">
|
<Link href="/admin/runs">
|
||||||
<a className="text-xs text-primary hover:underline">← Back to runs</a>
|
<a className="text-xs text-primary hover:underline"><- Back to runs</a>
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="mt-2 text-2xl font-semibold">{run.run_id}</h2>
|
<h2 className="mt-2 text-2xl font-semibold">{run.run_id}</h2>
|
||||||
<p className="text-xs text-muted-foreground">User: {run.user_id}</p>
|
<p className="text-xs text-muted-foreground">User: {run.user_id}</p>
|
||||||
@ -38,17 +46,17 @@ export default function AdminRunDetail({ runId }: { runId: string }) {
|
|||||||
<p className="text-sm font-semibold">Metadata</p>
|
<p className="text-sm font-semibold">Metadata</p>
|
||||||
<div className="mt-3 space-y-1 text-xs text-muted-foreground">
|
<div className="mt-3 space-y-1 text-xs text-muted-foreground">
|
||||||
<p>Status: {run.status}</p>
|
<p>Status: {run.status}</p>
|
||||||
<p>Mode: {run.mode ?? "—"}</p>
|
<p>Mode: {run.mode ?? "-"}</p>
|
||||||
<p>Strategy: {run.strategy ?? "—"}</p>
|
<p>Strategy: {run.strategy ?? "-"}</p>
|
||||||
<p>Started: {run.started_at ?? "—"}</p>
|
<p>Started: {run.started_at ?? "-"}</p>
|
||||||
<p>Last event: {run.last_event_time ?? "—"}</p>
|
<p>Last event: {run.last_event_time ?? "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
||||||
<p className="text-sm font-semibold">Engine Status</p>
|
<p className="text-sm font-semibold">Engine Status</p>
|
||||||
<div className="mt-3 space-y-1 text-xs text-muted-foreground">
|
<div className="mt-3 space-y-1 text-xs text-muted-foreground">
|
||||||
<p>Status: {engine_status?.status ?? "—"}</p>
|
<p>Status: {engine_status?.status ?? "-"}</p>
|
||||||
<p>Updated: {engine_status?.last_updated ?? "—"}</p>
|
<p>Updated: {engine_status?.last_updated ?? "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -56,13 +64,13 @@ export default function AdminRunDetail({ runId }: { runId: string }) {
|
|||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
||||||
<p className="text-sm font-semibold">Config</p>
|
<p className="text-sm font-semibold">Config</p>
|
||||||
<pre className="mt-3 text-xs text-muted-foreground whitespace-pre-wrap">
|
<pre className="mt-3 whitespace-pre-wrap text-xs text-muted-foreground">
|
||||||
{JSON.stringify(config, null, 2)}
|
{JSON.stringify(config, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
||||||
<p className="text-sm font-semibold">State Snapshot</p>
|
<p className="text-sm font-semibold">State Snapshot</p>
|
||||||
<pre className="mt-3 text-xs text-muted-foreground whitespace-pre-wrap">
|
<pre className="mt-3 whitespace-pre-wrap text-xs text-muted-foreground">
|
||||||
{JSON.stringify(state_snapshot, null, 2)}
|
{JSON.stringify(state_snapshot, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@ -74,9 +82,7 @@ export default function AdminRunDetail({ runId }: { runId: string }) {
|
|||||||
{Object.entries(invariants).map(([key, value]) => (
|
{Object.entries(invariants).map(([key, value]) => (
|
||||||
<div key={key} className="flex items-center justify-between">
|
<div key={key} className="flex items-center justify-between">
|
||||||
<span>{key}</span>
|
<span>{key}</span>
|
||||||
<span className={value ? "text-red-400" : "text-emerald-400"}>
|
<span className={value ? "text-red-400" : "text-emerald-400"}>{String(value)}</span>
|
||||||
{value}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -97,7 +103,7 @@ export default function AdminRunDetail({ runId }: { runId: string }) {
|
|||||||
<div className="grid gap-4 md:grid-cols-2">
|
<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">
|
<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>
|
<p className="text-sm font-semibold">Orders</p>
|
||||||
<table className="min-w-full text-xs mt-3">
|
<table className="mt-3 min-w-full text-xs">
|
||||||
<thead className="text-muted-foreground">
|
<thead className="text-muted-foreground">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-2 py-1 text-left">ID</th>
|
<th className="px-2 py-1 text-left">ID</th>
|
||||||
@ -119,7 +125,7 @@ export default function AdminRunDetail({ runId }: { runId: string }) {
|
|||||||
|
|
||||||
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm overflow-x-auto">
|
<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>
|
<p className="text-sm font-semibold">Trades</p>
|
||||||
<table className="min-w-full text-xs mt-3">
|
<table className="mt-3 min-w-full text-xs">
|
||||||
<thead className="text-muted-foreground">
|
<thead className="text-muted-foreground">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-2 py-1 text-left">ID</th>
|
<th className="px-2 py-1 text-left">ID</th>
|
||||||
|
|||||||
@ -32,7 +32,15 @@ export default function AdminRuns() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (runsQuery.isLoading) {
|
if (runsQuery.isLoading) {
|
||||||
return <div className="text-sm text-muted-foreground">Loading runs…</div>;
|
return <div className="text-sm text-muted-foreground">Loading runs...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runsQuery.isError) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">
|
||||||
|
{runsQuery.error instanceof Error ? runsQuery.error.message : "Failed to load runs."}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = runsQuery.data;
|
const data = runsQuery.data;
|
||||||
@ -97,7 +105,7 @@ export default function AdminRuns() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 text-xs">{run.user_id}</td>
|
<td className="px-4 py-2 text-xs">{run.user_id}</td>
|
||||||
<td className="px-4 py-2 text-xs">{run.status}</td>
|
<td className="px-4 py-2 text-xs">{run.status}</td>
|
||||||
<td className="px-4 py-2 text-xs">{run.mode ?? "—"}</td>
|
<td className="px-4 py-2 text-xs">{run.mode ?? "-"}</td>
|
||||||
<td className="px-4 py-2 text-xs">{run.order_count}</td>
|
<td className="px-4 py-2 text-xs">{run.order_count}</td>
|
||||||
<td className="px-4 py-2 text-xs">{run.trade_count}</td>
|
<td className="px-4 py-2 text-xs">{run.trade_count}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -25,7 +25,7 @@ type TicketsResponse = {
|
|||||||
|
|
||||||
export default function AdminSupportTickets() {
|
export default function AdminSupportTickets() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data, isLoading } = useQuery<TicketsResponse>({
|
const { data, isLoading, isError, error } = useQuery<TicketsResponse>({
|
||||||
queryKey: ["admin/support-tickets"],
|
queryKey: ["admin/support-tickets"],
|
||||||
queryFn: getQueryFn<TicketsResponse>({ on401: "throw" }),
|
queryFn: getQueryFn<TicketsResponse>({ on401: "throw" }),
|
||||||
});
|
});
|
||||||
@ -46,6 +46,14 @@ export default function AdminSupportTickets() {
|
|||||||
return <div className="text-muted-foreground">Loading tickets...</div>;
|
return <div className="text-muted-foreground">Loading tickets...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">
|
||||||
|
{error instanceof Error ? error.message : "Failed to load support tickets."}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
@ -13,7 +13,15 @@ export default function AdminUserDetail({ userId }: { userId: string }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (detailQuery.isLoading) {
|
if (detailQuery.isLoading) {
|
||||||
return <div className="text-sm text-muted-foreground">Loading user…</div>;
|
return <div className="text-sm text-muted-foreground">Loading user...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detailQuery.isError) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">
|
||||||
|
{detailQuery.error instanceof Error ? detailQuery.error.message : "Failed to load user."}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!detailQuery.data) {
|
if (!detailQuery.data) {
|
||||||
@ -26,7 +34,7 @@ export default function AdminUserDetail({ userId }: { userId: string }) {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
||||||
<Link href="/admin/users">
|
<Link href="/admin/users">
|
||||||
<a className="text-xs text-primary hover:underline">← Back to users</a>
|
<a className="text-xs text-primary hover:underline"><- Back to users</a>
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="mt-2 text-2xl font-semibold">{user.username}</h2>
|
<h2 className="mt-2 text-2xl font-semibold">{user.username}</h2>
|
||||||
<p className="text-xs text-muted-foreground">User ID: {user.user_id}</p>
|
<p className="text-xs text-muted-foreground">User ID: {user.user_id}</p>
|
||||||
@ -36,17 +44,17 @@ export default function AdminUserDetail({ userId }: { userId: string }) {
|
|||||||
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
||||||
<p className="text-sm font-semibold">Capital Summary</p>
|
<p className="text-sm font-semibold">Capital Summary</p>
|
||||||
<div className="mt-3 space-y-1 text-xs text-muted-foreground">
|
<div className="mt-3 space-y-1 text-xs text-muted-foreground">
|
||||||
<p>Cash: {capital_summary.cash ?? "—"}</p>
|
<p>Cash: {capital_summary.cash ?? "-"}</p>
|
||||||
<p>Invested: {capital_summary.invested ?? "—"}</p>
|
<p>Invested: {capital_summary.invested ?? "-"}</p>
|
||||||
<p>MTM: {capital_summary.mtm ?? "—"}</p>
|
<p>MTM: {capital_summary.mtm ?? "-"}</p>
|
||||||
<p>Equity: {capital_summary.equity ?? "—"}</p>
|
<p>Equity: {capital_summary.equity ?? "-"}</p>
|
||||||
<p>PnL: {capital_summary.pnl ?? "—"}</p>
|
<p>PnL: {capital_summary.pnl ?? "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
|
||||||
<p className="text-sm font-semibold">Current Config</p>
|
<p className="text-sm font-semibold">Current Config</p>
|
||||||
<pre className="mt-3 text-xs text-muted-foreground whitespace-pre-wrap">
|
<pre className="mt-3 whitespace-pre-wrap text-xs text-muted-foreground">
|
||||||
{JSON.stringify(current_config, null, 2)}
|
{JSON.stringify(current_config, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@ -61,7 +69,7 @@ export default function AdminUserDetail({ userId }: { userId: string }) {
|
|||||||
<a className="text-primary hover:underline">{run.run_id}</a>
|
<a className="text-primary hover:underline">{run.run_id}</a>
|
||||||
</Link>
|
</Link>
|
||||||
<span className="text-muted-foreground">{run.status}</span>
|
<span className="text-muted-foreground">{run.status}</span>
|
||||||
<span className="text-muted-foreground">{run.created_at ?? "—"}</span>
|
<span className="text-muted-foreground">{run.created_at ?? "-"}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -74,7 +82,7 @@ export default function AdminUserDetail({ userId }: { userId: string }) {
|
|||||||
<div key={`${evt.ts}-${idx}`} className="text-xs text-muted-foreground">
|
<div key={`${evt.ts}-${idx}`} className="text-xs text-muted-foreground">
|
||||||
<span className="text-foreground">{evt.event}</span>{" "}
|
<span className="text-foreground">{evt.event}</span>{" "}
|
||||||
<span>({evt.source})</span>{" "}
|
<span>({evt.source})</span>{" "}
|
||||||
<span>{evt.ts ?? "—"}</span>
|
<span>{evt.ts ?? "-"}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -109,6 +109,14 @@ export default function AdminUsers() {
|
|||||||
return <div className="text-sm text-muted-foreground">Loading users...</div>;
|
return <div className="text-sm text-muted-foreground">Loading users...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (usersQuery.isError) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">
|
||||||
|
{usersQuery.error instanceof Error ? usersQuery.error.message : "Failed to load users."}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const data = usersQuery.data;
|
const data = usersQuery.data;
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <div className="text-sm text-muted-foreground">No users.</div>;
|
return <div className="text-sm text-muted-foreground">No users.</div>;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user