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:
2026-04-17 21:55:44 +03:00
parent b54746c74c
commit c8d0caa9cf
7 changed files with 68 additions and 7 deletions
+36
View File
@@ -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}`;
+5 -1
View File
@@ -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}
+4
View File
@@ -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 });
+6 -1
View File
@@ -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();
}