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:
2569718930@qq.com
2026-04-29 11:22:49 +08:00
parent 4630cee06f
commit 569f3eef97
36 changed files with 471 additions and 282 deletions
+8 -8
View File
@@ -3,6 +3,10 @@ import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -41,20 +45,16 @@ export async function GET(
});
if (!res.ok) {
const raw = await res.text();
const response = NextResponse.json(
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
{ status: 502 },
);
const response = buildUpstreamErrorResponse(res.status, raw);
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
const response = NextResponse.json(data);
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
const response = NextResponse.json(
{ error: "Failed to fetch city detail aggregate", detail: String(error) },
{ status: 500 },
);
const response = buildProxyExceptionResponse(error, {
publicMessage: "Failed to fetch city detail aggregate",
});
return response;
}
}
@@ -3,17 +3,13 @@ import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
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(
req: NextRequest,
context: { params: Promise<{ name: string }> },
@@ -56,14 +52,10 @@ export async function GET(
});
if (!res.ok) {
const raw = await res.text();
const response = NextResponse.json(
{
error: "Backend city market scan failed",
upstream_status: res.status,
detail: parseBackendError(raw),
},
{ status: 502 },
);
const response = buildUpstreamErrorResponse(res.status, raw, {
detailLimit: 800,
error: "Backend city market scan failed",
});
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
@@ -74,13 +66,10 @@ export async function GET(
});
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
const response = NextResponse.json(
{
error: "Failed to fetch city market scan",
detail: String(error),
},
{ status: 502 },
);
const response = buildProxyExceptionResponse(error, {
publicMessage: "Failed to fetch city market scan",
status: 502,
});
return response;
}
}
+8 -8
View File
@@ -3,6 +3,10 @@ import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -150,20 +154,16 @@ export async function GET(
return applyAuthResponseCookies(response, auth.response);
}
const response = NextResponse.json(
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
{ status: 502 },
);
const response = buildUpstreamErrorResponse(res.status, raw);
return applyAuthResponseCookies(response, auth.response);
}
const data = normalizeCityDetailPayload(await res.json());
const response = NextResponse.json(data);
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
const response = NextResponse.json(
{ error: "Failed to fetch city detail", detail: String(error) },
{ status: 500 },
);
const response = buildProxyExceptionResponse(error, {
publicMessage: "Failed to fetch city detail",
});
return response;
}
}
@@ -3,6 +3,10 @@ import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
import { buildCachedJsonResponse } from "@/lib/http-cache";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -43,10 +47,7 @@ export async function GET(
});
if (!res.ok) {
const raw = await res.text();
const response = NextResponse.json(
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
{ status: 502 },
);
const response = buildUpstreamErrorResponse(res.status, raw);
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
@@ -65,10 +66,9 @@ export async function GET(
);
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
const response = NextResponse.json(
{ error: "Failed to fetch city summary", detail: String(error) },
{ status: 500 },
);
const response = buildProxyExceptionResponse(error, {
publicMessage: "Failed to fetch city summary",
});
return response;
}
}