admin_page

This commit is contained in:
Thigazhezhilan J 2026-03-26 00:17:22 +05:30
parent 6a653b55df
commit f5d4206127
9 changed files with 173 additions and 39 deletions

View File

@ -9,6 +9,14 @@ const API_BASE_URL = (ENV_API_BASE_URL || (IS_LOCALHOST ? "http://localhost:8000
.replace(/\/api$/, "");
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) {
const parsed = new URL(url, "https://frontend.local");
const pathname = parsed.pathname.startsWith("/api")
@ -29,10 +37,60 @@ export function resolveApiUrl(url: string) {
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) {
if (!res.ok) {
const text = (await res.text()) || res.statusText;
throw new Error(`${res.status}: ${text}`);
throw new Error(await getResponseErrorMessage(res));
}
}
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> {
const res = await fetchWithTimeout(resolveApiUrl(url), {
method,
headers: data ? { "Content-Type": "application/json" } : {},
headers: {
Accept: "application/json",
...(data ? { "Content-Type": "application/json" } : {}),
},
body: data ? JSON.stringify(data) : undefined,
credentials: "include",
});
await throwIfResNotOk(res);
await throwIfUnexpectedApiPayload(res);
return res;
}
@ -78,6 +140,9 @@ export const getQueryFn: <T>(options: {
({ on401: unauthorizedBehavior }) =>
async ({ queryKey }) => {
const res = await fetchWithTimeout(resolveApiUrl(queryKey.join("/") as string), {
headers: {
Accept: "application/json",
},
credentials: "include",
});
@ -86,6 +151,7 @@ export const getQueryFn: <T>(options: {
}
await throwIfResNotOk(res);
await throwIfUnexpectedApiPayload(res);
return await res.json();
};

View File

@ -12,7 +12,17 @@ export default function AdminInvariants() {
});
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) {
@ -36,7 +46,7 @@ export default function AdminInvariants() {
>
<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"}`}>
{value}
{String(value)}
</p>
</div>
))}

View File

@ -21,7 +21,17 @@ export default function AdminOverview() {
});
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) {
@ -66,7 +76,7 @@ export default function AdminOverview() {
<div className="grid gap-4 lg:grid-cols-2">
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
<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%">
<BarChart data={runStatusData}>
<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">
<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%">
<BarChart data={activityData}>
<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">
<p className="text-sm font-semibold">Latest Errors</p>
{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">
{data.top_errors.map((err, idx) => (
@ -107,7 +117,7 @@ export default function AdminOverview() {
<span className="text-xs text-muted-foreground">{err.ts}</span>
</div>
<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 className="mt-1">{err.message ?? "No message"}</p>
</div>

View File

@ -31,7 +31,12 @@ export default function AdminPage() {
const checkAccess = useCallback(async () => {
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) {
setAccessState("unauthenticated");
return;
@ -40,6 +45,11 @@ export default function AdminPage() {
setAccessState("forbidden");
return;
}
const contentType = (res.headers.get("content-type") || "").toLowerCase();
if (res.ok && contentType.includes("text/html")) {
setAccessState("forbidden");
return;
}
if (!res.ok) {
setAccessState("forbidden");
return;

View File

@ -13,7 +13,15 @@ export default function AdminRunDetail({ runId }: { runId: string }) {
});
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) {
@ -27,7 +35,7 @@ export default function AdminRunDetail({ runId }: { runId: string }) {
<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>
<a className="text-xs text-primary hover:underline">&lt;- 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>
@ -38,17 +46,17 @@ export default function AdminRunDetail({ runId }: { runId: string }) {
<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>
<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>
<p>Status: {engine_status?.status ?? "-"}</p>
<p>Updated: {engine_status?.last_updated ?? "-"}</p>
</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="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 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)}
</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 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)}
</pre>
</div>
@ -74,9 +82,7 @@ export default function AdminRunDetail({ runId }: { runId: string }) {
{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"}>
{value}
</span>
<span className={value ? "text-red-400" : "text-emerald-400"}>{String(value)}</span>
</div>
))}
</div>
@ -97,7 +103,7 @@ export default function AdminRunDetail({ runId }: { runId: string }) {
<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="min-w-full text-xs mt-3">
<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>
@ -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">
<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">
<tr>
<th className="px-2 py-1 text-left">ID</th>

View File

@ -32,7 +32,15 @@ export default function AdminRuns() {
});
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;
@ -97,7 +105,7 @@ export default function AdminRuns() {
</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.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.trade_count}</td>
</tr>

View File

@ -25,7 +25,7 @@ type TicketsResponse = {
export default function AdminSupportTickets() {
const queryClient = useQueryClient();
const { data, isLoading } = useQuery<TicketsResponse>({
const { data, isLoading, isError, error } = useQuery<TicketsResponse>({
queryKey: ["admin/support-tickets"],
queryFn: getQueryFn<TicketsResponse>({ on401: "throw" }),
});
@ -46,6 +46,14 @@ export default function AdminSupportTickets() {
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 (
<div className="space-y-6">
<div className="flex items-center justify-between">

View File

@ -13,7 +13,15 @@ export default function AdminUserDetail({ userId }: { userId: string }) {
});
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) {
@ -26,7 +34,7 @@ export default function AdminUserDetail({ userId }: { userId: string }) {
<div className="space-y-6">
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
<Link href="/admin/users">
<a className="text-xs text-primary hover:underline"> Back to users</a>
<a className="text-xs text-primary hover:underline">&lt;- Back to users</a>
</Link>
<h2 className="mt-2 text-2xl font-semibold">{user.username}</h2>
<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">
<p className="text-sm font-semibold">Capital Summary</p>
<div className="mt-3 space-y-1 text-xs text-muted-foreground">
<p>Cash: {capital_summary.cash ?? ""}</p>
<p>Invested: {capital_summary.invested ?? ""}</p>
<p>MTM: {capital_summary.mtm ?? ""}</p>
<p>Equity: {capital_summary.equity ?? ""}</p>
<p>PnL: {capital_summary.pnl ?? ""}</p>
<p>Cash: {capital_summary.cash ?? "-"}</p>
<p>Invested: {capital_summary.invested ?? "-"}</p>
<p>MTM: {capital_summary.mtm ?? "-"}</p>
<p>Equity: {capital_summary.equity ?? "-"}</p>
<p>PnL: {capital_summary.pnl ?? "-"}</p>
</div>
</div>
<div className="rounded-2xl border border-border/60 bg-card/70 p-6 shadow-sm">
<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)}
</pre>
</div>
@ -61,7 +69,7 @@ export default function AdminUserDetail({ userId }: { userId: string }) {
<a className="text-primary hover:underline">{run.run_id}</a>
</Link>
<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>
@ -74,7 +82,7 @@ export default function AdminUserDetail({ userId }: { userId: string }) {
<div key={`${evt.ts}-${idx}`} className="text-xs text-muted-foreground">
<span className="text-foreground">{evt.event}</span>{" "}
<span>({evt.source})</span>{" "}
<span>{evt.ts ?? ""}</span>
<span>{evt.ts ?? "-"}</span>
</div>
))}
</div>

View File

@ -109,6 +109,14 @@ export default function AdminUsers() {
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;
if (!data) {
return <div className="text-sm text-muted-foreground">No users.</div>;