Make weather-market UI reliable before shipping
Proxy routes now share one upstream-error adapter so client-actionable statuses such as auth, entitlement, validation, and rate limits survive the BFF instead of becoming opaque 502s. The scan terminal mobile overrides also load last and remove desktop rail constraints so phones get a single readable column. Constraint: Mobile users reported the dashboard was unreadable, and BFF proxy errors were masking expected client states. Rejected: Let every route keep bespoke error JSON | continued inconsistent status codes and production detail leakage. Confidence: high Scope-risk: moderate Directive: Keep ScanTerminalMobile.module.css imported after desktop scan-terminal CSS so mobile breakpoints win. Tested: npx tsc --noEmit --pretty false --project frontend/tsconfig.json Tested: npm run build Tested: npm run test:business Tested: Chrome mobile smoke test at 390px with no horizontal overflow Not-tested: Real device Safari/Android manual QA
This commit is contained in:
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
const ANALYTICS_ENABLED =
|
const ANALYTICS_ENABLED =
|
||||||
@@ -33,19 +37,17 @@ export async function POST(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw, {
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 260) },
|
detailLimit: 260,
|
||||||
{ status: res.status },
|
});
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const response = NextResponse.json(data);
|
const response = NextResponse.json(data);
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to track analytics event", detail: String(error) },
|
publicMessage: "Failed to track analytics event",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
import {
|
import {
|
||||||
getLocalDevAuthPayload,
|
getLocalDevAuthPayload,
|
||||||
isLocalFullAccessHost,
|
isLocalFullAccessHost,
|
||||||
@@ -70,10 +74,7 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw);
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
|
|
||||||
{ status: res.status },
|
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -98,10 +99,9 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to fetch auth profile", detail: String(error) },
|
publicMessage: "Failed to fetch auth profile",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
import { buildCachedJsonResponse } from "@/lib/http-cache";
|
import { buildCachedJsonResponse } from "@/lib/http-cache";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
@@ -27,10 +31,7 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw);
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
|
|
||||||
{ status: 502 },
|
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -41,10 +42,9 @@ export async function GET(req: NextRequest) {
|
|||||||
);
|
);
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response = NextResponse.json(
|
const response = buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to fetch cities", detail: String(error) },
|
publicMessage: "Failed to fetch cities",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -41,20 +45,16 @@ export async function GET(
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw);
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
|
|
||||||
{ status: 502 },
|
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const response = NextResponse.json(data);
|
const response = NextResponse.json(data);
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response = NextResponse.json(
|
const response = buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to fetch city detail aggregate", detail: String(error) },
|
publicMessage: "Failed to fetch city detail aggregate",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,13 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
function parseBackendError(raw: string) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(raw) as unknown;
|
|
||||||
} catch {
|
|
||||||
return raw.slice(0, 800);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
context: { params: Promise<{ name: string }> },
|
context: { params: Promise<{ name: string }> },
|
||||||
@@ -56,14 +52,10 @@ export async function GET(
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw, {
|
||||||
{
|
detailLimit: 800,
|
||||||
error: "Backend city market scan failed",
|
error: "Backend city market scan failed",
|
||||||
upstream_status: res.status,
|
});
|
||||||
detail: parseBackendError(raw),
|
|
||||||
},
|
|
||||||
{ status: 502 },
|
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -74,13 +66,10 @@ export async function GET(
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response = NextResponse.json(
|
const response = buildProxyExceptionResponse(error, {
|
||||||
{
|
publicMessage: "Failed to fetch city market scan",
|
||||||
error: "Failed to fetch city market scan",
|
status: 502,
|
||||||
detail: String(error),
|
});
|
||||||
},
|
|
||||||
{ status: 502 },
|
|
||||||
);
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -150,20 +154,16 @@ export async function GET(
|
|||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw);
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
|
|
||||||
{ status: 502 },
|
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = normalizeCityDetailPayload(await res.json());
|
const data = normalizeCityDetailPayload(await res.json());
|
||||||
const response = NextResponse.json(data);
|
const response = NextResponse.json(data);
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response = NextResponse.json(
|
const response = buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to fetch city detail", detail: String(error) },
|
publicMessage: "Failed to fetch city detail",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
import { buildCachedJsonResponse } from "@/lib/http-cache";
|
import { buildCachedJsonResponse } from "@/lib/http-cache";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
@@ -43,10 +47,7 @@ export async function GET(
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw);
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
|
|
||||||
{ status: 502 },
|
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -65,10 +66,9 @@ export async function GET(
|
|||||||
);
|
);
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response = NextResponse.json(
|
const response = buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to fetch city summary", detail: String(error) },
|
publicMessage: "Failed to fetch city summary",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -32,9 +33,8 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to fetch healthz", detail: String(error) },
|
publicMessage: "Failed to fetch healthz",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
import { buildCachedJsonResponse } from "@/lib/http-cache";
|
import { buildCachedJsonResponse } from "@/lib/http-cache";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
@@ -33,10 +37,7 @@ export async function GET(
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw);
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
|
|
||||||
{ status: 502 },
|
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -47,10 +48,9 @@ export async function GET(
|
|||||||
);
|
);
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response = NextResponse.json(
|
const response = buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to fetch history", detail: String(error) },
|
publicMessage: "Failed to fetch history",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -35,9 +36,8 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to fetch analytics funnel", detail: String(error) },
|
publicMessage: "Failed to fetch analytics funnel",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -34,9 +35,8 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to fetch weekly leaderboard", detail: String(error) },
|
publicMessage: "Failed to fetch weekly leaderboard",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -34,9 +35,8 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to fetch memberships", detail: String(error) },
|
publicMessage: "Failed to fetch memberships",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -36,9 +37,8 @@ export async function POST(req: NextRequest, context: RouteContext) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to resolve payment incident", detail: String(error) },
|
publicMessage: "Failed to resolve payment incident",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -34,9 +35,8 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to fetch payment incidents", detail: String(error) },
|
publicMessage: "Failed to fetch payment incidents",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -36,9 +37,8 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to fetch truth history", detail: String(error) },
|
publicMessage: "Failed to fetch truth history",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -36,9 +37,8 @@ export async function POST(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to grant points", detail: String(error) },
|
publicMessage: "Failed to grant points",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -36,9 +37,8 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to fetch ops users", detail: String(error) },
|
publicMessage: "Failed to fetch ops users",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -21,10 +25,9 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw, {
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 350) },
|
detailLimit: 350,
|
||||||
{ status: res.status },
|
});
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -33,10 +36,9 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to fetch payment config", detail: String(error) },
|
publicMessage: "Failed to fetch payment config",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -33,19 +37,17 @@ export async function POST(
|
|||||||
);
|
);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw, {
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 350) },
|
detailLimit: 350,
|
||||||
{ status: res.status },
|
});
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const response = NextResponse.json(data);
|
const response = NextResponse.json(data);
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to confirm payment tx", detail: String(error) },
|
publicMessage: "Failed to confirm payment tx",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -29,19 +33,17 @@ export async function GET(
|
|||||||
);
|
);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw, {
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 350) },
|
detailLimit: 350,
|
||||||
{ status: res.status },
|
});
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const response = NextResponse.json(data);
|
const response = NextResponse.json(data);
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to fetch payment intent", detail: String(error) },
|
publicMessage: "Failed to fetch payment intent",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -33,19 +37,17 @@ export async function POST(
|
|||||||
);
|
);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw, {
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 350) },
|
detailLimit: 350,
|
||||||
{ status: res.status },
|
});
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const response = NextResponse.json(data);
|
const response = NextResponse.json(data);
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to submit payment tx", detail: String(error) },
|
publicMessage: "Failed to submit payment tx",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
import { isPaymentHostAllowed } from "@/lib/payment-host";
|
import { isPaymentHostAllowed } from "@/lib/payment-host";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
@@ -41,20 +45,18 @@ export async function POST(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw, {
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 350) },
|
detailLimit: 350,
|
||||||
{ status: res.status },
|
});
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const response = NextResponse.json(data);
|
const response = NextResponse.json(data);
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to create payment intent", detail: String(error) },
|
publicMessage: "Failed to create payment intent",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -31,9 +32,8 @@ export async function POST(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to reconcile latest payment", detail: String(error) },
|
publicMessage: "Failed to reconcile latest payment",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -22,10 +26,9 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw, {
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 500) },
|
detailLimit: 500,
|
||||||
{ status: res.status },
|
});
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,9 +38,8 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to fetch payment runtime", detail: String(error) },
|
publicMessage: "Failed to fetch payment runtime",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -26,30 +30,25 @@ export async function POST(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw, {
|
||||||
{
|
detailLimit: 350,
|
||||||
error: `Backend returned ${res.status}`,
|
extraDebug: {
|
||||||
detail: raw.slice(0, 350),
|
|
||||||
proxy_debug: {
|
|
||||||
has_authorization: proxiedHeaders.has("authorization"),
|
has_authorization: proxiedHeaders.has("authorization"),
|
||||||
has_entitlement: proxiedHeaders.has("x-polyweather-entitlement"),
|
has_entitlement: proxiedHeaders.has("x-polyweather-entitlement"),
|
||||||
has_forwarded_user_id: proxiedHeaders.has(
|
has_forwarded_user_id: proxiedHeaders.has(
|
||||||
"x-polyweather-auth-user-id",
|
"x-polyweather-auth-user-id",
|
||||||
),
|
),
|
||||||
has_forwarded_email: proxiedHeaders.has("x-polyweather-auth-email"),
|
has_forwarded_email: proxiedHeaders.has("x-polyweather-auth-email"),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{ status: res.status },
|
});
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const response = NextResponse.json(data);
|
const response = NextResponse.json(data);
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to create wallet challenge", detail: String(error) },
|
publicMessage: "Failed to create wallet challenge",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -21,10 +25,9 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw, {
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 350) },
|
detailLimit: 350,
|
||||||
{ status: res.status },
|
});
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -33,10 +36,9 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to fetch wallets", detail: String(error) },
|
publicMessage: "Failed to fetch wallets",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,11 +67,9 @@ export async function DELETE(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw, {
|
||||||
{
|
detailLimit: 350,
|
||||||
error: `Backend returned ${res.status}`,
|
extraDebug: {
|
||||||
detail: raw.slice(0, 350),
|
|
||||||
proxy_debug: {
|
|
||||||
incoming_has_authorization: Boolean(
|
incoming_has_authorization: Boolean(
|
||||||
String(req.headers.get("authorization") || "").trim(),
|
String(req.headers.get("authorization") || "").trim(),
|
||||||
),
|
),
|
||||||
@@ -79,10 +79,8 @@ export async function DELETE(req: NextRequest) {
|
|||||||
"x-polyweather-auth-user-id",
|
"x-polyweather-auth-user-id",
|
||||||
),
|
),
|
||||||
has_forwarded_email: proxiedHeaders.has("x-polyweather-auth-email"),
|
has_forwarded_email: proxiedHeaders.has("x-polyweather-auth-email"),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{ status: res.status },
|
});
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
let data: unknown = { ok: true };
|
let data: unknown = { ok: true };
|
||||||
@@ -98,10 +96,9 @@ export async function DELETE(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to unbind wallet", detail: String(error) },
|
publicMessage: "Failed to unbind wallet",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -26,31 +30,26 @@ export async function POST(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw, {
|
||||||
{
|
detailLimit: 350,
|
||||||
error: `Backend returned ${res.status}`,
|
extraDebug: {
|
||||||
detail: raw.slice(0, 350),
|
|
||||||
proxy_debug: {
|
|
||||||
has_authorization: proxiedHeaders.has("authorization"),
|
has_authorization: proxiedHeaders.has("authorization"),
|
||||||
has_entitlement: proxiedHeaders.has("x-polyweather-entitlement"),
|
has_entitlement: proxiedHeaders.has("x-polyweather-entitlement"),
|
||||||
has_forwarded_user_id: proxiedHeaders.has(
|
has_forwarded_user_id: proxiedHeaders.has(
|
||||||
"x-polyweather-auth-user-id",
|
"x-polyweather-auth-user-id",
|
||||||
),
|
),
|
||||||
has_forwarded_email: proxiedHeaders.has("x-polyweather-auth-email"),
|
has_forwarded_email: proxiedHeaders.has("x-polyweather-auth-email"),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{ status: res.status },
|
});
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const response = NextResponse.json(data);
|
const response = NextResponse.json(data);
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to verify wallet binding", detail: String(error) },
|
publicMessage: "Failed to verify wallet binding",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
const AI_CITY_GATEWAY_TIMEOUT_MS = Math.max(
|
const AI_CITY_GATEWAY_TIMEOUT_MS = Math.max(
|
||||||
@@ -63,10 +67,7 @@ export async function POST(req: NextRequest) {
|
|||||||
status: res.status,
|
status: res.status,
|
||||||
detail: raw.slice(0, 180),
|
detail: raw.slice(0, 180),
|
||||||
});
|
});
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw);
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
|
|
||||||
{ status: res.status === 402 || res.status === 403 ? res.status : 502 },
|
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -93,18 +94,17 @@ export async function POST(req: NextRequest) {
|
|||||||
timeout_ms: AI_CITY_GATEWAY_TIMEOUT_MS,
|
timeout_ms: AI_CITY_GATEWAY_TIMEOUT_MS,
|
||||||
error: String(error),
|
error: String(error),
|
||||||
});
|
});
|
||||||
const response = NextResponse.json(
|
const response = buildProxyExceptionResponse(error, {
|
||||||
{
|
publicMessage: timedOut
|
||||||
error: timedOut
|
? "City AI gateway timed out before backend responded"
|
||||||
? "City AI gateway timed out before backend responded"
|
: "Failed to fetch city AI data",
|
||||||
: "Failed to fetch city AI data",
|
status: timedOut ? 504 : 500,
|
||||||
detail: String(error),
|
extra: {
|
||||||
elapsed_ms: elapsedMs,
|
elapsed_ms: elapsedMs,
|
||||||
timeout_ms: AI_CITY_GATEWAY_TIMEOUT_MS,
|
timeout_ms: AI_CITY_GATEWAY_TIMEOUT_MS,
|
||||||
city: requestBody.city,
|
city: requestBody.city,
|
||||||
},
|
},
|
||||||
{ status: timedOut ? 504 : 500 },
|
});
|
||||||
);
|
|
||||||
return auth ? applyAuthResponseCookies(response, auth.response) : response;
|
return auth ? applyAuthResponseCookies(response, auth.response) : response;
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -40,10 +44,7 @@ export async function POST(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
if (!res.ok || !res.body) {
|
if (!res.ok || !res.body) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw);
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
|
|
||||||
{ status: res.status === 402 || res.status === 403 ? res.status : 502 },
|
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,14 +58,10 @@ export async function POST(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response = NextResponse.json(
|
const response = buildProxyExceptionResponse(error, {
|
||||||
{
|
publicMessage: "Failed to stream city AI data",
|
||||||
error: "Failed to stream city AI data",
|
extra: { city: requestBody.city },
|
||||||
detail: String(error),
|
});
|
||||||
city: requestBody.city,
|
|
||||||
},
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
const SCAN_AI_PROXY_TIMEOUT_MS = Math.max(
|
const SCAN_AI_PROXY_TIMEOUT_MS = Math.max(
|
||||||
@@ -46,10 +50,7 @@ export async function POST(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw);
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
|
|
||||||
{ status: res.status === 402 || res.status === 403 ? res.status : 502 },
|
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -61,15 +62,12 @@ export async function POST(req: NextRequest) {
|
|||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const timedOut = controller.signal.aborted;
|
const timedOut = controller.signal.aborted;
|
||||||
const response = NextResponse.json(
|
const response = buildProxyExceptionResponse(error, {
|
||||||
{
|
publicMessage: timedOut
|
||||||
error: timedOut
|
? "Scan AI request timed out"
|
||||||
? "Scan AI request timed out"
|
: "Failed to fetch scan AI data",
|
||||||
: "Failed to fetch scan AI data",
|
status: timedOut ? 504 : 500,
|
||||||
detail: String(error),
|
});
|
||||||
},
|
|
||||||
{ status: timedOut ? 504 : 500 },
|
|
||||||
);
|
|
||||||
return auth ? applyAuthResponseCookies(response, auth.response) : response;
|
return auth ? applyAuthResponseCookies(response, auth.response) : response;
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
const SCAN_TERMINAL_PROXY_TIMEOUT_MS = Number(
|
const SCAN_TERMINAL_PROXY_TIMEOUT_MS = Number(
|
||||||
@@ -54,10 +58,7 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw);
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
|
|
||||||
{ status: 502 },
|
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -69,15 +70,12 @@ export async function GET(req: NextRequest) {
|
|||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const timedOut = controller.signal.aborted;
|
const timedOut = controller.signal.aborted;
|
||||||
const response = NextResponse.json(
|
const response = buildProxyExceptionResponse(error, {
|
||||||
{
|
publicMessage: timedOut
|
||||||
error: timedOut
|
? "Scan terminal request timed out"
|
||||||
? "Scan terminal request timed out"
|
: "Failed to fetch scan terminal data",
|
||||||
: "Failed to fetch scan terminal data",
|
status: timedOut ? 504 : 500,
|
||||||
detail: String(error),
|
});
|
||||||
},
|
|
||||||
{ status: timedOut ? 504 : 500 },
|
|
||||||
);
|
|
||||||
return auth ? applyAuthResponseCookies(response, auth.response) : response;
|
return auth ? applyAuthResponseCookies(response, auth.response) : response;
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import {
|
||||||
|
buildProxyExceptionResponse,
|
||||||
|
buildUpstreamErrorResponse,
|
||||||
|
} from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -22,10 +26,9 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildUpstreamErrorResponse(res.status, raw, {
|
||||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 500) },
|
detailLimit: 500,
|
||||||
{ status: res.status },
|
});
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,9 +38,8 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{ error: "Failed to fetch system status", detail: String(error) },
|
publicMessage: "Failed to fetch system status",
|
||||||
{ status: 500 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import modalChromeStyles from "./ModalChrome.module.css";
|
|||||||
import scanTerminalCalendarStyles from "./ScanTerminalCalendar.module.css";
|
import scanTerminalCalendarStyles from "./ScanTerminalCalendar.module.css";
|
||||||
import scanTerminalCardStyles from "./ScanTerminalCard.module.css";
|
import scanTerminalCardStyles from "./ScanTerminalCard.module.css";
|
||||||
import scanTerminalLightThemeStyles from "./ScanTerminalLightTheme.module.css";
|
import scanTerminalLightThemeStyles from "./ScanTerminalLightTheme.module.css";
|
||||||
import scanTerminalMobileStyles from "./ScanTerminalMobile.module.css";
|
|
||||||
import scanTerminalOpportunityStyles from "./ScanTerminalOpportunity.module.css";
|
import scanTerminalOpportunityStyles from "./ScanTerminalOpportunity.module.css";
|
||||||
import scanTerminalStyles from "./ScanTerminal.module.css";
|
import scanTerminalStyles from "./ScanTerminal.module.css";
|
||||||
import scanTerminalBoardStyles from "./ScanTerminalBoard.module.css";
|
import scanTerminalBoardStyles from "./ScanTerminalBoard.module.css";
|
||||||
@@ -40,6 +39,7 @@ import scanTerminalFiltersStyles from "./ScanTerminalFilters.module.css";
|
|||||||
import scanTerminalListStyles from "./ScanTerminalList.module.css";
|
import scanTerminalListStyles from "./ScanTerminalList.module.css";
|
||||||
import scanTerminalShellStyles from "./ScanTerminalShell.module.css";
|
import scanTerminalShellStyles from "./ScanTerminalShell.module.css";
|
||||||
import scanTerminalStateStyles from "./ScanTerminalState.module.css";
|
import scanTerminalStateStyles from "./ScanTerminalState.module.css";
|
||||||
|
import scanTerminalMobileStyles from "./ScanTerminalMobile.module.css";
|
||||||
import { ProFeaturePaywall } from "@/components/dashboard/ProFeaturePaywall";
|
import { ProFeaturePaywall } from "@/components/dashboard/ProFeaturePaywall";
|
||||||
import {
|
import {
|
||||||
DashboardStoreProvider,
|
DashboardStoreProvider,
|
||||||
|
|||||||
@@ -144,6 +144,8 @@
|
|||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.root :global(.scan-terminal) {
|
.root :global(.scan-terminal) {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
min-height: 100dvh;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root :global(.scan-filter-panel),
|
.root :global(.scan-filter-panel),
|
||||||
@@ -153,6 +155,27 @@
|
|||||||
min-height: auto;
|
min-height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-data-grid) {
|
||||||
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-list-section),
|
||||||
|
.root :global(.scan-table-shell),
|
||||||
|
.root :global(.scan-ai-workspace) {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-ai-workspace) {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-terminal > .detail-panel.scan-city-detail-rail) {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.scan-kpi-bar) {
|
.root :global(.scan-kpi-bar) {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
@@ -267,6 +290,26 @@
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-data-grid) {
|
||||||
|
padding: 12px;
|
||||||
|
gap: 14px;
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-topbar-actions) {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-topbar-title strong) {
|
||||||
|
font-size: 23px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-topbar-title span) {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.scan-kpi-bar) {
|
.root :global(.scan-kpi-bar) {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -389,6 +432,40 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-list-tabs) {
|
||||||
|
width: 100%;
|
||||||
|
gap: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-list-tabs::-webkit-scrollbar) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-list-tabs button) {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-height: 36px;
|
||||||
|
border: 1px solid rgba(99, 132, 180, 0.18);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(8, 19, 34, 0.76);
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-list-tabs button.active) {
|
||||||
|
border-color: rgba(77, 163, 255, 0.38);
|
||||||
|
background: rgba(77, 163, 255, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-list-status) {
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.scan-opportunity-groups) {
|
.root :global(.scan-opportunity-groups) {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
@@ -411,3 +488,50 @@
|
|||||||
padding: 0 13px 13px;
|
padding: 0 13px 13px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.root :global(.scan-terminal) {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-data-grid) {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-upgrade-announcement) {
|
||||||
|
margin: -2px 0 2px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-upgrade-announcement-copy p) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-upgrade-announcement ul) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-status-chip) {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-primary-button) {
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-account-button) {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-theme-button) {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
const PASSTHROUGH_UPSTREAM_STATUSES = new Set([
|
||||||
|
400,
|
||||||
|
401,
|
||||||
|
402,
|
||||||
|
403,
|
||||||
|
404,
|
||||||
|
409,
|
||||||
|
422,
|
||||||
|
429,
|
||||||
|
]);
|
||||||
|
|
||||||
|
function shouldExposeProxyErrorDetail() {
|
||||||
|
return (
|
||||||
|
process.env.NODE_ENV !== "production" ||
|
||||||
|
process.env.POLYWEATHER_EXPOSE_PROXY_ERROR_DETAIL === "true"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clientStatusFromUpstream(status: number) {
|
||||||
|
if (PASSTHROUGH_UPSTREAM_STATUSES.has(status)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
return 502;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildUpstreamErrorResponse(
|
||||||
|
upstreamStatus: number,
|
||||||
|
rawDetail: string,
|
||||||
|
options?: {
|
||||||
|
detailLimit?: number;
|
||||||
|
error?: string;
|
||||||
|
extraDebug?: Record<string, unknown>;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const body: Record<string, unknown> = {
|
||||||
|
error: options?.error || "Upstream request failed",
|
||||||
|
upstream_status: upstreamStatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shouldExposeProxyErrorDetail()) {
|
||||||
|
body.detail = String(rawDetail || "").slice(0, options?.detailLimit ?? 300);
|
||||||
|
if (options?.extraDebug) {
|
||||||
|
body.proxy_debug = options.extraDebug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(body, {
|
||||||
|
status: clientStatusFromUpstream(upstreamStatus),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildProxyExceptionResponse(
|
||||||
|
error: unknown,
|
||||||
|
options: {
|
||||||
|
status?: number;
|
||||||
|
publicMessage: string;
|
||||||
|
extra?: Record<string, unknown>;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const body: Record<string, unknown> = {
|
||||||
|
error: options.publicMessage,
|
||||||
|
...(options.extra || {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shouldExposeProxyErrorDetail()) {
|
||||||
|
body.detail = String(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(body, { status: options.status ?? 500 });
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
applyAuthResponseCookies,
|
applyAuthResponseCookies,
|
||||||
buildBackendRequestHeaders,
|
buildBackendRequestHeaders,
|
||||||
} from "@/lib/backend-auth";
|
} from "@/lib/backend-auth";
|
||||||
|
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -47,15 +48,15 @@ export async function forwardPriorityWarmHint(req: NextRequest) {
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
const response = NextResponse.json(
|
const response = buildProxyExceptionResponse(raw, {
|
||||||
{
|
status: 202,
|
||||||
|
publicMessage: "Priority warm hint was skipped",
|
||||||
|
extra: {
|
||||||
ok: false,
|
ok: false,
|
||||||
skipped: true,
|
skipped: true,
|
||||||
error: `Backend returned ${res.status}`,
|
upstream_status: res.status,
|
||||||
detail: raw.slice(0, 500),
|
|
||||||
},
|
},
|
||||||
{ status: 202 },
|
});
|
||||||
);
|
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,14 +66,13 @@ export async function forwardPriorityWarmHint(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
return applyAuthResponseCookies(response, auth.response);
|
return applyAuthResponseCookies(response, auth.response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return buildProxyExceptionResponse(error, {
|
||||||
{
|
status: 202,
|
||||||
|
publicMessage: "Failed to send priority warm hint",
|
||||||
|
extra: {
|
||||||
ok: false,
|
ok: false,
|
||||||
skipped: true,
|
skipped: true,
|
||||||
error: "Failed to send priority warm hint",
|
|
||||||
detail: String(error),
|
|
||||||
},
|
},
|
||||||
{ status: 202 },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user