diff --git a/external/photon b/external/photon index 84ddfc0f58..54ead0a77e 160000 --- a/external/photon +++ b/external/photon @@ -1 +1 @@ -Subproject commit 84ddfc0f586806373567faf75f45158076a4f133 +Subproject commit 54ead0a77e50eed0f7beb704868ce6cb0b914b8f diff --git a/forester/dashboard/src/app/api/[...path]/route.ts b/forester/dashboard/src/app/api/[...path]/route.ts new file mode 100644 index 0000000000..0fa4cf84fc --- /dev/null +++ b/forester/dashboard/src/app/api/[...path]/route.ts @@ -0,0 +1,83 @@ +import { NextResponse } from "next/server"; + +export const dynamic = "force-dynamic"; + +const BACKEND_URL = + process.env.FORESTER_API_URL || "http://127.0.0.1:8080"; + +const BACKEND_TIMEOUT_MS = Number( + process.env.FORESTER_API_TIMEOUT_MS ?? 8000 +); + +function isAbortError(error: unknown): boolean { + return ( + typeof error === "object" && + error !== null && + "name" in error && + (error as { name?: string }).name === "AbortError" + ); +} + +function joinBackendUrl(path: string): string { + const base = BACKEND_URL.replace(/\/+$/, ""); + return `${base}/${path}`; +} + +export async function GET( + _request: Request, + { params }: { params: Promise<{ path: string[] }> } +) { + const { path } = await params; + const backendPath = path.join("/"); + const upstream = joinBackendUrl(backendPath); + + const timeoutMs = + Number.isFinite(BACKEND_TIMEOUT_MS) && BACKEND_TIMEOUT_MS > 0 + ? BACKEND_TIMEOUT_MS + : 8000; + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetch(upstream, { + cache: "no-store", + signal: controller.signal, + }); + + const contentType = response.headers.get("content-type") ?? ""; + const payload = contentType.includes("application/json") + ? await response.json() + : { message: await response.text() }; + + if (!response.ok) { + return NextResponse.json( + { + error: `Forester backend returned ${response.status}`, + upstream, + details: payload, + }, + { status: response.status } + ); + } + + return NextResponse.json(payload, { status: response.status }); + } catch (error) { + if (isAbortError(error)) { + return NextResponse.json( + { + error: `Backend request timed out after ${timeoutMs}ms`, + upstream, + }, + { status: 504 } + ); + } + + return NextResponse.json( + { error: "Backend unavailable", upstream }, + { status: 502 } + ); + } finally { + clearTimeout(timer); + } +} diff --git a/forester/dashboard/src/app/compressible/page.tsx b/forester/dashboard/src/app/compressible/page.tsx index 8ea4fc3274..72f857cfcf 100644 --- a/forester/dashboard/src/app/compressible/page.tsx +++ b/forester/dashboard/src/app/compressible/page.tsx @@ -23,7 +23,13 @@ export default function CompressiblePage() { return (
+ Track what is currently compressible, what is waiting on rent + windows, and how fresh this view is. +
+- No compressible account data available. -
-- The dashboard will query on-chain data automatically. If this - persists, check the RPC connection. -
-+ No compressible account data available yet. +
+ )} + + {data.error && ( ++ ATA counts are included in Light Token totals. +
+ +