diff --git a/src/app/api/fee-compare/route.ts b/src/app/api/fee-compare/route.ts index 6e64189c2..f79297d36 100644 --- a/src/app/api/fee-compare/route.ts +++ b/src/app/api/fee-compare/route.ts @@ -706,7 +706,8 @@ function buildHlWalletData( function reconstructHlPositions(fills: HlFill[], cutoffMs: number): PositionSlice[] { const state = new Map(); const slices: PositionSlice[] = []; - const sorted = fills.filter((f) => f.time >= cutoffMs).sort((a, b) => a.time - b.time); + // Process ALL fills chronologically so positions opened before cutoffMs are visible + const sorted = fills.slice().sort((a, b) => a.time - b.time); for (const f of sorted) { const sz = parseFloat(f.sz); @@ -724,7 +725,12 @@ function reconstructHlPositions(fills: HlFill[], cutoffMs: number): PositionSlic const pos = state.get(key); if (pos && pos.sz > 0) { const closeSz = Math.min(sz, pos.sz); - slices.push({ coin: f.coin, notionalUsd: closeSz * pos.openPx, openMs: pos.openTs, closeMs: f.time, isLong }); + // Cap slice start to cutoffMs so carry is only for the measurement window + const sliceOpen = Math.max(pos.openTs, cutoffMs); + const sliceClose = f.time; + if (sliceClose > cutoffMs) { + slices.push({ coin: f.coin, notionalUsd: closeSz * pos.openPx, openMs: sliceOpen, closeMs: sliceClose, isLong }); + } pos.sz -= closeSz; if (pos.sz < 0.00001) state.delete(key); else state.set(key, pos); @@ -732,7 +738,7 @@ function reconstructHlPositions(fills: HlFill[], cutoffMs: number): PositionSlic } } - // Still-open positions — close at now + // Still-open positions — close at now, cap open to cutoffMs const now = Date.now(); for (const [key, pos] of state) { if (pos.sz > 0.00001) { @@ -742,7 +748,7 @@ function reconstructHlPositions(fills: HlFill[], cutoffMs: number): PositionSlic slices.push({ coin, notionalUsd: pos.sz * parseFloat(lastFill.px), - openMs: pos.openTs, + openMs: Math.max(pos.openTs, cutoffMs), closeMs: now, isLong: pos.isLong, }); diff --git a/src/app/sitemap.xml/route.ts b/src/app/sitemap.xml/route.ts index 69348443c..709354229 100644 --- a/src/app/sitemap.xml/route.ts +++ b/src/app/sitemap.xml/route.ts @@ -15,11 +15,15 @@ import { buildSitemap } from "@/lib/sitemap-builder"; // serialized to XML here and shipped with a real edge cache header. export const runtime = "nodejs"; export const dynamic = "force-dynamic"; -// Hard platform ceiling so Vercel never holds the connection open longer -// than this when the SRH/Redis fan-out runs over budget. The internal -// buildSitemap() timeout is 20 s; this gives 40 s of headroom and still -// replies well within the smoke-test's 90 s limit. -export const maxDuration = 60; +// 300 s: the aggregate blob is 4.25 MB — larger than Next.js Data Cache's +// 2 MB limit, so unstable_cache can never store it and every request must +// re-fetch from the network. The /api/aggregate ISR proxy responds in +// 18-65 s on a cold cache. With maxDuration=60 the process received +// SIGKILL before the fetch resolved, discarding the buffered response and +// returning 500. 300 s gives the full build time to complete on cold +// starts; the internal JS timeout (240 s) fires first and falls back to +// the static sitemap if the full build hangs. +export const maxDuration = 300; function escapeXml(s: string): string { return s diff --git a/src/lib/sitemap-builder.ts b/src/lib/sitemap-builder.ts index 669fae044..957b39f8c 100644 --- a/src/lib/sitemap-builder.ts +++ b/src/lib/sitemap-builder.ts @@ -577,11 +577,13 @@ async function buildFullSitemap(): Promise { export async function buildSitemap(): Promise { try { - // 20 s: tight enough to reply well within the smoke-test's 90 s window - // even if the static fallback itself takes a few seconds. The Vercel - // maxDuration on the route is 60 s; this leaves 40 s of headroom. + // 240 s: maxDuration on the route is 300 s. The aggregate blob (4.25 MB) + // exceeds Next.js Data Cache's 2 MB cap, so every sitemap request + // re-fetches from /api/aggregate whose FETCH_TIMEOUT_MS is 65 s. A 240 s + // budget covers cold aggregate + HL-builder Prometheus sweeps with 60 s of + // headroom for buildStaticFallback() before the 300 s SIGKILL ceiling. const timeout = new Promise((_, reject) => - setTimeout(() => reject(new Error("sitemap build timeout")), 20_000), + setTimeout(() => reject(new Error("sitemap build timeout")), 240_000), ); // Attach a no-op .catch() to prevent unhandled-rejection crashes if // buildFullSitemap() rejects AFTER the race has already settled (via