Fix five console-level issues surfaced by live-deploy Playwright audit
1. FlightsMap tiles didn't render: MapCanvas inline height:100% resolved
to 0 against min-height parents. Hand sizing to consumer CSS so
.flights-map-start__map height:500px wins.
2. FlightsMap /map/api/tile/{z}/{x}/{y}.jpeg requests fell through to
Modern.js SSR (HTML body). Dev proxy now forwards /map/* to the
test env via curl with image headers and binary-safe piping.
3. PopularRequestsPanel duplicate React key (Route-SVO-LED appears
twice in upstream). Suffix the key with the visible index.
4. OnlineBoardDetailsPage /onlineboard/details 400. Upstream expects
an ISO datetime (yyyy-MM-DDTHH:mm:ss), matching Angular's
ApiFormatterService.formatDate. Append T00:00:00.
5. Browser-level SignalR CORS errors on every details page: the
default SIGNALR_HUB_URL pointed at an unreachable placeholder.
Default to empty + skip the connection in useLiveFlights when
blank. Also configureLogging(LogLevel.None) so SignalR stops
writing its own negotiation failures to console. Live updates
re-enable by setting SIGNALR_HUB_URL on a deployment.
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
* Port 8080 (browser-facing):
|
||||
* /api/* → curl → https://flights.test.aeroflot.ru (bypasses WAF via curl TLS)
|
||||
* /flights/* → curl → https://flights.test.aeroflot.ru
|
||||
* /map/* → curl → https://flights.test.aeroflot.ru (binary JPEG tiles)
|
||||
* /* → localhost:8081 (Modern.js SSR + HMR)
|
||||
*/
|
||||
import express from "express";
|
||||
@@ -58,6 +59,41 @@ app.get("/api/appSettings", (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- Tile proxy via curl (binary JPEG, pipe stdout straight to response) ---
|
||||
const TILE_CONTENT_TYPES = {
|
||||
".jpeg": "image/jpeg",
|
||||
".jpg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".webp": "image/webp",
|
||||
};
|
||||
app.use("/map", (req, res) => {
|
||||
const targetUrl = `${API_TARGET}${req.originalUrl}`;
|
||||
const ext = req.path.substring(req.path.lastIndexOf("."));
|
||||
const contentType = TILE_CONTENT_TYPES[ext] ?? "application/octet-stream";
|
||||
|
||||
res.status(200);
|
||||
res.setHeader("Content-Type", contentType);
|
||||
res.setHeader("Cache-Control", "public, max-age=86400");
|
||||
|
||||
const args = [
|
||||
"-sS",
|
||||
"-f", // fail on HTTP 4xx/5xx
|
||||
"-H", `Accept: image/webp,image/jpeg,image/*,*/*`,
|
||||
"-H", `User-Agent: ${req.headers["user-agent"] || "Mozilla/5.0"}`,
|
||||
"-H", `Referer: ${API_TARGET}/`,
|
||||
targetUrl,
|
||||
];
|
||||
const child = spawn("/usr/bin/curl", args, { stdio: ["ignore", "pipe", "pipe"] });
|
||||
|
||||
child.stdout.pipe(res);
|
||||
child.on("exit", (code) => {
|
||||
if (code !== 0) {
|
||||
// -f means upstream failed; close cleanly with empty body.
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// --- API proxy via curl (bypasses WAF TLS fingerprinting) ---
|
||||
app.use(["/api", "/flights"], (req, res) => {
|
||||
const targetUrl = `${API_TARGET}${req.originalUrl}`;
|
||||
|
||||
Vendored
+5
-1
@@ -15,7 +15,11 @@ const EnvSchema = z.object({
|
||||
// reverse proxy / CDN forwards /api to the real host. Deployments that
|
||||
// call the API directly (no proxy) must set API_BASE_URL explicitly.
|
||||
API_BASE_URL: z.string().url().default("http://localhost:8080/api"),
|
||||
SIGNALR_HUB_URL: z.string().url().default("http://platform.yc.webzavod.ru/tracker/hub"),
|
||||
// Empty default = live-updates disabled. A deployment with a real hub
|
||||
// (same-origin or with CORS) sets this explicitly. Hooks (useLiveFlights)
|
||||
// skip the connection when blank so the browser does not emit CORS
|
||||
// errors for an unreachable placeholder host.
|
||||
SIGNALR_HUB_URL: z.string().default(""),
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url().optional(),
|
||||
OTEL_EXPORTER_OTLP_HEADERS: z.string().optional(),
|
||||
LOGS_ENDPOINT: z.string().url().optional(),
|
||||
|
||||
@@ -445,11 +445,14 @@ export const MapCanvas: FC<MapCanvasProps> = ({
|
||||
syncTooltips();
|
||||
}, [intermediateIds]);
|
||||
|
||||
// Sizing is controlled by the consumer via `className` styles. Inline
|
||||
// height:100% would resolve against a parent whose `height` is `auto`
|
||||
// (our wrappers typically use `min-height`), which makes Leaflet's
|
||||
// canvas collapse to 0px. Let the consumer's CSS own the dimensions.
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={className}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
data-testid="map-canvas"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -167,10 +167,13 @@ export const OnlineBoardDetailsPage: FC<OnlineBoardDetailsPageProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Fetch flight details
|
||||
// Fetch flight details.
|
||||
// `dates` must be a full ISO datetime (yyyy-MM-DDTHH:mm:ss); the API
|
||||
// returns 400 "Invalid request parameters" when only a date is sent.
|
||||
// Matches Angular's ApiFormatterService.formatDate.
|
||||
const detailsParams = {
|
||||
flights: `${flightId.carrier}${flightId.flightNumber}${flightId.suffix ?? ""}`,
|
||||
dates: `${flightId.date.slice(0, 4)}-${flightId.date.slice(4, 6)}-${flightId.date.slice(6, 8)}`,
|
||||
dates: `${flightId.date.slice(0, 4)}-${flightId.date.slice(4, 6)}-${flightId.date.slice(6, 8)}T00:00:00`,
|
||||
};
|
||||
const { flight: firstFlight, allFlights, daysOfFlight, loading, error } = useFlightDetails(detailsParams);
|
||||
|
||||
|
||||
@@ -59,8 +59,14 @@ export function PopularRequestsPanel({
|
||||
<h3 className="popular-requests__title">
|
||||
{t("BOARD.POPULAR-CHAPTERS")}
|
||||
</h3>
|
||||
{visibleRequests.map((request) => (
|
||||
<div key={getRequestKey(request)} className="popular-requests__item">
|
||||
{visibleRequests.map((request, index) => (
|
||||
// Index suffix guarantees uniqueness when the upstream returns two
|
||||
// entries with the same mode/departure/arrival triple (the API has
|
||||
// been observed returning duplicate "Route-SVO-LED" rows).
|
||||
<div
|
||||
key={`${getRequestKey(request)}-${index}`}
|
||||
className="popular-requests__item"
|
||||
>
|
||||
<PopularRequestItem
|
||||
request={request}
|
||||
onClick={onRequestClick}
|
||||
|
||||
@@ -38,6 +38,10 @@ export function useLiveFlights<TParams, TData>(
|
||||
|
||||
useEffect(() => {
|
||||
if (!isClient) return;
|
||||
// Blank hubUrl = live updates disabled for this deployment. Skip the
|
||||
// connection entirely; a deployment that wants live updates sets a
|
||||
// real SIGNALR_HUB_URL (same-origin or CORS-enabled).
|
||||
if (!config.hubUrl) return;
|
||||
|
||||
const channel = config.channelKey(params);
|
||||
const connection = getSharedConnection({ hubUrl: config.hubUrl });
|
||||
|
||||
@@ -203,9 +203,14 @@ async function defaultBuildConnection(
|
||||
url: string,
|
||||
delays: number[],
|
||||
): Promise<HubConnectionLike> {
|
||||
const { HubConnectionBuilder, HttpTransportType } = await import("@microsoft/signalr");
|
||||
const { HubConnectionBuilder, HttpTransportType, LogLevel } = await import("@microsoft/signalr");
|
||||
return new HubConnectionBuilder()
|
||||
.withUrl(url, { transport: HttpTransportType.WebSockets })
|
||||
.withAutomaticReconnect(delays)
|
||||
// Suppress SignalR's internal console logging. The hub is optional
|
||||
// (failures degrade to polling) and we already handle status via
|
||||
// setStatus("offline"); the console noise (CORS errors, negotiate
|
||||
// failures) adds no actionable information for users.
|
||||
.configureLogging(LogLevel.None)
|
||||
.build();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user