diff --git a/src/lib/queryClient.ts b/src/lib/queryClient.ts index 25c0b99e..a2bbb31d 100644 --- a/src/lib/queryClient.ts +++ b/src/lib/queryClient.ts @@ -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 /]|]|]/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 { 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: (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: (options: { } await throwIfResNotOk(res); + await throwIfUnexpectedApiPayload(res); return await res.json(); }; diff --git a/src/pages/admin/AdminInvariants.tsx b/src/pages/admin/AdminInvariants.tsx index 67cab651..8d3551b8 100644 --- a/src/pages/admin/AdminInvariants.tsx +++ b/src/pages/admin/AdminInvariants.tsx @@ -12,7 +12,17 @@ export default function AdminInvariants() { }); if (invariantsQuery.isLoading) { - return
Loading invariants…
; + return
Loading invariants...
; + } + + if (invariantsQuery.isError) { + return ( +
+ {invariantsQuery.error instanceof Error + ? invariantsQuery.error.message + : "Failed to load invariant data."} +
+ ); } if (!invariantsQuery.data) { @@ -36,7 +46,7 @@ export default function AdminInvariants() { >

{key}

- {value} + {String(value)}

))} diff --git a/src/pages/admin/AdminOverview.tsx b/src/pages/admin/AdminOverview.tsx index 23bd5b6e..ed53745a 100644 --- a/src/pages/admin/AdminOverview.tsx +++ b/src/pages/admin/AdminOverview.tsx @@ -21,7 +21,17 @@ export default function AdminOverview() { }); if (overviewQuery.isLoading) { - return
Loading overview…
; + return
Loading overview...
; + } + + if (overviewQuery.isError) { + return ( +
+ {overviewQuery.error instanceof Error + ? overviewQuery.error.message + : "Failed to load admin overview."} +
+ ); } if (!overviewQuery.data) { @@ -66,7 +76,7 @@ export default function AdminOverview() {

Run Status

-
+
@@ -79,7 +89,7 @@ export default function AdminOverview() {

Activity (last 24h)

-
+
@@ -94,7 +104,7 @@ export default function AdminOverview() {

Latest Errors

{data.top_errors.length === 0 ? ( -

No recent errors.

+

No recent errors.

) : (
{data.top_errors.map((err, idx) => ( @@ -107,7 +117,7 @@ export default function AdminOverview() { {err.ts}

- {err.source} • {err.user_id ?? "unknown"} • {err.run_id ?? "n/a"} + {err.source} | {err.user_id ?? "unknown"} | {err.run_id ?? "n/a"}

{err.message ?? "No message"}

diff --git a/src/pages/admin/AdminPage.tsx b/src/pages/admin/AdminPage.tsx index f560d63f..31f6dcfd 100644 --- a/src/pages/admin/AdminPage.tsx +++ b/src/pages/admin/AdminPage.tsx @@ -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; diff --git a/src/pages/admin/AdminRunDetail.tsx b/src/pages/admin/AdminRunDetail.tsx index 232de3c4..e0f18c00 100644 --- a/src/pages/admin/AdminRunDetail.tsx +++ b/src/pages/admin/AdminRunDetail.tsx @@ -13,7 +13,15 @@ export default function AdminRunDetail({ runId }: { runId: string }) { }); if (detailQuery.isLoading) { - return
Loading run…
; + return
Loading run...
; + } + + if (detailQuery.isError) { + return ( +
+ {detailQuery.error instanceof Error ? detailQuery.error.message : "Failed to load run."} +
+ ); } if (!detailQuery.data) { @@ -27,7 +35,7 @@ export default function AdminRunDetail({ runId }: { runId: string }) {
- ← Back to runs + <- Back to runs

{run.run_id}

User: {run.user_id}

@@ -38,17 +46,17 @@ export default function AdminRunDetail({ runId }: { runId: string }) {

Metadata

Status: {run.status}

-

Mode: {run.mode ?? "—"}

-

Strategy: {run.strategy ?? "—"}

-

Started: {run.started_at ?? "—"}

-

Last event: {run.last_event_time ?? "—"}

+

Mode: {run.mode ?? "-"}

+

Strategy: {run.strategy ?? "-"}

+

Started: {run.started_at ?? "-"}

+

Last event: {run.last_event_time ?? "-"}

Engine Status

-

Status: {engine_status?.status ?? "—"}

-

Updated: {engine_status?.last_updated ?? "—"}

+

Status: {engine_status?.status ?? "-"}

+

Updated: {engine_status?.last_updated ?? "-"}

@@ -56,13 +64,13 @@ export default function AdminRunDetail({ runId }: { runId: string }) {

Config

-
+          
 {JSON.stringify(config, null, 2)}
           

State Snapshot

-
+          
 {JSON.stringify(state_snapshot, null, 2)}
           
@@ -74,9 +82,7 @@ export default function AdminRunDetail({ runId }: { runId: string }) { {Object.entries(invariants).map(([key, value]) => (
{key} - - {value} - + {String(value)}
))}
@@ -97,7 +103,7 @@ export default function AdminRunDetail({ runId }: { runId: string }) {

Orders

- +
@@ -119,7 +125,7 @@ export default function AdminRunDetail({ runId }: { runId: string }) {

Trades

-
ID
+
diff --git a/src/pages/admin/AdminRuns.tsx b/src/pages/admin/AdminRuns.tsx index 7adc2b25..3aeb4d82 100644 --- a/src/pages/admin/AdminRuns.tsx +++ b/src/pages/admin/AdminRuns.tsx @@ -32,7 +32,15 @@ export default function AdminRuns() { }); if (runsQuery.isLoading) { - return
Loading runs…
; + return
Loading runs...
; + } + + if (runsQuery.isError) { + return ( +
+ {runsQuery.error instanceof Error ? runsQuery.error.message : "Failed to load runs."} +
+ ); } const data = runsQuery.data; @@ -97,7 +105,7 @@ export default function AdminRuns() { - + diff --git a/src/pages/admin/AdminSupportTickets.tsx b/src/pages/admin/AdminSupportTickets.tsx index ab040be0..31b4813a 100644 --- a/src/pages/admin/AdminSupportTickets.tsx +++ b/src/pages/admin/AdminSupportTickets.tsx @@ -25,7 +25,7 @@ type TicketsResponse = { export default function AdminSupportTickets() { const queryClient = useQueryClient(); - const { data, isLoading } = useQuery({ + const { data, isLoading, isError, error } = useQuery({ queryKey: ["admin/support-tickets"], queryFn: getQueryFn({ on401: "throw" }), }); @@ -46,6 +46,14 @@ export default function AdminSupportTickets() { return
Loading tickets...
; } + if (isError) { + return ( +
+ {error instanceof Error ? error.message : "Failed to load support tickets."} +
+ ); + } + return (
diff --git a/src/pages/admin/AdminUserDetail.tsx b/src/pages/admin/AdminUserDetail.tsx index e8a2629b..cfb18827 100644 --- a/src/pages/admin/AdminUserDetail.tsx +++ b/src/pages/admin/AdminUserDetail.tsx @@ -13,7 +13,15 @@ export default function AdminUserDetail({ userId }: { userId: string }) { }); if (detailQuery.isLoading) { - return
Loading user…
; + return
Loading user...
; + } + + if (detailQuery.isError) { + return ( +
+ {detailQuery.error instanceof Error ? detailQuery.error.message : "Failed to load user."} +
+ ); } if (!detailQuery.data) { @@ -26,7 +34,7 @@ export default function AdminUserDetail({ userId }: { userId: string }) {
- ← Back to users + <- Back to users

{user.username}

User ID: {user.user_id}

@@ -36,17 +44,17 @@ export default function AdminUserDetail({ userId }: { userId: string }) {

Capital Summary

-

Cash: {capital_summary.cash ?? "—"}

-

Invested: {capital_summary.invested ?? "—"}

-

MTM: {capital_summary.mtm ?? "—"}

-

Equity: {capital_summary.equity ?? "—"}

-

PnL: {capital_summary.pnl ?? "—"}

+

Cash: {capital_summary.cash ?? "-"}

+

Invested: {capital_summary.invested ?? "-"}

+

MTM: {capital_summary.mtm ?? "-"}

+

Equity: {capital_summary.equity ?? "-"}

+

PnL: {capital_summary.pnl ?? "-"}

Current Config

-
+          
 {JSON.stringify(current_config, null, 2)}
           
@@ -61,7 +69,7 @@ export default function AdminUserDetail({ userId }: { userId: string }) { {run.run_id} {run.status} - {run.created_at ?? "—"} + {run.created_at ?? "-"}
))}
@@ -74,7 +82,7 @@ export default function AdminUserDetail({ userId }: { userId: string }) {
{evt.event}{" "} ({evt.source}){" "} - {evt.ts ?? "—"} + {evt.ts ?? "-"}
))}
diff --git a/src/pages/admin/AdminUsers.tsx b/src/pages/admin/AdminUsers.tsx index c1dec4f6..f54a7959 100644 --- a/src/pages/admin/AdminUsers.tsx +++ b/src/pages/admin/AdminUsers.tsx @@ -109,6 +109,14 @@ export default function AdminUsers() { return
Loading users...
; } + if (usersQuery.isError) { + return ( +
+ {usersQuery.error instanceof Error ? usersQuery.error.message : "Failed to load users."} +
+ ); + } + const data = usersQuery.data; if (!data) { return
No users.
;
ID {run.user_id} {run.status}{run.mode ?? "—"}{run.mode ?? "-"} {run.order_count} {run.trade_count}