diff --git a/scripts/dev-server.mjs b/scripts/dev-server.mjs index 2989d3dc..62b6c7dc 100644 --- a/scripts/dev-server.mjs +++ b/scripts/dev-server.mjs @@ -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}`; diff --git a/src/env/index.ts b/src/env/index.ts index 51d269c1..a968ee7d 100644 --- a/src/env/index.ts +++ b/src/env/index.ts @@ -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(), diff --git a/src/features/flights-map/components/MapCanvas.tsx b/src/features/flights-map/components/MapCanvas.tsx index 41cf77c4..2bc9662b 100644 --- a/src/features/flights-map/components/MapCanvas.tsx +++ b/src/features/flights-map/components/MapCanvas.tsx @@ -445,11 +445,14 @@ export const MapCanvas: FC = ({ 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 (
); diff --git a/src/features/online-board/components/OnlineBoardDetailsPage.tsx b/src/features/online-board/components/OnlineBoardDetailsPage.tsx index 8bb4e991..1ed7215a 100644 --- a/src/features/online-board/components/OnlineBoardDetailsPage.tsx +++ b/src/features/online-board/components/OnlineBoardDetailsPage.tsx @@ -167,10 +167,13 @@ export const OnlineBoardDetailsPage: FC = ({ }) => { 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); diff --git a/src/features/popular-requests/components/PopularRequestsPanel.tsx b/src/features/popular-requests/components/PopularRequestsPanel.tsx index 42c46340..a5781958 100644 --- a/src/features/popular-requests/components/PopularRequestsPanel.tsx +++ b/src/features/popular-requests/components/PopularRequestsPanel.tsx @@ -59,8 +59,14 @@ export function PopularRequestsPanel({

{t("BOARD.POPULAR-CHAPTERS")}

- {visibleRequests.map((request) => ( -
+ {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). +
( 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 }); diff --git a/src/shared/signalr/connection.ts b/src/shared/signalr/connection.ts index 0255b11e..3d464407 100644 --- a/src/shared/signalr/connection.ts +++ b/src/shared/signalr/connection.ts @@ -203,9 +203,14 @@ async function defaultBuildConnection( url: string, delays: number[], ): Promise { - 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(); }